Skip to content

Commit

Permalink
feat(mobile): mobile menus: horizontal, select, level
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergey Yuferev committed Apr 22, 2019
1 parent 0d61036 commit 5557e16
Show file tree
Hide file tree
Showing 15 changed files with 610 additions and 0 deletions.
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'
66 changes: 66 additions & 0 deletions packages/core/src/menu/MenuLevelBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {ReactNode} from 'react'

export interface BaseMenuItem {
id: string
title: ReactNode
}

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

export class MenuLevel<Item extends BaseMenuItem> {
constructor(public items: Item[], public activeId: string) {}
}

export class MenuLevelBuilder<Item extends BaseMenuItem> {
constructor(protected items: MenuTree<Item>[]) {}

protected buildLevels(
activeId: string,
items: MenuTree<Item>[],
levels: (MenuLevel<Item> | undefined)[],
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 === activeId
const sub = item.sub
const childActive =
sub && sub.length > 0
? this.buildLevels(
activeId,
sub,
levels,
depth + 1,
currentActive || parentActive
)
: false
if (currentActive) activeItem = item
if (childActive && !activeItem) activeItem = item
}

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

return !!activeItem
}

/**
* Split MenuTree structure to levels.
* Returns array, where index is a menu level.
*
* @param activeId current menu item id
*/
levels(activeId: string): (MenuLevel<Item> | undefined)[] {
const result: (MenuLevel<Item>[] | undefined) = []
this.buildLevels(activeId, this.items, result)
return result
}
}
1 change: 1 addition & 0 deletions packages/core/src/menu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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 = {
activeId: 'requests',
}
}

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

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

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

import {HorizontalMenuItem} from './HorizontalMenuItem'

export interface HorizontalMenuRenderItemProps<Item extends BaseMenuItem> {
/**
* Html item id
*/
id: string
item: Item
active: boolean
onClick: () => void
}

const horizontalMenuRenderItemDefault = <Item extends BaseMenuItem>({
item,
active,
id,
onClick,
}: HorizontalMenuRenderItemProps<Item>): ReactNode => (
<ButtonControl
key={item.id}
onClick={onClick}
children={renderProps => (
<HorizontalMenuItem
active={active}
onClick={renderProps.onClick}
id={id}
children={item.title}
/>
)}
/>
)

// @see https://iamsteve.me/blog/entry/using-flexbox-for-horizontal-scrolling-navigation
export const HorizontalMenuContainer = styled('nav')(({theme}) => ({
display: 'flex',
flexWrap: 'nowrap',
overflowX: 'auto',
webkitOverflowScrolling: 'auto',
msOverflowStyle: '-ms-autohiding-scrollbar',
'&::-webkit-scrollbar': {
display: 'none',
},
'&:last-child': {
marginRight: 0,
},
paddingLeft: '16px',
paddingRight: '16px',
borderBottom: '1px solid ' + theme.color.gray.light,
}))

export interface HorizontalMenuProps<Item extends BaseMenuItem = BaseMenuItem> {
/**
* Menu html id and item ids prefix
*/
id: string
/**
* Active menu item id
*/
activeId: string
items: Item[]
onClick: (item: Item) => void
renderItem: (props: HorizontalMenuRenderItemProps<Item>) => ReactNode
}

// Do not use React.FC
// See https://github.com/sw-yx/react-typescript-cheatsheet#typing-defaultprops
export const HorizontalMenu = <Item extends BaseMenuItem = BaseMenuItem>({
items,
renderItem,
onClick,
activeId,
id,
}: HorizontalMenuProps<Item>) => (
<HorizontalMenuContainer
id={id}
children={items.map(item =>
renderItem({
item,
id: `${id}-${item.id}`,
active: item.id === activeId,
onClick: onClick.bind(null, item),
})
)}
/>
)
HorizontalMenu.defaultProps = {
renderItem: horizontalMenuRenderItemDefault,
}
33 changes: 33 additions & 0 deletions packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {styled} from '@qiwi/pijma-core'

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

export const HorizontalMenuItem = styled('a', {
shouldForwardProp: prop => prop !== 'active',
})<HorizontalMenuItemProps>(({active, theme}) => ({
cursor: 'pointer',
fontFamily: theme.font.family,
fontWeight: theme.font.weight.normal,
fontSize: '16px',
textDecoration: 'none',
color: theme.color.black,
flexShrink: 0,
flexGrow: 0,
whiteSpace: 'nowrap',
paddingBottom: '4px',
marginRight: '24px',
borderBottomWidth: '4px',
borderBottomStyle: 'solid',
borderBottomColor: active ? theme.color.brand : 'transparent',
'&:hover, &:active, &:focus': {
textDecoration: 'none',
borderBottomColor: theme.color.brand,
},
}))
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 = {
activeId: 't2',
}
}

render() {
return (
<LevelMenu
id="kassa-menus"
activeId={this.state.activeId}
onClick={item => this.setState({activeId: 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

0 comments on commit 5557e16

Please sign in to comment.