From 48c6cfedd509c8ea884810c8dde6e89a7bf425fe Mon Sep 17 00:00:00 2001 From: Artur Yorsh <10753921+artyorsh@users.noreply.github.com> Date: Wed, 26 Dec 2018 19:06:00 +0300 Subject: [PATCH] feat(ui): Radio. Closes #201 (#212) * feat(ui/radio): initial compomponent implementation * feat(ui/radio): state mapping support * feat(ui/radio): apply design styles * test(radio): add radio component tests * test(ui/radio): [temporary] radio tests * feat(theme): add appearances mapping config * refactor(theme): split theme service * refactor(theme): remove useless tests * refactor(theme): [temporary] mapping & style services * merge: style-consumer-appearance-support * refactor(theme): style-consumer methods refactor, move to service * refactor(theme): remove unused imports from style-consumer * refactor(theme): move method from hoc to service (style-consumer) * refactor(theme): framework ui-component imports refactor (style-component is decorator now). * test(theme): style-consumer-service spec add * refactor(theme): style-consumer-service functions refactor * refactor(theme): style-consumer-service as a class implement and integrate * build(tsconfig): allow using decorators switch-on * refactor(theme): styled decorator rename * feat(theme): createStyle function supporting appearances * refactor(theme): style consumer service * refactor(ui/radio): adopt component to new config * test(theme): adopt theme test to new config * refactor(playground): remove sample component * feat(playground): update radio screen examples * build(package): react-native-testing-library update * test(ui/radio): add radio component snapshot tests * refactor(playground): adopt radio screen to appearances config --- package-lock.json | 89 ++- package.json | 2 +- .../theme/component/mapping/mappingContext.ts | 2 +- .../mapping/mappingProvider.component.tsx | 2 +- src/framework/theme/component/mapping/type.ts | 23 +- src/framework/theme/component/style/index.ts | 2 +- .../style/styleConsumer.component.tsx | 47 +- .../style/styleProvider.component.tsx | 6 +- src/framework/theme/index.ts | 6 +- src/framework/theme/service/index.ts | 2 + .../theme/service/mappingUtil.service.ts | 125 ++- .../theme/service/styleConsumer.service.ts | 19 + .../theme/service/styleUtil.service.ts | 200 +++++ .../theme/service/themeUtil.service.ts | 11 + .../theme/service/themeUtil.service.tsx | 82 -- src/framework/theme/tests/config.ts | 81 -- src/framework/theme/tests/config/index.ts | 3 + src/framework/theme/tests/config/mapping.json | 90 +++ .../theme/tests/config/theme-inverse.json | 11 + src/framework/theme/tests/config/theme.json | 11 + src/framework/theme/tests/mapping.spec.ts | 51 ++ src/framework/theme/tests/mapping.spec.tsx | 85 --- src/framework/theme/tests/style.spec.ts | 469 ++++++++++++ src/framework/theme/tests/style.spec.tsx | 199 ----- .../theme/tests/styleConsumer.spec.tsx | 262 +++++++ src/framework/theme/tests/theme.spec.tsx | 369 +++------ src/framework/tsconfig.json | 1 + src/framework/ui/index.ts | 6 + src/framework/ui/radio/radio.component.tsx | 129 ++++ .../tests/__snapshots__/radio.spec.tsx.snap | 715 ++++++++++++++++++ src/framework/ui/tests/config/index.ts | 3 + src/framework/ui/tests/config/mapping.json | 70 ++ src/framework/ui/tests/config/theme.json | 9 + src/framework/ui/tests/radio.spec.tsx | 109 +++ src/playground/package-lock.json | 149 ++++ src/playground/src/app.component.tsx | 16 +- src/playground/src/theme-token/index.ts | 12 +- src/playground/src/theme-token/mapping.json | 99 ++- src/playground/src/theme-token/theme.json | 15 +- src/playground/src/ui/component/index.ts | 9 - .../src/ui/component/sample.component.tsx | 88 --- src/playground/src/ui/screen/index.ts | 2 +- .../src/ui/screen/radio.component.tsx | 152 ++++ .../src/ui/screen/sample.component.tsx | 61 -- tsconfig.json | 1 + 45 files changed, 2855 insertions(+), 1040 deletions(-) create mode 100644 src/framework/theme/service/styleConsumer.service.ts create mode 100644 src/framework/theme/service/styleUtil.service.ts create mode 100644 src/framework/theme/service/themeUtil.service.ts delete mode 100644 src/framework/theme/service/themeUtil.service.tsx delete mode 100644 src/framework/theme/tests/config.ts create mode 100644 src/framework/theme/tests/config/index.ts create mode 100644 src/framework/theme/tests/config/mapping.json create mode 100644 src/framework/theme/tests/config/theme-inverse.json create mode 100644 src/framework/theme/tests/config/theme.json create mode 100644 src/framework/theme/tests/mapping.spec.ts delete mode 100644 src/framework/theme/tests/mapping.spec.tsx create mode 100644 src/framework/theme/tests/style.spec.ts delete mode 100644 src/framework/theme/tests/style.spec.tsx create mode 100644 src/framework/theme/tests/styleConsumer.spec.tsx create mode 100644 src/framework/ui/radio/radio.component.tsx create mode 100644 src/framework/ui/tests/__snapshots__/radio.spec.tsx.snap create mode 100644 src/framework/ui/tests/config/index.ts create mode 100644 src/framework/ui/tests/config/mapping.json create mode 100644 src/framework/ui/tests/config/theme.json create mode 100644 src/framework/ui/tests/radio.spec.tsx delete mode 100644 src/playground/src/ui/component/index.ts delete mode 100644 src/playground/src/ui/component/sample.component.tsx create mode 100644 src/playground/src/ui/screen/radio.component.tsx delete mode 100644 src/playground/src/ui/screen/sample.component.tsx diff --git a/package-lock.json b/package-lock.json index 55f56b3df..ff493a8e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -950,6 +950,16 @@ "@types/react": "*" } }, + "@types/react-navigation": { + "version": "2.13.9", + "resolved": "https://registry.npmjs.org/@types/react-navigation/-/react-navigation-2.13.9.tgz", + "integrity": "sha512-Xtk9LRt9TljafJpkbMmgwNTja2942nNBWZOOFDahfTzuZ3FQrZ09PBN0glrns1k87Xzxv5g4pS+xY5bdscHN+Q==", + "dev": true, + "requires": { + "@types/react": "*", + "@types/react-native": "*" + } + }, "@types/react-test-renderer": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.0.3.tgz", @@ -2732,6 +2742,28 @@ "parse-json": "^4.0.0" } }, + "coveralls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.2.tgz", + "integrity": "sha512-Tv0LKe/MkBOilH2v7WBiTBdudg2ChfGbdXafc/s330djpF3zKOmuehTeRwjXWc7pzfj9FrDUTA7tEx6Div8NFw==", + "dev": true, + "requires": { + "growl": "~> 1.10.0", + "js-yaml": "^3.11.0", + "lcov-parse": "^0.0.10", + "log-driver": "^1.2.7", + "minimist": "^1.2.0", + "request": "^2.85.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "create-react-class": { "version": "15.6.3", "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", @@ -3632,12 +3664,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3652,17 +3686,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3779,7 +3816,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3791,6 +3829,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3805,6 +3844,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3812,12 +3852,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3836,6 +3878,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3916,7 +3959,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3928,6 +3972,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4049,6 +4094,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4227,6 +4273,12 @@ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "dev": true }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -4365,6 +4417,14 @@ } } }, + "hoist-non-react-statics": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz", + "integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==", + "requires": { + "react-is": "^16.3.2" + } + }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -5635,6 +5695,12 @@ "invert-kv": "^1.0.0" } }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", + "dev": true + }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -5733,6 +5799,12 @@ "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=", "dev": true }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8256,8 +8328,7 @@ "react-is": { "version": "16.6.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.0.tgz", - "integrity": "sha512-q8U7k0Fi7oxF1HvQgyBjPwDXeMplEsArnKt2iYhuIF86+GBbgLHdAmokL3XUFjTd7Q363OSNG55FOGUdONVn1g==", - "dev": true + "integrity": "sha512-q8U7k0Fi7oxF1HvQgyBjPwDXeMplEsArnKt2iYhuIF86+GBbgLHdAmokL3XUFjTd7Q363OSNG55FOGUdONVn1g==" }, "react-native": { "version": "0.57.4", diff --git a/package.json b/package.json index 81e5cf342..1874c3d11 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "react-addons-test-utils": "^15.6.2", "react-native": "^0.57.4", "react-native-mock": "^0.3.1", - "react-native-testing-library": "^1.3.0", + "react-native-testing-library": "^1.5.0", "react-native-typescript-transformer": "^1.2.10", "react-test-renderer": "^16.6.0", "rimraf": "^2.6.2", diff --git a/src/framework/theme/component/mapping/mappingContext.ts b/src/framework/theme/component/mapping/mappingContext.ts index 2249d85d2..ae54db96a 100644 --- a/src/framework/theme/component/mapping/mappingContext.ts +++ b/src/framework/theme/component/mapping/mappingContext.ts @@ -1,7 +1,7 @@ import React from 'react'; import { ThemeMappingType } from './type'; -const defaultValue: ThemeMappingType[] = []; +const defaultValue: ThemeMappingType = {}; const MappingContext = React.createContext(defaultValue); export default MappingContext; diff --git a/src/framework/theme/component/mapping/mappingProvider.component.tsx b/src/framework/theme/component/mapping/mappingProvider.component.tsx index fd0e83e13..fdd95700f 100644 --- a/src/framework/theme/component/mapping/mappingProvider.component.tsx +++ b/src/framework/theme/component/mapping/mappingProvider.component.tsx @@ -3,7 +3,7 @@ import MappingContext from './mappingContext'; import { ThemeMappingType } from './type'; export interface Props { - mapping: ThemeMappingType[]; + mapping: ThemeMappingType; children: JSX.Element | React.ReactNode; } diff --git a/src/framework/theme/component/mapping/type.ts b/src/framework/theme/component/mapping/type.ts index 9d722e42a..9afa3e823 100644 --- a/src/framework/theme/component/mapping/type.ts +++ b/src/framework/theme/component/mapping/type.ts @@ -1,14 +1,25 @@ -// TODO(mapping/type): declare Config/Token type +export type ThemeMappingType = any; -export type ThemeMappingConfigType = any; +export interface ComponentMappingType { + appearance: any; +} -export interface ThemeMappingType { - parameters: string[]; - variants: any; +export interface AppearanceType { + mapping: MappingType; + variant?: any; } +export type VariantGroupType = any; + export interface VariantType { - state: any; + mapping: MappingType; +} + +export interface MappingType { + state?: StateType; } +export type StateType = any; + + export type TokenType = any; diff --git a/src/framework/theme/component/style/index.ts b/src/framework/theme/component/style/index.ts index a1c3a0479..3394f96b8 100644 --- a/src/framework/theme/component/style/index.ts +++ b/src/framework/theme/component/style/index.ts @@ -4,6 +4,6 @@ export { } from './styleProvider.component'; export { - StyledComponent, + styled, Props as StyledComponentProps, } from './styleConsumer.component'; diff --git a/src/framework/theme/component/style/styleConsumer.component.tsx b/src/framework/theme/component/style/styleConsumer.component.tsx index a9cf3b07c..c16e3964e 100644 --- a/src/framework/theme/component/style/styleConsumer.component.tsx +++ b/src/framework/theme/component/style/styleConsumer.component.tsx @@ -4,12 +4,16 @@ import { ThemeType, StyleType, } from '../theme'; -import { ThemeMappingType } from '../mapping'; +import { + ThemeMappingType, + ComponentMappingType, +} from '../mapping'; import ThemeContext from '../theme/themeContext'; import MappingContext from '../mapping/mappingContext'; import { createStyle, - getComponentThemeMapping, + getComponentMapping, + StyleConsumerService, } from '../../service'; interface PrivateProps { @@ -17,44 +21,51 @@ interface PrivateProps { } interface ConsumerProps { - mapping: ThemeMappingType[]; + mapping: ThemeMappingType; theme: ThemeType; } export interface Props { - variant?: string; + appearance?: string; theme?: ThemeType; themedStyle?: StyleType; - requestStateStyle?: (state: string[] | string) => StyleType; + requestStateStyle?: (state: string[]) => StyleType; } -export const StyledComponent = (Component: React.ComponentClass

) => { +export const styled = (Component: React.ComponentClass

) => { type ComponentProps = Props & P; type WrapperProps = PrivateProps & ComponentProps; class Wrapper extends React.Component { + service: StyleConsumerService = new StyleConsumerService(); + getComponentName = (): string => Component.displayName || Component.name; - createStyle = (theme: ThemeType, - mapping: ThemeMappingType, - variant: string[] | string, - state: string[] | string): StyleType => { + createComponentStyle = (theme: ThemeType, + mapping: ComponentMappingType, + appearance: string, + variant: string[], + state: string[]): StyleType => { if (state.length === 0) { console.warn('Redundant `requestStateStyle` call! Use `this.props.themedStyle` instead!'); } - return createStyle(theme, mapping, variant, state); + return createStyle(theme, mapping, appearance, variant, state); }; - createCustomProps = (props: ConsumerProps, variant: string): Props => { - const mapping = getComponentThemeMapping(this.getComponentName(), props.mapping); + createCustomProps = (props: ConsumerProps, componentProps: P & Props): Props => { + const mapping = getComponentMapping(props.mapping, this.getComponentName()); + const variants = this.service.getVariantPropKeys

(mapping, componentProps); + return { - variant: variant, + appearance: componentProps.appearance, theme: props.theme, - themedStyle: createStyle(props.theme, mapping, variant), - requestStateStyle: state => this.createStyle(props.theme, mapping, variant, state), + themedStyle: createStyle(props.theme, mapping, componentProps.appearance, variants), + requestStateStyle: state => { + return this.createComponentStyle(props.theme, mapping, componentProps.appearance, variants, state); + }, }; }; @@ -65,7 +76,7 @@ export const StyledComponent = (Com return ( ); @@ -73,7 +84,7 @@ export const StyledComponent = (Com render() { return ( - {(mapping: ThemeMappingType[]) => ( + {(mapping: ThemeMappingType) => ( {(theme: ThemeType) => { return this.renderWrappedComponent({ mapping: mapping, theme: theme }); }} diff --git a/src/framework/theme/component/style/styleProvider.component.tsx b/src/framework/theme/component/style/styleProvider.component.tsx index b3c31abbb..bc67197ae 100644 --- a/src/framework/theme/component/style/styleProvider.component.tsx +++ b/src/framework/theme/component/style/styleProvider.component.tsx @@ -5,17 +5,17 @@ import { } from '../theme'; import { ThemeMappingProvider, - ThemeMappingConfigType, + ThemeMappingType, } from '../mapping'; export interface Props { - mapping: ThemeMappingConfigType; + mapping: ThemeMappingType; theme: ThemeType; children: JSX.Element | React.ReactNode; } interface State { - mapping: ThemeMappingConfigType; + mapping: ThemeMappingType; theme: ThemeType; } diff --git a/src/framework/theme/index.ts b/src/framework/theme/index.ts index 0525ade8e..7a0f27d7d 100644 --- a/src/framework/theme/index.ts +++ b/src/framework/theme/index.ts @@ -1,6 +1,6 @@ export { StyleProvider, - StyledComponent, + styled, ThemeProvider, withStyles, @@ -10,8 +10,8 @@ export { ThemedComponentProps, ThemeType, - ThemeMappingConfigType, ThemeMappingType, + ComponentMappingType, TokenType, ThemedStyleType, StyleSheetType, @@ -19,6 +19,6 @@ export { } from './component'; export { - VARIANT_DEFAULT, + APPEARANCE_DEFAULT, } from './service'; diff --git a/src/framework/theme/service/index.ts b/src/framework/theme/service/index.ts index dc3e7d761..b7d771164 100644 --- a/src/framework/theme/service/index.ts +++ b/src/framework/theme/service/index.ts @@ -1,2 +1,4 @@ export * from './mappingUtil.service'; export * from './themeUtil.service'; +export * from './styleUtil.service'; +export * from './styleConsumer.service'; diff --git a/src/framework/theme/service/mappingUtil.service.ts b/src/framework/theme/service/mappingUtil.service.ts index 050ad5d6c..d515204c8 100644 --- a/src/framework/theme/service/mappingUtil.service.ts +++ b/src/framework/theme/service/mappingUtil.service.ts @@ -1,89 +1,78 @@ import { ThemeMappingType, - VariantType, - TokenType, + ComponentMappingType, + AppearanceType, + VariantGroupType, + MappingType, + StateType, } from '../component'; -export const VARIANT_DEFAULT = 'default'; +export const APPEARANCE_DEFAULT = 'default'; /** - * @param component: string - component name. Using displayName is recommended - * @param mapping: ThemeMappingType[] - theme mapping configuration array + * @param component: string - component name + * @param mapping: ThemeMappingType - theme mapping configuration object * - * @return ThemeMappingType if presents in mapping, undefined otherwise + * @return ComponentMappingType if presents in mapping, undefined otherwise */ -export function getComponentThemeMapping(component: string, mapping: any): ThemeMappingType | undefined { +export function getComponentMapping(mapping: ThemeMappingType, + component: string): ComponentMappingType | undefined { + return mapping[component]; } -/** - * @param token: string - theme mapping token name - * @param tokens: TokenType - theme tokens - * - * @return TokenType if presents in tokens, undefined otherwise - */ -export function getThemeMappingToken(token: string, tokens: TokenType): TokenType | undefined { - if (tokens[token] === undefined) { - return undefined; - } - const value = {}; - value[token] = tokens[token]; - - return value; +export function getAppearance(mapping: ComponentMappingType, + appearance: string): AppearanceType | undefined { + + return mapping.appearance[appearance]; } -/** - * @param variant: string - variant name. Default is 'default' - * @param mapping: ThemeMappingType - component mapping configuration - * @param state: string - variant state name. Default is `undefined` - * - * @return variant if presents in mapping, undefined otherwise - */ -export function getComponentVariant(variant: string, - mapping: ThemeMappingType, - state?: string): any | undefined { +export function getAppearanceMapping(mapping: ComponentMappingType, + appearance: string): MappingType | undefined { - const componentVariant: VariantType = mapping.variants[variant]; - if (componentVariant === undefined) { - return undefined; - } - const { state: variantStates, ...variantParameters } = componentVariant; + const appearanceConfig = getAppearance(mapping, appearance); - return state === undefined ? variantParameters : variantStates && variantStates[state]; + return appearanceConfig && appearanceConfig.mapping; } -/** - * @param parameter: string - parameter name. - * @param variant: string - variant name. - * @param mapping: ThemeMappingType - component mapping configuration - * @param state: string - variant state name - * - * @return parameterMapping if presents in variant, undefined otherwise - */ -export function getParameterMapping(parameter: string, - variant: string, - mapping: ThemeMappingType, - state?: string): any | undefined { +export function getAppearanceMappingSafe(mapping: ComponentMappingType, + appearance: string, + fallback: MappingType): MappingType { - const componentVariant = getComponentVariant(variant, mapping, state); - return componentVariant && componentVariant[parameter]; + return getAppearanceMapping(mapping, appearance) || fallback; } -/** - * @param parameter: string - parameter name. - * @param variant: string - variant name. - * @param mapping: ThemeMappingType - component mapping configuration - * @param tokens: TokenType - theme tokens - * @param state: string - variant state name - * - * @return theme token if presents in variant, undefined otherwise - */ -export function getParameterValue(parameter: string, - variant: string, - mapping: ThemeMappingType, - tokens: TokenType, - state?: string): any | undefined { - - const parameterMapping = getParameterMapping(parameter, variant, mapping, state); - return parameterMapping && getThemeMappingToken(parameterMapping, tokens); +export function getAppearanceVariants(mapping: ComponentMappingType, + appearance: string): VariantGroupType | undefined { + + const appearanceConfig = getAppearance(mapping, appearance); + + return appearanceConfig && appearanceConfig.variant; +} + +export function getVariantMapping(mapping: ComponentMappingType, + appearance: string, + variant: string): MappingType | undefined { + + const variantGroupConfig = getAppearanceVariants(mapping, appearance); + const variantGroupName = variantGroupConfig && Object.keys(variantGroupConfig).find(group => { + return variantGroupConfig[group][variant] !== undefined; + }); + const variantConfig = variantGroupName && variantGroupConfig[variantGroupName][variant]; + + return variantConfig && variantConfig.mapping; +} + +export function getVariantMappingSafe(mapping: ComponentMappingType, + appearance: string, + variant: string, + fallback: MappingType): MappingType { + + return getVariantMapping(mapping, appearance, variant) || fallback; +} + +export function getMappingState(mapping: MappingType, + state: string): StateType | undefined { + + return mapping.state && mapping.state[state]; } diff --git a/src/framework/theme/service/styleConsumer.service.ts b/src/framework/theme/service/styleConsumer.service.ts new file mode 100644 index 000000000..b77b59673 --- /dev/null +++ b/src/framework/theme/service/styleConsumer.service.ts @@ -0,0 +1,19 @@ +import { ComponentMappingType } from '../component'; +import { Props } from '../component/style/styleConsumer.component'; +import { + APPEARANCE_DEFAULT, + getAppearanceVariants, +} from './mappingUtil.service'; + +export class StyleConsumerService { + + public getVariantPropKeys

(mapping: ComponentMappingType, props: P): string[] { + const variants = getAppearanceVariants(mapping, APPEARANCE_DEFAULT); + if (variants === undefined) { + return []; + } + return Object.keys(props) + .filter((key: string) => variants[key]) + .map((key: string) => props[key]); + } +} diff --git a/src/framework/theme/service/styleUtil.service.ts b/src/framework/theme/service/styleUtil.service.ts new file mode 100644 index 000000000..da84778a7 --- /dev/null +++ b/src/framework/theme/service/styleUtil.service.ts @@ -0,0 +1,200 @@ +import { + getAppearanceMappingSafe, + getVariantMappingSafe, + APPEARANCE_DEFAULT, +} from './mappingUtil.service'; +import { + ComponentMappingType, + MappingType, + StateType, + ThemeType, + StyleType, +} from '../component'; + +const SEPARATOR_STATE = '.'; +const FALLBACK_MAPPING_APPEARANCE: MappingType = {}; +const FALLBACK_MAPPING_VARIANT: MappingType = {}; + +/** + * Creates style object for variant/list of variants and its state/list of states(optional) + * + * Examples + * + * 1. Default: + * + * + * + * - will return styles for `default` appearance + * + * 2. Custom appearance: + * + * + * + * - will return styles for `default` + `bold` appearances + * + * 3. With Variants: + * + * + * + * - will return styles for `default` appearance + (`success` + `tiny`) variants + * + * 4. With states: + * + * which is currently `active` and `checked` + * + * - will return styles for + * `default` appearance + (`active` + `checked` + `active.checked`) states + * + `success` variant + (`active` + `checked` + `active.checked`) variant states + * + * State merging is the same as variant merging + * But state parameters override variant parameters + * + * @param theme: ThemeType - theme object + * @param mapping: ComponentMappingType - component theme mapping configuration + * @param appearance: string - appearance applied to component + * @param variants: string[] - variants applied to component. Default is [] + * @param states: string[] - states in which component is. Default is [] + * + * @return StyleType - compiled component styles declared in mappings, mapped to theme values + * + */ +export function createStyle(theme: ThemeType, + mapping: ComponentMappingType, + appearance: string = APPEARANCE_DEFAULT, + variants: string[] = [], + states: string[] = []): StyleType { + + const appearanceMapping = createAppearanceMapping( + mapping, + normalizeAppearance(appearance), + normalizeVariants(variants), + normalizeStates(states), + ); + return createStyleFromMapping(appearanceMapping, theme); +} + +function createAppearanceMapping(mapping: ComponentMappingType, + appearances: string[], + variants: string[], + states: string[]): any { + + return appearances.reduce((acc, current) => { + const { state, ...appearanceMapping } = getAppearanceMappingSafe(mapping, current, FALLBACK_MAPPING_APPEARANCE); + const stateMapping = state && createStateMapping(state, states); + const variantMapping = createVariantMapping(mapping, current, variants, states); + + return { ...acc, ...appearanceMapping, ...stateMapping, ...variantMapping }; + }, {}); +} + +function createVariantMapping(mapping: ComponentMappingType, + appearance: string, + variants: string[], + states: string[]): any { + + return variants.reduce((acc, current) => { + const { state, ...variantMapping } = getVariantMappingSafe(mapping, appearance, current, FALLBACK_MAPPING_VARIANT); + const stateMapping = state && createStateMapping(state, states); + + return { ...acc, ...variantMapping, ...stateMapping }; + }, {}); +} + +function createStateMapping(state: StateType, states: string[]): any { + return states.reduce((acc, current) => ({ ...acc, ...state[current] }), {}); +} + +function createStyleFromMapping(mapping: any, theme: ThemeType): StyleType { + return Object.keys(mapping).reduce((acc, current) => { + const key = mapping[current]; + acc[current] = theme[key] || key; + return acc; + }, {}); +} + +/** + * Creates normalized to design system array of component appearances + * + * Example: + * + * '' => ['default'] + * 'bold' => ['default', 'bold'] + * 'default' => ['default'] + * ... + * + * @param appearance: string - appearance applied to component + * + * @return string[] - array of merged appearances + */ +export function normalizeAppearance(appearance: string): string[] { + return normalize([APPEARANCE_DEFAULT, appearance]); +} + +/** + * Creates normalized to design system array of component variants + * + * Example: + * + * [''] => [] + * ['success'] => ['success'] + * ['success', 'tiny'] => ['success', 'tiny'] + * ... + * + * @param variants: string[] - variants applied to component + * + * @return string[] - array of merged variants + */ +export function normalizeVariants(variants: string[]): string[] { + return normalize(variants); +} + +/** + * Creates normalized to design system array of component states + * + * Example: + * + * [''] => [] + * ['active'] => ['active'] + * ['active', 'checked'] => ['active', 'checked', 'active.checked'] + * ['active', 'checked', 'disabled'] => ['active', 'checked', 'active.checked', 'disabled', 'active.checked.disabled'] + * ... + * + * @param states: string[] - states in which component is + * @param separator - state separator. `.` in example + * + * @return string[] - array of merged states + */ +export function normalizeStates(states: string[], separator = SEPARATOR_STATE): string[] { + const preprocess = normalize(states); + if (preprocess.length === 0) { + return preprocess; + } else { + return createStateDescription(preprocess, separator, []); + } +} + +function createStateDescription(state: string[], separator: string, result: string[]): string[] { + if (state.length === 1) { + const last = state[state.length - 1]; + return [last, ...result]; + } else { + const concat = state.reduce((acc, current) => acc.concat(separator, current)); + const last = state.pop(); + + result.unshift(last, concat); + + return createStateDescription(state, separator, result); + } +} + +function normalize(params: string[]): string[] { + return noNulls(noDuplicates(params)); +} + +function noDuplicates(params: string[]): string[] { + return [...new Set(params)]; +} + +function noNulls(params: string[]): string[] { + return params.filter(Boolean); +} diff --git a/src/framework/theme/service/themeUtil.service.ts b/src/framework/theme/service/themeUtil.service.ts new file mode 100644 index 000000000..0f08cc9d4 --- /dev/null +++ b/src/framework/theme/service/themeUtil.service.ts @@ -0,0 +1,11 @@ +import { ThemeType } from '../component'; + +/** + * @param name: string - theme property name, like `backgroundColor` + * @param theme: ThemeType - theme + * + * @return any. Theme property value if it presents in theme, undefined otherwise + */ +export function getThemeValue(name: string, theme: ThemeType): any | undefined { + return theme[name]; +} diff --git a/src/framework/theme/service/themeUtil.service.tsx b/src/framework/theme/service/themeUtil.service.tsx deleted file mode 100644 index 50ffe2676..000000000 --- a/src/framework/theme/service/themeUtil.service.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { - getComponentVariant, - VARIANT_DEFAULT, -} from './mappingUtil.service'; -import { - ThemeMappingType, - ThemeType, - StyleType, -} from '../component'; - -const SEPARATOR_VARIANT = ' '; -const SEPARATOR_STATE = ' '; - -/** - * Creates style object which can be used to create StyleSheet styles. - * - * @param theme: ThemeType - theme object - * @param mapping: ThemeMappingType - component theme mapping configuration - * @param variant: string | string[] - variant name. - * @param state: string - variant state. Default is `undefined`. - * Supported argument formats: - * - 'dark' - * - 'dark success' - * - ['dark', 'success'] - * - * @return any. - */ -export function createStyle(theme: ThemeType, - mapping: ThemeMappingType, - variant: string[] | string = [VARIANT_DEFAULT], - state: string[] | string = []): StyleType { - - const variants: string[] = Array.isArray(variant) ? variant : variant.split(SEPARATOR_VARIANT); - const states: string[] = Array.isArray(state) ? state : state.split(SEPARATOR_STATE); - - const mapVariant = (v: string) => { - return createStyleFromVariant(theme, mapping, v); - }; - const mapVariantState = (v: string, s: string) => { - const isEmpty = s === undefined || s.length === 0; - return isEmpty ? undefined : createStyleFromVariant(theme, mapping, v, s); - }; - const mapVariantStates = (v: string) => { - return states.map(s => mapVariantState(v, s)).reduce(mergeStyles, {}); - }; - const mergeStyles = (origin: StyleType, next: StyleType) => { - return { ...origin, ...next }; - }; - - const defaultStyle = mapVariant(VARIANT_DEFAULT); - const defaultStateStyle = mapVariantStates(VARIANT_DEFAULT); - const variantStyle = variants.map(mapVariant).reduce(mergeStyles, defaultStyle); - const variantStateStyle = variants.map(mapVariantStates).reduce(mergeStyles, defaultStateStyle); - - return mergeStyles(variantStyle, variantStateStyle); -} - -/** - * @param name: string - theme property name, like `backgroundColor` - * @param theme: ThemeType - theme - * - * @return any. Theme property value if it presents in theme, undefined otherwise - */ -export function getThemeValue(name: string, theme: ThemeType): any | undefined { - return theme[name]; -} - -export function createStyleFromVariant(theme: ThemeType, - mapping: ThemeMappingType, - variant: string, - state?: string): StyleType | undefined { - - const componentVariant = getComponentVariant(variant, mapping, state); - if (componentVariant === undefined) { - return undefined; - } - const assignParameter = (style: StyleType, parameter: any) => { - style[parameter] = getThemeValue(componentVariant[parameter], theme); - return style; - }; - return Object.keys(componentVariant).reduce(assignParameter, {}); -} diff --git a/src/framework/theme/tests/config.ts b/src/framework/theme/tests/config.ts deleted file mode 100644 index 125f04fd1..000000000 --- a/src/framework/theme/tests/config.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { ThemeType } from '../component'; - -export const values = { - backgroundDefault: '#ffffff', - backgroundDark: '#000000', - textDefault: '#000000', - textDefaultDisabled: '#9E9E9E', - textDark: '#ffffff', - textSuccess: '#4CAF50', - textSuccessActive: '#81C784', - backgroundSuccessDisabled: '#F5F5F5', -}; - -export const mappings = { - Test: { - parameters: [ - 'backgroundColor', - 'textColor', - ], - states: [ - 'active', - 'disabled', - ], - variants: { - default: { - backgroundColor: 'backgroundColorTestDefault', - textColor: 'textColorTestDefault', - state: { - active: { - backgroundColor: 'backgroundColorTestDark', - textColor: 'textColorTestDark', - }, - disabled: { - textColor: 'textColorTestDefaultDisabled', - }, - }, - }, - dark: { - backgroundColor: 'backgroundColorTestDark', - textColor: 'textColorTestDark', - state: { - active: { - backgroundColor: 'backgroundColorTestDefault', - textColor: 'textColorTestDefault', - }, - }, - }, - success: { - textColor: 'textColorTestSuccess', - state: { - active: { - backgroundColor: 'backgroundColorTestDefault', - textColor: 'textColorTestSuccessActive', - }, - disabled: { - backgroundColor: 'backgroundColorTestSuccessDisabled', - }, - }, - }, - }, - }, -}; - -export const theme: ThemeType = { - backgroundColorTestDefault: values.backgroundDefault, - backgroundColorTestDark: values.backgroundDark, - textColorTestDefault: values.textDefault, - textColorTestDefaultDisabled: values.textDefaultDisabled, - textColorTestDark: values.textDark, - textColorTestSuccess: values.textSuccess, - textColorTestSuccessActive: values.textSuccessActive, - backgroundColorTestSuccessDisabled: values.backgroundSuccessDisabled, -}; - -export const themeInverse: ThemeType = { - backgroundColorTestDefault: values.backgroundDark, - backgroundColorTestDark: values.backgroundDefault, - textColorTestDefault: values.textDark, - textColorTestDark: values.textDefault, - textColorTestSuccess: values.textDefault, -}; diff --git a/src/framework/theme/tests/config/index.ts b/src/framework/theme/tests/config/index.ts new file mode 100644 index 000000000..6c07d8719 --- /dev/null +++ b/src/framework/theme/tests/config/index.ts @@ -0,0 +1,3 @@ +export { default as mapping } from './mapping.json'; +export { default as theme } from './theme.json'; +export { default as themeInverse } from './theme-inverse.json'; diff --git a/src/framework/theme/tests/config/mapping.json b/src/framework/theme/tests/config/mapping.json new file mode 100644 index 000000000..fd4083eb7 --- /dev/null +++ b/src/framework/theme/tests/config/mapping.json @@ -0,0 +1,90 @@ +{ + "Test": { + "appearance": { + "default": { + "mapping": { + "size": 36, + "innerSize": 24, + "borderWidth": 2, + "borderColor": "grayPrimary", + "selectColor": "transparent", + "state": { + "active": { + "borderColor": "grayDark" + }, + "checked": { + "borderColor": "bluePrimary", + "selectColor": "bluePrimary" + }, + "disabled": { + "borderColor": "grayLight" + }, + "active.checked": { + "borderColor": "blueDark" + }, + "checked.disabled": { + "selectColor": "grayPrimary" + } + } + }, + "variant": { + "status": { + "info": { + "mapping": { + "state": { + "checked": { + "borderColor": "orangePrimary", + "selectColor": "orangePrimary" + }, + "active.checked": { + "borderColor": "orangeDark" + } + } + } + }, + "success": { + "mapping": { + "state": { + "checked": { + "borderColor": "tealPrimary", + "selectColor": "tealPrimary" + }, + "active.checked": { + "borderColor": "tealDark" + } + } + } + } + }, + "size": { + "big": { + "mapping": { + "size": 42, + "innerSize": 28 + } + }, + "small": { + "mapping": { + "size": 30, + "innerSize": 20 + } + } + } + } + }, + "custom": { + "mapping": { + "borderWidth": 4, + "state": {} + } + } + } + }, + "Empty": { + "appearance": { + "default": { + + } + } + } +} diff --git a/src/framework/theme/tests/config/theme-inverse.json b/src/framework/theme/tests/config/theme-inverse.json new file mode 100644 index 000000000..f4aa9e8d3 --- /dev/null +++ b/src/framework/theme/tests/config/theme-inverse.json @@ -0,0 +1,11 @@ +{ + "grayLight": "#616161", + "grayPrimary": "#9E9E9E", + "grayDark": "#E0E0E0", + "bluePrimary": "#2196F3", + "blueDark": "#1976D2", + "orangePrimary": "#FF9800", + "orangeDark": "#F57C00", + "tealPrimary": "#009688", + "tealDark": "#00796B" +} diff --git a/src/framework/theme/tests/config/theme.json b/src/framework/theme/tests/config/theme.json new file mode 100644 index 000000000..3096aca2f --- /dev/null +++ b/src/framework/theme/tests/config/theme.json @@ -0,0 +1,11 @@ +{ + "grayLight": "#E0E0E0", + "grayPrimary": "#9E9E9E", + "grayDark": "#616161", + "bluePrimary": "#2196F3", + "blueDark": "#1976D2", + "orangePrimary": "#FF9800", + "orangeDark": "#F57C00", + "tealPrimary": "#009688", + "tealDark": "#00796B" +} diff --git a/src/framework/theme/tests/mapping.spec.ts b/src/framework/theme/tests/mapping.spec.ts new file mode 100644 index 000000000..3eec9e1ef --- /dev/null +++ b/src/framework/theme/tests/mapping.spec.ts @@ -0,0 +1,51 @@ +import { mapping } from './config'; +import * as Service from '../service/mappingUtil.service'; + +describe('@mapping: service methods checks', () => { + + const { Test: testMapping } = mapping; + + const json = (object: any) => JSON.stringify(object); + + it('finds appearance mapping properly', () => { + const defaultMapping = Service.getAppearanceMapping(testMapping, 'default'); + const customMapping = Service.getAppearanceMapping(testMapping, 'custom'); + const undefinedMapping = Service.getAppearanceMapping(testMapping, 'undefined'); + + expect(json(defaultMapping)).toEqual(json(testMapping.appearance.default.mapping)); + expect(json(customMapping)).toEqual(json(testMapping.appearance.custom.mapping)); + expect(undefinedMapping).toBeUndefined(); + }); + + it('finds variant mapping properly', () => { + const variantMapping = Service.getVariantMapping(testMapping, + 'default', + 'info'); + const withUndefinedAppearance = Service.getVariantMapping(testMapping, + 'undefined', + 'info'); + const withUndefinedVariant = Service.getVariantMapping(testMapping, + 'default', + 'undefined'); + + expect(json(variantMapping)).toEqual(json(testMapping.appearance.default.variant.status.info.mapping)); + expect(withUndefinedAppearance).toBeUndefined(); + expect(withUndefinedVariant).toBeUndefined(); + }); + + it('finds mapping state properly', () => { + const appearanceMapping = testMapping.appearance.default.mapping; + const variantMapping = testMapping.appearance.default.variant.status.info.mapping; + + const appearanceState = Service.getMappingState(appearanceMapping, 'checked'); + const variantState = Service.getMappingState(variantMapping, 'checked'); + const undefinedAppearanceState = Service.getMappingState(appearanceMapping, 'undefined'); + const undefinedVariantState = Service.getMappingState(variantMapping, 'undefined'); + + expect(json(appearanceState)).toEqual(json(appearanceMapping.state.checked)); + expect(json(variantState)).toEqual(json(variantMapping.state.checked)); + expect(undefinedAppearanceState).toBeUndefined(); + expect(undefinedVariantState).toBeUndefined(); + }); + +}); diff --git a/src/framework/theme/tests/mapping.spec.tsx b/src/framework/theme/tests/mapping.spec.tsx deleted file mode 100644 index b40f118da..000000000 --- a/src/framework/theme/tests/mapping.spec.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import * as config from './config'; -import { - getComponentThemeMapping, - getComponentVariant, - getParameterValue, - getThemeMappingToken, -} from '../service'; - -describe('@mapping: service methods checks', () => { - - it('finds mappings properly', async () => { - const componentMappings = getComponentThemeMapping('Test', config.mappings); - const undefinedMappings = getComponentThemeMapping('Undefined', config.mappings); - - expect(componentMappings).not.toBeNull(); - expect(componentMappings).not.toBeUndefined(); - expect(JSON.stringify(componentMappings)).toEqual(JSON.stringify(config.mappings.Test)); - expect(undefinedMappings).toBeUndefined(); - }); - - it('finds variant properly', async () => { - const componentVariant = getComponentVariant('default', config.mappings.Test); - const componentStateVariant = getComponentVariant('default', config.mappings.Test, 'active'); - const undefinedVariant = getComponentVariant('undefined', config.mappings.Test); - const undefinedStateVariant = getComponentVariant('default', config.mappings.Test, 'undefined'); - - expect(componentVariant).not.toBeNull(); - expect(componentVariant).not.toBeUndefined(); - expect(componentStateVariant).not.toBeNull(); - expect(componentStateVariant).not.toBeUndefined(); - expect(undefinedVariant).toBeUndefined(); - expect(undefinedStateVariant).toBeUndefined(); - - const { state: variantState, ...variant } = config.mappings.Test.variants.default; - expect(JSON.stringify(componentVariant)).toEqual(JSON.stringify(variant)); - expect(JSON.stringify(componentStateVariant)).toEqual(JSON.stringify(variantState.active)); - }); - - it('finds parameter value properly', async () => { - const parameterValue = getParameterValue( - 'backgroundColor', - 'default', - config.mappings.Test, - config.theme, - ); - const stateParameterValue = getParameterValue( - 'backgroundColor', - 'default', - config.mappings.Test, - config.theme, - 'active', - ); - const undefinedValue = getParameterValue( - 'undefined', - 'default', - config.mappings.Test, - config.theme, - ); - const undefinedStateValue = getParameterValue( - 'backgroundColor', - 'default', - config.mappings.Test, - config.theme, - 'undefined', - ); - - expect(parameterValue).not.toBeNull(); - expect(parameterValue).not.toBeUndefined(); - expect(stateParameterValue).not.toBeNull(); - expect(stateParameterValue).not.toBeUndefined(); - expect(undefinedValue).toBeUndefined(); - expect(undefinedStateValue).toBeUndefined(); - }); - - it('finds token properly', async () => { - const mappingToken = getThemeMappingToken('backgroundColorTestDefault', config.theme); - const undefinedToken = getThemeMappingToken('undefined', config.theme); - - expect(mappingToken).not.toBeNull(); - expect(mappingToken).not.toBeUndefined(); - expect(mappingToken).not.toEqual(config.values.backgroundDefault); - expect(undefinedToken).toBeUndefined(); - }); - -}); diff --git a/src/framework/theme/tests/style.spec.ts b/src/framework/theme/tests/style.spec.ts new file mode 100644 index 000000000..4cbc78ad8 --- /dev/null +++ b/src/framework/theme/tests/style.spec.ts @@ -0,0 +1,469 @@ +import { + mapping, + theme, +} from './config'; +import * as Service from '../service/styleUtil.service'; +import { APPEARANCE_DEFAULT } from '../service'; + +describe('@style: service methods checks', () => { + + const { Test: testMapping } = mapping; + + const json = (object: any) => JSON.stringify(object); + + it('normalizes appearance properly', () => { + const implicitDefault = Service.normalizeAppearance('default'); + const custom = Service.normalizeAppearance('custom'); + const empty = Service.normalizeAppearance(''); + const nullable = Service.normalizeAppearance(undefined); + + expect(implicitDefault).toEqual([ + 'default', + ]); + expect(custom).toEqual([ + 'default', + 'custom', + ]); + expect(empty).toEqual([ + 'default', + ]); + expect(nullable).toEqual([ + 'default', + ]); + }); + + it('normalizes variants properly', () => { + const success = Service.normalizeVariants([ + 'success', + ]); + const successTiny = Service.normalizeVariants([ + 'success', + 'tiny', + ]); + const withDuplicates = Service.normalizeVariants([ + 'success', + 'success', + 'tiny', + ]); + const withNulls = Service.normalizeVariants([ + 'success', + undefined, + 'tiny', + null, + ]); + const empty = Service.normalizeVariants([ + '', + ]); + + expect(success).toEqual([ + 'success', + ]); + expect(successTiny).toEqual([ + 'success', + 'tiny', + ]); + expect(withDuplicates).toEqual([ + 'success', + 'tiny', + ]); + expect(withNulls).toEqual([ + 'success', + 'tiny', + ]); + expect(withNulls).toEqual([ + 'success', + 'tiny', + ]); + expect(empty).toEqual([]); + }); + + it('normalizes states properly', () => { + const active = Service.normalizeStates([ + 'active', + ]); + const activeChecked = Service.normalizeStates([ + 'active', + 'checked', + ]); + const activeCheckedDisabled = Service.normalizeStates([ + 'active', + 'checked', + 'disabled', + ]); + const withDuplicates = Service.normalizeStates([ + 'active', + 'checked', + 'active', + ]); + const withNulls = Service.normalizeStates([ + 'active', + undefined, + 'checked', + null, + ]); + const empty = Service.normalizeStates([ + '', + ]); + const customSeparator = Service.normalizeStates([ + 'active', + 'checked', + ], '-'); + + expect(active).toEqual([ + 'active', + ]); + expect(activeChecked).toEqual([ + 'active', + 'checked', + 'active.checked', + ]); + expect(activeCheckedDisabled).toEqual([ + 'active', + 'checked', + 'active.checked', + 'disabled', + 'active.checked.disabled', + ]); + expect(withDuplicates).toEqual([ + 'active', + 'checked', + 'active.checked', + ]); + expect(withNulls).toEqual([ + 'active', + 'checked', + 'active.checked', + ]); + expect(empty).toEqual([]); + expect(customSeparator).toEqual([ + 'active', + 'checked', + 'active-checked', + ]); + }); + + it('creates styles for default appearance properly', () => { + const style = Service.createStyle(theme, testMapping); + const withVariant = Service.createStyle( + theme, + testMapping, + APPEARANCE_DEFAULT, + ['success'], + ); + const withVariants = Service.createStyle( + theme, + testMapping, + APPEARANCE_DEFAULT, + ['success', 'big'], + ); + const withState = Service.createStyle( + theme, + testMapping, + APPEARANCE_DEFAULT, + [], + ['active'], + ); + const withStates = Service.createStyle( + theme, + testMapping, + APPEARANCE_DEFAULT, + [], + ['active', 'checked'], + ); + const withVariantAndState = Service.createStyle( + theme, + testMapping, + APPEARANCE_DEFAULT, + ['success'], + ['active'], + ); + const withVariantAndStates = Service.createStyle( + theme, + testMapping, + APPEARANCE_DEFAULT, + ['success'], + ['active', 'checked'], + ); + const withVariantsAndStates = Service.createStyle( + theme, + testMapping, + APPEARANCE_DEFAULT, + ['success', 'big'], + ['active', 'checked'], + ); + + expect(json(style)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#9E9E9E', + selectColor: 'transparent', + })); + expect(json(withVariant)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#9E9E9E', + selectColor: 'transparent', + })); + expect(json(withVariants)).toEqual(json({ + size: 42, + innerSize: 28, + borderWidth: 2, + borderColor: '#9E9E9E', + selectColor: 'transparent', + })); + expect(json(withState)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#616161', + selectColor: 'transparent', + })); + expect(json(withStates)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#1976D2', + selectColor: '#2196F3', + })); + expect(json(withVariantAndState)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#616161', + selectColor: 'transparent', + })); + expect(json(withVariantAndStates)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#00796B', + selectColor: '#009688', + })); + expect(json(withVariantsAndStates)).toEqual(json({ + size: 42, + innerSize: 28, + borderWidth: 2, + borderColor: '#00796B', + selectColor: '#009688', + })); + }); + + it('creates styles for custom appearance properly', () => { + const style = Service.createStyle(theme, testMapping, 'custom'); + const withVariant = Service.createStyle( + theme, + testMapping, + 'custom', + ['success'], + ); + const withVariants = Service.createStyle( + theme, + testMapping, + 'custom', + ['success', 'big'], + ); + const withState = Service.createStyle( + theme, + testMapping, + 'custom', + [], + ['active'], + ); + const withStates = Service.createStyle( + theme, + testMapping, + 'custom', + [], + ['active', 'checked'], + ); + const withVariantAndState = Service.createStyle( + theme, + testMapping, + 'custom', + ['success'], + ['active'], + ); + const withVariantAndStates = Service.createStyle( + theme, + testMapping, + 'custom', + ['success'], + ['active', 'checked'], + ); + const withVariantsAndStates = Service.createStyle( + theme, + testMapping, + 'custom', + ['success', 'big'], + ['active', 'checked'], + ); + + expect(json(style)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 4, + borderColor: '#9E9E9E', + selectColor: 'transparent', + })); + expect(json(withVariant)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 4, + borderColor: '#9E9E9E', + selectColor: 'transparent', + })); + expect(json(withVariants)).toEqual(json({ + size: 42, + innerSize: 28, + borderWidth: 4, + borderColor: '#9E9E9E', + selectColor: 'transparent', + })); + expect(json(withState)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 4, + borderColor: '#616161', + selectColor: 'transparent', + })); + expect(json(withStates)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 4, + borderColor: '#1976D2', + selectColor: '#2196F3', + })); + expect(json(withVariantAndState)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 4, + borderColor: '#616161', + selectColor: 'transparent', + })); + expect(json(withVariantAndStates)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 4, + borderColor: '#00796B', + selectColor: '#009688', + })); + expect(json(withVariantsAndStates)).toEqual(json({ + size: 42, + innerSize: 28, + borderWidth: 4, + borderColor: '#00796B', + selectColor: '#009688', + })); + }); + + it('creates styles for undefined appearance properly', () => { + const style = Service.createStyle(theme, testMapping, 'undefined'); + const withVariant = Service.createStyle( + theme, + testMapping, + 'undefined', + ['success'], + ); + const withVariants = Service.createStyle( + theme, + testMapping, + 'undefined', + ['success', 'big'], + ); + const withState = Service.createStyle( + theme, + testMapping, + 'undefined', + [], + ['active'], + ); + const withStates = Service.createStyle( + theme, + testMapping, + 'undefined', + [], + ['active', 'checked'], + ); + const withVariantAndState = Service.createStyle( + theme, + testMapping, + 'undefined', + ['success'], + ['active'], + ); + const withVariantAndStates = Service.createStyle( + theme, + testMapping, + 'undefined', + ['success'], + ['active', 'checked'], + ); + const withVariantsAndStates = Service.createStyle( + theme, + testMapping, + 'undefined', + ['success', 'big'], + ['active', 'checked'], + ); + + expect(json(style)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#9E9E9E', + selectColor: 'transparent', + })); + expect(json(withVariant)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#9E9E9E', + selectColor: 'transparent', + })); + expect(json(withVariants)).toEqual(json({ + size: 42, + innerSize: 28, + borderWidth: 2, + borderColor: '#9E9E9E', + selectColor: 'transparent', + })); + expect(json(withState)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#616161', + selectColor: 'transparent', + })); + expect(json(withStates)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#1976D2', + selectColor: '#2196F3', + })); + expect(json(withVariantAndState)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#616161', + selectColor: 'transparent', + })); + expect(json(withVariantAndStates)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: '#00796B', + selectColor: '#009688', + })); + expect(json(withVariantsAndStates)).toEqual(json({ + size: 42, + innerSize: 28, + borderWidth: 2, + borderColor: '#00796B', + selectColor: '#009688', + })); + }); + +}); diff --git a/src/framework/theme/tests/style.spec.tsx b/src/framework/theme/tests/style.spec.tsx deleted file mode 100644 index a847d2863..000000000 --- a/src/framework/theme/tests/style.spec.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import * as config from './config'; -import React from 'react'; -import { - View, - TouchableOpacity, -} from 'react-native'; -import { - render, - fireEvent, - waitForElement, -} from 'react-native-testing-library'; -import { - StyleProvider, - StyledComponent, - StyleProviderProps, - StyledComponentProps, - ThemeMappingConfigType, - ThemeType, -} from '../component'; - -const styleConsumerTestId = '@style/consumer'; -const styleTouchableTestId = '@style/touchable'; - -interface ComplexStyleProviderProps { - changedMappings: ThemeMappingConfigType; - changedTheme: ThemeType; -} - -class ComplexStyleProvider extends React.Component { - - state = { - mappings: [], - theme: {}, - }; - - constructor(props) { - super(props); - this.state = { - mappings: this.props.mapping, - theme: this.props.theme, - }; - } - - onTouchablePress = () => { - this.setState({ - mappings: this.props.changedMappings, - theme: this.props.changedTheme, - }); - }; - - render() { - return ( - - - {this.props.children} - - - ); - } -} - -type TestComponentProps = any; - -class Test extends React.Component { - static defaultProps = { - testID: styleConsumerTestId, - }; - - render() { - return ( - - ); - } -} - -describe('@style: style consumer checks', () => { - - it('receives custom props', async () => { - const StyleConsumer = StyledComponent(Test); - - const component = render( - - - , - ); - - const styledComponent = component.getByTestId(styleConsumerTestId); - expect(styledComponent.props.variant).not.toBeNull(); - expect(styledComponent.props.variant).not.toBeUndefined(); - expect(styledComponent.props.theme).not.toBeNull(); - expect(styledComponent.props.theme).not.toBeUndefined(); - expect(styledComponent.props.themedStyle).not.toBeNull(); - expect(styledComponent.props.themedStyle).not.toBeUndefined(); - expect(styledComponent.props.requestStateStyle).not.toBeNull(); - expect(styledComponent.props.requestStateStyle).not.toBeUndefined(); - }); - - it('default variant styled properly', async () => { - const StyleConsumer = StyledComponent(Test); - - const component = render( - - - , - ); - - const styledComponent = component.getByTestId(styleConsumerTestId); - expect(styledComponent.props.themedStyle.backgroundColor).toEqual(config.values.backgroundDefault); - expect(styledComponent.props.themedStyle.textColor).toEqual(config.values.textDefault); - }); - - it('list of non-default variants styled properly', async () => { - const StyleConsumer = StyledComponent(Test); - - const component = render( - - - , - ); - - const styledComponent = component.getByTestId(styleConsumerTestId); - expect(styledComponent.props.themedStyle.backgroundColor).toEqual(config.values.backgroundDark); - expect(styledComponent.props.themedStyle.textColor).toEqual(config.values.textSuccess); - }); - - it('style request works properly', async () => { - const StyleConsumer = StyledComponent(Test); - - const component = render( - - - , - ); - - const styledComponent = component.getByTestId(styleConsumerTestId); - const stateStyle = styledComponent.props.requestStateStyle(['active']); - const undefinedStateStyle = styledComponent.props.requestStateStyle('undefined'); - - expect(stateStyle).not.toBeNull(); - expect(stateStyle).not.toBeUndefined(); - expect(stateStyle.backgroundColor).toEqual(config.values.backgroundDefault); - expect(stateStyle.textColor).toEqual(config.values.textSuccessActive); - - expect(undefinedStateStyle).not.toBeNull(); - expect(undefinedStateStyle).not.toBeUndefined(); - expect(undefinedStateStyle.backgroundColor).toEqual(config.values.backgroundDark); - expect(undefinedStateStyle.textColor).toEqual(config.values.textSuccess); - - styledComponent.props.requestStateStyle([]); - jest.spyOn(console, 'warn'); - }); - - it('static methods are copied over', async () => { - // @ts-ignore: test-case - Test.staticMethod = function() {}; - const StyleConsumer = StyledComponent(Test); - - // @ts-ignore: test-case - expect(StyleConsumer.staticMethod).not.toBeUndefined(); - }); - -}); - -describe('@style: complex hierarchy checks', async () => { - - it('@style: provides correct styles on mapping/theme change', async () => { - const StyleConsumer = StyledComponent(Test); - - const component = render( - - - , - ); - const styledComponent = component.getByTestId(styleConsumerTestId); - - const { themedStyle: initialStyle } = styledComponent.props; - expect(initialStyle.backgroundColor).toEqual(config.theme.backgroundColorTestDefault); - - const touchableComponent = component.getByTestId(styleTouchableTestId); - - fireEvent.press(touchableComponent); - - const styledComponentChanged = await waitForElement(() => { - return component.getByTestId(styleConsumerTestId); - }); - - const { themedStyle: changedStyle } = styledComponentChanged.props; - expect(changedStyle.backgroundColor).toEqual(config.themeInverse.backgroundColorTestDefault); - }); - -}); diff --git a/src/framework/theme/tests/styleConsumer.spec.tsx b/src/framework/theme/tests/styleConsumer.spec.tsx new file mode 100644 index 000000000..956d10f09 --- /dev/null +++ b/src/framework/theme/tests/styleConsumer.spec.tsx @@ -0,0 +1,262 @@ +import React from 'react'; +import { + TouchableOpacity, + View, + ViewProps, +} from 'react-native'; +import { + fireEvent, + render, + waitForElement, +} from 'react-native-testing-library'; +import { + styled, + StyledComponentProps, + StyleProvider, + StyleProviderProps, + ThemeType, +} from '../component'; +import { + StyleConsumerService, + APPEARANCE_DEFAULT, +} from '../service'; +import { + mapping, + theme, + themeInverse, +} from './config'; + +describe('@style: service methods check', () => { + + const { Test: testMapping, Empty: emptyMapping } = mapping; + const service: StyleConsumerService = new StyleConsumerService(); + + it('retrieves variant prop keys properly', () => { + const defaultAppearanceKeys = service.getVariantPropKeys(testMapping, { + appearance: APPEARANCE_DEFAULT, + checked: false, + status: 'info', + size: 'small', + }); + const customAppearanceKeys = service.getVariantPropKeys(testMapping, { + appearance: 'custom', + checked: false, + size: 'small', + }); + const undefinedAppearanceKeys = service.getVariantPropKeys(testMapping, { + appearance: 'undefined', + checked: false, + status: 'info', + }); + const emptyAppearanceKeys = service.getVariantPropKeys(emptyMapping, { + appearance: 'default', + checked: false, + status: 'info', + }); + + expect(defaultAppearanceKeys).toEqual(['info', 'small']); + expect(customAppearanceKeys).toEqual(['small']); + expect(undefinedAppearanceKeys).toEqual(['info']); + expect(emptyAppearanceKeys).toEqual([]); + }); + +}); + +describe('@style: ui component checks', () => { + + const styleConsumerTestId = '@style/consumer'; + const styleTouchableTestId = '@style/touchable'; + + const json = (object: any) => JSON.stringify(object); + + interface ComplexStyleProviderProps { + themeInverse: ThemeType; + } + + class ComplexStyleProvider extends React.Component { + + state = { + mappings: [], + theme: {}, + }; + + constructor(props) { + super(props); + this.state = { + mappings: this.props.mapping, + theme: this.props.theme, + }; + } + + onTouchablePress = () => { + this.setState({ + theme: this.props.themeInverse, + }); + }; + + render() { + return ( + + + {this.props.children} + + + ); + } + } + + interface TestComponentProps { + status?: string | 'success'; + checked?: boolean; + } + + class Test extends React.Component { + static defaultProps = { + testID: styleConsumerTestId, + }; + + render() { + return ( + + ); + } + } + + it('static methods are copied over', async () => { + // @ts-ignore: test-case + Test.staticMethod = function () { + }; + const StyleConsumer = styled(Test); + + // @ts-ignore: test-case + expect(StyleConsumer.staticMethod).not.toBeUndefined(); + }); + + it('receives custom props', async () => { + const StyleConsumer = styled(Test); + + const component = render( + + + , + ); + + const styledComponent = component.getByTestId(styleConsumerTestId); + expect(styledComponent.props.appearance).not.toBeNull(); + expect(styledComponent.props.appearance).not.toBeUndefined(); + expect(styledComponent.props.theme).not.toBeNull(); + expect(styledComponent.props.theme).not.toBeUndefined(); + expect(styledComponent.props.themedStyle).not.toBeNull(); + expect(styledComponent.props.themedStyle).not.toBeUndefined(); + expect(styledComponent.props.requestStateStyle).not.toBeNull(); + expect(styledComponent.props.requestStateStyle).not.toBeUndefined(); + }); + + it('default appearance styled properly', async () => { + const StyleConsumer = styled(Test); + + const component = render( + + + , + ); + const withAppearance = render( + + + , + ); + + const styledComponent = component.getByTestId(styleConsumerTestId); + const withAppearanceComponent = withAppearance.getByTestId(styleConsumerTestId); + + expect(json(styledComponent.props.themedStyle)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: theme.grayPrimary, + selectColor: 'transparent', + })); + expect(json(withAppearanceComponent.props.themedStyle)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: theme.grayPrimary, + selectColor: 'transparent', + })); + }); + + it('style request works properly', async () => { + const StyleConsumer = styled(Test); + + const component = render( + + + , + ); + + const styledComponent = component.getByTestId(styleConsumerTestId); + const stateStyle = styledComponent.props.requestStateStyle(['active']); + const undefinedStateStyle = styledComponent.props.requestStateStyle('undefined'); + + expect(json(stateStyle)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: theme.grayDark, + selectColor: 'transparent', + })); + expect(json(undefinedStateStyle)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: theme.grayPrimary, + selectColor: 'transparent', + })); + + styledComponent.props.requestStateStyle([]); + jest.spyOn(console, 'warn'); + }); + + it('@style: provides correct styles on theme change', async () => { + const StyleConsumer = styled(Test); + + const component = render( + + + , + ); + const styledComponent = component.getByTestId(styleConsumerTestId); + + expect(json(styledComponent.props.themedStyle)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: theme.grayPrimary, + selectColor: 'transparent', + })); + + const touchableComponent = component.getByTestId(styleTouchableTestId); + + fireEvent.press(touchableComponent); + + const styledComponentChanged = await waitForElement(() => { + return component.getByTestId(styleConsumerTestId); + }); + + expect(json(styledComponentChanged.props.themedStyle)).toEqual(json({ + size: 36, + innerSize: 24, + borderWidth: 2, + borderColor: themeInverse.grayPrimary, + selectColor: 'transparent', + })); + }); + +}); diff --git a/src/framework/theme/tests/theme.spec.tsx b/src/framework/theme/tests/theme.spec.tsx index 207203ffa..9f1f8081a 100644 --- a/src/framework/theme/tests/theme.spec.tsx +++ b/src/framework/theme/tests/theme.spec.tsx @@ -1,4 +1,3 @@ -import * as config from './config'; import React from 'react'; import { View, @@ -14,154 +13,139 @@ import { withStyles, ThemeType, } from '../component'; -import { createStyle } from '../service'; +import { getThemeValue } from '../service'; +import { + theme, + themeInverse, +} from './config'; -const themeConsumerTestId = '@theme/consumer'; -const themeChangeTouchableTestId = '@theme/btnChangeTheme'; +describe('@theme: service method checks', () => { -class ThemedConsumer extends React.Component { - static defaultProps = { - testID: themeConsumerTestId, - }; + it('finds theme value properly', async () => { + const themeValue = getThemeValue('grayPrimary', theme); + const undefinedValue = getThemeValue('undefined', theme); - render() { - return ( - - ); - } -} - -class ThemedStyleConsumer extends React.Component { - static defaultProps = { - testID: themeConsumerTestId, - }; - - render() { - return ( - - ); - } -} + expect(themeValue).toEqual(theme.grayPrimary); + expect(undefinedValue).toBeUndefined(); + }); -class ActionedProvider extends React.Component { +}); - static initialThemeColor = '#ffffff'; - static onChangeThemeColor = '#000000'; +describe('@theme: ui component checks', () => { - state = { - theme: { - backgroundColor: ActionedProvider.initialThemeColor, - }, - }; + const themeConsumerTestId = '@theme/consumer'; + const themeChangeTouchableTestId = '@theme/btnChangeTheme'; - isInitialColor = (color: string): boolean => color === ActionedProvider.initialThemeColor; + const json = (object: any) => JSON.stringify(object); - getInversedColor = (color: string): string => { - const isInitialColor = this.isInitialColor(color); - return isInitialColor ? ActionedProvider.onChangeThemeColor : ActionedProvider.initialThemeColor; - }; + class ComponentMock extends React.Component { + static defaultProps = { + testID: themeConsumerTestId, + }; - onThemeChangeTouchablePress = () => { - this.setState({ - theme: { - backgroundColor: this.getInversedColor(this.state.theme.backgroundColor), - }, - }); - }; - - render() { - const ThemedComponent = withStyles(ThemedConsumer); - return ( - - - - - - - ); + render() { + return ( + + ); + } } -} - -export class ThemedStyleProvider extends React.Component { - createThemedComponent1Styles = (theme: ThemeType) => ({ - container: { - backgroundColor: theme.color, - }, - }); + interface ActionMockProps { + theme: ThemeType; + themeInverse: ThemeType; + } - createThemedComponent2Styles = (theme: ThemeType) => ({ - container: { - backgroundColor: theme.color, - }, - }); + class ActionMock extends React.Component { - render() { - const ThemedComponent1 = withStyles(ThemedStyleConsumer, this.createThemedComponent1Styles); - const ThemedComponent2 = withStyles(ThemedStyleConsumer, this.createThemedComponent2Styles); - return ( - - - - - - - ); - } -} + state = { + theme: undefined, + }; -describe('@theme: theme consumer checks', () => { + constructor(props) { + super(props); + this.state.theme = this.props.theme; + } - it('renders properly', async () => { - const ThemedComponent = withStyles(ThemedConsumer); + onThemeChangeTouchablePress = () => { + this.setState({ + theme: this.props.themeInverse, + }); + }; - const component = render( - - - , - ); + render() { + const ThemedComponent = withStyles(ComponentMock); + return ( + + + + + + + ); + } + } - const themedComponent = component.getByTestId(themeConsumerTestId); - expect(themedComponent).not.toBeNull(); - }); + interface OverrideMockProps { + theme: ThemeType; + themeInverse: ThemeType; + } - it('receives theme prop', async () => { - const ThemedComponent = withStyles(ThemedConsumer); + class OverrideMock extends React.Component { + + render() { + const ThemedComponent1 = withStyles(ComponentMock); + const ThemedComponent2 = withStyles(ComponentMock); + return ( + + + + + + + ); + } + } - const component = render( - - - , - ); + it('static methods are copied over', async () => { + // @ts-ignore: test-case + ComponentMock.staticMethod = function () { + }; + const ThemedComponent = withStyles(ComponentMock); - const themedComponent = component.getByTestId(themeConsumerTestId); - expect(themedComponent.props.theme).not.toBeNull(); + // @ts-ignore: test-case + expect(ThemedComponent.staticMethod).not.toBeUndefined(); }); - it('receives themedStyle prop', async () => { - const ThemedComponent = withStyles(ThemedConsumer, (theme: ThemeType) => { - return {}; - }); + it('receives custom props', async () => { + const ThemedComponent = withStyles(ComponentMock, (value: ThemeType) => ({ + container: { + backgroundColor: value.grayPrimary, + }, + })); const component = render( - + , ); const themedComponent = component.getByTestId(themeConsumerTestId); + + expect(json(themedComponent.props.theme)).toEqual(json(theme)); + expect(themedComponent.props.themedStyle.container.backgroundColor).toEqual(theme.grayPrimary); expect(themedComponent.props.themedStyle).not.toBeNull(); + expect(themedComponent.props.themedStyle).not.toBeUndefined(); }); - it('receives theme prop on theme change', async () => { + it('able to change theme', async () => { const component = render( - , + , ); const touchableComponent = component.getByTestId(themeChangeTouchableTestId); @@ -172,158 +156,27 @@ describe('@theme: theme consumer checks', () => { return component.getByTestId(themeConsumerTestId); }); - expect(themedComponent.props.theme.backgroundColor).toEqual(ActionedProvider.onChangeThemeColor); + expect(json(themedComponent.props.theme)).toEqual(json(themeInverse)); }); - it('child theme provider overrides parent theme', async () => { + it('able to override theme', async () => { const component = render( - , ); - const themedComponents = component.getAllByName(ThemedStyleConsumer); + const themedComponents = component.getAllByName(ComponentMock); expect(themedComponents.length).toBeGreaterThan(1); - const themedComponent1Color = themedComponents[0].props.themedStyle.container.backgroundColor; - const themedComponent2Color = themedComponents[1].props.themedStyle.container.backgroundColor; - - expect(themedComponent1Color).not.toEqual(themedComponent2Color); - }); - - it('static methods are copied over', async () => { - // @ts-ignore: test-case - ThemedConsumer.staticMethod = function () { - }; - const ThemedComponent = withStyles(ThemedConsumer); + const theme1 = themedComponents[0].props.theme; + const theme2 = themedComponents[1].props.theme; - // @ts-ignore: test-case - expect(ThemedComponent.staticMethod).not.toBeUndefined(); + expect(theme1).not.toEqual(theme2); + expect(json(theme1)).toEqual(json(theme)); + expect(json(theme2)).toEqual(json(themeInverse)); }); }); - -describe('@theme: service methods checks', () => { - - it('default variant styled properly', async () => { - const style = createStyle(config.theme, config.mappings.Test); - const withState = createStyle(config.theme, config.mappings.Test, 'default', 'active'); - const withUndefinedState = createStyle(config.theme, config.mappings.Test, 'default', 'undefined'); - - expect(style).not.toBeNull(); - expect(style).not.toBeUndefined(); - expect(style.backgroundColor).toEqual(config.values.backgroundDefault); - - expect(withState).not.toBeNull(); - expect(withState).not.toBeUndefined(); - expect(withState.backgroundColor).toEqual(config.values.backgroundDark); - - expect(withUndefinedState).not.toBeNull(); - expect(withUndefinedState).not.toBeUndefined(); - expect(withUndefinedState.backgroundColor).toEqual(config.values.backgroundDefault); - }); - - it('single non-default variant styled properly (string type)', async () => { - const style = createStyle(config.theme, config.mappings.Test, 'dark'); - const withState = createStyle(config.theme, config.mappings.Test, 'dark', 'active'); - const withUndefinedState = createStyle(config.theme, config.mappings.Test, 'dark', 'undefined'); - - expect(style).not.toBeNull(); - expect(style).not.toBeUndefined(); - expect(style.backgroundColor).toEqual(config.values.backgroundDark); - - expect(withState).not.toBeNull(); - expect(withState).not.toBeUndefined(); - expect(withState.backgroundColor).toEqual(config.values.backgroundDefault); - - expect(withUndefinedState).not.toBeNull(); - expect(withUndefinedState).not.toBeUndefined(); - expect(withUndefinedState.backgroundColor).toEqual(config.values.backgroundDark); - }); - - it('list of non-default variants styled created properly (string type)', async () => { - const style = createStyle(config.theme, config.mappings.Test, 'dark success'); - const withState = createStyle(config.theme, config.mappings.Test, 'dark success', 'active disabled'); - const withOneOfUndefined = createStyle(config.theme, config.mappings.Test, 'dark success', 'active '); - const withUndefinedState = createStyle(config.theme, config.mappings.Test, 'dark success', 'undefined'); - - expect(style).not.toBeNull(); - expect(style).not.toBeUndefined(); - expect(style.backgroundColor).toEqual(config.values.backgroundDark); - expect(style.textColor).toEqual(config.values.textSuccess); - - expect(withState).not.toBeNull(); - expect(withState).not.toBeUndefined(); - expect(withState.backgroundColor).toEqual(config.values.backgroundSuccessDisabled); - expect(withState.textColor).toEqual(config.values.textSuccessActive); - - expect(withOneOfUndefined).not.toBeNull(); - expect(withOneOfUndefined).not.toBeUndefined(); - expect(withOneOfUndefined.backgroundColor).toEqual(config.values.backgroundDefault); - expect(withOneOfUndefined.textColor).toEqual(config.values.textSuccessActive); - - expect(withUndefinedState).not.toBeNull(); - expect(withUndefinedState).not.toBeUndefined(); - expect(withUndefinedState.backgroundColor).toEqual(config.values.backgroundDark); - expect(withUndefinedState.textColor).toEqual(config.values.textSuccess); - }); - - it('single non-default variant styled properly (string[] type)', async () => { - const style = createStyle(config.theme, config.mappings.Test, ['dark']); - const withState = createStyle(config.theme, config.mappings.Test, ['dark'], ['active']); - const withOneOfUndefined = createStyle(config.theme, config.mappings.Test, ['dark'], ['active', undefined]); - const withUndefinedState = createStyle(config.theme, config.mappings.Test, ['dark'], ['undefined']); - - expect(style).not.toBeNull(); - expect(style).not.toBeUndefined(); - expect(style.backgroundColor).toEqual(config.values.backgroundDark); - - expect(withState).not.toBeNull(); - expect(withState).not.toBeUndefined(); - expect(withState.backgroundColor).toEqual(config.values.backgroundDefault); - - expect(withOneOfUndefined).not.toBeNull(); - expect(withOneOfUndefined).not.toBeUndefined(); - expect(withOneOfUndefined.backgroundColor).toEqual(config.values.backgroundDefault); - - expect(withUndefinedState).not.toBeNull(); - expect(withUndefinedState).not.toBeUndefined(); - expect(withUndefinedState.backgroundColor).toEqual(config.values.backgroundDark); - }); - - it('array of non-default variants styled properly (string[] type)', async () => { - const style = createStyle(config.theme, config.mappings.Test, ['dark', 'success']); - const withState = createStyle(config.theme, config.mappings.Test, ['dark', 'success'], ['active', 'disabled']); - const withOneOfUndefined = createStyle( - config.theme, - config.mappings.Test, - ['dark', 'success'], - ['active', undefined], - ); - const withUndefinedState = createStyle(config.theme, config.mappings.Test, ['dark', 'success'], ['undefined']); - - expect(style).not.toBeNull(); - expect(style).not.toBeUndefined(); - expect(style.backgroundColor).toEqual(config.values.backgroundDark); - expect(style.textColor).toEqual(config.values.textSuccess); - - expect(withState).not.toBeNull(); - expect(withState).not.toBeUndefined(); - expect(withState.backgroundColor).toEqual(config.values.backgroundSuccessDisabled); - expect(withState.textColor).toEqual(config.values.textSuccessActive); - - expect(withOneOfUndefined).not.toBeNull(); - expect(withOneOfUndefined).not.toBeUndefined(); - expect(withOneOfUndefined.backgroundColor).toEqual(config.values.backgroundDefault); - expect(withOneOfUndefined.textColor).toEqual(config.values.textSuccessActive); - - expect(withUndefinedState).not.toBeNull(); - expect(withUndefinedState).not.toBeUndefined(); - expect(withUndefinedState.backgroundColor).toEqual(config.values.backgroundDark); - expect(withUndefinedState.textColor).toEqual(config.values.textSuccess); - }); - -}); - diff --git a/src/framework/tsconfig.json b/src/framework/tsconfig.json index 6d3419de8..dad45b427 100644 --- a/src/framework/tsconfig.json +++ b/src/framework/tsconfig.json @@ -11,6 +11,7 @@ "jsx": "react-native", "module": "es2015", "target": "es2017", + "experimentalDecorators": true, "lib": [ "es2015", "es2016" diff --git a/src/framework/ui/index.ts b/src/framework/ui/index.ts index e69de29bb..ae9ecd275 100644 --- a/src/framework/ui/index.ts +++ b/src/framework/ui/index.ts @@ -0,0 +1,6 @@ +import { styled } from '@rk-kit/theme'; +import { Radio, Props } from './radio/radio.component'; + +const StyledRadio = styled(Radio); +export { StyledRadio as Radio, Props as RadioProps }; + diff --git a/src/framework/ui/radio/radio.component.tsx b/src/framework/ui/radio/radio.component.tsx new file mode 100644 index 000000000..075ca06e3 --- /dev/null +++ b/src/framework/ui/radio/radio.component.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { + TouchableOpacity, + View, + StyleSheet, + TouchableOpacityProps, +} from 'react-native'; +import { + APPEARANCE_DEFAULT, + StyledComponentProps, + StyleType, +} from '@rk-kit/theme'; + +interface RadioProps { + checked?: boolean; + onChange?: (selected: boolean) => void; + appearance?: string | 'default'; + status?: string | 'error'; + size?: string | 'big' | 'small'; +} + +export type Props = RadioProps & StyledComponentProps & TouchableOpacityProps; + +interface State { + active: boolean; +} + +export class Radio extends React.Component { + + static defaultProps: Props = { + appearance: APPEARANCE_DEFAULT, + checked: false, + }; + + state: State = { + active: false, + }; + + shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean { + const selectedChanged = nextProps.checked !== this.props.checked; + const activeChanged = nextState.active !== this.state.active; + const disabledChanged = nextProps.disabled !== this.props.disabled; + return selectedChanged || activeChanged || disabledChanged; + } + + onPress = () => { + this.props.onChange && this.props.onChange(this.props.checked); + }; + + onPressIn = () => { + this.setState({ + active: true, + }); + }; + + onPressOut = () => { + this.setState({ + active: false, + }); + }; + + isStateStyle = (): boolean => { + return this.state.active || this.props.checked || this.props.disabled; + }; + + getStateStyle = (): StyleType => { + return this.props.requestStateStyle([ + this.state.active && 'active', + this.props.checked && 'checked', + this.props.disabled && 'disabled', + ]); + }; + + getComponentStyle = (): StyleType => { + const style = this.isStateStyle() ? this.getStateStyle() : this.props.themedStyle; + return ({ + border: { + width: style.size, + height: style.size, + borderRadius: style.size / 2, + borderWidth: style.borderWidth, + borderColor: style.borderColor, + }, + select: { + width: style.innerSize, + height: style.innerSize, + borderRadius: style.innerSize / 2, + backgroundColor: style.selectColor, + }, + highlight: { + width: style.highlightSize, + height: style.highlightSize, + borderRadius: style.highlightSize / 2, + backgroundColor: style.highlightColor, + opacity: this.state.active ? 0.4 : 0.0, + }, + }); + }; + + render() { + const componentStyle = this.getComponentStyle(); + return ( + + + + + + + + + ); + } +} + +const styles = StyleSheet.create({ + border: { + justifyContent: 'center', + alignItems: 'center', + }, + highlight: { + position: 'absolute', + alignSelf: 'center', + }, +}); diff --git a/src/framework/ui/tests/__snapshots__/radio.spec.tsx.snap b/src/framework/ui/tests/__snapshots__/radio.spec.tsx.snap new file mode 100644 index 000000000..29ab3b555 --- /dev/null +++ b/src/framework/ui/tests/__snapshots__/radio.spec.tsx.snap @@ -0,0 +1,715 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`@radio: matches snapshot active 1`] = ` + + + + + + + + +`; + +exports[`@radio: matches snapshot active checked 1`] = ` + + + + + + + + +`; + +exports[`@radio: matches snapshot active checked: checked 1`] = ` + + + + + + + + +`; + +exports[`@radio: matches snapshot active: default 1`] = ` + + + + + + + + +`; + +exports[`@radio: matches snapshot checked 1`] = ` + + + + + + + + +`; + +exports[`@radio: matches snapshot checked disabled 1`] = ` + + + + + + + + +`; + +exports[`@radio: matches snapshot default 1`] = ` + + + + + + + + +`; + +exports[`@radio: matches snapshot disabled 1`] = ` + + + + + + + + +`; diff --git a/src/framework/ui/tests/config/index.ts b/src/framework/ui/tests/config/index.ts new file mode 100644 index 000000000..cbc0a33c3 --- /dev/null +++ b/src/framework/ui/tests/config/index.ts @@ -0,0 +1,3 @@ +export { default as mapping } from './mapping.json'; +export { default as theme } from './theme.json'; + diff --git a/src/framework/ui/tests/config/mapping.json b/src/framework/ui/tests/config/mapping.json new file mode 100644 index 000000000..ac773265b --- /dev/null +++ b/src/framework/ui/tests/config/mapping.json @@ -0,0 +1,70 @@ +{ + "Radio": { + "appearance": { + "default": { + "mapping": { + "size": 36, + "innerSize": 24, + "highlightSize": 60, + "borderWidth": 2, + "borderColor": "gray-primary", + "selectColor": "transparent", + "highlightColor": "transparent", + "state": { + "active": { + "borderColor": "gray-dark", + "highlightColor": "gray-light" + }, + "checked": { + "borderColor": "blue-primary", + "selectColor": "blue-primary" + }, + "disabled": { + "borderColor": "gray-light" + }, + "active.checked": { + "borderColor": "blue-dark" + }, + "checked.disabled": { + "selectColor": "gray-primary" + } + } + }, + "variant": { + "status": { + "error": { + "mapping": { + "borderColor": "pink-primary", + "state": { + "checked": { + "borderColor": "pink-primary", + "selectColor": "pink-primary" + }, + "active.checked": { + "borderColor": "pink-primary" + } + } + } + } + }, + "size": { + "big": { + "mapping": { + "size": 42, + "innerSize": 28, + "highlightSize": 70 + } + }, + "small": { + "mapping": { + "size": 30, + "innerSize": 20, + "highlightSize": 50 + } + } + } + } + } + } + } +} diff --git a/src/framework/ui/tests/config/theme.json b/src/framework/ui/tests/config/theme.json new file mode 100644 index 000000000..aef22263b --- /dev/null +++ b/src/framework/ui/tests/config/theme.json @@ -0,0 +1,9 @@ +{ + "blue-primary": "#3366FF", + "blue-dark": "#2541CC", + "gray-light": "#DDE1EB", + "gray-primary": "#A6AEBD", + "gray-dark": "#8992A3", + "gray-highlight": "#EDF0F5", + "pink-primary": "#FF3D71" +} diff --git a/src/framework/ui/tests/radio.spec.tsx b/src/framework/ui/tests/radio.spec.tsx new file mode 100644 index 000000000..e8d3960c5 --- /dev/null +++ b/src/framework/ui/tests/radio.spec.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { TouchableOpacity } from 'react-native'; +import { + render, + fireEvent, + waitForElement, + shallow, + RenderAPI, +} from 'react-native-testing-library'; +import { + styled, + StyleProvider, + StyleProviderProps, +} from '@rk-kit/theme'; +import { + Radio, + Props, +} from '../radio/radio.component'; +import { + mapping, + theme, +} from './config'; + +const StyledComponent = styled(Radio); + +const Mock = (props?: Props): React.ReactElement => ( + + + +); + +const renderComponent = (props?: Props): RenderAPI => render(); + +describe('@radio: matches snapshot', () => { + + it('default', () => { + const component = renderComponent(); + const { output } = shallow(component.getByType(Radio)); + + expect(output).toMatchSnapshot(); + }); + + it('checked', () => { + const component = renderComponent({checked: true}); + const { output } = shallow(component.getByType(Radio)); + + expect(output).toMatchSnapshot(); + }); + + it('disabled', () => { + const component = renderComponent({disabled: true}); + const { output } = shallow(component.getByType(Radio)); + + expect(output).toMatchSnapshot(); + }); + + it('checked disabled', () => { + const component = renderComponent({checked: true, disabled: true}); + const { output } = shallow(component.getByType(Radio)); + + expect(output).toMatchSnapshot(); + }); + + it('active', async () => { + const component = renderComponent(); + + fireEvent(component.getByType(TouchableOpacity), 'pressIn'); + + const active = await waitForElement(() => component.getByType(Radio)); + const { output: activeOutput } = shallow(active); + + fireEvent(component.getByType(TouchableOpacity), 'pressOut'); + + const inactive = await waitForElement(() => component.getByType(Radio)); + const { output: inactiveOutput } = shallow(inactive); + + expect(activeOutput).toMatchSnapshot(); + expect(inactiveOutput).toMatchSnapshot('default'); + }); + + it('active checked', async () => { + const component = renderComponent({checked: true}); + + fireEvent(component.getByType(TouchableOpacity), 'pressIn'); + const active = await waitForElement(() => component.getByType(Radio)); + const { output: activeOutput } = shallow(active); + + fireEvent(component.getByType(TouchableOpacity), 'pressOut'); + + const inactive = await waitForElement(() => component.getByType(Radio)); + const { output: inactiveOutput } = shallow(inactive); + + expect(activeOutput).toMatchSnapshot(); + expect(inactiveOutput).toMatchSnapshot('checked'); + }); + +}); + +describe('@radio: component checks', () => { + + it('emits onChange', () => { + const onChange = jest.fn(); + const component = renderComponent({ onChange: onChange }); + fireEvent.press(component.getByType(TouchableOpacity)); + + expect(onChange).toBeCalled(); + }); + +}); diff --git a/src/playground/package-lock.json b/src/playground/package-lock.json index f9c02eb1a..5a8100e92 100644 --- a/src/playground/package-lock.json +++ b/src/playground/package-lock.json @@ -749,6 +749,50 @@ "tiny-queue": "^0.2.1" } }, + "@react-navigation/core": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-3.0.2.tgz", + "integrity": "sha512-E0ETZJUuJRHvjtb0f0U416NcDxt9T5HvRLxXu5K4DNxtmjpOfkT9Sh+Q309/zrCwSkHY85ZpGKvewZTSGI7Q1Q==", + "requires": { + "create-react-context": "0.2.2", + "hoist-non-react-statics": "^3.0.1", + "path-to-regexp": "^1.7.0", + "query-string": "^6.2.0", + "react-is": "^16.5.2", + "react-lifecycles-compat": "^3.0.4" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz", + "integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==", + "requires": { + "react-is": "^16.3.2" + } + } + } + }, + "@react-navigation/native": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-3.0.3.tgz", + "integrity": "sha512-1T3OnI6DpHPYvrb6OSMvdpcou0NAZKYBeOs66Uimy6oT5tkkj8jwaksAwuSCTIMxaRl1nROPd22yXYq6gBnUVA==", + "requires": { + "hoist-non-react-statics": "^3.0.1", + "react-native-gesture-handler": "^1.0.0", + "react-native-safe-area-view": "^0.11.0", + "react-native-screens": "^1.0.0 || ^1.0.0-alpha" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz", + "integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==", + "requires": { + "react-is": "^16.3.2" + } + } + } + }, "@types/fbemitter": { "version": "2.0.32", "resolved": "http://registry.npmjs.org/@types/fbemitter/-/fbemitter-2.0.32.tgz", @@ -2435,6 +2479,15 @@ "object-assign": "^4.1.1" } }, + "create-react-context": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.2.tgz", + "integrity": "sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==", + "requires": { + "fbjs": "^0.8.0", + "gud": "^1.0.0" + } + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -3780,6 +3833,11 @@ "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -5315,6 +5373,21 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, "path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", @@ -5452,6 +5525,15 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, + "query-string": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.2.0.tgz", + "integrity": "sha512-5wupExkIt8RYL4h/FE+WTg3JHk62e6fFPWtAZA9J5IWK1PfTfKkMS93HBUHcFpeYi9KsY5pFbh+ldvEyaz5MyA==", + "requires": { + "decode-uri-component": "^0.2.0", + "strict-uri-encode": "^2.0.0" + } + }, "randomatic": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", @@ -5526,6 +5608,16 @@ } } }, + "react-is": { + "version": "16.6.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.3.tgz", + "integrity": "sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA==" + }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-native": { "version": "https://github.com/expo/react-native/archive/sdk-31.0.1.tar.gz", "integrity": "sha512-zU2Dtyc6p2yKgkb+rDyk0inqSaG9vyVHpwDt7zpgEHBcmdHqvyTUeiuHHwfEIRgfV8/KxaLGGK4lJ2q4+S86ZQ==", @@ -5683,6 +5775,14 @@ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-1.0.0-alpha.10.tgz", "integrity": "sha512-dbZG/Lh5Q+6zRvS7+gIkZKmXTG7XVqHbpMROL1LApBBMQwuLq/uLtKk/nBSn1+mNmazPrPMTehI7TG3AEkctww==" }, + "react-native-safe-area-view": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.11.0.tgz", + "integrity": "sha512-N3nElaahu1Me2ltnfc9acpgt1znm6pi8DSadKy79kvdzKwvVIzw0IXueA/Hjr51eCW1BsfNw7D1SgBT9U6qEkA==", + "requires": { + "hoist-non-react-statics": "^2.3.1" + } + }, "react-native-safe-module": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/react-native-safe-module/-/react-native-safe-module-1.2.0.tgz", @@ -5706,6 +5806,14 @@ "pegjs": "^0.10.0" } }, + "react-native-tab-view": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.3.1.tgz", + "integrity": "sha512-QNt6VkEW8SP1UJ7yjD5P4bOTWwHQfoIMD5CqnA06pcubdNwHR1NmjiNZsVnIvp5wAEVbW6yTHjLXOh1fzab4xg==", + "requires": { + "prop-types": "^15.6.1" + } + }, "react-native-typescript-transformer": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/react-native-typescript-transformer/-/react-native-typescript-transformer-1.2.10.tgz", @@ -5734,6 +5842,42 @@ "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-2.5.0.tgz", "integrity": "sha512-xFJA+N7wh8Ik/17I4QB24e0a0L3atg1ScVehvtYR5UBTgHdzTFA0ZylvXp9gkZt7V+AT5Pni0H3NQItpqSKFoQ==" }, + "react-navigation": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/react-navigation/-/react-navigation-3.0.8.tgz", + "integrity": "sha512-gU55gHwytRczQnOLatFyF89eI8bv8NivPVoe0cEU8sxCKvX2RbuElGtLxKPWKJiIGz4ZScrmNqiJpkjTsmwiTg==", + "requires": { + "@react-navigation/core": "3.0.2", + "@react-navigation/native": "3.0.3", + "react-navigation-drawer": "1.0.5", + "react-navigation-stack": "1.0.5", + "react-navigation-tabs": "1.0.1" + } + }, + "react-navigation-drawer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/react-navigation-drawer/-/react-navigation-drawer-1.0.5.tgz", + "integrity": "sha512-WeGrXFn84R75IAt3ndDfkHw9FNvPsi4JPGO1iopqUoA/2tMPA6WJbhuE3dqmmEu3TZRjI+2LatCgpx00tT1kiQ==", + "requires": { + "react-native-tab-view": "^1.2.0" + } + }, + "react-navigation-stack": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/react-navigation-stack/-/react-navigation-stack-1.0.5.tgz", + "integrity": "sha512-X/rsSKD+dvfuDitmAJvqelRjD9hmA5SP7uq7F6CncaUX6M2BLb8Q39KBxcjsBMLVHOSrfOoUq3HciwN1xSBxvg==" + }, + "react-navigation-tabs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-navigation-tabs/-/react-navigation-tabs-1.0.1.tgz", + "integrity": "sha512-XDftTg0sxh2ZMA4yJ4g8POCSova1gJM3heIUUup7/mDeUKcQRZzE9Xf9gQrbZteybJLAxATy+LAjaUpDvvdKmg==", + "requires": { + "hoist-non-react-statics": "^2.5.0", + "prop-types": "^15.6.1", + "react-lifecycles-compat": "^3.0.4", + "react-native-tab-view": "^1.0.0" + } + }, "react-proxy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-1.1.8.tgz", @@ -6750,6 +6894,11 @@ "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", "integrity": "sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ=" }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", diff --git a/src/playground/src/app.component.tsx b/src/playground/src/app.component.tsx index 5c9e96098..9a2a193b7 100644 --- a/src/playground/src/app.component.tsx +++ b/src/playground/src/app.component.tsx @@ -1,18 +1,18 @@ import React from 'react'; import { StyleProvider, - ThemeMappingConfigType, + ThemeMappingType, ThemeType, } from '@rk-kit/theme'; import { - Mappings, - Theme, + mapping, + theme, } from './theme-token'; import { withNavigation } from './navigation'; import * as Screens from './ui/screen'; interface State { - mappings: ThemeMappingConfigType; + mapping: ThemeMappingType; theme: ThemeType; } @@ -21,16 +21,16 @@ export default class App extends React.Component { constructor(props) { super(props); this.state = { - mappings: Mappings, - theme: Theme, + mapping: mapping, + theme: theme, }; } render() { - const { SampleScreen: RootScreen, ...nestedScreens } = Screens; + const { HomeScreen: RootScreen, ...nestedScreens } = Screens; const Router = withNavigation(RootScreen, nestedScreens); return ( - + ); diff --git a/src/playground/src/theme-token/index.ts b/src/playground/src/theme-token/index.ts index 1f456b43a..aa55e82b2 100644 --- a/src/playground/src/theme-token/index.ts +++ b/src/playground/src/theme-token/index.ts @@ -1,10 +1,2 @@ -/** - * Mocked component mapping configurations - */ -import * as Mappings from './mapping.json'; -import * as Theme from './theme.json'; - -export { - Mappings, - Theme, -}; +export { default as mapping } from './mapping.json'; +export { default as theme } from './theme.json'; diff --git a/src/playground/src/theme-token/mapping.json b/src/playground/src/theme-token/mapping.json index b8258006e..ac773265b 100644 --- a/src/playground/src/theme-token/mapping.json +++ b/src/playground/src/theme-token/mapping.json @@ -1,46 +1,67 @@ { - "Sample": { - "parameters": [ - "backgroundColor", - "textColor" - ], - "states": [ - "active", - "disabled" - ], - "variants": { + "Radio": { + "appearance": { "default": { - "backgroundColor": "background-color-sample-default", - "textColor": "text-color-sample-default", - "state": { - "active": { - "backgroundColor": "background-color-sample-dark", - "textColor": "text-color-sample-dark" - }, - "disabled": { - "textColor": "text-color-sample-default-disabled" - } - } - }, - "dark": { - "backgroundColor": "background-color-sample-dark", - "textColor": "text-color-sample-dark", - "state": { - "active": { - "backgroundColor": "background-color-sample-default", - "textColor": "text-color-sample-default" + "mapping": { + "size": 36, + "innerSize": 24, + "highlightSize": 60, + "borderWidth": 2, + "borderColor": "gray-primary", + "selectColor": "transparent", + "highlightColor": "transparent", + "state": { + "active": { + "borderColor": "gray-dark", + "highlightColor": "gray-light" + }, + "checked": { + "borderColor": "blue-primary", + "selectColor": "blue-primary" + }, + "disabled": { + "borderColor": "gray-light" + }, + "active.checked": { + "borderColor": "blue-dark" + }, + "checked.disabled": { + "selectColor": "gray-primary" + } } - } - }, - "success": { - "textColor": "text-color-sample-success", - "state": { - "active": { - "backgroundColor": "background-color-sample-default", - "textColor": "text-color-sample-success-active" + }, + "variant": { + "status": { + "error": { + "mapping": { + "borderColor": "pink-primary", + "state": { + "checked": { + "borderColor": "pink-primary", + "selectColor": "pink-primary" + }, + "active.checked": { + "borderColor": "pink-primary" + } + } + } + } }, - "disabled": { - "backgroundColor": "background-color-sample-success-disabled" + "size": { + "big": { + "mapping": { + "size": 42, + "innerSize": 28, + "highlightSize": 70 + } + }, + "small": { + "mapping": { + "size": 30, + "innerSize": 20, + "highlightSize": 50 + } + } } } } diff --git a/src/playground/src/theme-token/theme.json b/src/playground/src/theme-token/theme.json index 8cffd8748..aef22263b 100644 --- a/src/playground/src/theme-token/theme.json +++ b/src/playground/src/theme-token/theme.json @@ -1,10 +1,9 @@ { - "background-color-sample-default": "#ffffff", - "background-color-sample-dark": "#000000", - "text-color-sample-default": "#000000", - "text-color-sample-default-disabled": "#9E9E9E", - "text-color-sample-dark": "#ffffff", - "text-color-sample-success": "#4CAF50", - "text-color-sample-success-active": "#81C784", - "background-color-sample-success-disabled": "#F5F5F5" + "blue-primary": "#3366FF", + "blue-dark": "#2541CC", + "gray-light": "#DDE1EB", + "gray-primary": "#A6AEBD", + "gray-dark": "#8992A3", + "gray-highlight": "#EDF0F5", + "pink-primary": "#FF3D71" } diff --git a/src/playground/src/ui/component/index.ts b/src/playground/src/ui/component/index.ts deleted file mode 100644 index f94f09851..000000000 --- a/src/playground/src/ui/component/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { StyledComponent } from '@rk-kit/theme'; -import { Sample, Props } from './sample.component'; - -const StyledSample = StyledComponent(Sample); - -export { - StyledSample as Sample, - Props as SampleProps, -}; diff --git a/src/playground/src/ui/component/sample.component.tsx b/src/playground/src/ui/component/sample.component.tsx deleted file mode 100644 index fb4dd9ab0..000000000 --- a/src/playground/src/ui/component/sample.component.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import { - TouchableWithoutFeedback, - Text, - View, - StyleSheet, -} from 'react-native'; -import { - StyledComponentProps, - StyleType, -} from '@rk-kit/theme'; - -interface SampleProps { - text?: string; - disabled?: boolean; -} - -export type Props = SampleProps & StyledComponentProps; - -interface State { - active: boolean; -} - -export class Sample extends React.Component { - static defaultProps: Props = { - text: `This is React Native UI Kitten playground`, - }; - - state: State = { - active: false, - }; - - onPressIn = () => { - this.setState({ - active: true, - }); - }; - - onPressOut = () => { - this.setState({ - active: false, - }); - }; - - isStateStyle = (): boolean => this.state.active || this.props.disabled; - - getStateStyle = (): StyleType => { - const activeDescription = this.state.active ? 'active' : undefined; - const disabledDescription = this.props.disabled ? 'disabled' : undefined; - return this.props.requestStateStyle([activeDescription, disabledDescription]); - }; - - getComponentStyle = (): StyleType => { - const style = this.isStateStyle() ? this.getStateStyle() : this.props.themedStyle; - return ({ - container: { - backgroundColor: style.backgroundColor, - }, - text: { - color: style.textColor, - }, - }); - }; - - render() { - const componentStyle = this.getComponentStyle(); - return ( - - - {this.props.text} - - - ); - } -} - -const styles = StyleSheet.create({ - container: { - padding: 16, - }, - text: { - textAlign: 'center', - fontSize: 16, - }, -}); diff --git a/src/playground/src/ui/screen/index.ts b/src/playground/src/ui/screen/index.ts index 8d7dbabc3..6d0dd9e71 100644 --- a/src/playground/src/ui/screen/index.ts +++ b/src/playground/src/ui/screen/index.ts @@ -1,2 +1,2 @@ export { HomeScreen } from './home.component'; -export { SampleScreen } from './sample.component'; +export { RadioScreen } from './radio.component'; diff --git a/src/playground/src/ui/screen/radio.component.tsx b/src/playground/src/ui/screen/radio.component.tsx new file mode 100644 index 000000000..408629e24 --- /dev/null +++ b/src/playground/src/ui/screen/radio.component.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { + Text, + View, +} from 'react-native'; +import { NavigationScreenProps } from 'react-navigation'; +import { + withStyles, + ThemeType, + ThemedComponentProps, +} from '@rk-kit/theme'; +import { Radio as RadioComponent } from '@rk-kit/ui'; + +type Props = & ThemedComponentProps & NavigationScreenProps; + +interface State { + isRadio1Checked: boolean; + isRadio2Checked: boolean; + isRadio3Checked: boolean; + isRadio4Checked: boolean; + variant: string; +} + +class Radio extends React.Component { + + static navigationOptions = { + title: 'Radio', + }; + + state: State = { + isRadio1Checked: false, + isRadio2Checked: true, + isRadio3Checked: false, + isRadio4Checked: true, + variant: 'default', + }; + + onRadio1Change = (selected: boolean) => { + this.setState({ isRadio1Checked: !selected }); + }; + + onRadio2Change = (selected: boolean) => { + this.setState({ isRadio2Checked: !selected }); + }; + + onRadio3Change = (selected: boolean) => { + this.setState({ isRadio3Checked: !selected }); + }; + + onRadio4Change = (selected: boolean) => { + this.setState({ isRadio4Checked: !selected }); + }; + + render() { + return ( + + + Interactive + + + + + + + + + Error + + + + + + + + + Size + + + + + + + + ); + } +} + +export const RadioScreen = withStyles(Radio, (theme: ThemeType) => ({ + container: { + paddingVertical: 8, + paddingHorizontal: 16, + }, + containerSection: { + marginVertical: 16, + }, + containerPreview: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 4, + }, + textDescription: { + fontSize: 18, + }, + component: { + marginHorizontal: 4, + }, +})); diff --git a/src/playground/src/ui/screen/sample.component.tsx b/src/playground/src/ui/screen/sample.component.tsx deleted file mode 100644 index 8e3bd992f..000000000 --- a/src/playground/src/ui/screen/sample.component.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { - Button, - View, -} from 'react-native'; -import { NavigationScreenProps } from 'react-navigation'; -import { - withStyles, - ThemeType, - ThemedComponentProps, -} from '@rk-kit/theme'; -import { Sample as SampleComponent } from '../component'; - -type Props = & ThemedComponentProps & NavigationScreenProps; - -interface State { - sampleDisabled: boolean; -} - -class Sample extends React.Component { - - static navigationOptions = { - title: 'Sample', - }; - - state = { - sampleDisabled: false, - }; - - onDisableButtonPress = () => { - this.setState({ - sampleDisabled: !this.state.sampleDisabled, - }); - }; - - getDisableButtonTitle = () => this.state.sampleDisabled ? 'Enable' : 'Disable'; - - render() { - return ( - - -