Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

Commit

Permalink
test: add mergeTheme tests
Browse files Browse the repository at this point in the history
  • Loading branch information
levithomason committed Aug 4, 2018
1 parent a9bbbac commit 9328b1b
Show file tree
Hide file tree
Showing 21 changed files with 556 additions and 180 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import ComponentControls from '../ComponentControls'
import ComponentExampleTitle from './ComponentExampleTitle'
import ContributionPrompt from '../ContributionPrompt'
import getSourceCodeManager, { ISourceCodeManager, SourceCodeType } from './SourceCodeManager'
import { IMergedThemes, ITheme } from 'types/theme'
import { IThemePrepared, IThemeInput } from 'types/theme'

export interface IComponentExampleProps extends RouteComponentProps<any, any> {
title: string
Expand All @@ -35,7 +35,7 @@ export interface IComponentExampleProps extends RouteComponentProps<any, any> {

interface IComponentExampleState {
knobs: Object
theme: ITheme
theme: IThemeInput
exampleElement?: JSX.Element
handleMouseLeave?: () => void
handleMouseMove?: () => void
Expand Down Expand Up @@ -345,7 +345,7 @@ class ComponentExample extends PureComponent<IComponentExampleProps, IComponentE
renderWithProvider(ExampleComponent) {
const { showRtl, theme } = this.state

const newTheme: ITheme = {
const newTheme: IThemeInput = {
siteVariables: teamsTheme.siteVariables,
componentVariables: [teamsTheme.componentVariables, theme.componentVariables],
componentStyles: teamsTheme.componentStyles,
Expand Down Expand Up @@ -539,7 +539,7 @@ class ComponentExample extends PureComponent<IComponentExampleProps, IComponentE
<span style={{ opacity: 0.5 }}>Theme</span>
</Divider>
<Provider.Consumer
render={({ siteVariables, componentVariables }: ITheme | IMergedThemes) => {
render={({ siteVariables, componentVariables }: IThemeInput | IThemePrepared) => {
// TODO: refactor to handle variables as a call stack once mergeThemes is updated
// TODO: refactor to handle variables as a call stack once mergeThemes is updated
// TODO: refactor to handle variables as a call stack once mergeThemes is updated
Expand Down
79 changes: 22 additions & 57 deletions src/components/Provider/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,34 @@ import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { Provider as RendererProvider, ThemeProvider } from 'react-fela'

import { felaRenderer as felaLtrRenderer, felaRtlRenderer, toCompactArray } from '../../lib'
import { FontFaces, IMergedThemes, ITheme, StaticStyles } from '../../../types/theme'
import {
callable,
felaRenderer as felaLtrRenderer,
felaRtlRenderer,
mergeThemes,
toCompactArray,
} from '../../lib'
import {
FontFaces,
IThemePrepared,
IThemeInput,
StaticStyles,
ComponentVariablesInput,
IThemeComponentStylesInput,
IComponentPartStylesInput,
IThemeComponentVariablesInput,
ComponentStyleFunctionParam,
ComponentPartStyleFunction,
} from '../../../types/theme'
import ProviderConsumer from './ProviderConsumer'

export interface IProviderProps {
fontFaces?: FontFaces
theme: ITheme
theme: IThemeInput
staticStyles?: StaticStyles
children: React.ReactNode
}

const mergeThemes = (...themes: ITheme[]): IMergedThemes => {
const [first, ...rest]: ITheme[] = toCompactArray(...themes)

const merged = {
siteVariables: first.siteVariables,
componentVariables: toCompactArray(first.componentVariables),
componentStyles: toCompactArray(first.componentStyles),
rtl: first.rtl,
renderer: first.renderer,
}

if (rest.length === 0) {
return merged
}

return rest.reduce((acc, next) => {
// Site variables can safely be merged at each Provider in the tree.
// They are flat objects and do not depend on render-time values, such as props.
acc.siteVariables = { ...acc.siteVariables, ...next.siteVariables }

// Do not resolve variables in the middle of the tree.
// Component variables can be objects, functions, or an array of these.
// The functions must be called with the final result of siteVariables.
// Component variable objects have no ability to apply siteVariables.
// Therefore, componentVariables must be resolved by the component at render time.
// We instead pass down an array of variables to be resolved at the end of the tree.
// TODO: refactor to call stack, variables should always be a single function
// TODO: refactor to call stack, variables should always be a single function
// TODO: refactor to call stack, variables should always be a single function
if (next.componentVariables) {
acc.componentVariables = acc.componentVariables.concat(next.componentVariables)
}

// See component variables reasoning above.
// (Component styles are just like component variables, except they return style objects.)
// TODO: refactor to call stack, styles should always be a single function
// TODO: refactor to call stack, styles should always be a single function
// TODO: refactor to call stack, styles should always be a single function
if (next.componentStyles) {
acc.componentStyles = acc.componentStyles.concat(next.componentStyles)
}

// Latest RTL value wins
acc.rtl = next.rtl || acc.rtl

// Use the correct renderer for RTL
acc.renderer = acc.rtl ? felaRtlRenderer : felaLtrRenderer

return acc
}, merged)
}

/**
* The Provider passes the CSS in JS renderer and theme down context.
*/
Expand Down Expand Up @@ -172,12 +137,12 @@ class Provider extends Component<IProviderProps, any> {

return (
<ProviderConsumer
render={(incomingTheme: ITheme | IMergedThemes) => {
render={(incomingTheme: IThemePrepared) => {
// The provider must:
// 1. Normalize it's theme props, reducing and merging where possible.
// 2. Merge prop values onto any incoming context values.
// 3. Provide the result down stream.
const outgoingTheme: IMergedThemes = mergeThemes(incomingTheme, theme)
const outgoingTheme: IThemePrepared = mergeThemes(incomingTheme, theme)

return (
<RendererProvider renderer={outgoingTheme.renderer}>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Provider/ProviderConsumer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import PropTypes from 'prop-types'
import React from 'react'
import { FelaTheme } from 'react-fela'

import { IMergedThemes, ITheme } from '../../../types/theme'
import { IThemePrepared } from '../../../types/theme'

export interface IProviderConsumerProps {
render: (theme: ITheme | IMergedThemes) => React.ReactNode
render: (theme: IThemePrepared) => React.ReactNode
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './factories'
export * from './themeUtils'
export { default as getElementType } from './getElementType'
export { default as getUnhandledProps } from './getUnhandledProps'
export { default as mergeThemes } from './mergeThemes'
export { default as renderComponent, IRenderResultConfig } from './renderComponent'
export {
useKeyOnly,
Expand Down
122 changes: 122 additions & 0 deletions src/lib/mergeThemes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import _ from 'lodash'
import {
ISiteVariables,
IThemeComponentStylesInput,
IThemeComponentStylesPrepared,
IThemeComponentVariablesInput,
IThemeComponentVariablesPrepared,
IThemeInput,
IThemePrepared,
} from '../../types/theme'
import { callable, felaRenderer, felaRtlRenderer } from './index'

/**
* Site variables can safely be merged at each Provider in the tree.
* They are flat objects and do not depend on render-time values, such as props.
*/
const mergeSiteVariables = (
target: ISiteVariables,
...sources: ISiteVariables[]
): ISiteVariables => {
return sources.reduce((acc, next) => ({ ...acc, ...next }), target)
}

/**
* Component variables can be objects, functions, or an array of these.
* The functions must be called with the final result of siteVariables, otherwise
* the component variable objects would have no ability to apply siteVariables.
* Therefore, componentVariables must be resolved by the component at render time.
* We instead pass down call stack of component variable functions to be resolved later.
*/
const mergeComponentVariables = (
target: IThemeComponentVariablesInput,
...sources: IThemeComponentVariablesInput[]
): IThemeComponentVariablesPrepared => {
const displayNames = _.union(_.keys(target), ..._.map(sources, _.keys))

return sources.reduce((acc, next) => {
return displayNames.reduce((componentVariables, displayName) => {
// Break references to avoid an infinite loop.
// We are replacing functions with new ones that calls the originals.
const originalTarget = acc[displayName]
const originalSource = next[displayName]

componentVariables[displayName] = siteVariables => {
return {
...callable(originalTarget)(siteVariables),
...callable(originalSource)(siteVariables),
}
}

return componentVariables
}, {})
}, target)
}

/**
* See mergeComponentVariables() description.
* Component styles adhere to the same pattern as component variables, except
* that they return style objects.
*/
const mergeComponentStyles = (
target: IThemeComponentStylesInput,
...sources: IThemeComponentStylesInput[]
): IThemeComponentStylesPrepared => {
const initial: IThemeComponentStylesPrepared = _.mapValues(target, stylesByPart => {
return _.mapValues(stylesByPart, callable)
})

return sources.reduce<IThemeComponentStylesPrepared>((acc, next) => {
_.forEach(next, (stylesByPart, displayName) => {
acc[displayName] = acc[displayName] || {}

_.forEach(stylesByPart, (partStyle, partName) => {
// Break references to avoid an infinite loop.
// We are replacing functions with a new ones that calls the originals.
const originalTarget = acc[displayName][partName]
const originalSource = next[displayName][partName]

acc[displayName][partName] = styleParam => {
return _.merge(callable(originalTarget)(styleParam), callable(originalSource)(styleParam))
}
})
})

return acc
}, initial)
}

const mergeRTL = (target, ...sources) => {
return !!sources.reduce((acc, next) => {
return typeof next === 'boolean' ? next : acc
}, target)
}

const mergeThemes = (...themes: IThemeInput[]): IThemePrepared => {
const emptyTheme = {
siteVariables: {},
componentVariables: {},
componentStyles: {},
} as IThemePrepared

return themes.reduce<IThemePrepared>((acc: IThemePrepared, next: IThemeInput) => {
acc.siteVariables = mergeSiteVariables(acc.siteVariables, next.siteVariables)

acc.componentVariables = mergeComponentVariables(
acc.componentVariables,
next.componentVariables,
)

acc.componentStyles = mergeComponentStyles(acc.componentStyles, next.componentStyles)

// Latest RTL value wins
acc.rtl = mergeRTL(acc.rtl, next.rtl)

// Use the correct renderer for RTL
acc.renderer = acc.rtl === true ? felaRtlRenderer : felaRenderer

return acc
}, emptyTheme)
}

export default mergeThemes
39 changes: 18 additions & 21 deletions src/lib/renderComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import _ from 'lodash'
import cx from 'classnames'
import React from 'react'
import { FelaTheme } from 'react-fela'

import callable from './callable'
import getElementType from './getElementType'
import getUnhandledProps from './getUnhandledProps'
import toCompactArray from './toCompactArray'
import { renderComponentStyles, resolveComponentVariables } from './themeUtils'

import { renderComponentStyles } from './themeUtils'
import {
ComponentVariables,
ComponentStyleFunctionParam,
ComponentVariablesInput,
ComponentVariablesObject,
IComponentStyles,
IMergedThemes,
ITheme,
IComponentPartStylesInput,
IThemeInput,
IThemePrepared,
} from '../../types/theme'

export interface IRenderResultConfig<P> {
Expand All @@ -25,8 +27,8 @@ export type RenderComponentCallback<P> = (config: IRenderResultConfig<P>) => any

type IRenderConfigProps = {
[key: string]: any
variables?: ComponentVariables
styles?: IComponentStyles
variables?: ComponentVariablesInput
styles?: IComponentPartStylesInput
}

export interface IRenderConfig {
Expand All @@ -45,22 +47,17 @@ const renderComponent = <P extends {}>(

return (
<FelaTheme
render={(theme: ITheme | IMergedThemes) => {
render={(theme: IThemeInput | IThemePrepared) => {
const ElementType = getElementType({ defaultProps }, props)
const rest = getUnhandledProps({ handledProps }, props)

//
// Resolve variables using final siteVariables, allow props.variables to override
// Resolve variables for this component, allow props.variables to override
//
const variablesForComponent = toCompactArray(theme.componentVariables)
.map(variables => variables[displayName])
.concat(props.variables)
.filter(Boolean)

const variables: ComponentVariablesObject = resolveComponentVariables(
variablesForComponent,
theme.siteVariables,
)
const variables: ComponentVariablesObject = {
...callable(theme.componentVariables[displayName])(theme.siteVariables),
...callable(props.variables)(theme.siteVariables),
}

//
// Resolve styles using resolved variables, merge results, allow props.styles to override
Expand All @@ -70,7 +67,7 @@ const renderComponent = <P extends {}>(
.concat(props.styles)
.filter(Boolean)

const styleArg = {
const styleParam: ComponentStyleFunctionParam = {
props,
variables,
siteVariables: theme.siteVariables,
Expand All @@ -80,7 +77,7 @@ const renderComponent = <P extends {}>(
const classes: ComponentVariablesObject = renderComponentStyles(
theme.renderer,
stylesForComponent,
styleArg,
styleParam,
)
classes.root = cx(className, classes.root, props.className)

Expand Down
Loading

0 comments on commit 9328b1b

Please sign in to comment.