Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mobile menu #129

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ export * from './block'
export * from './recaptcha'
export * from './recaptcha-field'
export * from './link'
export * from './menu'
25 changes: 25 additions & 0 deletions packages/core/src/menu/MenuControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {PureComponent} from 'react'

import RenderChild from '../RenderChild'
import {MenuLevel, BaseMenuItem, MenuLevelBuilder} from './MenuLevelBuilder'

export interface MenuControlProps<Item extends BaseMenuItem> {
items: Item[]
selected: string
children: RenderChild<{
levels: MenuLevel<Item>[]
}>
}

export class MenuControl<Item extends BaseMenuItem> extends PureComponent<
MenuControlProps<Item>
> {

render() {
const {props} = this
return props.children({
levels: new MenuLevelBuilder(props.items).levels(props.selected),
})
}

}
69 changes: 69 additions & 0 deletions packages/core/src/menu/MenuLevelBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {ReactNode} from 'react'

export interface BaseMenuItem {
id: string
title: ReactNode
}

export type MenuTree<Item extends BaseMenuItem> = Item & {
sub?: MenuTree<Item>[]
}

export type MenuLevel<Item extends BaseMenuItem> = {
items: Item[]
selected: string
} | undefined

export class MenuLevelBuilder<Item extends BaseMenuItem> {

constructor(protected items: MenuTree<Item>[]) {}

protected buildLevels(
selected: string,
items: MenuTree<Item>[],
levels: MenuLevel<Item>[],
depth = 0,
parentActive = false,
): boolean {
if (levels.length <= depth) levels.push(undefined)
let activeItem: MenuTree<Item> | undefined
for (let item of items) {
const currentActive = item.id === selected
const sub = item.sub
const childActive =
sub && sub.length > 0
? this.buildLevels(
selected,
sub,
levels,
depth + 1,
currentActive || parentActive,
)
: false
if (currentActive) activeItem = item
if (childActive && !activeItem) activeItem = item
}

if (activeItem || parentActive) {
levels[depth] = {
items,
selected: activeItem ? activeItem.id : items[0].id,
}
}

return !!activeItem
}

/**
* Split MenuTree structure to levels.
* Returns array, where index is a menu level.
*
* @param selected current menu item id
*/
levels(selected: string): MenuLevel<Item>[] {
const result: MenuLevel<Item>[] = []
this.buildLevels(selected, this.items, result)
return result
}

}
2 changes: 2 additions & 0 deletions packages/core/src/menu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './MenuControl'
export * from './MenuLevelBuilder'
47 changes: 47 additions & 0 deletions packages/mobile/src/horizontal-menu/HorizontalMenu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## Horizontal menu

Горизонтальное меню с прокруткой через touch-события. Используется как первый уровень меню.

```jsx
class HorizontalMenuDemo extends React.Component {
constructor() {
super()
this.state = {
selected: 'requests',
}
}

render() {
return (
<HorizontalMenu
id="kassa-menu-first"
selected={this.state.selected}
onSelect={item => this.setState({selected: item.id})}
items={[
{
title: 'Услуги',
id: 'merchants',
},
{
title: 'Заявки',
id: 'requests',
},

{
title: 'Пользователи',
id: 'users',
},

{
title: 'Поддержка',
id: 'support',
},
]}
/>
)
}
}
;<Spacer size="xl">
<HorizontalMenuDemo />
</Spacer>
```
62 changes: 62 additions & 0 deletions packages/mobile/src/horizontal-menu/HorizontalMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {BaseMenuItem, styled, Flex, Card, Box} from '@qiwi/pijma-core'
import React from 'react'

import {HorizontalMenuItem} from './HorizontalMenuItem'

// @see https://iamsteve.me/blog/entry/using-flexbox-for-horizontal-scrolling-navigation
const ScrollableFlex = styled(Flex)({
overflowX: 'auto',
webkitOverflowScrolling: 'auto',
msOverflowStyle: '-ms-autohiding-scrollbar',
'&::-webkit-scrollbar': {
display: 'none',
},
})

