diff --git a/jest.config.js b/jest.config.js index 421ea85e1..a0e4552a3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,6 @@ const { defaults: tsJestConfig } = require('ts-jest/presets'); +const { pathsToModuleNameMapper } = require('ts-jest/utils'); +const jestConfig = require('./tsconfig.jest'); module.exports = { ...tsJestConfig, @@ -14,6 +16,9 @@ module.exports = { }, }, cacheDirectory: './dist/jest', + moduleNameMapper: pathsToModuleNameMapper(jestConfig.compilerOptions.paths, { + prefix: '/', + }), modulePathIgnorePatterns: [ '/src/playground/' ], diff --git a/src/framework/theme/component/index.ts b/src/framework/theme/component/index.ts index fa2a24fc9..5bb14447d 100644 --- a/src/framework/theme/component/index.ts +++ b/src/framework/theme/component/index.ts @@ -1,4 +1,3 @@ -export { ThemeProvider } from './themeProvider.component'; -export { withTheme } from './themeConsumer.component'; -export { withThemedStyles } from './themeConsumerStyled.component'; -export { ThemeType } from './type'; +export * from './mapping'; +export * from './theme'; +export * from './style'; diff --git a/src/framework/theme/component/mapping/index.ts b/src/framework/theme/component/mapping/index.ts new file mode 100644 index 000000000..b59594a63 --- /dev/null +++ b/src/framework/theme/component/mapping/index.ts @@ -0,0 +1,5 @@ +export * from './type'; +export { + MappingProvider as ThemeMappingProvider, + Props as ThemeMappingProviderProps, +} from './mappingProvider.component'; diff --git a/src/framework/theme/component/mapping/mappingContext.ts b/src/framework/theme/component/mapping/mappingContext.ts new file mode 100644 index 000000000..2249d85d2 --- /dev/null +++ b/src/framework/theme/component/mapping/mappingContext.ts @@ -0,0 +1,7 @@ +import React from 'react'; +import { ThemeMappingType } from './type'; + +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 new file mode 100644 index 000000000..fd0e83e13 --- /dev/null +++ b/src/framework/theme/component/mapping/mappingProvider.component.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import MappingContext from './mappingContext'; +import { ThemeMappingType } from './type'; + +export interface Props { + mapping: ThemeMappingType[]; + children: JSX.Element | React.ReactNode; +} + +export class MappingProvider extends React.PureComponent { + + render() { + return ( + + {this.props.children} + + ); + } +} diff --git a/src/framework/theme/component/mapping/type.ts b/src/framework/theme/component/mapping/type.ts new file mode 100644 index 000000000..49b627dbc --- /dev/null +++ b/src/framework/theme/component/mapping/type.ts @@ -0,0 +1,22 @@ +export interface ThemeMappingType { + name: string; + parameters: ParameterType[]; + variants: VariantType[]; +} + +export interface ParameterType { + name: string; +} + +export interface VariantType { + name: string; + mapping: MappingType[]; +} + +export interface MappingType { + parameter: string; + token: string; +} + +// TODO(mapping/type): declare Token type +export type TokenType = any; diff --git a/src/framework/theme/component/style/index.ts b/src/framework/theme/component/style/index.ts new file mode 100644 index 000000000..a1c3a0479 --- /dev/null +++ b/src/framework/theme/component/style/index.ts @@ -0,0 +1,9 @@ +export { + StyleProvider, + Props as StyleProviderProps, +} from './styleProvider.component'; + +export { + StyledComponent, + 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 new file mode 100644 index 000000000..5bf91696b --- /dev/null +++ b/src/framework/theme/component/style/styleConsumer.component.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { + ThemeType, + StyleType, +} from '../theme'; +import { ThemeMappingType } from '../mapping'; +import ThemeContext from '../theme/themeContext'; +import MappingContext from '../mapping/mappingContext'; +import { + createStyle, + getComponentThemeMapping, +} from '../../service'; + +interface PrivateProps { + forwardedRef: React.RefObject; +} + +interface ConsumerProps { + mapping: ThemeMappingType[]; + theme: ThemeType; +} + +export interface Props { + variant: string; + theme?: ThemeType; + themedStyle?: StyleType; +} + +export const StyledComponent = (Component: React.ComponentClass

) => { + + type ComponentProps = Props & P; + type WrapperProps = PrivateProps & ComponentProps; + + class Wrapper extends React.Component { + + getComponentName = (): string => Component.displayName || Component.name; + + createCustomProps = (props: ConsumerProps, variant: string): Props => { + const mapping = getComponentThemeMapping(this.getComponentName(), props.mapping); + return { + variant: variant, + theme: props.theme, + themedStyle: mapping && props.theme && createStyle(props.theme, mapping, variant), + }; + }; + + renderWrappedComponent = (props: ConsumerProps) => { + // TS issue: with spreading Generics https://github.com/Microsoft/TypeScript/issues/15792 + const { forwardedRef, ...restProps } = this.props as PrivateProps; + const componentProps = restProps as P & Props; + return ( + + ); + }; + + render() { + return ( + {(mapping: ThemeMappingType[]) => ( + {(theme: ThemeType) => { + return this.renderWrappedComponent({ mapping: mapping, theme: theme }); + }} + )} + ); + } + } + + const RefForwardingFactory = (props: WrapperProps, ref: T) => { + return ( + + ); + }; + + return React.forwardRef(RefForwardingFactory as any); +}; diff --git a/src/framework/theme/component/style/styleProvider.component.tsx b/src/framework/theme/component/style/styleProvider.component.tsx new file mode 100644 index 000000000..8ef455452 --- /dev/null +++ b/src/framework/theme/component/style/styleProvider.component.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { + ThemeProvider, + ThemeType, +} from '../theme'; +import { + ThemeMappingProvider, + ThemeMappingType, +} from '../mapping'; + +export interface Props { + mapping: ThemeMappingType[]; + theme: ThemeType; + children: JSX.Element | React.ReactNode; +} + +interface State { + mapping: ThemeMappingType[]; + theme: ThemeType; +} + +export class StyleProvider extends React.Component { + + static getDerivedStateFromProps(props: Props): State { + return { + mapping: props.mapping, + theme: props.theme, + }; + } + + state: State = { + mapping: [], + theme: {}, + }; + + render() { + return ( + + + {this.props.children} + + + ); + } +} diff --git a/src/framework/theme/component/theme/index.ts b/src/framework/theme/component/theme/index.ts new file mode 100644 index 000000000..18a4f71b6 --- /dev/null +++ b/src/framework/theme/component/theme/index.ts @@ -0,0 +1,9 @@ +export * from './type'; +export { + ThemeProvider, + Props as ThemeProviderProps, +} from './themeProvider.component'; +export { + withStyles, + Props as ThemedComponentProps, +} from './themeConsumer.component'; diff --git a/src/framework/theme/component/theme/themeConsumer.component.tsx b/src/framework/theme/component/theme/themeConsumer.component.tsx new file mode 100644 index 000000000..34ee6960e --- /dev/null +++ b/src/framework/theme/component/theme/themeConsumer.component.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import ThemeContext from './themeContext'; +import { + ThemeType, + ThemedStyleType, + StyleSheetType, +} from './type'; + +interface PrivateProps { + forwardedRef: React.RefObject; +} + +interface ConsumerProps { + theme: ThemeType; +} + +export interface Props { + theme: ThemeType; + themedStyle: ThemedStyleType | undefined; +} + +export type CreateStylesFunction = (theme: ThemeType) => StyleSheetType; + +export const withStyles = (Component: React.ComponentClass

, + createStyles?: CreateStylesFunction) => { + + type ComponentProps = Props & P; + type WrapperProps = PrivateProps & ComponentProps; + + class Wrapper extends React.Component { + + createCustomProps = (props: ConsumerProps): Props => { + return ({ + theme: props.theme, + themedStyle: createStyles ? createStyles(props.theme) : undefined, + }); + }; + + renderWrappedComponent = (props: ConsumerProps) => { + // TS issue: with spreading Generics https://github.com/Microsoft/TypeScript/issues/15792 + const { forwardedRef, ...restProps } = this.props as PrivateProps; + const componentProps = restProps as P & Props; + return ( + + ); + }; + + render() { + return ( + {(theme: ThemeType) => { + return this.renderWrappedComponent({theme: theme}); + }} + ); + } + } + + const RefForwardingFactory = (props: WrapperProps, ref: T) => { + return ( + + ); + }; + + return React.forwardRef(RefForwardingFactory as any); +}; diff --git a/src/framework/theme/component/theme/themeContext.ts b/src/framework/theme/component/theme/themeContext.ts new file mode 100644 index 000000000..f6a3151c2 --- /dev/null +++ b/src/framework/theme/component/theme/themeContext.ts @@ -0,0 +1,7 @@ +import React from 'react'; +import { ThemeType } from './type'; + +const defaultValue: ThemeType = {}; +const ThemeContext = React.createContext(defaultValue); + +export default ThemeContext; diff --git a/src/framework/theme/component/themeProvider.component.tsx b/src/framework/theme/component/theme/themeProvider.component.tsx similarity index 68% rename from src/framework/theme/component/themeProvider.component.tsx rename to src/framework/theme/component/theme/themeProvider.component.tsx index 1651b8ae5..cd6a902f8 100644 --- a/src/framework/theme/component/themeProvider.component.tsx +++ b/src/framework/theme/component/theme/themeProvider.component.tsx @@ -1,24 +1,20 @@ import React, { ReactNode } from 'react'; -import { Provider } from '../service'; +import ThemeContext from './themeContext'; import { ThemeType } from './type'; -interface Props { - children: JSX.Element | ReactNode; +export interface Props { theme: ThemeType; + children: JSX.Element | ReactNode; } export class ThemeProvider extends React.PureComponent { - static defaultProps = { - theme: {}, - }; - render() { return ( - {this.props.children} - + ); } } diff --git a/src/framework/theme/component/type.ts b/src/framework/theme/component/theme/type.ts similarity index 58% rename from src/framework/theme/component/type.ts rename to src/framework/theme/component/theme/type.ts index f846cf4cc..2f95f6784 100644 --- a/src/framework/theme/component/type.ts +++ b/src/framework/theme/component/theme/type.ts @@ -1,5 +1,6 @@ -// TODO(@type): declare theme types +// TODO(theme/type): declare Theme types export type ThemeType = any; +export type StyleType = any; export type ThemedStyleType = any; export type StyleSheetType = any; diff --git a/src/framework/theme/component/themeConsumer.component.tsx b/src/framework/theme/component/themeConsumer.component.tsx deleted file mode 100644 index 084cafdcb..000000000 --- a/src/framework/theme/component/themeConsumer.component.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { ComponentType } from 'react'; -import { ThemeType } from './type'; -import { - Consumer, - forwardProps, -} from '../service'; - -export interface Props

extends React.ClassAttributes

{ - theme: ThemeType; -} - -export function withTheme

>(Component: ComponentType

) { - type TExcept = Exclude>; - type ForwardedProps = Pick; - - class Shadow extends React.Component { - wrappedComponentRef = undefined; - getWrappedInstance = undefined; - - setWrappedComponentRef = (ref) => { - this.wrappedComponentRef = ref; - }; - - renderWrappedComponent = (theme: ThemeType) => ( - - ); - - render() { - return ( - - {this.renderWrappedComponent} - - ); - } - } - - const Result = Shadow; - Result.prototype.getWrappedInstance = function getWrappedInstance() { - const hasWrappedInstance = this.wrappedComponentRef && this.wrappedComponentRef.getWrappedInstance; - return hasWrappedInstance ? this.wrappedComponentRef.getWrappedInstance() : this.wrappedComponentRef; - }; - - return forwardProps(Component, Result); -} diff --git a/src/framework/theme/component/themeConsumerStyled.component.tsx b/src/framework/theme/component/themeConsumerStyled.component.tsx deleted file mode 100644 index 6708af454..000000000 --- a/src/framework/theme/component/themeConsumerStyled.component.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { StyleSheet } from 'react-native'; -import { - ThemeType, - ThemedStyleType, - StyleSheetType, -} from './type'; -import { - Consumer, - forwardProps, -} from '../service'; - -export interface Props

extends React.ClassAttributes

{ - theme: ThemeType; - themedStyle: ThemedStyleType; -} - -export function withThemedStyles

>( - Component: React.ComponentType

, - createStyles: (theme: ThemeType) => StyleSheetType, -) { - type TExcept = Exclude>; - type ForwardedProps = Pick; - - class Shadow extends React.Component { - wrappedComponentRef = undefined; - getWrappedInstance = undefined; - - setWrappedComponentRef = (ref) => { - this.wrappedComponentRef = ref; - }; - - renderWrappedComponent = (theme: ThemeType) => { - const styles = StyleSheet.create(createStyles(theme)); - return ( - - ); - }; - - render() { - return ( - - {this.renderWrappedComponent} - - ); - } - } - - const Result = Shadow; - Result.prototype.getWrappedInstance = function getWrappedInstance() { - const hasWrappedInstance = this.wrappedComponentRef && this.wrappedComponentRef.getWrappedInstance; - return hasWrappedInstance ? this.wrappedComponentRef.getWrappedInstance() : this.wrappedComponentRef; - }; - - return forwardProps(Component, Result); -} diff --git a/src/framework/theme/service/index.ts b/src/framework/theme/service/index.ts index a2d0bfaa7..c93480167 100644 --- a/src/framework/theme/service/index.ts +++ b/src/framework/theme/service/index.ts @@ -1,5 +1,3 @@ -export { - Provider, - Consumer, -} from './reactContext.service'; -export { forwardProps } from './reactPropsForward.service'; +export * from './mappingUtil.service'; +export * from './themeUtil.service'; +export * from './reactPropsForward.service'; diff --git a/src/framework/theme/service/mappingUtil.service.ts b/src/framework/theme/service/mappingUtil.service.ts new file mode 100644 index 000000000..cde9f18f8 --- /dev/null +++ b/src/framework/theme/service/mappingUtil.service.ts @@ -0,0 +1,77 @@ +import { + ThemeMappingType, + VariantType, + MappingType, + TokenType, +} from '../component'; + +export const VARIANT_DEFAULT = 'default'; + +/** + * @param component: string - component name. Using displayName is recommended + * @param mapping: ThemeMappingType[] - theme mapping configuration array + * + * @return ThemeMappingType if presents in theme mapping, undefined otherwise + */ +export function getComponentThemeMapping(component: string, mapping: ThemeMappingType[]): ThemeMappingType | undefined { + return mapping.find(value => value.name === component); +} + +/** + * @param token: string - theme mapping token name + * @param tokens: TokenType[] - theme mapping tokens array + * + * @return TokenType if presents in tokens, undefined otherwise + */ +export function getThemeMappingToken(token: string, tokens: TokenType): TokenType | undefined { + const value = {}; + if (tokens[token] !== undefined) { + value[token] = tokens[token]; + return value; + } else { + return undefined; + } +} + +/** + * @param name: string - variant name. Default is 'default' + * @param mapping: ThemeMappingType - component mapping configuration + * + * @return VariantType if presents in mapping, undefined otherwise + */ +export function getComponentVariant(name: string, mapping: ThemeMappingType): VariantType | undefined { + return mapping.variants.find(value => value.name === name); +} + +/** + * @param mapping: ThemeMappingType - component mapping configuration + * @param variant: string - variant name. Default is 'default' + * + * @return MappingType[] if presents in variant, undefined otherwise + */ +export function getComponentMappings(mapping: ThemeMappingType, + variant: string = VARIANT_DEFAULT): MappingType[] | undefined { + const componentVariant = getComponentVariant(variant, mapping); + return componentVariant && componentVariant.mapping; +} + +/** + * @param tokens: TokenType[] - theme mapping tokens array + * @param mapping: ThemeMappingType - component mapping configuration + * @param variant: string - variant name. Default is 'default' + * + * @return TokenType[] specific for component's variant if presents in variant, undefined otherwise + */ +export function getVariantTokens(tokens: TokenType, + mapping: ThemeMappingType, + variant: string = VARIANT_DEFAULT): TokenType | undefined { + + const assignParameter = (origin: TokenType, prop: MappingType) => { + return { + ...origin, + ...getThemeMappingToken(prop.token, tokens), + }; + }; + const componentMappings = getComponentMappings(mapping, variant); + return componentMappings.reduce(assignParameter, {}); +} diff --git a/src/framework/theme/service/reactContext.service.ts b/src/framework/theme/service/reactContext.service.ts deleted file mode 100644 index 6815d15a8..000000000 --- a/src/framework/theme/service/reactContext.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { ThemeType } from '../component'; - -const defaultThemeValue: ThemeType = {}; - -const { - Provider, - Consumer, -} = React.createContext(defaultThemeValue); - -export { - Provider, - Consumer, -}; diff --git a/src/framework/theme/service/themeUtil.service.ts b/src/framework/theme/service/themeUtil.service.ts new file mode 100644 index 000000000..ca9715249 --- /dev/null +++ b/src/framework/theme/service/themeUtil.service.ts @@ -0,0 +1,61 @@ +import { + getComponentMappings, + VARIANT_DEFAULT, +} from './mappingUtil.service'; +import { + ThemeMappingType, + MappingType, + ThemeType, + StyleType, +} from '../component'; + +const variantSeparator = ' '; + +/** + * 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. Default is 'default'. + * Supported argument formats: + * - 'dark' + * - 'dark success' + * - ['dark', 'success'] + * + * @return any. + */ +export function createStyle(theme: ThemeType, + mapping: ThemeMappingType, + variant: string[] | string = [VARIANT_DEFAULT]): StyleType { + + const variants: string[] = Array.isArray(variant) ? variant : variant.split(variantSeparator); + + const mapVariant = (v: string) => createStyleFromVariant(theme, mapping, v); + const mergeStyles = (origin: StyleType, next: StyleType) => ({ ...origin, ...next }); + + const defaultStyle = createStyleFromVariant(theme, mapping, VARIANT_DEFAULT); + return variants.map(mapVariant).reduce(mergeStyles, defaultStyle); +} + +/** + * @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]; +} + +function createStyleFromVariant(theme: ThemeType, mapping: ThemeMappingType, variant: string): StyleType { + const variantMapping = getComponentMappings(mapping, variant); + return createStyleFromMapping(variantMapping, theme); +} + +function createStyleFromMapping(mapping: MappingType[], theme: ThemeType): StyleType { + const assignParameter = (style: any, prop: MappingType) => { + style[prop.parameter] = getThemeValue(prop.token, theme); + return style; + }; + return mapping.reduce(assignParameter, {}); +} diff --git a/src/framework/theme/tests/config.ts b/src/framework/theme/tests/config.ts new file mode 100644 index 000000000..1983310a0 --- /dev/null +++ b/src/framework/theme/tests/config.ts @@ -0,0 +1,87 @@ +import { ThemeType } from '../component'; + +export const values = { + backgroundDefault: '#ffffff', + backgroundDark: '#000000', + textDefault: '#000000', + textDark: '#ffffff', + textSuccess: '#00E676', +}; + +export const theme: ThemeType = { + backgroundColorTestDefault: values.backgroundDefault, + backgroundColorTestDark: values.backgroundDark, + textColorTestDefault: values.textDefault, + textColorTestSuccess: values.textSuccess, +}; + +export const mappings = { + testDefault: [ + { + parameter: 'backgroundColor', + token: 'backgroundColorTestDefault', + }, + { + parameter: 'textColor', + token: 'textColorTestDefault', + }, + ], + testDark: [ + { + parameter: 'backgroundColor', + token: 'backgroundColorTestDark', + }, + ], + testSuccess: [ + { + parameter: 'textColor', + token: 'textColorTestSuccess', + }, + ], + mockBackground: [ + { + parameter: 'backgroundColor', + token: 'backgroundColorTestDefault', + }, + ], +}; + +export const variants = { + testDefault: { + name: 'default', + mapping: mappings.testDefault, + }, + testDark: { + name: 'dark', + mapping: mappings.testDark, + }, + testSuccess: { + name: 'success', + mapping: mappings.testSuccess, + }, + mockDefault: { + name: 'default', + mapping: mappings.mockBackground, + }, +}; + +export const themeMappings = { + test: { + name: 'Test', + parameters: [ + { + name: 'backgroundColor', + }, + ], + variants: [variants.testDefault, variants.testDark, variants.testSuccess], + }, + mock: { + name: 'Mock', + parameters: [ + { + name: 'backgroundColor', + }, + ], + variants: [variants.mockDefault], + }, +}; diff --git a/src/framework/theme/tests/mapping.spec.tsx b/src/framework/theme/tests/mapping.spec.tsx new file mode 100644 index 000000000..ee358eeea --- /dev/null +++ b/src/framework/theme/tests/mapping.spec.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { ThemeMappingType } from '../component'; +import { + getComponentThemeMapping, + getComponentMappings, + getComponentVariant, + getThemeMappingToken, + getVariantTokens, +} from '../service'; +import * as config from './config'; + +describe('@mapping: service methods checks', () => { + + const mappings: ThemeMappingType[] = [config.themeMappings.test, config.themeMappings.mock]; + + it('finds mappings properly', async () => { + const componentMappings = getComponentThemeMapping('Test', mappings); + + expect(componentMappings).not.toBeNull(); + expect(componentMappings).not.toBeUndefined(); + expect(JSON.stringify(componentMappings)).toEqual(JSON.stringify(config.themeMappings.test)); + }); + + it('finds variant properly', async () => { + const componentVariant = getComponentVariant('default', config.themeMappings.test); + + expect(componentVariant).not.toBeNull(); + expect(componentVariant).not.toBeUndefined(); + expect(JSON.stringify(componentVariant)).toEqual(JSON.stringify(config.variants.testDefault)); + }); + + it('finds mappings properly', async () => { + const componentMappings = getComponentMappings(config.themeMappings.test); + + expect(componentMappings).not.toBeNull(); + expect(componentMappings).not.toBeUndefined(); + expect(JSON.stringify(componentMappings)).toEqual(JSON.stringify(config.mappings.testDefault)); + }); + + it('finds token properly', async () => { + const mappingToken = getThemeMappingToken('backgroundColorTestDefault', config.theme); + + expect(mappingToken).not.toBeNull(); + expect(mappingToken).not.toBeUndefined(); + expect(mappingToken).not.toEqual(config.values.backgroundDefault); + }); + + it('finds mapping tokens properly', async () => { + const variantTokens = getVariantTokens(config.theme, config.themeMappings.test); + + expect(variantTokens.backgroundColorTestDefault).not.toBeNull(); + expect(variantTokens.backgroundColorTestDefault).not.toBeUndefined(); + expect(variantTokens.textColorTestDefault).not.toBeNull(); + expect(variantTokens.textColorTestDefault).not.toBeUndefined(); + }); + +}); diff --git a/src/framework/theme/tests/style.spec.tsx b/src/framework/theme/tests/style.spec.tsx new file mode 100644 index 000000000..7af6ce203 --- /dev/null +++ b/src/framework/theme/tests/style.spec.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { View } from 'react-native'; +import { render } from 'react-native-testing-library'; +import { StyledComponentProps, StyleProvider, StyledComponent } from '@rk-kit/theme'; +import * as config from './config'; + +const styleConsumerTestId = '@style/consumer'; + +type TestComponentProps = any; + +class Test extends React.Component { + static defaultProps = { + testID: styleConsumerTestId, + }; + + render() { + return ( + + ); + } +} + +describe('@style: style consumer checks', () => { + + const mappings = [config.themeMappings.test, config.themeMappings.mock]; + + 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.toBeUndefined(); + expect(styledComponent.props.themedStyle).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); + }); + +}); diff --git a/src/framework/theme/theme.spec.tsx b/src/framework/theme/tests/theme.spec.tsx similarity index 64% rename from src/framework/theme/theme.spec.tsx rename to src/framework/theme/tests/theme.spec.tsx index 3490c8bd9..5549cee8f 100644 --- a/src/framework/theme/theme.spec.tsx +++ b/src/framework/theme/tests/theme.spec.tsx @@ -10,18 +10,15 @@ import { } from 'react-native-testing-library'; import { ThemeProvider, - withTheme, - withThemedStyles, + withStyles, ThemeType, -} from './component'; +} from '../component'; +import { createStyle } from '../service'; +import * as config from './config'; const themeConsumerTestId = '@theme/consumer'; const themeChangeTouchableTestId = '@theme/btnChangeTheme'; -interface Theme extends ThemeType { - color: string; -} - class ThemedConsumer extends React.Component { static defaultProps = { testID: themeConsumerTestId, @@ -76,7 +73,7 @@ class ActionedProvider extends React.Component { }; render() { - const ThemedComponent = withTheme(ThemedConsumer); + const ThemedComponent = withStyles(ThemedConsumer); return ( @@ -93,21 +90,21 @@ class ActionedProvider extends React.Component { export class ThemedStyleProvider extends React.Component { - createThemedComponent1Styles = (theme: Theme) => ({ + createThemedComponent1Styles = (theme: ThemeType) => ({ container: { backgroundColor: theme.color, }, }); - createThemedComponent2Styles = (theme: Theme) => ({ + createThemedComponent2Styles = (theme: ThemeType) => ({ container: { backgroundColor: theme.color, }, }); render() { - const ThemedComponent1 = withThemedStyles(ThemedStyleConsumer, this.createThemedComponent1Styles); - const ThemedComponent2 = withThemedStyles(ThemedStyleConsumer, this.createThemedComponent2Styles); + const ThemedComponent1 = withStyles(ThemedStyleConsumer, this.createThemedComponent1Styles); + const ThemedComponent2 = withStyles(ThemedStyleConsumer, this.createThemedComponent2Styles); return ( @@ -122,7 +119,7 @@ export class ThemedStyleProvider extends React.Component { describe('@theme: theme consumer checks', () => { it('renders properly', async () => { - const ThemedComponent = withTheme(ThemedConsumer); + const ThemedComponent = withStyles(ThemedConsumer); const component = render( @@ -135,7 +132,7 @@ describe('@theme: theme consumer checks', () => { }); it('receives theme prop', async () => { - const ThemedComponent = withTheme(ThemedConsumer); + const ThemedComponent = withStyles(ThemedConsumer); const component = render( @@ -147,6 +144,21 @@ describe('@theme: theme consumer checks', () => { expect(themedComponent.props.theme).not.toBeNull(); }); + it('receives themedStyle prop', async () => { + const ThemedComponent = withStyles(ThemedConsumer, (theme: ThemeType) => { + return {}; + }); + + const component = render( + + + , + ); + + const themedComponent = component.getByTestId(themeConsumerTestId); + expect(themedComponent.props.themedStyle).not.toBeNull(); + }); + it('receives theme prop on theme change', async () => { const component = render( , @@ -163,78 +175,63 @@ describe('@theme: theme consumer checks', () => { expect(themedComponent.props.theme.backgroundColor).toEqual(ActionedProvider.onChangeThemeColor); }); -}); + it('child theme provider overrides parent theme', async () => { + const component = render( + , + ); -describe('@theme: styled theme consumer checks', () => { + const themedComponents = component.getAllByName(ThemedStyleConsumer); - it('renders properly', async () => { - const ThemedComponent = withThemedStyles(ThemedConsumer, (theme: ThemeType) => { - return {}; - }); + expect(themedComponents.length).toBeGreaterThan(1); - const component = render( - - - , - ); + const themedComponent1Color = themedComponents[0].props.themedStyle.container.backgroundColor; + const themedComponent2Color = themedComponents[1].props.themedStyle.container.backgroundColor; - const themedComponent = component.getByTestId(themeConsumerTestId); - expect(themedComponent).not.toBeNull(); + expect(themedComponent1Color).not.toEqual(themedComponent2Color); }); - it('receives theme prop', async () => { - const ThemedComponent = withThemedStyles(ThemedConsumer, (theme: ThemeType) => { - return {}; - }); +}); - const component = render( - - - , - ); +describe('@theme: service methods checks', () => { - const themedComponent = component.getByTestId(themeConsumerTestId); - expect(themedComponent.props.theme).not.toBeNull(); - }); + it('default variant styled properly', async () => { + const style = createStyle(config.theme, config.themeMappings.test); - it('receives themedStyle prop', async () => { - const ThemedComponent = withThemedStyles(ThemedConsumer, (theme: ThemeType) => { - return {}; - }); + expect(style).not.toBeNull(); + expect(style).not.toBeUndefined(); + expect(style.backgroundColor).toEqual(config.values.backgroundDefault); + }); - const component = render( - - - , - ); + it('single non-default variant styled properly (string type)', async () => { + const style = createStyle(config.theme, config.themeMappings.test, 'dark'); - const themedComponent = component.getByTestId(themeConsumerTestId); - expect(themedComponent.props.themedStyle).not.toBeNull(); + expect(style.backgroundColor).toEqual(config.values.backgroundDark); + expect(style.textColor).toEqual(config.values.textDefault); }); - it('child theme provider overrides parent theme', async () => { - const theme1: Theme = { - color: '#3F51B5', - }; - const theme2: Theme = { - color: '#009688', - }; + it('list of non-default variants styled created properly (string type)', async () => { + const style = createStyle(config.theme, config.themeMappings.test, 'dark success'); - const component = render( - , - ); + expect(style.backgroundColor).toEqual(config.values.backgroundDark); + expect(style.textColor).toEqual(config.values.textSuccess); + }); - const themedComponents = component.getAllByName(ThemedStyleConsumer); + it('single non-default variant styled properly (string[] type)', async () => { + const style = createStyle(config.theme, config.themeMappings.test, ['dark']); - expect(themedComponents.length).toBeGreaterThan(1); + expect(style.backgroundColor).toEqual(config.values.backgroundDark); + expect(style.textColor).toEqual(config.values.textDefault); + }); - const themedComponent1Color = themedComponents[0].props.themedStyle.container.backgroundColor; - const themedComponent2Color = themedComponents[1].props.themedStyle.container.backgroundColor; + it('array of non-default variants styled created properly (string[] type)', async () => { + const style = createStyle(config.theme, config.themeMappings.test, ['dark', 'success']); - expect(themedComponent1Color).not.toEqual(themedComponent2Color); + expect(style.backgroundColor).toEqual(config.values.backgroundDark); + expect(style.textColor).toEqual(config.values.textSuccess); }); }); + diff --git a/src/framework/tsconfig.json b/src/framework/tsconfig.json index b0af6ee95..6d3419de8 100644 --- a/src/framework/tsconfig.json +++ b/src/framework/tsconfig.json @@ -5,6 +5,7 @@ "outDir": "../../dist/tsc-out", "allowSyntheticDefaultImports": true, "esModuleInterop": true, + "resolveJsonModule": true, "moduleResolution": "node", "sourceMap": true, "jsx": "react-native", diff --git a/src/framework/ui/index.ts b/src/framework/ui/index.ts index a2c8e8981..c4626d3ee 100644 --- a/src/framework/ui/index.ts +++ b/src/framework/ui/index.ts @@ -1 +1,9 @@ -export { Sample } from './sample/sample.component'; +import { StyledComponent } from '@rk-kit/theme'; +import { Sample, Props } from './sample/sample.component'; + +const StyledSample = StyledComponent(Sample); + +export { + StyledSample as Sample, + Props as SampleProps, +}; diff --git a/src/framework/ui/sample/sample.component.spec.tsx b/src/framework/ui/sample/sample.component.spec.tsx deleted file mode 100644 index b733d8ca0..000000000 --- a/src/framework/ui/sample/sample.component.spec.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; -import { render } from 'react-native-testing-library'; -import { Sample } from './sample.component'; - -it('Checks Sample component passes correct text props', async () => { - const text = 'Hello, World!'; - const component = render( - , - ); - const textComponent = component.getByName('Text'); - expect(textComponent.props.children).toEqual(text); -}); - -it('Checks Sample component renders correctly', async () => { - const component = render( - , - ); - const textComponent = component.getByName('Text'); - expect(textComponent).not.toBeNull(); -}); diff --git a/src/framework/ui/sample/sample.component.tsx b/src/framework/ui/sample/sample.component.tsx index b5474ca60..03080713a 100644 --- a/src/framework/ui/sample/sample.component.tsx +++ b/src/framework/ui/sample/sample.component.tsx @@ -4,23 +4,28 @@ import { Text, StyleSheet, } from 'react-native'; +import { StyledComponentProps } from '@rk-kit/theme'; -interface Props { - text: string; +interface SampleProps { + text?: string; } +export type Props = SampleProps & StyledComponentProps; export class Sample extends React.Component { - static defaultProps = { - text: 'This is React Native UI Kitten playground.\n\n' + - 'Create your awesome components inside ./src/framework dir ' + - 'which will be automatically synchronized with npm package.\n\n' + - 'Enjoy!', + static defaultProps: Props = { + text: `This is React Native UI Kitten playground.\n\n + Create your awesome components inside + ./src/framework dir + which will be automatically synchronized with playground. + Enjoy!`, + variant: 'default', }; render() { + const { themedStyle } = this.props; return ( - - {this.props.text} + + {this.props.text} ); } diff --git a/src/playground/App.js b/src/playground/App.js index 720a0e531..f7a2af0ee 100644 --- a/src/playground/App.js +++ b/src/playground/App.js @@ -1,3 +1,3 @@ -import App from './src/App.component'; +import App from './src/app.component'; export default App; diff --git a/src/playground/src/App.component.tsx b/src/playground/src/App.component.tsx deleted file mode 100644 index 04b031c40..000000000 --- a/src/playground/src/App.component.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { Sample } from '@rk-kit/ui'; - -export default class App extends React.Component { - render() { - return ( - - ); - } -} diff --git a/src/playground/src/app.component.tsx b/src/playground/src/app.component.tsx new file mode 100644 index 000000000..38656cc22 --- /dev/null +++ b/src/playground/src/app.component.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { + StyleProvider, + ThemeMappingType, + ThemeType, +} from '@rk-kit/theme'; +import { Sample } from '@rk-kit/ui'; +import { + Mappings, + Theme, +} from './theme-token'; + +interface State { + mappings: ThemeMappingType[]; + theme: ThemeType; +} + +export default class App extends React.Component { + + sampleRef = undefined; + + constructor(props) { + super(props); + this.state = { + mappings: Mappings, + theme: Theme, + }; + } + + setSampleRef = (ref) => { + this.sampleRef = ref; + }; + + render() { + return ( + + + + ); + } +} diff --git a/src/playground/src/theme-token/index.ts b/src/playground/src/theme-token/index.ts new file mode 100644 index 000000000..ad1c9d5e2 --- /dev/null +++ b/src/playground/src/theme-token/index.ts @@ -0,0 +1,10 @@ +/** + * Mocked component mapping configurations + */ +import { config as Mappings } from './mapping.json'; +import * as Theme from './theme.json'; + +export { + Mappings, + Theme, +}; diff --git a/src/playground/src/theme-token/mapping.json b/src/playground/src/theme-token/mapping.json new file mode 100644 index 000000000..2fc768c6e --- /dev/null +++ b/src/playground/src/theme-token/mapping.json @@ -0,0 +1,52 @@ +{ + "config": [ + { + "name": "Sample", + "parameters": [ + { + "name": "backgroundColor" + }, + { + "name": "textColor" + } + ], + "variants": [ + { + "name": "default", + "mapping": [ + { + "parameter": "backgroundColor", + "token": "background-color-sample-default" + }, + { + "parameter": "textColor", + "token": "text-color-sample-default" + } + ] + }, + { + "name": "dark", + "mapping": [ + { + "parameter": "backgroundColor", + "token": "background-color-sample-dark" + }, + { + "parameter": "textColor", + "token": "text-color-sample-dark" + } + ] + }, + { + "name": "success", + "mapping": [ + { + "parameter": "textColor", + "token": "text-color-sample-success" + } + ] + } + ] + } + ] +} diff --git a/src/playground/src/theme-token/theme.json b/src/playground/src/theme-token/theme.json new file mode 100644 index 000000000..ae038518f --- /dev/null +++ b/src/playground/src/theme-token/theme.json @@ -0,0 +1,7 @@ +{ + "background-color-sample-default": "#c0c0c0", + "background-color-sample-dark": "#000000", + "text-color-sample-default": "#ffffff", + "text-color-sample-dark": "#ffffff", + "text-color-sample-success": "#00E676" +} diff --git a/tsconfig.jest.json b/tsconfig.jest.json index acac3561a..0d09fd42b 100644 --- a/tsconfig.jest.json +++ b/tsconfig.jest.json @@ -1,6 +1,10 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "jsx": "react" + "baseUrl": "./", + "jsx": "react", + "paths": { + "@rk-kit/*": ["./src/framework/*"] + } } -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 089755ff1..f00d309f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, + "resolveJsonModule": true, "moduleResolution": "node", "jsx": "react-native", "target": "es2017", @@ -17,4 +18,4 @@ "exclude": [ "./node_modules" ] -} \ No newline at end of file +}