From 6d45b6f5b69a7401aebcd34bac91ff91ada3a890 Mon Sep 17 00:00:00 2001 From: Yauhen Penkin Date: Mon, 11 Mar 2019 17:57:05 +0300 Subject: [PATCH 1/6] feat(ui): overflow-menu component add --- src/framework/ui/index.ts | 16 +++ .../overflowMenu/overflowMenu.component.tsx | 134 ++++++++++++++++++ .../overflowMenuItem.component.tsx | 118 +++++++++++++++ src/playground/src/ui/screen/index.ts | 1 + .../src/ui/screen/overflowMenu.component.tsx | 108 ++++++++++++++ 5 files changed, 377 insertions(+) create mode 100644 src/framework/ui/overflowMenu/overflowMenu.component.tsx create mode 100644 src/framework/ui/overflowMenu/overflowMenuItem.component.tsx create mode 100644 src/playground/src/ui/screen/overflowMenu.component.tsx diff --git a/src/framework/ui/index.ts b/src/framework/ui/index.ts index 09d98978a..64a838774 100644 --- a/src/framework/ui/index.ts +++ b/src/framework/ui/index.ts @@ -83,6 +83,15 @@ import { Placement as PopoverPlacement, Placements as PopoverPlacements, } from './popover/type'; +import { + OverflowMenuItem as OverflowMenuItemComponent, + Props as OverflowMenuItemProps, + OverflowMenuItemType, +} from './overflowMenu/overflowMenuItem.component'; +import { + OverflowMenu as OverflowMenuComponent, + Props as OverflowMenuProps, +} from './overflowMenu/overflowMenu.component'; const Button = styled(ButtonComponent); const ButtonGroup = styled(ButtonGroupComponent); @@ -102,6 +111,8 @@ const TopNavigationBar = styled(TopNavigationBarActionComponent); const Modal = styled(ModalComponent); +const OverflowMenuItem = styled(OverflowMenuItemComponent); +const OverflowMenu = styled(OverflowMenuComponent); export { Button, @@ -145,4 +156,9 @@ export { ButtonAlignments, PopoverPlacement, PopoverPlacements, + OverflowMenuItem, + OverflowMenuItemProps, + OverflowMenuItemType, + OverflowMenu, + OverflowMenuProps, }; diff --git a/src/framework/ui/overflowMenu/overflowMenu.component.tsx b/src/framework/ui/overflowMenu/overflowMenu.component.tsx new file mode 100644 index 000000000..0edb5f61d --- /dev/null +++ b/src/framework/ui/overflowMenu/overflowMenu.component.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { + TouchableOpacity, + TouchableOpacityProps, + GestureResponderEvent, + StyleSheet, + ImageProps, + ViewProps, + View, +} from 'react-native'; +import { + StyledComponentProps, + StyleType, + Interaction, + styled, +} from '@kitten/theme'; +import { + OverflowMenuItemType, + OverflowMenuItem as OverflowMenuItemComponent, + Props as OverflowMenuItemProps, +} from './overflowMenuItem.component'; +import { + Popover as PopoverComponent, + Props as PopoverProps, +} from '../popover/popover.component'; +import { Omit } from '../service/type'; + +type MenuItemElement = React.ReactElement; +export type MenuItemType = Omit; + +interface OverflowMenuProps { + children: React.ReactElement; + items: MenuItemType[]; + size?: string; + onSelect?: (index: number) => void; +} + +const Popover = styled(PopoverComponent); +const OverflowMenuItem = + styled(OverflowMenuItemComponent); + +export type Props = & StyledComponentProps & OverflowMenuProps & Omit; + +export class OverflowMenu extends React.Component { + + static defaultProps: Partial = { + size: 'medium', + }; + + private onSelect = (index: number): void => { + if (this.props.onSelect) { + this.props.onSelect(index); + } + }; + + private isFirstItem = (index: number): boolean => { + return index === 0; + }; + + private isLastItem = (index: number): boolean => { + return this.props.items.length - 1 === index; + }; + + private isSingleItem = (): boolean => { + return this.props.items.length === 1; + }; + + private getMenuItemStyle = (style: StyleType, index: number): StyleType => { + const borderRadius: number = style.menuItem.borderRadius; + + if (this.isFirstItem(index)) { + return { + borderTopLeftRadius: borderRadius, + borderTopRightRadius: borderRadius, + }; + } else if (this.isLastItem(index)) { + return { + borderBottomLeftRadius: borderRadius, + borderBottomRightRadius: borderRadius, + }; + } else if (this.isSingleItem()) { + return { + borderRadius: borderRadius, + }; + } else { + return null; + } + }; + + private renderMenuItem = (item: MenuItemType, index: number): MenuItemElement => { + const { size, themedStyle } = this.props; + const itemStyle: StyleType = this.getMenuItemStyle(themedStyle, index); + + return ( + this.onSelect(index)} + /> + ); + }; + + private renderMenuContent = (): React.ReactElement => { + const { items, style } = this.props; + const menuItems: MenuItemElement[] = items + .map((item: MenuItemType, index: number) => this.renderMenuItem(item, index)); + + return ( + + {menuItems} + + ); + }; + + public render(): React.ReactNode { + const { children, themedStyle, ...restProps } = this.props; + const menu: React.ReactElement = this.renderMenuContent(); + + return ( + + {children} + + ); + } +} + +const styles = StyleSheet.create({}); diff --git a/src/framework/ui/overflowMenu/overflowMenuItem.component.tsx b/src/framework/ui/overflowMenu/overflowMenuItem.component.tsx new file mode 100644 index 000000000..90a4d1ae6 --- /dev/null +++ b/src/framework/ui/overflowMenu/overflowMenuItem.component.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { + TouchableOpacity, + TouchableOpacityProps, + GestureResponderEvent, + StyleSheet, + ImageProps, +} from 'react-native'; +import { + StyledComponentProps, + StyleType, + Interaction, + styled, +} from '@kitten/theme'; +import { + Text as TextComponent, + Props as TextProps, +} from '../text/text.component'; + +export interface OverflowMenuItemType { + icon?: (style: StyleType) => React.ReactElement; + text: React.ReactText; + size?: string; + isLastItem?: boolean; + onPress?: () => void; +} + +const Text = styled(TextComponent); + +export type Props = OverflowMenuItemType & StyledComponentProps & TouchableOpacityProps; + +export class OverflowMenuItem extends React.Component { + + static defaultProps: Partial = { + isLastItem: false, + }; + + private onPress = (event: GestureResponderEvent) => { + if (this.props.onPress) { + this.props.onPress(event); + } + }; + + private onPressIn = (event: GestureResponderEvent) => { + this.props.dispatch([Interaction.ACTIVE]); + + if (this.props.onPressIn) { + this.props.onPressIn(event); + } + }; + + private onPressOut = (event: GestureResponderEvent) => { + this.props.dispatch([]); + + if (this.props.onPressOut) { + this.props.onPressOut(event); + } + }; + + private getComponentStyle = (style: StyleType): StyleType => { + const { isLastItem } = this.props; + const { text, icon, ...container } = style; + const { borderColor, borderWidth, ...restContainerProps } = container; + + return { + container: { + borderBottomColor: !isLastItem ? borderColor : null, + borderBottomWidth: !isLastItem ? borderWidth : null, + ...restContainerProps, + }, + text: text, + icon: icon, + }; + }; + + private renderTextElement = (style: StyleType): React.ReactElement => ( + + {this.props.text} + + ); + + private renderImageElement = (style: StyleType): React.ReactElement | null => { + const { icon } = this.props; + return icon ? React.cloneElement(icon(style), { key: 1 }) : null; + }; + + private renderComponentChildren = (style: StyleType): React.ReactNode => ([ + this.props.icon ? this.renderImageElement(style.icon) : null, + this.renderTextElement(style.text), + ]); + + public render(): React.ReactNode { + const { style, themedStyle, ...restProps } = this.props; + const { container, ...componentStyles } = this.getComponentStyle(themedStyle); + const componentChildren: React.ReactNode = this.renderComponentChildren(componentStyles); + + return ( + + {componentChildren} + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + }, +}); diff --git a/src/playground/src/ui/screen/index.ts b/src/playground/src/ui/screen/index.ts index 7e4eb9e36..4f8457c80 100644 --- a/src/playground/src/ui/screen/index.ts +++ b/src/playground/src/ui/screen/index.ts @@ -16,3 +16,4 @@ export { TooltipScreen } from './tooltip.component'; export { TabNavigatorScreen } from './tabNavigator.component'; export { TopNavigationBarScreen } from './topNavigationBar.component'; export { ModalScreen } from './modal.component'; +export { OverflowMenuScreen } from './overflowMenu.component'; diff --git a/src/playground/src/ui/screen/overflowMenu.component.tsx b/src/playground/src/ui/screen/overflowMenu.component.tsx new file mode 100644 index 000000000..a36a9a5b6 --- /dev/null +++ b/src/playground/src/ui/screen/overflowMenu.component.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { + ImageSourcePropType, + Text, + View, + Image, + TouchableOpacity, +} from 'react-native'; +import { NavigationScreenProps } from 'react-navigation'; +import { + withStyles, + ThemeType, + ThemedComponentProps, + StyleType, +} from '@kitten/theme'; +import { + OverflowMenu as OverflowMenuComponent, + OverflowMenuItemType, +} from '@kitten/ui'; + +type Props = & ThemedComponentProps & NavigationScreenProps; + +interface State { + overflowMenu1Visible: boolean; + overflowMenu2Visible: boolean; + overflowMenu3Visible: boolean; +} + +const iconUri1: string = 'https://akveo.github.io/eva-icons/fill/png/128/star.png'; +const iconUri2: string = 'https://akveo.github.io/eva-icons/fill/png/128/alert-triangle.png'; +const iconUri3: string = 'https://akveo.github.io/eva-icons/fill/png/128/book-open.png'; +const menuIconUri: string = 'https://akveo.github.io/eva-icons/fill/png/128/menu.png'; + +const menu1Items: OverflowMenuItemType[] = [ + { + text: 'Menu Item 1', + icon: (style: StyleType) => , + }, + { + text: 'Menu Item 2', + icon: (style: StyleType) => , + }, + { + text: 'Menu Item 3', + icon: (style: StyleType) => , + }, +]; + +class OverflowMenu extends React.Component { + + + static navigationOptions = { + title: 'Overflow Menu', + }; + + public state: State = { + overflowMenu1Visible: false, + overflowMenu2Visible: false, + overflowMenu3Visible: false, + }; + + private setMenu1Visible = (): void => { + this.setState({ overflowMenu1Visible: !this.state.overflowMenu1Visible }); + }; + + private onSelectItem = (index: number): void => { + console.log('Selected item\'s index: ', index); + }; + + public render(): React.ReactNode { + return ( + + + + + this.onSelectItem(index)} + onRequestClose={this.setMenu1Visible} + > + + + + + + ); + } +} + +export const OverflowMenuScreen = withStyles(OverflowMenu, (theme: ThemeType) => ({ + container: { + paddingVertical: 8, + paddingHorizontal: 16, + }, + menuIcon: { + width: 30, + height: 30, + tintColor: '#3366FF', + }, + test: { + width: 100, + height: 100, + backgroundColor: 'yellow', + }, +})); + From ebc4a983c5c6ed0b165cb8783c63dcbce5abdab3 Mon Sep 17 00:00:00 2001 From: Yauhen Penkin Date: Tue, 12 Mar 2019 15:37:05 +0300 Subject: [PATCH 2/6] refactor(ui): disabled state and popover style for overflow-menu refactor --- .../overflowMenu/overflowMenu.component.tsx | 20 +++++++------- .../overflowMenuItem.component.tsx | 1 + .../src/ui/screen/overflowMenu.component.tsx | 27 ++++++++++++------- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/framework/ui/overflowMenu/overflowMenu.component.tsx b/src/framework/ui/overflowMenu/overflowMenu.component.tsx index 0edb5f61d..853519ab9 100644 --- a/src/framework/ui/overflowMenu/overflowMenu.component.tsx +++ b/src/framework/ui/overflowMenu/overflowMenu.component.tsx @@ -1,17 +1,12 @@ import React from 'react'; import { - TouchableOpacity, - TouchableOpacityProps, - GestureResponderEvent, StyleSheet, - ImageProps, ViewProps, View, } from 'react-native'; import { StyledComponentProps, StyleType, - Interaction, styled, } from '@kitten/theme'; import { @@ -65,15 +60,20 @@ export class OverflowMenu extends React.Component { return this.props.items.length === 1; }; + private getPopoverStyle = (style: StyleType): StyleType => ({ + ...style.popover, + borderRadius: style.borderRadius, + }); + private getMenuItemStyle = (style: StyleType, index: number): StyleType => { const borderRadius: number = style.menuItem.borderRadius; - if (this.isFirstItem(index)) { + if (this.isFirstItem(index) && !this.isSingleItem()) { return { borderTopLeftRadius: borderRadius, borderTopRightRadius: borderRadius, }; - } else if (this.isLastItem(index)) { + } else if (this.isLastItem(index) && !this.isSingleItem()) { return { borderBottomLeftRadius: borderRadius, borderBottomRightRadius: borderRadius, @@ -93,8 +93,7 @@ export class OverflowMenu extends React.Component { return ( { public render(): React.ReactNode { const { children, themedStyle, ...restProps } = this.props; const menu: React.ReactElement = this.renderMenuContent(); + const popoverStyle: StyleType = this.getPopoverStyle(themedStyle); return ( + style={[popoverStyle, restProps.style]}> {children} ); diff --git a/src/framework/ui/overflowMenu/overflowMenuItem.component.tsx b/src/framework/ui/overflowMenu/overflowMenuItem.component.tsx index 90a4d1ae6..b0687e9e8 100644 --- a/src/framework/ui/overflowMenu/overflowMenuItem.component.tsx +++ b/src/framework/ui/overflowMenu/overflowMenuItem.component.tsx @@ -22,6 +22,7 @@ export interface OverflowMenuItemType { text: React.ReactText; size?: string; isLastItem?: boolean; + disabled?: boolean; onPress?: () => void; } diff --git a/src/playground/src/ui/screen/overflowMenu.component.tsx b/src/playground/src/ui/screen/overflowMenu.component.tsx index a36a9a5b6..2190f30cd 100644 --- a/src/playground/src/ui/screen/overflowMenu.component.tsx +++ b/src/playground/src/ui/screen/overflowMenu.component.tsx @@ -5,6 +5,7 @@ import { View, Image, TouchableOpacity, + Button, } from 'react-native'; import { NavigationScreenProps } from 'react-navigation'; import { @@ -39,9 +40,13 @@ const menu1Items: OverflowMenuItemType[] = [ { text: 'Menu Item 2', icon: (style: StyleType) => , + disabled: true, }, { text: 'Menu Item 3', + }, + { + text: 'Menu Item 4', icon: (style: StyleType) => , }, ]; @@ -70,17 +75,18 @@ class OverflowMenu extends React.Component { public render(): React.ReactNode { return ( - - - +