export interface HorizontalMenuProps<Item extends BaseMenuItem = BaseMenuItem> {
/**
* Menu html id and item ids prefix
*/
id: string
/**
* Active menu item id
*/
selected: string
items: Item[]
onSelect: (item: Item) => void
}

export const HorizontalMenu = <Item extends BaseMenuItem>({
items,
onSelect,
selected,
id,
}: HorizontalMenuProps<Item>) => (
<Card
id={id}
bb="1px solid #e6e6e6"
children={
<ScrollableFlex
id={`${id}-wrap`}
as="nav"
wrap="nowrap"
px={4}
children={items.map((item, index) => (
<Box
key={item.id}
id={`${id}-${item.id}-wrap`}
mr={index === items.length - 1 ? 0 : 6}
children={
<HorizontalMenuItem
id={`${id}-${item.id}`}
active={selected === item.id}
onClick={onSelect.bind(null, item)}
children={item.title}
/>
}
/>
))}
/>
}
/>
)
43 changes: 43 additions & 0 deletions packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {Card, LinkControl, Lnk} from '@qiwi/pijma-core'
import React from 'react'

import {Text} from '../typography'

export interface HorizontalMenuItemProps {
active: boolean
id: string
href?: string
target?: string
reverse?: boolean
onClick?: () => void
}

const CardLink = Card.withComponent(Lnk)

export const HorizontalMenuItem: React.FC<HorizontalMenuItemProps> = props => (
<LinkControl
onClick={props.onClick}
target={props.target}
children={renderProps => (
<CardLink
id={props.id}
href={props.href}
target={props.target}
onClick={renderProps.onClick}
onFocus={renderProps.onFocus}
onBlur={renderProps.onBlur}
onMouseEnter={renderProps.onMouseEnter}
onMouseLeave={renderProps.onMouseLeave}
onMouseUp={renderProps.onMouseUp}
onMouseDown={renderProps.onMouseDown}
cursor="pointer"
display="block"
pb={1}
bb={`4px solid ${
renderProps.hover || props.active ? '#ff8c00' : 'transparent'
}`}
children={<Text bold size="m" children={props.children} />}
/>
)}
/>
)
2 changes: 2 additions & 0 deletions packages/mobile/src/horizontal-menu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './HorizontalMenu'
export * from './HorizontalMenuItem'
3 changes: 3 additions & 0 deletions packages/mobile/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export * from './checkbox-field'
export * from './radio-field'
export * from './typography'
export * from './link'
export * from './horizontal-menu'
export * from './select-menu'
export * from './level-menu'

export {TextField as MaskTextField} from './text-field'
export {PasswordField as MaskPasswordField} from './password-field'
81 changes: 81 additions & 0 deletions packages/mobile/src/level-menu/LevelMenu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
## Level menu

Горизонтальное меню + вертикальное меню.

```jsx
class LevelMenuDemo extends React.Component {
constructor() {
super()
this.state = {
selected: 't2',
}
}

render() {
return (
<LevelMenu
id="kassa-menus"
selected={this.state.selected}
onSelect={item => this.setState({selected: item.id})}
items={[
{
title: 'Услуги',
id: 'merchants',
sub: [
{
title: 'Транзакции',
id: 'transactions',
sub: [
{
title: 'Что-то там 1',
id: 't1',
},
{
title: 'Что-то там 2',
id: 't2',
},
],
},
{
title: 'Акты',
id: 'reports',
},
{
title: 'Реестры',
id: 'registry',
},
],
},
{
title: 'Заявки',
id: 'requests',
sub: [
{
title: 'Список 1',
id: 'list1',
},
{
title: 'Список 2',
id: 'list2',
},
],
},

{
title: 'Пользователи',
id: 'users',
},

{
title: 'Поддержка',
id: 'support',
},
]}
/>
)
}
}
;<Spacer size="xl">
<LevelMenuDemo />
</Spacer>
```
Loading