From 1ea28bd0e1cc60f1d656e57005bb5fd1f3647e6d Mon Sep 17 00:00:00 2001 From: Artur Yorsh <10753921+artyorsh@users.noreply.github.com> Date: Tue, 26 Feb 2019 15:41:38 +0300 Subject: [PATCH] feat(ui): button component. Closes #260 (#268) * fix(playground): bundler and compiler issues * feat(ui): button component * test(ui): add button component tests * feat(ui): button - add ability to pass alignment as object --- src/framework/ui/button/button.component.tsx | 145 +++++ src/framework/ui/button/button.spec.config.ts | 508 ++++++++++++++++++ src/framework/ui/button/button.spec.tsx | 104 ++++ src/framework/ui/button/button.spec.tsx.snap | 290 ++++++++++ src/framework/ui/button/type.ts | 50 ++ src/framework/ui/index.ts | 13 + src/playground/metro.config.js | 5 +- .../src/ui/screen/button.component.tsx | 208 +++++++ .../src/ui/screen/dialog.component.tsx | 7 +- src/playground/src/ui/screen/index.ts | 1 + 10 files changed, 1326 insertions(+), 5 deletions(-) create mode 100644 src/framework/ui/button/button.component.tsx create mode 100644 src/framework/ui/button/button.spec.config.ts create mode 100644 src/framework/ui/button/button.spec.tsx create mode 100644 src/framework/ui/button/button.spec.tsx.snap create mode 100644 src/framework/ui/button/type.ts create mode 100644 src/playground/src/ui/screen/button.component.tsx diff --git a/src/framework/ui/button/button.component.tsx b/src/framework/ui/button/button.component.tsx new file mode 100644 index 000000000..83278d14a --- /dev/null +++ b/src/framework/ui/button/button.component.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { + TouchableOpacity, + TouchableOpacityProps, + GestureResponderEvent, + StyleSheet, + ImageSourcePropType, + Image, + ImageProps, +} from 'react-native'; +import { + StyledComponentProps, + StyleType, + Interaction, + styled, +} from '@kitten/theme'; +import { + Text as TextComponent, + Props as TextProps, +} from '../text/text.component'; +import { + ButtonAlignment, + ButtonAlignments, +} from './type'; + +interface ButtonProps { + icon?: ImageSourcePropType; + status?: string; + size?: string; + alignment?: string | ButtonAlignment; + children?: React.ReactText; +} + +const Text = styled(TextComponent); + +export type Props = ButtonProps & StyledComponentProps & TouchableOpacityProps; + +const ALIGNMENT_DEFAULT: ButtonAlignment = ButtonAlignments.LEFT; + +export class Button extends React.Component { + + static defaultProps: Partial = { + status: 'primary', + size: 'medium', + }; + + 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 { text, icon, ...container } = style; + const alignment: ButtonAlignment = ButtonAlignments.parse(this.props.alignment, ALIGNMENT_DEFAULT); + + return { + container: { + ...container, + flexDirection: alignment.flex(), + }, + text: text, + icon: icon, + }; + }; + + private createTextElement = (style: StyleType): React.ReactElement => { + const { children: text } = this.props; + + return ( + + {text} + + ); + }; + + private createImageElement = (style: StyleType): React.ReactElement => { + const { icon: image } = this.props; + + return ( + + ); + }; + + private createComponentChildren = (style: StyleType): React.ReactNode => { + const { icon, children } = this.props; + + const hasIcon: boolean = icon !== undefined; + const hasText: boolean = children !== undefined; + + return [ + hasIcon ? this.createImageElement(style.icon) : undefined, + hasText ? this.createTextElement(style.text) : undefined, + ]; + }; + + public render(): React.ReactElement { + const { style, themedStyle, ...derivedProps } = this.props; + const { container, ...componentStyles } = this.getComponentStyle(themedStyle); + const componentChildren: React.ReactNode = this.createComponentChildren(componentStyles); + + return ( + + {componentChildren} + + ); + } +} + +const strictStyles = StyleSheet.create({ + container: { + justifyContent: 'space-evenly', + alignItems: 'center', + }, + text: {}, + icon: {}, +}); diff --git a/src/framework/ui/button/button.spec.config.ts b/src/framework/ui/button/button.spec.config.ts new file mode 100644 index 000000000..5da57ab1d --- /dev/null +++ b/src/framework/ui/button/button.spec.config.ts @@ -0,0 +1,508 @@ +import { ThemeMappingType } from 'eva/packages/common'; +import { ThemeType } from '@kitten/theme'; + +export const mapping: ThemeMappingType = { + 'Button': { + 'meta': { + 'variants': { + 'size': [ + 'tiny', + 'small', + 'medium', + 'large', + 'giant', + ], + 'status': [ + 'primary', + 'success', + 'info', + 'warning', + 'danger', + ], + }, + 'states': [ + 'disabled', + 'active', + ], + }, + 'appearance': { + 'default': { + 'mapping': { + 'minHeight': 24, + 'minWidth': 24, + 'padding': 4, + 'borderRadius': 6, + 'text': { + 'color': 'text-primary-inverse', + 'fontSize': 12, + 'fontWeight': '800', + 'marginHorizontal': 4, + }, + 'icon': { + 'tintColor': '#ffffff', + 'width': 14, + 'height': 14, + 'marginHorizontal': 4, + }, + }, + 'variant': { + 'status': { + 'primary': { + 'mapping': { + 'backgroundColor': '#2196F3', + 'state': { + 'active': { + 'backgroundColor': '#1E88E5', + }, + }, + }, + }, + 'success': { + 'mapping': { + 'backgroundColor': '#4CAF50', + 'state': { + 'active': { + 'backgroundColor': '#43A047', + }, + }, + }, + }, + 'info': { + 'mapping': { + 'backgroundColor': '#03A9F4', + 'state': { + 'active': { + 'backgroundColor': '#039BE5', + }, + }, + }, + }, + 'warning': { + 'mapping': { + 'backgroundColor': '#FFC107', + 'state': { + 'active': { + 'backgroundColor': '#FFB300', + }, + }, + }, + }, + 'danger': { + 'mapping': { + 'backgroundColor': '#F44336', + 'state': { + 'active': { + 'backgroundColor': '#E53935', + }, + }, + }, + }, + }, + 'size': { + 'tiny': { + 'mapping': { + 'minHeight': 16, + 'minWidth': 16, + 'padding': 3, + 'text': { + 'fontSize': 10, + 'marginHorizontal': 3, + }, + 'icon': { + 'width': 11, + 'height': 11, + 'marginHorizontal': 3, + }, + }, + }, + 'small': { + 'mapping': { + 'minHeight': 20, + 'minWidth': 20, + 'padding': 3.5, + 'text': { + 'fontSize': 11, + 'marginHorizontal': 3.5, + }, + 'icon': { + 'width': 12, + 'height': 12, + 'marginHorizontal': 3.5, + }, + }, + }, + 'medium': { + 'mapping': { + 'minHeight': 24, + 'minWidth': 24, + 'padding': 4, + 'text': { + 'fontSize': 12, + 'marginHorizontal': 4, + }, + 'icon': { + 'width': 13, + 'height': 13, + 'marginHorizontal': 4, + }, + }, + }, + 'large': { + 'mapping': { + 'minHeight': 30, + 'minWidth': 30, + 'padding': 4.5, + 'text': { + 'fontSize': 13, + 'marginHorizontal': 4.5, + }, + }, + 'icon': { + 'width': 14, + 'height': 14, + 'marginHorizontal': 4.5, + }, + }, + 'giant': { + 'mapping': { + 'minHeight': 36, + 'minWidth': 36, + 'padding': 5, + 'text': { + 'fontSize': 14, + 'marginHorizontal': 5, + }, + 'icon': { + 'width': 15, + 'height': 15, + 'marginHorizontal': 5, + }, + }, + }, + }, + }, + }, + 'outline': { + 'mapping': { + 'borderWidth': 2, + }, + 'variant': { + 'status': { + 'primary': { + 'mapping': { + 'backgroundColor': 'transparent', + 'borderColor': '#2196F3', + 'text': { + 'color': '#2196F3', + }, + 'icon': { + 'tintColor': '#2196F3', + }, + 'state': { + 'active': { + 'backgroundColor': 'transparent', + 'borderColor': '#1E88E5', + 'text': { + 'color': '#1E88E5', + }, + 'icon': { + 'tintColor': '#1E88E5', + }, + }, + }, + }, + }, + 'success': { + 'mapping': { + 'backgroundColor': 'transparent', + 'borderColor': '#4CAF50', + 'text': { + 'color': '#4CAF50', + }, + 'icon': { + 'tintColor': '#4CAF50', + }, + 'state': { + 'active': { + 'backgroundColor': 'transparent', + 'borderColor': '#43A047', + 'text': { + 'color': '#43A047', + }, + 'icon': { + 'tintColor': '#43A047', + }, + }, + }, + }, + }, + 'info': { + 'mapping': { + 'backgroundColor': 'transparent', + 'borderColor': '#03A9F4', + 'text': { + 'color': '#03A9F4', + }, + 'icon': { + 'tintColor': '#03A9F4', + }, + 'state': { + 'active': { + 'backgroundColor': 'transparent', + 'borderColor': '#039BE5', + 'text': { + 'color': '#039BE5', + }, + 'icon': { + 'tintColor': '#039BE5', + }, + }, + }, + }, + }, + 'warning': { + 'mapping': { + 'backgroundColor': 'transparent', + 'borderColor': '#FFC107', + 'text': { + 'color': '#FFC107', + }, + 'icon': { + 'tintColor': '#FFC107', + }, + 'state': { + 'active': { + 'backgroundColor': 'transparent', + 'borderColor': '#FFB300', + 'text': { + 'color': '#FFB300', + }, + 'icon': { + 'tintColor': '#FFB300', + }, + }, + }, + }, + }, + 'danger': { + 'mapping': { + 'backgroundColor': 'transparent', + 'borderColor': '#F44336', + 'text': { + 'color': '#F44336', + }, + 'icon': { + 'tintColor': '#F44336', + }, + 'state': { + 'active': { + 'backgroundColor': 'transparent', + 'borderColor': '#E53935', + 'text': { + 'color': '#E53935', + }, + 'icon': { + 'tintColor': '#E53935', + }, + }, + }, + }, + }, + }, + }, + }, + 'ghost': { + 'mapping': {}, + 'variant': { + 'status': { + 'primary': { + 'mapping': { + 'backgroundColor': 'transparent', + 'text': { + 'color': '#2196F3', + }, + 'icon': { + 'tintColor': '#2196F3', + }, + 'state': { + 'active': { + 'backgroundColor': 'transparent', + 'text': { + 'color': '#1E88E5', + }, + 'icon': { + 'tintColor': '#1E88E5', + }, + }, + }, + }, + }, + 'success': { + 'mapping': { + 'backgroundColor': 'transparent', + 'text': { + 'color': '#4CAF50', + }, + 'icon': { + 'tintColor': '#4CAF50', + }, + 'state': { + 'active': { + 'backgroundColor': 'transparent', + 'text': { + 'color': '#43A047', + }, + 'icon': { + 'tintColor': '#43A047', + }, + }, + }, + }, + }, + 'info': { + 'mapping': { + 'backgroundColor': 'transparent', + 'text': { + 'color': '#03A9F4', + }, + 'icon': { + 'tintColor': '#03A9F4', + }, + 'state': { + 'active': { + 'backgroundColor': 'transparent', + 'text': { + 'color': '#039BE5', + }, + 'icon': { + 'tintColor': '#039BE5', + }, + }, + }, + }, + }, + 'warning': { + 'mapping': { + 'backgroundColor': 'transparent', + 'text': { + 'color': '#FFC107', + }, + 'icon': { + 'tintColor': '#FFC107', + }, + 'state': { + 'active': { + 'backgroundColor': 'transparent', + 'text': { + 'color': '#FFB300', + }, + 'icon': { + 'tintColor': '#FFB300', + }, + }, + }, + }, + }, + 'danger': { + 'mapping': { + 'backgroundColor': 'transparent', + 'text': { + 'color': '#F44336', + }, + 'icon': { + 'tintColor': '#F44336', + }, + 'state': { + 'active': { + 'backgroundColor': 'transparent', + 'text': { + 'color': '#E53935', + }, + 'icon': { + 'tintColor': '#E53935', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + 'Text': { + 'meta': { + 'variants': { + 'category': [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'body', + ], + }, + 'states': [], + }, + 'appearance': { + 'default': { + 'mapping': { + 'color': 'text-primary', + 'fontSize': 16, + }, + 'variant': { + 'category': { + 'h1': { + 'mapping': { + 'fontSize': 64, + }, + }, + 'h2': { + 'mapping': { + 'fontSize': 40, + }, + }, + 'h3': { + 'mapping': { + 'fontSize': 32, + }, + }, + 'h4': { + 'mapping': { + 'fontSize': 24, + }, + }, + 'h5': { + 'mapping': { + 'fontSize': 16, + }, + }, + 'h6': { + 'mapping': { + 'fontSize': 14, + }, + }, + 'body': { + 'mapping': { + 'fontSize': 16, + }, + }, + }, + }, + }, + }, + }, +}; + +export const theme: ThemeType = { + 'blue-primary': '#3366FF', + 'blue-dark': '#2541CC', + 'gray-light': '#DDE1EB', + 'gray-primary': '#A6AEBD', + 'gray-dark': '#8992A3', + 'gray-highlight': '#EDF0F5', + 'pink-primary': '#FF3D71', + 'text-primary': '#000000', + 'text-primary-inverse': '#ffffff', +}; diff --git a/src/framework/ui/button/button.spec.tsx b/src/framework/ui/button/button.spec.tsx new file mode 100644 index 000000000..9df414d5a --- /dev/null +++ b/src/framework/ui/button/button.spec.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { + ImageSourcePropType, + TouchableOpacity, +} from 'react-native'; +import { + render, + fireEvent, + shallow, + RenderAPI, +} from 'react-native-testing-library'; +import { + styled, + StyleProvider, + StyleProviderProps, +} from '@kitten/theme'; +import { + Button as ButtonComponent, + Props as ButtonProps, +} from './button.component'; +import * as config from './button.spec.config'; + +const Button = styled(ButtonComponent); + +const Mock = (props?: ButtonProps): React.ReactElement => ( + +