From 5557e16871440cd1b065a18d40a4801c9ee64c65 Mon Sep 17 00:00:00 2001 From: Sergey Yuferev Date: Wed, 20 Mar 2019 18:33:05 +0300 Subject: [PATCH 1/4] feat(mobile): mobile menus: horizontal, select, level --- packages/core/src/index.ts | 1 + packages/core/src/menu/MenuLevelBuilder.ts | 66 ++++++++ packages/core/src/menu/index.ts | 1 + .../src/horizontal-menu/HorizontalMenu.md | 47 ++++++ .../src/horizontal-menu/HorizontalMenu.tsx | 91 +++++++++++ .../horizontal-menu/HorizontalMenuItem.tsx | 33 ++++ packages/mobile/src/horizontal-menu/index.ts | 2 + packages/mobile/src/index.ts | 3 + packages/mobile/src/level-menu/LevelMenu.md | 81 ++++++++++ packages/mobile/src/level-menu/LevelMenu.tsx | 58 +++++++ packages/mobile/src/level-menu/index.ts | 1 + packages/mobile/src/select-menu/SelectMenu.md | 42 +++++ .../mobile/src/select-menu/SelectMenu.tsx | 153 ++++++++++++++++++ .../mobile/src/select-menu/SelectMenuItem.tsx | 29 ++++ packages/mobile/src/select-menu/index.ts | 2 + 15 files changed, 610 insertions(+) create mode 100644 packages/core/src/menu/MenuLevelBuilder.ts create mode 100644 packages/core/src/menu/index.ts create mode 100644 packages/mobile/src/horizontal-menu/HorizontalMenu.md create mode 100644 packages/mobile/src/horizontal-menu/HorizontalMenu.tsx create mode 100644 packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx create mode 100644 packages/mobile/src/horizontal-menu/index.ts create mode 100644 packages/mobile/src/level-menu/LevelMenu.md create mode 100644 packages/mobile/src/level-menu/LevelMenu.tsx create mode 100644 packages/mobile/src/level-menu/index.ts create mode 100644 packages/mobile/src/select-menu/SelectMenu.md create mode 100644 packages/mobile/src/select-menu/SelectMenu.tsx create mode 100644 packages/mobile/src/select-menu/SelectMenuItem.tsx create mode 100644 packages/mobile/src/select-menu/index.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 45e42fc05..4adcf6bee 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -40,3 +40,4 @@ export * from './block' export * from './recaptcha' export * from './recaptcha-field' export * from './link' +export * from './menu' diff --git a/packages/core/src/menu/MenuLevelBuilder.ts b/packages/core/src/menu/MenuLevelBuilder.ts new file mode 100644 index 000000000..01da8ab0f --- /dev/null +++ b/packages/core/src/menu/MenuLevelBuilder.ts @@ -0,0 +1,66 @@ +import {ReactNode} from 'react' + +export interface BaseMenuItem { + id: string + title: ReactNode +} + +export type MenuTree = Item & { + sub?: MenuTree[] +} + +export class MenuLevel { + constructor(public items: Item[], public activeId: string) {} +} + +export class MenuLevelBuilder { + constructor(protected items: MenuTree[]) {} + + protected buildLevels( + activeId: string, + items: MenuTree[], + levels: (MenuLevel | undefined)[], + depth = 0, + parentActive = false + ): boolean { + if (levels.length <= depth) levels.push(undefined) + let activeItem: MenuTree | 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 | undefined)[] { + const result: (MenuLevel[] | undefined) = [] + this.buildLevels(activeId, this.items, result) + return result + } +} diff --git a/packages/core/src/menu/index.ts b/packages/core/src/menu/index.ts new file mode 100644 index 000000000..a8db89a5b --- /dev/null +++ b/packages/core/src/menu/index.ts @@ -0,0 +1 @@ +export * from './MenuLevelBuilder' diff --git a/packages/mobile/src/horizontal-menu/HorizontalMenu.md b/packages/mobile/src/horizontal-menu/HorizontalMenu.md new file mode 100644 index 000000000..532cf6887 --- /dev/null +++ b/packages/mobile/src/horizontal-menu/HorizontalMenu.md @@ -0,0 +1,47 @@ +## Horizontal menu + +Горизонтальное меню с прокруткой через touch-события. Используется как первый уровень меню. + +```jsx +class HorizontalMenuDemo extends React.Component { + constructor() { + super() + this.state = { + activeId: 'requests', + } + } + + render() { + return ( + this.setState({activeId: item.id})} + items={[ + { + title: 'Услуги', + id: 'merchants', + }, + { + title: 'Заявки', + id: 'requests', + }, + + { + title: 'Пользователи', + id: 'users', + }, + + { + title: 'Поддержка', + id: 'support', + }, + ]} + /> + ) + } +} +; + + +``` diff --git a/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx b/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx new file mode 100644 index 000000000..ab1fb9bd4 --- /dev/null +++ b/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx @@ -0,0 +1,91 @@ +import {BaseMenuItem, ButtonControl, styled} from '@qiwi/pijma-core' +import React, {ReactNode} from 'react' + +import {HorizontalMenuItem} from './HorizontalMenuItem' + +export interface HorizontalMenuRenderItemProps { + /** + * Html item id + */ + id: string + item: Item + active: boolean + onClick: () => void +} + +const horizontalMenuRenderItemDefault = ({ + item, + active, + id, + onClick, +}: HorizontalMenuRenderItemProps): ReactNode => ( + ( + + )} + /> +) + +// @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 { + /** + * Menu html id and item ids prefix + */ + id: string + /** + * Active menu item id + */ + activeId: string + items: Item[] + onClick: (item: Item) => void + renderItem: (props: HorizontalMenuRenderItemProps) => ReactNode +} + +// Do not use React.FC +// See https://github.com/sw-yx/react-typescript-cheatsheet#typing-defaultprops +export const HorizontalMenu = ({ + items, + renderItem, + onClick, + activeId, + id, +}: HorizontalMenuProps) => ( + + renderItem({ + item, + id: `${id}-${item.id}`, + active: item.id === activeId, + onClick: onClick.bind(null, item), + }) + )} + /> +) +HorizontalMenu.defaultProps = { + renderItem: horizontalMenuRenderItemDefault, +} diff --git a/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx b/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx new file mode 100644 index 000000000..8ae9bf0a6 --- /dev/null +++ b/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx @@ -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', +})(({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, + }, +})) diff --git a/packages/mobile/src/horizontal-menu/index.ts b/packages/mobile/src/horizontal-menu/index.ts new file mode 100644 index 000000000..93fe7d2f6 --- /dev/null +++ b/packages/mobile/src/horizontal-menu/index.ts @@ -0,0 +1,2 @@ +export * from './HorizontalMenu' +export * from './HorizontalMenuItem' diff --git a/packages/mobile/src/index.ts b/packages/mobile/src/index.ts index 5d3772d72..ea193a689 100644 --- a/packages/mobile/src/index.ts +++ b/packages/mobile/src/index.ts @@ -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' diff --git a/packages/mobile/src/level-menu/LevelMenu.md b/packages/mobile/src/level-menu/LevelMenu.md new file mode 100644 index 000000000..92d144213 --- /dev/null +++ b/packages/mobile/src/level-menu/LevelMenu.md @@ -0,0 +1,81 @@ +## Level menu + +Горизонтальное меню + вертикальное меню. + +```jsx +class LevelMenuDemo extends React.Component { + constructor() { + super() + this.state = { + activeId: 't2', + } + } + + render() { + return ( + 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', + }, + ]} + /> + ) + } +} +; + + +``` diff --git a/packages/mobile/src/level-menu/LevelMenu.tsx b/packages/mobile/src/level-menu/LevelMenu.tsx new file mode 100644 index 000000000..972470817 --- /dev/null +++ b/packages/mobile/src/level-menu/LevelMenu.tsx @@ -0,0 +1,58 @@ +import {BaseMenuItem, Box, MenuLevelBuilder} from '@qiwi/pijma-core' +import React, {Component} from 'react' + +import {HorizontalMenu} from '../horizontal-menu' +import {SelectMenu} from '../select-menu' + +export interface LevelMenuProps { + id: string + activeId: string + items: Item[] + onClick: (item: Item) => void +} + +export class LevelMenu extends Component< + LevelMenuProps +> { + render() { + const { + props: {id, onClick, activeId, items}, + } = this + const levels = new MenuLevelBuilder(items).levels(activeId) + const level0 = levels[0] + const level1 = levels[1] + const level2 = levels[2] + return ( +
+ {level0 && ( + + )} + {level1 && ( + + + + )} + {level2 && ( + + + + )} +
+ ) + } +} diff --git a/packages/mobile/src/level-menu/index.ts b/packages/mobile/src/level-menu/index.ts new file mode 100644 index 000000000..2920316e2 --- /dev/null +++ b/packages/mobile/src/level-menu/index.ts @@ -0,0 +1 @@ +export * from './LevelMenu' diff --git a/packages/mobile/src/select-menu/SelectMenu.md b/packages/mobile/src/select-menu/SelectMenu.md new file mode 100644 index 000000000..d3bb0ee23 --- /dev/null +++ b/packages/mobile/src/select-menu/SelectMenu.md @@ -0,0 +1,42 @@ +## Select menu + +Вертикальное меню, похожее на dropdown. Используется как второй уровень. + +```jsx +class SelectMenuDemo extends React.Component { + constructor() { + super() + this.state = { + activeId: 'transactions', + } + } + + render() { + return ( + this.setState({activeId: item.id})} + items={[ + { + title: 'Транзакции и отчеты', + id: 'transactions', + }, + { + title: 'Реестры', + id: 'registry', + }, + + { + title: 'Акты', + id: 'reports', + }, + ]} + /> + ) + } +} +; + + +``` diff --git a/packages/mobile/src/select-menu/SelectMenu.tsx b/packages/mobile/src/select-menu/SelectMenu.tsx new file mode 100644 index 000000000..2e4f48975 --- /dev/null +++ b/packages/mobile/src/select-menu/SelectMenu.tsx @@ -0,0 +1,153 @@ +import { + BaseMenuItem, + ButtonControl, + Card, + Flex, + FlexItem, + Icon, + styled, +} from '@qiwi/pijma-core' +import React, {Component, ReactNode} from 'react' + +import {SelectMenuItem} from './SelectMenuItem' + +export interface SelectMenuRenderItemProps { + /** + * Html item id + */ + id: string + item: Item + onClick: () => void +} + +const selectMenuRenderItemDefault = ({ + item, + id, + onClick, +}: SelectMenuRenderItemProps): ReactNode => ( + ( + + )} + /> +) + +export const SelectMenuActiveItemContainer = styled(Flex)(({theme}) => ({ + fontWeight: theme.font.weight.normal, + fontFamily: theme.font.family, + fontSize: '16px', + cursor: 'pointer', + alignItems: 'center', + justifyContent: 'space-between', + padding: '8px 16px', + whiteSpace: 'nowrap', +})) + +export interface SelectMenuRenderActiveItemProps + extends SelectMenuRenderItemProps { + expanded: boolean +} + +const selectMenuRenderActiveItemDefault = ({ + item, + id, + onClick, + expanded, +}: SelectMenuRenderActiveItemProps) => ( + + + {item.title} + + + + + +) + +// Flex.withComponent('nav') not working +export const SelectMenuActiveItemsContainer = styled('nav')({ + flexDirection: 'column', + display: 'flex', +}) + +export interface SelectMenuProps { + /** + * Menu html id and item ids prefix + */ + id: string + /** + * Active menu item id + */ + activeId: string + items: Item[] + onClick: (item: Item) => void + renderItem: (props: SelectMenuRenderItemProps) => ReactNode + renderActiveItem: (props: SelectMenuRenderActiveItemProps) => ReactNode +} + +export interface SelectMenuState { + expanded: boolean +} + +export class SelectMenu< + Item extends BaseMenuItem = BaseMenuItem +> extends Component, SelectMenuState> { + static defaultProps = { + renderItem: selectMenuRenderItemDefault, + renderActiveItem: selectMenuRenderActiveItemDefault, + } + + state: SelectMenuState = { + expanded: false, + } + + protected toggle(item: Item) { + this.setState({expanded: !this.state.expanded}) + this.props.onClick(item) + } + + render() { + const { + toggle, + state: {expanded}, + props: {items, renderItem, renderActiveItem, activeId, id}, + } = this + const activeItem = items.find(item => item.id === activeId) || items[0] + return ( + + {renderActiveItem({ + id: `${id}-active`, + item: activeItem, + expanded, + onClick: toggle.bind(this, activeItem), + })} + + {expanded && ( + + {items.map(item => + item.id === activeId + ? null + : renderItem({ + item, + id: `${id}-item-${item.id}`, + onClick: toggle.bind(this, item), + }) + )} + + )} + + ) + } +} diff --git a/packages/mobile/src/select-menu/SelectMenuItem.tsx b/packages/mobile/src/select-menu/SelectMenuItem.tsx new file mode 100644 index 000000000..b1f03f90a --- /dev/null +++ b/packages/mobile/src/select-menu/SelectMenuItem.tsx @@ -0,0 +1,29 @@ +import {styled} from '@qiwi/pijma-core' + +export interface SelectMenuItemProps { + id: string + href?: string + target?: string + reverse?: boolean + onClick?: (e: React.MouseEvent) => void +} + +export const SelectMenuItem = styled('a')< + SelectMenuItemProps +>(({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', + lineHeight: '21px', + padding: '8px 16px', + '&:hover, &:active, &:focus': { + textDecoration: 'none', + backgroundColor: theme.color.gray.lightest, + }, +})) diff --git a/packages/mobile/src/select-menu/index.ts b/packages/mobile/src/select-menu/index.ts new file mode 100644 index 000000000..b28d489d8 --- /dev/null +++ b/packages/mobile/src/select-menu/index.ts @@ -0,0 +1,2 @@ +export * from './SelectMenu' +export * from './SelectMenuItem' From 9f938f33265ac57217f20d7fe8b8f0fe9218b4e1 Mon Sep 17 00:00:00 2001 From: Sergey Yuferev Date: Thu, 21 Mar 2019 22:58:22 +0300 Subject: [PATCH 2/4] fix: review fixes, without menu item control integration --- packages/core/src/menu/MenuControl.tsx | 23 +++ packages/core/src/menu/MenuLevelBuilder.ts | 29 ++-- packages/core/src/menu/index.ts | 1 + .../src/horizontal-menu/HorizontalMenu.md | 6 +- .../src/horizontal-menu/HorizontalMenu.tsx | 71 +++------ packages/mobile/src/level-menu/LevelMenu.md | 6 +- packages/mobile/src/level-menu/LevelMenu.tsx | 52 ++++--- packages/mobile/src/select-menu/SelectMenu.md | 6 +- .../mobile/src/select-menu/SelectMenu.tsx | 138 ++++++------------ 9 files changed, 135 insertions(+), 197 deletions(-) create mode 100644 packages/core/src/menu/MenuControl.tsx diff --git a/packages/core/src/menu/MenuControl.tsx b/packages/core/src/menu/MenuControl.tsx new file mode 100644 index 000000000..91e57e037 --- /dev/null +++ b/packages/core/src/menu/MenuControl.tsx @@ -0,0 +1,23 @@ +import {PureComponent} from 'react' + +import RenderChild from '../RenderChild' +import {MenuLevel, BaseMenuItem, MenuLevelBuilder} from './MenuLevelBuilder' + +export interface MenuControlProps { + items: Item[] + selected: string + children: RenderChild<{ + levels: MenuLevel[] + }> +} + +export class MenuControl extends PureComponent< + MenuControlProps +> { + render() { + const {props} = this + return props.children({ + levels: new MenuLevelBuilder(props.items).levels(props.selected), + }) + } +} diff --git a/packages/core/src/menu/MenuLevelBuilder.ts b/packages/core/src/menu/MenuLevelBuilder.ts index 01da8ab0f..c81a4a96f 100644 --- a/packages/core/src/menu/MenuLevelBuilder.ts +++ b/packages/core/src/menu/MenuLevelBuilder.ts @@ -9,29 +9,30 @@ export type MenuTree = Item & { sub?: MenuTree[] } -export class MenuLevel { - constructor(public items: Item[], public activeId: string) {} -} +export type MenuLevel = { + items: Item[] + selected: string +} | undefined export class MenuLevelBuilder { constructor(protected items: MenuTree[]) {} protected buildLevels( - activeId: string, + selected: string, items: MenuTree[], - levels: (MenuLevel | undefined)[], + levels: MenuLevel[], depth = 0, parentActive = false ): boolean { if (levels.length <= depth) levels.push(undefined) let activeItem: MenuTree | undefined for (let item of items) { - const currentActive = item.id === activeId + const currentActive = item.id === selected const sub = item.sub const childActive = sub && sub.length > 0 ? this.buildLevels( - activeId, + selected, sub, levels, depth + 1, @@ -43,10 +44,10 @@ export class MenuLevelBuilder { } if (activeItem || parentActive) { - levels[depth] = new MenuLevel( + levels[depth] = { items, - activeItem ? activeItem.id : items[0].id - ) + selected: activeItem ? activeItem.id : items[0].id + } } return !!activeItem @@ -56,11 +57,11 @@ export class MenuLevelBuilder { * Split MenuTree structure to levels. * Returns array, where index is a menu level. * - * @param activeId current menu item id + * @param selected current menu item id */ - levels(activeId: string): (MenuLevel | undefined)[] { - const result: (MenuLevel[] | undefined) = [] - this.buildLevels(activeId, this.items, result) + levels(selected: string): MenuLevel[] { + const result: MenuLevel[] = [] + this.buildLevels(selected, this.items, result) return result } } diff --git a/packages/core/src/menu/index.ts b/packages/core/src/menu/index.ts index a8db89a5b..c05dea70d 100644 --- a/packages/core/src/menu/index.ts +++ b/packages/core/src/menu/index.ts @@ -1 +1,2 @@ +export * from './MenuControl' export * from './MenuLevelBuilder' diff --git a/packages/mobile/src/horizontal-menu/HorizontalMenu.md b/packages/mobile/src/horizontal-menu/HorizontalMenu.md index 532cf6887..2a0be62db 100644 --- a/packages/mobile/src/horizontal-menu/HorizontalMenu.md +++ b/packages/mobile/src/horizontal-menu/HorizontalMenu.md @@ -7,7 +7,7 @@ class HorizontalMenuDemo extends React.Component { constructor() { super() this.state = { - activeId: 'requests', + selected: 'requests', } } @@ -15,8 +15,8 @@ class HorizontalMenuDemo extends React.Component { return ( this.setState({activeId: item.id})} + selected={this.state.selected} + onSelect={item => this.setState({selected: item.id})} items={[ { title: 'Услуги', diff --git a/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx b/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx index ab1fb9bd4..387dc5fb4 100644 --- a/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx +++ b/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx @@ -1,38 +1,8 @@ import {BaseMenuItem, ButtonControl, styled} from '@qiwi/pijma-core' -import React, {ReactNode} from 'react' +import React from 'react' import {HorizontalMenuItem} from './HorizontalMenuItem' -export interface HorizontalMenuRenderItemProps { - /** - * Html item id - */ - id: string - item: Item - active: boolean - onClick: () => void -} - -const horizontalMenuRenderItemDefault = ({ - item, - active, - id, - onClick, -}: HorizontalMenuRenderItemProps): ReactNode => ( - ( - - )} - /> -) - // @see https://iamsteve.me/blog/entry/using-flexbox-for-horizontal-scrolling-navigation export const HorizontalMenuContainer = styled('nav')(({theme}) => ({ display: 'flex', @@ -59,33 +29,32 @@ export interface HorizontalMenuProps { /** * Active menu item id */ - activeId: string + selected: string items: Item[] - onClick: (item: Item) => void - renderItem: (props: HorizontalMenuRenderItemProps) => ReactNode + onSelect: (item: Item) => void } -// Do not use React.FC -// See https://github.com/sw-yx/react-typescript-cheatsheet#typing-defaultprops -export const HorizontalMenu = ({ +export const HorizontalMenu = ({ items, - renderItem, - onClick, - activeId, + onSelect, + selected, id, }: HorizontalMenuProps) => ( - renderItem({ - item, - id: `${id}-${item.id}`, - active: item.id === activeId, - onClick: onClick.bind(null, item), - }) - )} + children={items.map(item => ( + ( + + )} + /> + ))} /> ) -HorizontalMenu.defaultProps = { - renderItem: horizontalMenuRenderItemDefault, -} diff --git a/packages/mobile/src/level-menu/LevelMenu.md b/packages/mobile/src/level-menu/LevelMenu.md index 92d144213..f88556f0c 100644 --- a/packages/mobile/src/level-menu/LevelMenu.md +++ b/packages/mobile/src/level-menu/LevelMenu.md @@ -7,7 +7,7 @@ class LevelMenuDemo extends React.Component { constructor() { super() this.state = { - activeId: 't2', + selected: 't2', } } @@ -15,8 +15,8 @@ class LevelMenuDemo extends React.Component { return ( this.setState({activeId: item.id})} + selected={this.state.selected} + onSelect={item => this.setState({selected: item.id})} items={[ { title: 'Услуги', diff --git a/packages/mobile/src/level-menu/LevelMenu.tsx b/packages/mobile/src/level-menu/LevelMenu.tsx index 972470817..410b15de3 100644 --- a/packages/mobile/src/level-menu/LevelMenu.tsx +++ b/packages/mobile/src/level-menu/LevelMenu.tsx @@ -1,35 +1,33 @@ -import {BaseMenuItem, Box, MenuLevelBuilder} from '@qiwi/pijma-core' -import React, {Component} from 'react' +import {BaseMenuItem, Box, MenuControl} from '@qiwi/pijma-core' +import React, {Fragment} from 'react' import {HorizontalMenu} from '../horizontal-menu' import {SelectMenu} from '../select-menu' export interface LevelMenuProps { id: string - activeId: string + selected: string items: Item[] - onClick: (item: Item) => void + onSelect: (item: Item) => void } -export class LevelMenu extends Component< - LevelMenuProps -> { - render() { - const { - props: {id, onClick, activeId, items}, - } = this - const levels = new MenuLevelBuilder(items).levels(activeId) - const level0 = levels[0] - const level1 = levels[1] - const level2 = levels[2] - return ( -
+export const LevelMenu = ({ + id, + onSelect, + selected, + items, +}: LevelMenuProps) => ( + ( + {level0 && ( )} {level1 && ( @@ -37,8 +35,8 @@ export class LevelMenu extends Component< )} @@ -47,12 +45,12 @@ export class LevelMenu extends Component< )} -
- ) - } -} + + )} + /> +) diff --git a/packages/mobile/src/select-menu/SelectMenu.md b/packages/mobile/src/select-menu/SelectMenu.md index d3bb0ee23..de658b363 100644 --- a/packages/mobile/src/select-menu/SelectMenu.md +++ b/packages/mobile/src/select-menu/SelectMenu.md @@ -7,7 +7,7 @@ class SelectMenuDemo extends React.Component { constructor() { super() this.state = { - activeId: 'transactions', + selected: 'transactions', } } @@ -15,8 +15,8 @@ class SelectMenuDemo extends React.Component { return ( this.setState({activeId: item.id})} + selected={this.state.selected} + onSelect={item => this.setState({selected: item.id})} items={[ { title: 'Транзакции и отчеты', diff --git a/packages/mobile/src/select-menu/SelectMenu.tsx b/packages/mobile/src/select-menu/SelectMenu.tsx index 2e4f48975..cead2914f 100644 --- a/packages/mobile/src/select-menu/SelectMenu.tsx +++ b/packages/mobile/src/select-menu/SelectMenu.tsx @@ -5,76 +5,11 @@ import { Flex, FlexItem, Icon, - styled, } from '@qiwi/pijma-core' -import React, {Component, ReactNode} from 'react' +import React, {Component} from 'react' import {SelectMenuItem} from './SelectMenuItem' - -export interface SelectMenuRenderItemProps { - /** - * Html item id - */ - id: string - item: Item - onClick: () => void -} - -const selectMenuRenderItemDefault = ({ - item, - id, - onClick, -}: SelectMenuRenderItemProps): ReactNode => ( - ( - - )} - /> -) - -export const SelectMenuActiveItemContainer = styled(Flex)(({theme}) => ({ - fontWeight: theme.font.weight.normal, - fontFamily: theme.font.family, - fontSize: '16px', - cursor: 'pointer', - alignItems: 'center', - justifyContent: 'space-between', - padding: '8px 16px', - whiteSpace: 'nowrap', -})) - -export interface SelectMenuRenderActiveItemProps - extends SelectMenuRenderItemProps { - expanded: boolean -} - -const selectMenuRenderActiveItemDefault = ({ - item, - id, - onClick, - expanded, -}: SelectMenuRenderActiveItemProps) => ( - - - {item.title} - - - - - -) - -// Flex.withComponent('nav') not working -export const SelectMenuActiveItemsContainer = styled('nav')({ - flexDirection: 'column', - display: 'flex', -}) +import {Text} from '../typography' export interface SelectMenuProps { /** @@ -84,11 +19,9 @@ export interface SelectMenuProps { /** * Active menu item id */ - activeId: string + selected: string items: Item[] - onClick: (item: Item) => void - renderItem: (props: SelectMenuRenderItemProps) => ReactNode - renderActiveItem: (props: SelectMenuRenderActiveItemProps) => ReactNode + onSelect: (item: Item) => void } export interface SelectMenuState { @@ -98,54 +31,67 @@ export interface SelectMenuState { export class SelectMenu< Item extends BaseMenuItem = BaseMenuItem > extends Component, SelectMenuState> { - static defaultProps = { - renderItem: selectMenuRenderItemDefault, - renderActiveItem: selectMenuRenderActiveItemDefault, - } - state: SelectMenuState = { expanded: false, } protected toggle(item: Item) { this.setState({expanded: !this.state.expanded}) - this.props.onClick(item) + this.props.onSelect(item) } render() { const { toggle, state: {expanded}, - props: {items, renderItem, renderActiveItem, activeId, id}, + props: {items, selected, id}, } = this - const activeItem = items.find(item => item.id === activeId) || items[0] + const activeItem = items.find(item => item.id === selected) || items[0] return ( - {renderActiveItem({ - id: `${id}-active`, - item: activeItem, - expanded, - onClick: toggle.bind(this, activeItem), - })} + + + + + + + + {expanded && ( - - {items.map(item => - item.id === activeId - ? null - : renderItem({ - item, - id: `${id}-item-${item.id}`, - onClick: toggle.bind(this, item), - }) + + item.id === selected ? null : ( + ( + + )} + /> + ) )} - + /> )} ) From 19b425deaf47e5a179255f140411598b81b4b974 Mon Sep 17 00:00:00 2001 From: Sergey Yuferev Date: Tue, 16 Apr 2019 18:58:01 +0300 Subject: [PATCH 3/4] fix: style fix, rewrite menu items with LinkControl --- packages/core/src/menu/MenuControl.tsx | 2 + packages/core/src/menu/MenuLevelBuilder.ts | 8 ++- .../src/horizontal-menu/HorizontalMenu.tsx | 37 +++++----- .../horizontal-menu/HorizontalMenuItem.tsx | 67 ++++++++++++------- .../mobile/src/select-menu/SelectMenu.tsx | 24 ++----- .../mobile/src/select-menu/SelectMenuItem.tsx | 61 +++++++++++------ 6 files changed, 114 insertions(+), 85 deletions(-) diff --git a/packages/core/src/menu/MenuControl.tsx b/packages/core/src/menu/MenuControl.tsx index 91e57e037..310e4ed8a 100644 --- a/packages/core/src/menu/MenuControl.tsx +++ b/packages/core/src/menu/MenuControl.tsx @@ -14,10 +14,12 @@ export interface MenuControlProps { export class MenuControl extends PureComponent< MenuControlProps > { + render() { const {props} = this return props.children({ levels: new MenuLevelBuilder(props.items).levels(props.selected), }) } + } diff --git a/packages/core/src/menu/MenuLevelBuilder.ts b/packages/core/src/menu/MenuLevelBuilder.ts index c81a4a96f..130d487a5 100644 --- a/packages/core/src/menu/MenuLevelBuilder.ts +++ b/packages/core/src/menu/MenuLevelBuilder.ts @@ -15,6 +15,7 @@ export type MenuLevel = { } | undefined export class MenuLevelBuilder { + constructor(protected items: MenuTree[]) {} protected buildLevels( @@ -22,7 +23,7 @@ export class MenuLevelBuilder { items: MenuTree[], levels: MenuLevel[], depth = 0, - parentActive = false + parentActive = false, ): boolean { if (levels.length <= depth) levels.push(undefined) let activeItem: MenuTree | undefined @@ -36,7 +37,7 @@ export class MenuLevelBuilder { sub, levels, depth + 1, - currentActive || parentActive + currentActive || parentActive, ) : false if (currentActive) activeItem = item @@ -46,7 +47,7 @@ export class MenuLevelBuilder { if (activeItem || parentActive) { levels[depth] = { items, - selected: activeItem ? activeItem.id : items[0].id + selected: activeItem ? activeItem.id : items[0].id, } } @@ -64,4 +65,5 @@ export class MenuLevelBuilder { this.buildLevels(selected, this.items, result) return result } + } diff --git a/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx b/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx index 387dc5fb4..a83669efb 100644 --- a/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx +++ b/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx @@ -1,25 +1,17 @@ -import {BaseMenuItem, ButtonControl, styled} from '@qiwi/pijma-core' +import {BaseMenuItem, styled, Flex, Card} from '@qiwi/pijma-core' import React from 'react' import {HorizontalMenuItem} from './HorizontalMenuItem' // @see https://iamsteve.me/blog/entry/using-flexbox-for-horizontal-scrolling-navigation -export const HorizontalMenuContainer = styled('nav')(({theme}) => ({ - display: 'flex', - flexWrap: 'nowrap', +const ScrollableFlex = styled(Flex)({ 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 { /** @@ -40,21 +32,26 @@ export const HorizontalMenu = ({ selected, id, }: HorizontalMenuProps) => ( - ( - ( + bb="1px solid #e6e6e6" + children={ + ( - )} + ))} /> - ))} + } /> ) diff --git a/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx b/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx index 8ae9bf0a6..6bf48fd80 100644 --- a/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx +++ b/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx @@ -1,4 +1,5 @@ -import {styled} from '@qiwi/pijma-core' +import {Card, LinkControl, Lnk, Typo} from '@qiwi/pijma-core' +import React from 'react' export interface HorizontalMenuItemProps { active: boolean @@ -6,28 +7,46 @@ export interface HorizontalMenuItemProps { href?: string target?: string reverse?: boolean - onClick?: (e: React.MouseEvent) => void + isLast?: boolean + onClick?: () => void } -export const HorizontalMenuItem = styled('a', { - shouldForwardProp: prop => prop !== 'active', -})(({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, - }, -})) +const CardLink = Card.withComponent(Lnk) + +export const HorizontalMenuItem: React.FC = props => ( + ( + + } + /> + )} + /> +) diff --git a/packages/mobile/src/select-menu/SelectMenu.tsx b/packages/mobile/src/select-menu/SelectMenu.tsx index cead2914f..01364d27c 100644 --- a/packages/mobile/src/select-menu/SelectMenu.tsx +++ b/packages/mobile/src/select-menu/SelectMenu.tsx @@ -1,11 +1,4 @@ -import { - BaseMenuItem, - ButtonControl, - Card, - Flex, - FlexItem, - Icon, -} from '@qiwi/pijma-core' +import {BaseMenuItem, Card, Flex, FlexItem, Icon} from '@qiwi/pijma-core' import React, {Component} from 'react' import {SelectMenuItem} from './SelectMenuItem' @@ -31,6 +24,7 @@ export interface SelectMenuState { export class SelectMenu< Item extends BaseMenuItem = BaseMenuItem > extends Component, SelectMenuState> { + state: SelectMenuState = { expanded: false, } @@ -78,22 +72,18 @@ export class SelectMenu< direction="column" children={items.map(item => item.id === selected ? null : ( - ( - - )} + children={item.title} /> - ) + ), )} /> )} ) } + } diff --git a/packages/mobile/src/select-menu/SelectMenuItem.tsx b/packages/mobile/src/select-menu/SelectMenuItem.tsx index b1f03f90a..db6773f0c 100644 --- a/packages/mobile/src/select-menu/SelectMenuItem.tsx +++ b/packages/mobile/src/select-menu/SelectMenuItem.tsx @@ -1,29 +1,48 @@ -import {styled} from '@qiwi/pijma-core' +import {Card, LinkControl, Lnk, Typo} from '@qiwi/pijma-core' +import React from 'react' export interface SelectMenuItemProps { id: string href?: string target?: string reverse?: boolean - onClick?: (e: React.MouseEvent) => void + onClick?: () => void } -export const SelectMenuItem = styled('a')< - SelectMenuItemProps ->(({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', - lineHeight: '21px', - padding: '8px 16px', - '&:hover, &:active, &:focus': { - textDecoration: 'none', - backgroundColor: theme.color.gray.lightest, - }, -})) +const CardLink = Card.withComponent(Lnk) + +export const SelectMenuItem: React.FC = props => ( + ( + + } + /> + )} + /> +) From 4c73ad28dfd88fbdf11719bab02280924f899aa5 Mon Sep 17 00:00:00 2001 From: Sergey Yuferev Date: Mon, 22 Apr 2019 16:26:36 +0300 Subject: [PATCH 4/4] fix(mobile): menu review 2, https://github.com/qiwi/pijma/pull/129 --- .../src/horizontal-menu/HorizontalMenu.tsx | 19 ++++++++++++------- .../horizontal-menu/HorizontalMenuItem.tsx | 19 +++++-------------- .../mobile/src/select-menu/SelectMenuItem.tsx | 17 +++++------------ 3 files changed, 22 insertions(+), 33 deletions(-) diff --git a/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx b/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx index a83669efb..3becf551f 100644 --- a/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx +++ b/packages/mobile/src/horizontal-menu/HorizontalMenu.tsx @@ -1,4 +1,4 @@ -import {BaseMenuItem, styled, Flex, Card} from '@qiwi/pijma-core' +import {BaseMenuItem, styled, Flex, Card, Box} from '@qiwi/pijma-core' import React from 'react' import {HorizontalMenuItem} from './HorizontalMenuItem' @@ -42,13 +42,18 @@ export const HorizontalMenu = ({ wrap="nowrap" px={4} children={items.map((item, index) => ( - + } /> ))} /> diff --git a/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx b/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx index 6bf48fd80..f48762bff 100644 --- a/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx +++ b/packages/mobile/src/horizontal-menu/HorizontalMenuItem.tsx @@ -1,13 +1,14 @@ -import {Card, LinkControl, Lnk, Typo} from '@qiwi/pijma-core' +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 - isLast?: boolean onClick?: () => void } @@ -30,22 +31,12 @@ export const HorizontalMenuItem: React.FC = props => ( onMouseUp={renderProps.onMouseUp} onMouseDown={renderProps.onMouseDown} cursor="pointer" - mr={props.isLast ? 0 : 6} + display="block" pb={1} bb={`4px solid ${ renderProps.hover || props.active ? '#ff8c00' : 'transparent' }`} - children={ - - } + children={} /> )} /> diff --git a/packages/mobile/src/select-menu/SelectMenuItem.tsx b/packages/mobile/src/select-menu/SelectMenuItem.tsx index db6773f0c..cf1b66bf3 100644 --- a/packages/mobile/src/select-menu/SelectMenuItem.tsx +++ b/packages/mobile/src/select-menu/SelectMenuItem.tsx @@ -1,6 +1,8 @@ -import {Card, LinkControl, Lnk, Typo} from '@qiwi/pijma-core' +import {Card, LinkControl, Lnk} from '@qiwi/pijma-core' import React from 'react' +import {Text} from '../typography' + export interface SelectMenuItemProps { id: string href?: string @@ -28,20 +30,11 @@ export const SelectMenuItem: React.FC = props => ( onMouseUp={renderProps.onMouseUp} onMouseDown={renderProps.onMouseDown} cursor="pointer" + display="block" px={4} py={2} bg={`${renderProps.hover ? '#f5f5f5' : 'transparent'}`} - children={ - - } + children={} /> )} />