diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e939ab65..e784348472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### BREAKING CHANGES - Rename `toolbarBehavior` to `menuAsToolbarBehavior` and `toolbarButtonBehavior` to `menuItemAsToolbarButtonBehavior` ([#1393](https://github.com/stardust-ui/react/pull/1393)) - Rename types related to accessibility ([#1421](https://github.com/stardust-ui/react/pull/1421)) +- Moved the `rtl` and `renderer` props from the `theme` prop object to the `Provider`'s props API @mnajdova ([#1377](https://github.com/stardust-ui/react/pull/1377)) ### Features - Add `Toolbar` component @miroslavstastny ([#1408](https://github.com/stardust-ui/react/pull/1408)) +- Add `disableAnimations` boolean prop on the `Provider` @mnajdova ([#1377](https://github.com/stardust-ui/react/pull/1377)) ## [v0.32.0](https://github.com/stardust-ui/react/tree/v0.32.0) (2019-06-03) diff --git a/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx b/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx index 1d0fdaa751..01100e856e 100644 --- a/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx +++ b/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx @@ -195,13 +195,12 @@ class ComponentExample extends React.Component + {element} ) diff --git a/docs/src/components/ExternalExampleLayout.tsx b/docs/src/components/ExternalExampleLayout.tsx index 69493b2ecd..1d97064019 100644 --- a/docs/src/components/ExternalExampleLayout.tsx +++ b/docs/src/components/ExternalExampleLayout.tsx @@ -1,4 +1,4 @@ -import { Provider, themes, ThemeInput } from '@stardust-ui/react' +import { Provider, themes } from '@stardust-ui/react' import * as _ from 'lodash' import * as React from 'react' import { match } from 'react-router' @@ -55,10 +55,16 @@ class ExternalExampleLayout extends React.Component< if (!examplePath) return const exampleSource: ExampleSource = exampleSourcesContext(examplePath) - const theme = this.getTheme() + + const { themeName } = this.state + const theme = (themeName && themes[themeName]) || {} return ( - + ) } - - private getTheme = (): ThemeInput => { - const { themeName } = this.state - const theme: ThemeInput = (themeName && themes[themeName]) || {} - - theme.rtl = this.props.match.params.rtl === 'true' - return theme - } } export default ExternalExampleLayout diff --git a/docs/src/examples/components/Provider/Types/ProviderDisableAnimationsExample.tsx b/docs/src/examples/components/Provider/Types/ProviderDisableAnimationsExample.tsx new file mode 100644 index 0000000000..1ccacafac0 --- /dev/null +++ b/docs/src/examples/components/Provider/Types/ProviderDisableAnimationsExample.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import { Animation, Icon, Provider } from '@stardust-ui/react' + +const spinner = { + keyframe: { + from: { + transform: 'rotate(0deg)', + }, + to: { + transform: 'rotate(360deg)', + }, + }, + duration: '5s', + iterationCount: 'infinite', +} + +const AnimatedIcon = () => ( + + + +) + +const ProviderExampleAnimation = () => ( + + {'This icon will be animated'} +
+ + + {'This icon will not be animated, as animations are disabled in this tree'} +
+ +
+
+) + +export default ProviderExampleAnimation diff --git a/docs/src/examples/components/Provider/Types/ProviderRtlExample.tsx b/docs/src/examples/components/Provider/Types/ProviderRtlExample.tsx new file mode 100644 index 0000000000..6fe3d06f4b --- /dev/null +++ b/docs/src/examples/components/Provider/Types/ProviderRtlExample.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import { Provider } from '@stardust-ui/react' + +const ProviderRtlExample = () => ( + +
{'مثال النص'}
+
+) + +export default ProviderRtlExample diff --git a/docs/src/examples/components/Provider/Types/index.tsx b/docs/src/examples/components/Provider/Types/index.tsx index 0f8722922f..282a36190e 100644 --- a/docs/src/examples/components/Provider/Types/index.tsx +++ b/docs/src/examples/components/Provider/Types/index.tsx @@ -12,6 +12,16 @@ const Types = () => ( description="A Provider defines the theme for your components." examplePath="components/Provider/Types/ProviderExample" /> + + diff --git a/docs/src/prototypes/meetingOptions/components/MSTeamsLink.tsx b/docs/src/prototypes/meetingOptions/components/MSTeamsLink.tsx index 47e4c0d541..1e561463de 100644 --- a/docs/src/prototypes/meetingOptions/components/MSTeamsLink.tsx +++ b/docs/src/prototypes/meetingOptions/components/MSTeamsLink.tsx @@ -1,15 +1,11 @@ import * as React from 'react' -import { Provider, Text } from '@stardust-ui/react' +import { Text } from '@stardust-ui/react' export default props => { const { content, children } = props return ( - ( - - {children} - - )} - /> + + {children} + ) } diff --git a/packages/react/src/components/Animation/Animation.tsx b/packages/react/src/components/Animation/Animation.tsx index 78b2abfc8e..a039f59c0e 100644 --- a/packages/react/src/components/Animation/Animation.tsx +++ b/packages/react/src/components/Animation/Animation.tsx @@ -1,5 +1,6 @@ import * as PropTypes from 'prop-types' import * as React from 'react' +import cx from 'classnames' import { UIComponent, @@ -8,8 +9,6 @@ import { commonPropTypes, ChildrenComponentProps, } from '../../lib' -import { AnimationProp } from '../../themes/types' -import createAnimationStyles from '../../lib/createAnimationStyles' import { WithAsProp, withSafeTypeForAs } from '../../types' export interface AnimationProps @@ -104,28 +103,14 @@ class Animation extends UIComponent, any> { timingFunction: PropTypes.string, } - renderComponent({ ElementType, classes, unhandledProps, styles, variables, theme }) { - const { children, name } = this.props - - const animation: AnimationProp = { - name, - keyframeParams: this.props.keyframeParams, - duration: this.props.duration, - delay: this.props.delay, - iterationCount: this.props.iterationCount, - direction: this.props.direction, - fillMode: this.props.fillMode, - playState: this.props.playState, - timingFunction: this.props.timingFunction, - } - - const animationStyle = createAnimationStyles(animation, theme) + renderComponent({ ElementType, classes, unhandledProps }) { + const { children } = this.props const child = childrenExist(children) && (React.Children.only(children) as React.ReactElement) const result = child ? React.cloneElement(child, { - style: { ...animationStyle, ...(child.props && child.props.style) }, + className: cx(child.props.className, classes.children), }) : '' diff --git a/packages/react/src/components/Icon/Icon.tsx b/packages/react/src/components/Icon/Icon.tsx index ce148409ca..4282d7b889 100644 --- a/packages/react/src/components/Icon/Icon.tsx +++ b/packages/react/src/components/Icon/Icon.tsx @@ -80,7 +80,7 @@ class Icon extends UIComponent, any> { renderComponent({ ElementType, classes, unhandledProps, accessibility, theme, rtl, styles }) { const { name } = this.props - const { icons = {} } = theme + const { icons = {} } = theme || {} const maybeIcon = icons[name] const isSvgIcon = maybeIcon && maybeIcon.isSvg diff --git a/packages/react/src/components/ItemLayout/ItemLayout.tsx b/packages/react/src/components/ItemLayout/ItemLayout.tsx index 25fe576675..e916bc3cbc 100644 --- a/packages/react/src/components/ItemLayout/ItemLayout.tsx +++ b/packages/react/src/components/ItemLayout/ItemLayout.tsx @@ -12,7 +12,7 @@ import { rtlTextContainer, } from '../../lib' import Layout from '../Layout/Layout' -import { ComponentSlotClasses, ICSSInJSStyle } from '../../themes/types' +import { ComponentSlotClasses } from '../../themes/types' import { WithAsProp, withSafeTypeForAs } from '../../types' export interface ItemLayoutSlotClassNames { @@ -49,19 +49,19 @@ export interface ItemLayoutProps extends UIComponentProps, ContentComponentProps classes: ComponentSlotClasses, ) => React.ReactNode /** Styled applied to the root element of the rendered component. */ - rootCSS?: ICSSInJSStyle + rootCSS?: React.CSSProperties /** Styled applied to the media element of the rendered component. */ - mediaCSS?: ICSSInJSStyle + mediaCSS?: React.CSSProperties /** Styled applied to the header element of the rendered component. */ - headerCSS?: ICSSInJSStyle + headerCSS?: React.CSSProperties /** Styled applied to the header media element of the rendered component. */ - headerMediaCSS?: ICSSInJSStyle + headerMediaCSS?: React.CSSProperties /** Styled applied to the content element of the rendered component. */ - contentCSS?: ICSSInJSStyle + contentCSS?: React.CSSProperties /** Styled applied to the content element of the rendered component. */ - contentMediaCSS?: ICSSInJSStyle + contentMediaCSS?: React.CSSProperties /** Styled applied to the end media element of the rendered component. */ - endMediaCSS?: ICSSInJSStyle + endMediaCSS?: React.CSSProperties } class ItemLayout extends UIComponent, any> { diff --git a/packages/react/src/components/Provider/Provider.tsx b/packages/react/src/components/Provider/Provider.tsx index b03078670a..2a7175f05c 100644 --- a/packages/react/src/components/Provider/Provider.tsx +++ b/packages/react/src/components/Provider/Provider.tsx @@ -1,35 +1,40 @@ import { IStyle } from 'fela' -import { render } from 'fela-dom' +import { render as felaDomRender } from 'fela-dom' import * as _ from 'lodash' import * as PropTypes from 'prop-types' import * as React from 'react' -import { RendererProvider, ThemeProvider } from 'react-fela' +// @ts-ignore +import { RendererProvider, ThemeProvider, ThemeContext } from 'react-fela' import * as customPropTypes from '@stardust-ui/react-proptypes' import { felaRenderer as felaLtrRenderer, felaRtlRenderer, isBrowser, - mergeThemes, ChildrenComponentProps, } from '../../lib' import { ThemePrepared, - ThemeInput, StaticStyleObject, StaticStyle, StaticStyleFunction, FontFace, ComponentVariablesInput, + Renderer, + ThemeInput, } from '../../themes/types' import ProviderConsumer from './ProviderConsumer' import { mergeSiteVariables } from '../../lib/mergeThemes' import ProviderBox from './ProviderBox' -import { WithAsProp } from '../../types' +import { WithAsProp, ProviderContextInput, ProviderContextPrepared } from '../../types' +import mergeContexts from '../../lib/mergeProviderContexts' export interface ProviderProps extends ChildrenComponentProps { + renderer?: Renderer + rtl?: boolean + disableAnimations?: boolean theme: ThemeInput variables?: ComponentVariablesInput } @@ -47,7 +52,6 @@ class Provider extends React.Component> { siteVariables: PropTypes.object, componentVariables: PropTypes.object, componentStyles: PropTypes.object, - rtl: PropTypes.bool, fontFaces: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string, @@ -67,11 +71,19 @@ class Provider extends React.Component> { ), animations: PropTypes.object, }), + renderer: PropTypes.object, + rtl: PropTypes.bool, + disableAnimations: PropTypes.bool, children: PropTypes.node.isRequired, } + static defaultProps = { + theme: {}, + } + static Consumer = ProviderConsumer static Box = ProviderBox + static contextType = ThemeContext staticStylesRendered: boolean = false @@ -79,7 +91,7 @@ class Provider extends React.Component> { private get topLevelFelaRenderer() { if (!Provider._topLevelFelaRenderer) { - Provider._topLevelFelaRenderer = this.props.theme.rtl ? felaRtlRenderer : felaLtrRenderer + Provider._topLevelFelaRenderer = this.props.rtl ? felaRtlRenderer : felaLtrRenderer } return Provider._topLevelFelaRenderer } @@ -146,41 +158,49 @@ class Provider extends React.Component> { } render() { - const { as, theme, variables, children, ...unhandledProps } = this.props - + const { + as, + theme, + rtl, + disableAnimations, + renderer, + variables, + children, + ...unhandledProps + } = this.props + const inputContext: ProviderContextInput = { + theme, + rtl, + disableAnimations, + renderer, + } // rehydration disabled to avoid leaking styles between renderers // https://github.com/rofrischmann/fela/blob/master/docs/api/fela-dom/rehydrate.md + const outgoingContext: ProviderContextPrepared = mergeContexts(this.context, inputContext) + + // Heads up! + // We should call render() to ensure that a subscription for DOM updates was created + // https://github.com/stardust-ui/react/issues/581 + if (isBrowser()) felaDomRender(outgoingContext.renderer) + this.renderStaticStylesOnce(outgoingContext.theme) + + const rtlProps: { dir?: 'rtl' | 'ltr' } = {} + // only add dir attribute for top level provider or when direction changes from parent to child + if ( + !this.context || + (this.context.rtl !== outgoingContext.rtl && _.isBoolean(outgoingContext.rtl)) + ) { + rtlProps.dir = outgoingContext.rtl ? 'rtl' : 'ltr' + } + return ( - { - const outgoingTheme: ThemePrepared = mergeThemes(incomingTheme, theme) - - // Heads up! - // We should call render() to ensure that a subscription for DOM updates was created - // https://github.com/stardust-ui/react/issues/581 - if (isBrowser()) render(outgoingTheme.renderer) - this.renderStaticStylesOnce(outgoingTheme) - - const rtlProps: { dir?: 'rtl' | 'ltr' } = {} - // only add dir attribute for top level provider or when direction changes from parent to child - if ( - !incomingTheme || - (incomingTheme.rtl !== outgoingTheme.rtl && _.isBoolean(outgoingTheme.rtl)) - ) { - rtlProps.dir = outgoingTheme.rtl ? 'rtl' : 'ltr' - } - - return ( - - - - {children} - - - - ) - }} - /> + + + + {children} + + + ) } diff --git a/packages/react/src/components/Provider/ProviderConsumer.tsx b/packages/react/src/components/Provider/ProviderConsumer.tsx index 31b8a365e6..c8cc85915a 100644 --- a/packages/react/src/components/Provider/ProviderConsumer.tsx +++ b/packages/react/src/components/Provider/ProviderConsumer.tsx @@ -3,6 +3,7 @@ import * as React from 'react' import { FelaTheme } from 'react-fela' import { ThemePrepared } from '../../themes/types' +import { ProviderContextPrepared } from '../../types' export interface ProviderConsumerProps { /** @@ -17,7 +18,7 @@ export interface ProviderConsumerProps { * The Provider's Consumer is for accessing theme. */ const ProviderConsumer: React.FunctionComponent = ({ render }) => ( - {render} + {(context: ProviderContextPrepared) => render(context.theme)} ) ProviderConsumer.displayName = 'ProviderConsumer' diff --git a/packages/react/src/lib/accessibility/Styles/accessibilityStyles.ts b/packages/react/src/lib/accessibility/Styles/accessibilityStyles.ts index 40ab608927..c1591bafa0 100644 --- a/packages/react/src/lib/accessibility/Styles/accessibilityStyles.ts +++ b/packages/react/src/lib/accessibility/Styles/accessibilityStyles.ts @@ -1,7 +1,7 @@ -import { ICSSInJSStyle } from '../../../themes/types' +import * as React from 'react' // Visually hides elements which remain visible for screen reader -export const screenReaderContainerStyles: ICSSInJSStyle = { +export const screenReaderContainerStyles: React.CSSProperties = { border: '0', clip: 'rect(0 0 0 0)', height: '1px', diff --git a/packages/react/src/lib/createAnimationStyles.tsx b/packages/react/src/lib/createAnimationStyles.tsx index 28da7ee6b4..5b56911266 100644 --- a/packages/react/src/lib/createAnimationStyles.tsx +++ b/packages/react/src/lib/createAnimationStyles.tsx @@ -1,5 +1,4 @@ -import { ThemePrepared, AnimationProp } from '../themes/types' -import callable from './callable' +import { AnimationProp, ThemePrepared } from '../themes/types' const createAnimationStyles = (animation: AnimationProp, theme: ThemePrepared) => { let animationCSSProp = {} @@ -28,14 +27,12 @@ const createAnimationStyles = (animation: AnimationProp, theme: ThemePrepared) = ? animationThemeKeyframeParams : { ...animationThemeKeyframeParams, ...(animationPropKeyframeParams || {}) } - const evaluatedKeyframe = - typeof keyframe === 'string' - ? keyframe - : theme.renderer.renderKeyframe(callable(keyframe), mergedKeyframeParams) + const keyframeDefinition = + typeof keyframe === 'string' ? keyframe : { keyframe, params: mergedKeyframeParams } if (typeof animation === 'string') { animationCSSProp = { - animationName: evaluatedKeyframe, + animationName: keyframeDefinition, animationDelay: delay, animationDirection: direction, animationDuration: duration, @@ -46,7 +43,7 @@ const createAnimationStyles = (animation: AnimationProp, theme: ThemePrepared) = } } else { animationCSSProp = { - animationName: evaluatedKeyframe, + animationName: keyframeDefinition, animationDelay: animation.delay || delay, animationDirection: animation.direction || direction, animationDuration: animation.duration || duration, @@ -56,6 +53,27 @@ const createAnimationStyles = (animation: AnimationProp, theme: ThemePrepared) = animationTimingFunction: animation.timingFunction || timingFunction, } } + } else { + // animations was not found in the theme object + animationCSSProp = + typeof animation === 'string' + ? { + animationName: animation, + } + : { + animationName: animation.name, + ...(animation.delay && { animationDelay: animation.delay }), + ...(animation.direction && { animationDirection: animation.direction }), + ...(animation.duration && { animationDuration: animation.duration }), + ...(animation.fillMode && { animationFillMode: animation.fillMode }), + ...(animation.iterationCount && { + animationIterationCount: animation.iterationCount, + }), + ...(animation.playState && { animationPlayState: animation.playState }), + ...(animation.timingFunction && { + animationTimingFunction: animation.timingFunction, + }), + } } } return animationCSSProp diff --git a/packages/react/src/lib/createComponent.tsx b/packages/react/src/lib/createComponent.tsx index 9016b5dda9..3a01c20bb5 100644 --- a/packages/react/src/lib/createComponent.tsx +++ b/packages/react/src/lib/createComponent.tsx @@ -7,8 +7,7 @@ import renderComponent, { RenderResultConfig } from './renderComponent' import { AccessibilityActionHandlers } from './accessibility/reactTypes' import { FocusZone } from './accessibility/FocusZone' import { createShorthandFactory } from './factories' -import { ObjectOf } from '../types' -import { ThemePrepared } from '../themes/types' +import { ObjectOf, ProviderContextPrepared } from '../types' export interface CreateComponentConfig

{ displayName: string @@ -43,7 +42,7 @@ const createComponent =

= any>({ } const StardustComponent: CreateComponentReturnType

= (props): React.ReactElement

=> { - const theme: ThemePrepared = React.useContext(ThemeContext) + const context: ProviderContextPrepared = React.useContext(ThemeContext) return renderComponent( { @@ -57,7 +56,7 @@ const createComponent =

= any>({ focusZoneRef, render: config => render(config, props), }, - theme, + context, ) } diff --git a/packages/react/src/lib/felaDisableAnimationsPlugin.ts b/packages/react/src/lib/felaDisableAnimationsPlugin.ts new file mode 100644 index 0000000000..98d5884cb8 --- /dev/null +++ b/packages/react/src/lib/felaDisableAnimationsPlugin.ts @@ -0,0 +1,44 @@ +const animationProps = [ + 'animation', + 'animationName', + 'animationDuration', + 'animationTimingFunction', + 'animationDelay', + 'animationIterationCount', + 'animationDirection', + 'animationFillMode', + 'animationPlayState', +] + +/** + * Fela plugin for disabling animations. The animations are disabled or not based on the + * props' disableAnimations param. If the value of the prop is true, all animation related + * styles are removed. + * + * Caution! Infinite recursion is possible in case if style object has links to self in the props + * tree. + */ +export default () => { + const disableAnimations = (styles: Object, type?, renderer?, props?) => { + if (props && props.disableAnimations && type === 'RULE') { + return Object.keys(styles).reduce((acc, cssPropertyName) => { + const cssPropertyValue = styles[cssPropertyName] + + if (typeof cssPropertyValue === 'object') { + return { + ...acc, + [cssPropertyName]: disableAnimations(cssPropertyValue, type, renderer, props), + } + } + + if (animationProps.indexOf(cssPropertyName) !== -1) { + return acc + } + return { ...acc, [cssPropertyName]: styles[cssPropertyName] } + }, {}) + } + return styles + } + + return disableAnimations +} diff --git a/packages/react/src/lib/felaRenderKeyframesPlugin.ts b/packages/react/src/lib/felaRenderKeyframesPlugin.ts new file mode 100644 index 0000000000..aced89be9a --- /dev/null +++ b/packages/react/src/lib/felaRenderKeyframesPlugin.ts @@ -0,0 +1,49 @@ +import callable from './callable' + +/** + * Fela plugin for rendering keyframes. The keyframes, defined in the animationName prop, are rendered + * with the params object, if defined in the animationName prop. + * + * Caution! Infinite recursion is possible in case if style object has links to self in the props + * tree. + */ +export default () => { + const renderKeyframes = (styles: Object, type?, renderer?, props?) => { + return Object.keys(styles).reduce((acc, cssPropertyName) => { + const cssPropertyValue = styles[cssPropertyName] + if (cssPropertyName === 'animationName' && typeof cssPropertyValue === 'object') { + if (Array.isArray(cssPropertyValue)) { + styles[cssPropertyName] = cssPropertyValue + .map(animation => { + if (animation.keyframe) { + return renderer.renderKeyframe(callable(animation.keyframe), animation.params || {}) + } + return renderer.renderKeyframe(() => animation) + }, props) + .join(',') + } else if (cssPropertyValue.keyframe) { + styles[cssPropertyName] = renderer.renderKeyframe( + callable(cssPropertyValue.keyframe), + cssPropertyValue.params || {}, + ) + } else { + styles[cssPropertyName] = renderer.renderKeyframe(() => cssPropertyValue) + } + + return { + ...acc, + [cssPropertyName]: styles[cssPropertyName], + } + } + if (typeof cssPropertyValue === 'object') { + return { + ...acc, + [cssPropertyName]: renderKeyframes(cssPropertyValue, type, renderer, props), + } + } + return { ...acc, [cssPropertyName]: styles[cssPropertyName] } + }, {}) + } + + return renderKeyframes +} diff --git a/packages/react/src/lib/felaRenderer.tsx b/packages/react/src/lib/felaRenderer.tsx index 13837c3c54..5480ebbccc 100644 --- a/packages/react/src/lib/felaRenderer.tsx +++ b/packages/react/src/lib/felaRenderer.tsx @@ -4,9 +4,11 @@ import felaExpandCssShorthandsPlugin from './felaExpandCssShorthandsPlugin' import felaPluginFallbackValue from 'fela-plugin-fallback-value' import felaPluginPlaceholderPrefixer from 'fela-plugin-placeholder-prefixer' import felaPluginPrefixer from 'fela-plugin-prefixer' +import felaDisableAnimationsPlugin from './felaDisableAnimationsPlugin' import rtl from 'fela-plugin-rtl' import { Renderer } from '../themes/types' +import felaRenderKeyframesPlugin from './felaRenderKeyframesPlugin' let felaDevMode = false @@ -50,7 +52,7 @@ const createRendererConfig = (options: any = {}) => ({ // is necessary to prevent accidental style typos // from breaking ALL the styles on the page felaSanitizeCss({ - skip: ['content'], + skip: ['content', 'keyframe'], }), felaExpandCssShorthandsPlugin(), @@ -60,6 +62,8 @@ const createRendererConfig = (options: any = {}) => ({ // Heads up! // This is required after fela-plugin-prefixer to resolve the array of fallback values prefixer produces. felaPluginFallbackValue(), + felaDisableAnimationsPlugin(), + felaRenderKeyframesPlugin(), ...(options.isRtl ? [rtl()] : []), ], filterClassName, @@ -75,5 +79,4 @@ export const felaRenderer: Renderer = createRenderer( export const felaRtlRenderer: Renderer = createRenderer( createRendererConfig({ isRtl: true, rendererId: 'rtl' }), ) - export default felaRenderer diff --git a/packages/react/src/lib/index.ts b/packages/react/src/lib/index.ts index 51f470620e..84f4a551f3 100644 --- a/packages/react/src/lib/index.ts +++ b/packages/react/src/lib/index.ts @@ -20,6 +20,7 @@ export { default as getClasses } from './getClasses' export { default as getElementType } from './getElementType' export { default as getUnhandledProps } from './getUnhandledProps' export { default as mergeThemes } from './mergeThemes' +export { default as mergeProviderContexts } from './mergeProviderContexts' export * from './renderComponent' export { default as renderComponent } from './renderComponent' diff --git a/packages/react/src/lib/mergeProviderContexts.ts b/packages/react/src/lib/mergeProviderContexts.ts new file mode 100644 index 0000000000..dc16784601 --- /dev/null +++ b/packages/react/src/lib/mergeProviderContexts.ts @@ -0,0 +1,56 @@ +import { felaRenderer, felaRtlRenderer } from './felaRenderer' +import { ProviderContextPrepared, ProviderContextInput } from '../types' +import mergeThemes from './mergeThemes' + +export const mergeBooleanValues = (target, ...sources) => { + return sources.reduce((acc, next) => { + return typeof next === 'boolean' ? next : acc + }, target) +} + +const mergeProviderContexts = (...contexts: ProviderContextInput[]): ProviderContextPrepared => { + const emptyContext = { + theme: { + siteVariables: {}, + componentVariables: {}, + componentStyles: {}, + fontFaces: [], + staticStyles: [], + icons: {}, + animations: {}, + }, + renderer: {}, + rtl: false, + disableAnimations: false, + } as ProviderContextPrepared + + return contexts.reduce( + (acc: ProviderContextPrepared, next: ProviderContextInput) => { + if (!next) return acc + + acc.theme = mergeThemes(acc.theme, next.theme) + + // Latest RTL value wins + const mergedRTL = mergeBooleanValues(acc.rtl, next.rtl) + if (typeof mergedRTL === 'boolean') { + acc.rtl = mergedRTL + } + + // Use the correct renderer for RTL + acc.renderer = acc.rtl ? felaRtlRenderer : felaRenderer + + // Latest disableAnimations value wins + const mergedDisableAnimations = mergeBooleanValues( + acc.disableAnimations, + next.disableAnimations, + ) + if (typeof mergedDisableAnimations === 'boolean') { + acc.disableAnimations = mergedDisableAnimations + } + return acc + }, + emptyContext, + ) +} + +export default mergeProviderContexts diff --git a/packages/react/src/lib/mergeThemes.ts b/packages/react/src/lib/mergeThemes.ts index caa12a7b73..ce43ec324e 100644 --- a/packages/react/src/lib/mergeThemes.ts +++ b/packages/react/src/lib/mergeThemes.ts @@ -19,7 +19,6 @@ import { ThemeAnimation, } from '../themes/types' import callable from './callable' -import { felaRenderer, felaRtlRenderer } from './felaRenderer' import toCompactArray from './toCompactArray' import { ObjectOf } from '../types' @@ -161,12 +160,6 @@ export const mergeThemeStyles = ( }, initial) } -export const mergeRTL = (target, ...sources) => { - return sources.reduce((acc, next) => { - return typeof next === 'boolean' ? next : acc - }, target) -} - export const mergeFontFaces = (...sources: FontFace[]) => { return toCompactArray(...sources) } @@ -217,15 +210,6 @@ const mergeThemes = (...themes: ThemeInput[]): ThemePrepared => { // Merge icons set, last one wins in case of collisions acc.icons = mergeIcons(acc.icons, next.icons) - // Latest RTL value wins - const mergedRTL = mergeRTL(acc.rtl, next.rtl) - if (typeof mergedRTL === 'boolean') { - acc.rtl = mergedRTL - } - - // Use the correct renderer for RTL - acc.renderer = acc.rtl ? felaRtlRenderer : felaRenderer - acc.fontFaces = mergeFontFaces(...acc.fontFaces, ...(next.fontFaces || [])) acc.staticStyles = mergeStaticStyles(...acc.staticStyles, ...(next.staticStyles || [])) diff --git a/packages/react/src/lib/renderComponent.tsx b/packages/react/src/lib/renderComponent.tsx index 442efe1cd0..4c372f1fe3 100644 --- a/packages/react/src/lib/renderComponent.tsx +++ b/packages/react/src/lib/renderComponent.tsx @@ -17,7 +17,7 @@ import { State, ThemePrepared, } from '../themes/types' -import { Props } from '../types' +import { Props, ProviderContextPrepared } from '../types' import { AccessibilityDefinition, FocusZoneMode, FocusZoneDefinition } from './accessibility/types' import { ReactAccessibilityBehavior, AccessibilityActionHandlers } from './accessibility/reactTypes' import { defaultBehavior } from './accessibility' @@ -126,7 +126,7 @@ const renderWithFocusZone =

( const renderComponent =

( config: RenderConfig

, - theme: ThemePrepared, + context: ProviderContextPrepared, ): React.ReactElement

=> { const { className, @@ -140,19 +140,20 @@ const renderComponent =

( render, } = config - if (_.isEmpty(theme)) { + if (_.isEmpty(context)) { logProviderMissingWarning() } + const { rtl = false, renderer = felaRenderer, disableAnimations = false } = context || {} + const { siteVariables = { fontSizes: {}, }, componentVariables = {}, componentStyles = {}, - rtl = false, - renderer = felaRenderer, - } = theme || {} + } = (context.theme as ThemePrepared) || {} + const ElementType = getElementType({ defaultProps }, props) as React.ReactType

const stateAndProps = { ...state, ...props } @@ -163,7 +164,9 @@ const renderComponent =

( props.variables, )(siteVariables) - const animationCSSProp = props.animation ? createAnimationStyles(props.animation, theme) : {} + const animationCSSProp = props.animation + ? createAnimationStyles(props.animation, context.theme) + : {} // Resolve styles using resolved variables, merge results, allow props.styles to override const mergedStyles: ComponentSlotStylesPrepared = mergeComponentStyles( @@ -184,7 +187,9 @@ const renderComponent =

( const styleParam: ComponentStyleFunctionParam = { props: stateAndProps, variables: resolvedVariables, - theme, + theme: context.theme, + rtl, + disableAnimations, } mergedStyles.root = { @@ -208,7 +213,7 @@ const renderComponent =

( styles: resolvedStyles, accessibility, rtl, - theme, + theme: context.theme, } if (accessibility.focusZone) { diff --git a/packages/react/src/themes/base/components/Icon/iconStyles.ts b/packages/react/src/themes/base/components/Icon/iconStyles.ts index 00dc742b45..e5df011d7c 100644 --- a/packages/react/src/themes/base/components/Icon/iconStyles.ts +++ b/packages/react/src/themes/base/components/Icon/iconStyles.ts @@ -35,7 +35,7 @@ const getPaddedStyle = (): ICSSInJSStyle => ({ }) const iconStyles: ComponentSlotStylesInput = { - root: ({ props: p, variables: v, theme: t }): ICSSInJSStyle => { + root: ({ props: p, variables: v, theme: t, rtl }): ICSSInJSStyle => { const iconSpec: ThemeIconSpec = t.icons[p.name] || emptyIcon const isFontIcon = !iconSpec.isSvg @@ -67,7 +67,7 @@ const iconStyles: ComponentSlotStylesInput = { content: (iconSpec.icon as FontIconSpec).content, }, - transform: t.rtl ? `scaleX(-1) rotate(${-1 * p.rotate}deg)` : `rotate(${p.rotate}deg)`, + transform: rtl ? `scaleX(-1) rotate(${-1 * p.rotate}deg)` : `rotate(${p.rotate}deg)`, }), } }, diff --git a/packages/react/src/themes/base/components/Loader/loaderStyles.ts b/packages/react/src/themes/base/components/Loader/loaderStyles.ts index 8d7b48aae0..45fa6cd274 100644 --- a/packages/react/src/themes/base/components/Loader/loaderStyles.ts +++ b/packages/react/src/themes/base/components/Loader/loaderStyles.ts @@ -27,18 +27,21 @@ export default { theme: t, variables: v, }: ComponentStyleFunctionParam): ICSSInJSStyle => { - const animationName = t.renderer.renderKeyframe( - () => + const animationName = { + keyframe: ({ from, to }) => ({ from: { - transform: 'rotate(0deg)', + transform: `rotate(${from})`, }, to: { - transform: 'rotate(360deg)', + transform: `rotate(${to}})`, }, } as any), - {}, - ) + params: { + from: '0deg', + to: '360deg', + }, + } const borderColor = `${v.foregroundColor} ${v.backgroundColor} ${v.backgroundColor}` return { diff --git a/packages/react/src/themes/teams/components/Animation/animationStyles.ts b/packages/react/src/themes/teams/components/Animation/animationStyles.ts index c30ba8d769..556763024e 100644 --- a/packages/react/src/themes/teams/components/Animation/animationStyles.ts +++ b/packages/react/src/themes/teams/components/Animation/animationStyles.ts @@ -1,5 +1,23 @@ +import { AnimationProp } from '../../../types' +import createAnimationStyles from '../../../../lib/createAnimationStyles' + export default { root: () => ({ display: 'inline-block', }), + children: ({ props: p, theme }) => { + const animation: AnimationProp = { + name: p.name, + keyframeParams: p.keyframeParams, + duration: p.duration, + delay: p.delay, + iterationCount: p.iterationCount, + direction: p.direction, + fillMode: p.fillMode, + playState: p.playState, + timingFunction: p.timingFunction, + } + + return createAnimationStyles(animation, theme) + }, } diff --git a/packages/react/src/themes/teams/components/Icon/iconStyles.ts b/packages/react/src/themes/teams/components/Icon/iconStyles.ts index 86b4e0cfd0..882785b795 100644 --- a/packages/react/src/themes/teams/components/Icon/iconStyles.ts +++ b/packages/react/src/themes/teams/components/Icon/iconStyles.ts @@ -99,10 +99,10 @@ const iconStyles: ComponentSlotStylesInput = { }, svgFlippingInRtl: config => { - const { props, theme } = config + const { props, rtl } = config return { ...callable(iconStyles.svg)(config), - ...(theme.rtl && { + ...(rtl && { transform: `scaleX(-1) rotate(${-1 * props.rotate}deg)`, }), } diff --git a/packages/react/src/themes/teams/components/Loader/loaderStyles.ts b/packages/react/src/themes/teams/components/Loader/loaderStyles.ts index 91bedeb10e..b52c2f22d2 100644 --- a/packages/react/src/themes/teams/components/Loader/loaderStyles.ts +++ b/packages/react/src/themes/teams/components/Loader/loaderStyles.ts @@ -27,15 +27,14 @@ export default { variables: v, }: ComponentStyleFunctionParam) => { const outerAnimation: ICSSInJSStyle = { - animationName: t.renderer.renderKeyframe( - () => + animationName: { + keyframe: () => ({ to: { opacity: 1, }, } as any), - {}, - ), + }, animationDelay: '1.5s', animationDirection: 'normal', animationDuration: '.3s', @@ -48,15 +47,14 @@ export default { position: 'relative', } const svgAnimation: ICSSInJSStyle = { - animationName: t.renderer.renderKeyframe( - () => + animationName: { + keyframe: () => ({ to: { transform: `translate3d(0, ${v.svgTranslatePosition[p.size]}, 0)`, }, } as any), - {}, - ), + }, animationDelay: '0s', animationDirection: 'normal', animationDuration: '2s', diff --git a/packages/react/src/themes/teams/components/Popup/popupContentStyles.ts b/packages/react/src/themes/teams/components/Popup/popupContentStyles.ts index e880e48360..a21e62c815 100644 --- a/packages/react/src/themes/teams/components/Popup/popupContentStyles.ts +++ b/packages/react/src/themes/teams/components/Popup/popupContentStyles.ts @@ -55,13 +55,13 @@ const getPointerStyles = ( } const popupContentStyles: ComponentSlotStylesInput = { - root: ({ props: p, theme: t, variables: v }): ICSSInJSStyle => ({ + root: ({ props: p, variables: v, rtl }): ICSSInJSStyle => ({ borderRadius: v.borderRadius, display: 'block', - ...(p.pointing && getPointerStyles(v, t.rtl, p.placement).root), + ...(p.pointing && getPointerStyles(v, rtl, p.placement).root), }), - pointer: ({ props: p, theme: t, variables: v }): ICSSInJSStyle => ({ + pointer: ({ props: p, variables: v, rtl }): ICSSInJSStyle => ({ display: 'block', position: 'absolute', @@ -72,7 +72,7 @@ const popupContentStyles: ComponentSlotStylesInput ({ display: 'block', diff --git a/packages/react/src/themes/types.ts b/packages/react/src/themes/types.ts index 7841984af2..8bbb717bc6 100644 --- a/packages/react/src/themes/types.ts +++ b/packages/react/src/themes/types.ts @@ -209,7 +209,18 @@ export interface ICSSPseudoElementStyle extends ICSSInJSStyle { content?: string } -export interface ICSSInJSStyle extends React.CSSProperties { +export interface StardustAnimationName { + keyframe?: any + params?: object +} + +type Omit = Pick> + +export type CSSProperties = Omit & { + animationName?: StardustAnimationName | string | 'none' +} + +export interface ICSSInJSStyle extends CSSProperties { // TODO Questionable: how else would users target their own children? [key: string]: any @@ -248,6 +259,8 @@ export interface ComponentStyleFunctionParam< props: State & TProps variables: TVars theme: ThemePrepared + rtl: boolean + disableAnimations: boolean } export type ComponentSlotStyleFunction = (( @@ -312,8 +325,6 @@ export interface ThemeInput { siteVariables?: SiteVariablesInput componentVariables?: ThemeComponentVariablesInput componentStyles?: ThemeComponentStylesInput - rtl?: boolean - renderer?: Renderer fontFaces?: FontFaces staticStyles?: StaticStyles icons?: ThemeIcons @@ -333,8 +344,6 @@ export interface ThemePrepared { componentVariables: { [key in keyof ThemeComponentVariablesPrepared]: ComponentVariablesPrepared } componentStyles: { [key in keyof ThemeComponentStylesPrepared]: ComponentSlotStylesPrepared } icons: ThemeIcons - rtl: boolean - renderer: Renderer fontFaces: FontFaces staticStyles: StaticStyles animations: { [key: string]: ThemeAnimation } diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index f5b2451130..fc064b7a25 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -3,6 +3,7 @@ // ======================================================== import * as React from 'react' +import { ThemeInput, Renderer, ThemePrepared } from './themes/types' export type Extendable = T & { [key: string]: V @@ -145,3 +146,21 @@ export const UNSAFE_typed = (componentType: TComponentType) => { (componentType as any) as UNSAFE_TypedComponent, } } + +// ======================================================== +// Provider's context +// ======================================================== + +export interface ProviderContextInput { + renderer?: Renderer + rtl?: boolean + disableAnimations?: boolean + theme?: ThemeInput +} + +export interface ProviderContextPrepared { + renderer: Renderer + rtl: boolean + disableAnimations: boolean + theme: ThemePrepared +} diff --git a/packages/react/test/specs/components/Provider/Provider-test.tsx b/packages/react/test/specs/components/Provider/Provider-test.tsx index b0d6f12919..110d373ab5 100644 --- a/packages/react/test/specs/components/Provider/Provider-test.tsx +++ b/packages/react/test/specs/components/Provider/Provider-test.tsx @@ -57,7 +57,7 @@ describe('Provider', () => { describe('RTL', () => { test('Sets dir="rtl" on the div for RTL theme', () => { const component = mount( - + , ) @@ -68,7 +68,7 @@ describe('Provider', () => { test('Sets dir="ltr" on the div for LTR theme', () => { const component = mount( - + , ) @@ -113,8 +113,8 @@ describe('Provider', () => { parentChildMatrix.forEach(({ parentIsRtl, childIsRtl, expectedChildDir }) => { test(`Nested providers: parent is RTL: ${parentIsRtl}, child is RTL: ${childIsRtl}, expected child dir: ${expectedChildDir}`, () => { const component = mount( - - + + , diff --git a/packages/react/test/specs/lib/createAnimationStyles-test.ts b/packages/react/test/specs/lib/createAnimationStyles-test.ts index 00a1233e4c..32c6bfe389 100644 --- a/packages/react/test/specs/lib/createAnimationStyles-test.ts +++ b/packages/react/test/specs/lib/createAnimationStyles-test.ts @@ -1,12 +1,10 @@ -import { createAnimationStyles, felaRenderer } from 'src/lib' +import { createAnimationStyles } from 'src/lib' const theme = { siteVariables: { fontSizes: {} }, componentVariables: {}, componentStyles: {}, icons: {}, - rtl: false, - renderer: felaRenderer, fontFaces: [], staticStyles: [], animations: { @@ -30,26 +28,19 @@ const theme = { }, } -const themeWithRenderedKeyframes = { - ...theme, - animations: { - spinner: { - keyframe: 'k1', - duration: '5s', - iterationCount: 'infinite', - fillMode: 'forwards', - playState: 'running', - timingFunction: 'ease', - direction: 'reverse', - delay: '2s', - }, - }, -} - describe('createAnimationStyles', () => { test('applies all animation props from the theme if the animation is string', () => { expect(createAnimationStyles('spinner', theme)).toMatchObject({ - animationName: expect.anything(), + animationName: { + keyframe: { + from: { + transform: 'rotate(0deg)', + }, + to: { + transform: 'rotate(360deg)', + }, + }, + }, animationDuration: '5s', animationIterationCount: 'infinite', animationFillMode: 'forwards', @@ -64,7 +55,16 @@ describe('createAnimationStyles', () => { expect( createAnimationStyles({ name: 'spinner', duration: '1s', delay: '3s' }, theme), ).toMatchObject({ - animationName: expect.anything(), + animationName: { + keyframe: { + from: { + transform: 'rotate(0deg)', + }, + to: { + transform: 'rotate(360deg)', + }, + }, + }, animationDuration: '1s', animationIterationCount: 'infinite', animationFillMode: 'forwards', @@ -74,19 +74,4 @@ describe('createAnimationStyles', () => { animationDelay: '3s', }) }) - - test('calls renderer renderKeyframe if the keyframe is an object', () => { - theme.renderer.renderKeyframe = jest.fn() - createAnimationStyles({ name: 'spinner', duration: '1s', delay: '3s' }, theme) - expect(theme.renderer.renderKeyframe).toHaveBeenCalledTimes(1) - }) - - test('does not call renderer renderKeyframe if the keyframe is a string', () => { - themeWithRenderedKeyframes.renderer.renderKeyframe = jest.fn() - createAnimationStyles( - { name: 'spinner', duration: '1s', delay: '3s' }, - themeWithRenderedKeyframes, - ) - expect(themeWithRenderedKeyframes.renderer.renderKeyframe).not.toHaveBeenCalled() - }) }) diff --git a/packages/react/test/specs/lib/felaDisableAnimationsPlugin-test.ts b/packages/react/test/specs/lib/felaDisableAnimationsPlugin-test.ts new file mode 100644 index 0000000000..e135b4c243 --- /dev/null +++ b/packages/react/test/specs/lib/felaDisableAnimationsPlugin-test.ts @@ -0,0 +1,54 @@ +import felaDisableAnimationsPlugin from 'src/lib/felaDisableAnimationsPlugin' + +const disableAnimationsPlugin = felaDisableAnimationsPlugin() + +const stylesWithAnimationShorthand = { + animation: 'k1', + margin: '0px 10px', +} + +const stylesWithAnimationProps = { + animationName: 'k1', + animationDuration: '1s', + margin: '0px 10px', +} + +describe('felaDisableAnimationsPlugin', () => { + test('does not disable animations if the props are not provided', () => { + expect(disableAnimationsPlugin(stylesWithAnimationShorthand, 'RULE')).toMatchObject( + stylesWithAnimationShorthand, + ) + }) + + test('does not disable animations if the disableAnimations flag is undefined', () => { + expect( + disableAnimationsPlugin(stylesWithAnimationShorthand, 'RULE', undefined, { + disableAnimations: undefined, + }), + ).toMatchObject(stylesWithAnimationShorthand) + }) + + test('does not disable animations if the disableAnimations flag is false', () => { + expect( + disableAnimationsPlugin(stylesWithAnimationProps, 'RULE', undefined, { + disableAnimations: false, + }), + ).toMatchObject(stylesWithAnimationProps) + }) + + test('disables animations if the disableAnimations flag is true', () => { + expect( + disableAnimationsPlugin(stylesWithAnimationProps, 'RULE', undefined, { + disableAnimations: true, + }), + ).toMatchObject({ margin: '0px 10px' }) + }) + + test('disables animations if the disableAnimations flag is true and the animation css shorthand is used', () => { + expect( + disableAnimationsPlugin(stylesWithAnimationShorthand, 'RULE', undefined, { + disableAnimations: true, + }), + ).toMatchObject({ margin: '0px 10px' }) + }) +}) diff --git a/packages/react/test/specs/lib/felaRenderKeyframesPlugin-test.ts b/packages/react/test/specs/lib/felaRenderKeyframesPlugin-test.ts new file mode 100644 index 0000000000..dd443ce159 --- /dev/null +++ b/packages/react/test/specs/lib/felaRenderKeyframesPlugin-test.ts @@ -0,0 +1,59 @@ +import felaRenderKeyframesPlugin from 'src/lib/felaRenderKeyframesPlugin' +import { felaRenderer } from 'src/lib' + +const renderKeyframesPlugin = felaRenderKeyframesPlugin() + +describe('felaRenderKeyframesPlugin', () => { + test('does not transform the animationName prop if it is already string', () => { + const style = { + animationName: 'k1', + animationDuration: '2s', + } + + expect(renderKeyframesPlugin(style, 'RULE', felaRenderer)).toMatchObject(style) + }) + + test('transforms the animationName prop if it contains keyframe in the definition', () => { + const style = { + animationName: { + keyframe: () => ({ from: { rotate: '0deg' }, to: { rotate: '360deg' } }), + }, + animationDuration: '2s', + } + + expect(renderKeyframesPlugin(style, 'RULE', felaRenderer)).toMatchObject({ + animationName: expect.any(String), + animationDuration: '2s', + }) + }) + + test('transforms the animationName prop if it contains keyframe in the definition', () => { + const style = { + animationName: { + keyframe: () => ({ from: { rotate: '0deg' }, to: { rotate: '360deg' } }), + params: {}, + }, + animationDuration: '2s', + } + + expect(renderKeyframesPlugin(style, 'RULE', felaRenderer)).toMatchObject({ + animationName: expect.any(String), + animationDuration: '2s', + }) + }) + + test('calls the renderer with the keyframe and params object', () => { + const params = { from: '0deg', to: '360deg' } + const style = { + animationName: { + keyframe: () => ({ from: { rotate: '0deg' }, to: { rotate: '360deg' } }), + params, + }, + animationDuration: '2s', + } + + const renderer = { renderKeyframe: jest.fn() } + renderKeyframesPlugin(style, 'RULE', renderer) + expect(renderer.renderKeyframe).toHaveBeenCalledWith(expect.any(Function), params) + }) +}) diff --git a/packages/react/test/specs/lib/mergeProviderContexts/mergeBooleanValues-test.ts b/packages/react/test/specs/lib/mergeProviderContexts/mergeBooleanValues-test.ts new file mode 100644 index 0000000000..87e03f43c2 --- /dev/null +++ b/packages/react/test/specs/lib/mergeProviderContexts/mergeBooleanValues-test.ts @@ -0,0 +1,33 @@ +import { mergeBooleanValues } from '../../../../src/lib/mergeProviderContexts' + +describe('mergeBooleanValues', () => { + test('latest boolean value wins', () => { + expect(mergeBooleanValues(false, true)).toEqual(true) + expect(mergeBooleanValues(true, false)).toEqual(false) + + expect(mergeBooleanValues(null, true)).toEqual(true) + expect(mergeBooleanValues(null, false)).toEqual(false) + + expect(mergeBooleanValues(undefined, true)).toEqual(true) + expect(mergeBooleanValues(undefined, false)).toEqual(false) + }) + + test('null values do not override boolean values', () => { + expect(mergeBooleanValues(false, null)).toEqual(false) + expect(mergeBooleanValues(true, null)).toEqual(true) + }) + + test('undefined values do not override boolean values', () => { + expect(mergeBooleanValues(false, undefined)).toEqual(false) + expect(mergeBooleanValues(true, undefined)).toEqual(true) + }) + + test('first value wins if no boolean was provided', () => { + // if a theme is created using mergeThemes() its rtl or disableAnimations should remain `undefined` to be able to inherit it from parent Provider + expect(mergeBooleanValues(null, null)).toEqual(null) + expect(mergeBooleanValues(undefined, null)).toEqual(undefined) + + expect(mergeBooleanValues(null, undefined)).toEqual(null) + expect(mergeBooleanValues(undefined, undefined)).toEqual(undefined) + }) +}) diff --git a/packages/react/test/specs/lib/mergeProviderContexts/mergeProviderContexts-test.ts b/packages/react/test/specs/lib/mergeProviderContexts/mergeProviderContexts-test.ts new file mode 100644 index 0000000000..0043851392 --- /dev/null +++ b/packages/react/test/specs/lib/mergeProviderContexts/mergeProviderContexts-test.ts @@ -0,0 +1,66 @@ +import mergeProviderContexts from 'src/lib/mergeProviderContexts' +import { felaRenderer, felaRtlRenderer } from 'src/lib' + +describe('mergeContexts', () => { + test(`always returns an object`, () => { + expect(mergeProviderContexts({}, {})).toMatchObject({}) + expect(mergeProviderContexts(null, null)).toMatchObject({}) + expect(mergeProviderContexts(undefined, undefined)).toMatchObject({}) + + expect(mergeProviderContexts(null, undefined)).toMatchObject({}) + expect(mergeProviderContexts(undefined, null)).toMatchObject({}) + + expect(mergeProviderContexts({}, undefined)).toMatchObject({}) + expect(mergeProviderContexts(undefined, {})).toMatchObject({}) + + expect(mergeProviderContexts({}, null)).toMatchObject({}) + expect(mergeProviderContexts(null, {})).toMatchObject({}) + }) + + test('gracefully handles merging a theme in with undefined values', () => { + const target = { + theme: { + siteVariables: { color: 'black' }, + componentVariables: { Button: { color: 'black' } }, + componentStyles: { Button: { root: { color: 'black' } } }, + }, + rtl: true, + disableAnimations: false, + } + const source = { + theme: undefined, + rtl: undefined, + disableAnimations: undefined, + } + expect(() => mergeProviderContexts(target, source)).not.toThrow() + }) + + test('gracefully handles merging onto a theme with undefined values', () => { + const target = { + theme: undefined, + rtl: undefined, + disableAnimations: undefined, + } + const source = { + theme: { + siteVariables: { color: 'black' }, + componentVariables: { Button: { color: 'black' } }, + componentStyles: { Button: { root: { color: 'black' } } }, + }, + rtl: true, + disableAnimations: false, + } + expect(() => mergeProviderContexts(target, source)).not.toThrow() + }) + + describe('renderer', () => { + test('felaRtlRenderer is chosen if rtl is true', () => { + expect(mergeProviderContexts({ rtl: true })).toHaveProperty('renderer', felaRtlRenderer) + }) + test('felaRenderer is chosen if rtl is not true', () => { + expect(mergeProviderContexts({ rtl: false })).toHaveProperty('renderer', felaRenderer) + expect(mergeProviderContexts({ rtl: null })).toHaveProperty('renderer', felaRenderer) + expect(mergeProviderContexts({ rtl: undefined })).toHaveProperty('renderer', felaRenderer) + }) + }) +}) diff --git a/packages/react/test/specs/lib/mergeThemes/mergeRTL-test.ts b/packages/react/test/specs/lib/mergeThemes/mergeRTL-test.ts deleted file mode 100644 index f2ef80c6cc..0000000000 --- a/packages/react/test/specs/lib/mergeThemes/mergeRTL-test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { mergeRTL } from '../../../../src/lib/mergeThemes' - -describe('mergeRTL', () => { - test('latest boolean value wins', () => { - expect(mergeRTL(false, true)).toEqual(true) - expect(mergeRTL(true, false)).toEqual(false) - - expect(mergeRTL(null, true)).toEqual(true) - expect(mergeRTL(null, false)).toEqual(false) - - expect(mergeRTL(undefined, true)).toEqual(true) - expect(mergeRTL(undefined, false)).toEqual(false) - }) - - test('null values do not override boolean values', () => { - expect(mergeRTL(false, null)).toEqual(false) - expect(mergeRTL(true, null)).toEqual(true) - }) - - test('undefined values do not override boolean values', () => { - expect(mergeRTL(false, undefined)).toEqual(false) - expect(mergeRTL(true, undefined)).toEqual(true) - }) - - test('first value wins if no boolean was provided', () => { - // if a theme is created using mergeThemes() its rtl should remain `undefined` to be able to inherit it from parent Provider - expect(mergeRTL(null, null)).toEqual(null) - expect(mergeRTL(undefined, null)).toEqual(undefined) - - expect(mergeRTL(null, undefined)).toEqual(null) - expect(mergeRTL(undefined, undefined)).toEqual(undefined) - }) -}) diff --git a/packages/react/test/specs/lib/mergeThemes/mergeThemes-test.ts b/packages/react/test/specs/lib/mergeThemes/mergeThemes-test.ts index 84272ff887..68afd0ae16 100644 --- a/packages/react/test/specs/lib/mergeThemes/mergeThemes-test.ts +++ b/packages/react/test/specs/lib/mergeThemes/mergeThemes-test.ts @@ -1,5 +1,4 @@ import mergeThemes, { mergeStyles } from 'src/lib/mergeThemes' -import { felaRenderer, felaRtlRenderer } from 'src/lib' import { ComponentStyleFunctionParam, ICSSInJSStyle } from 'src/themes/types' describe('mergeThemes', () => { @@ -307,48 +306,6 @@ describe('mergeThemes', () => { }) }) - describe('rtl', () => { - test('latest boolean value wins', () => { - expect(mergeThemes({ rtl: false }, { rtl: true })).toHaveProperty('rtl', true) - expect(mergeThemes({ rtl: true }, { rtl: false })).toHaveProperty('rtl', false) - - expect(mergeThemes({ rtl: null }, { rtl: true })).toHaveProperty('rtl', true) - expect(mergeThemes({ rtl: null }, { rtl: false })).toHaveProperty('rtl', false) - - expect(mergeThemes({ rtl: undefined }, { rtl: true })).toHaveProperty('rtl', true) - expect(mergeThemes({ rtl: undefined }, { rtl: false })).toHaveProperty('rtl', false) - }) - - test('null values do not override boolean values', () => { - expect(mergeThemes({ rtl: false }, { rtl: null })).toHaveProperty('rtl', false) - expect(mergeThemes({ rtl: true }, { rtl: null })).toHaveProperty('rtl', true) - }) - - test('undefined values do not override boolean values', () => { - expect(mergeThemes({ rtl: false }, { rtl: undefined })).toHaveProperty('rtl', false) - expect(mergeThemes({ rtl: true }, { rtl: undefined })).toHaveProperty('rtl', true) - }) - - test('is NOT set if no boolean was provided', () => { - expect(mergeThemes({ rtl: null }, { rtl: null })).not.toHaveProperty('rtl') - expect(mergeThemes({ rtl: null }, { rtl: undefined })).not.toHaveProperty('rtl') - - expect(mergeThemes({ rtl: undefined }, { rtl: null })).not.toHaveProperty('rtl') - expect(mergeThemes({ rtl: undefined }, { rtl: undefined })).not.toHaveProperty('rtl') - }) - }) - - describe('renderer', () => { - test('felaRtlRenderer is chosen if rtl is true', () => { - expect(mergeThemes({ rtl: true })).toHaveProperty('renderer', felaRtlRenderer) - }) - test('felaRenderer is chosen if rtl is not true', () => { - expect(mergeThemes({ rtl: false })).toHaveProperty('renderer', felaRenderer) - expect(mergeThemes({ rtl: null })).toHaveProperty('renderer', felaRenderer) - expect(mergeThemes({ rtl: undefined })).toHaveProperty('renderer', felaRenderer) - }) - }) - describe('styles', () => { test('merges styles object and function', () => { const stylesAsObject: ICSSInJSStyle = {