From 1ab81826ce4b64d7280858e3aac02a07c82d03f1 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Mon, 16 Dec 2019 18:09:53 +0100 Subject: [PATCH 01/13] -wip --- .../src/utils/create-component/BaseButton.tsx | 38 +++ .../src/utils/create-component/BaseMenu.tsx | 22 ++ .../utils/create-component/BaseMenuItem.tsx | 32 +++ .../utils/create-component/ClassCache.test.ts | 49 ++++ .../src/utils/create-component/ClassCache.ts | 47 ++++ .../utils/create-component/FluentButton.tsx | 4 + .../src/utils/create-component/FluentMenu.tsx | 9 + .../utils/create-component/FluentMenuItem.tsx | 13 + .../src/utils/create-component/FluentTheme.ts | 99 +++++++ .../create-component/FluentThemeShape.ts | 32 +++ .../src/utils/create-component/Provider.tsx | 14 + .../src/utils/create-component/ThemeShape.ts | 3 + .../utils/create-component/compose.test.tsx | 250 ++++++++++++++++++ .../src/utils/create-component/compose.tsx | 136 ++++++++++ 14 files changed, 748 insertions(+) create mode 100644 packages/react/src/utils/create-component/BaseButton.tsx create mode 100644 packages/react/src/utils/create-component/BaseMenu.tsx create mode 100644 packages/react/src/utils/create-component/BaseMenuItem.tsx create mode 100644 packages/react/src/utils/create-component/ClassCache.test.ts create mode 100644 packages/react/src/utils/create-component/ClassCache.ts create mode 100644 packages/react/src/utils/create-component/FluentButton.tsx create mode 100644 packages/react/src/utils/create-component/FluentMenu.tsx create mode 100644 packages/react/src/utils/create-component/FluentMenuItem.tsx create mode 100644 packages/react/src/utils/create-component/FluentTheme.ts create mode 100644 packages/react/src/utils/create-component/FluentThemeShape.ts create mode 100644 packages/react/src/utils/create-component/Provider.tsx create mode 100644 packages/react/src/utils/create-component/ThemeShape.ts create mode 100644 packages/react/src/utils/create-component/compose.test.tsx create mode 100644 packages/react/src/utils/create-component/compose.tsx diff --git a/packages/react/src/utils/create-component/BaseButton.tsx b/packages/react/src/utils/create-component/BaseButton.tsx new file mode 100644 index 0000000000..1524590148 --- /dev/null +++ b/packages/react/src/utils/create-component/BaseButton.tsx @@ -0,0 +1,38 @@ +import * as React from 'react' + +/** + * TODO: + * 1) do we really need slots prop? + */ +interface IBaseButtonProps extends React.AllHTMLAttributes { + slots?: any + slotProps?: any +} + +export const ButtonText: React.FunctionComponent = props => my button + +export const BaseButton: React.FunctionComponent = props => { + const { slots, children, slotProps, ...rest } = props + const { + root: Root = 'button', + icon: Icon, + primaryText: PrimaryText, + secondaryText: SecondaryText, + } = slots || {} + const { root = {}, icon = {}, primaryText = {}, secondaryText = {} } = slotProps || {} + + const rootClassName = `${root.className || ''}${` ${rest.className}` || ''}` + const content = children || ( + <> + {Icon && } + {PrimaryText && } + {SecondaryText && } + + ) + + return ( + + {content} + + ) +} diff --git a/packages/react/src/utils/create-component/BaseMenu.tsx b/packages/react/src/utils/create-component/BaseMenu.tsx new file mode 100644 index 0000000000..6fdf7ab17f --- /dev/null +++ b/packages/react/src/utils/create-component/BaseMenu.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import { BaseMenuItem } from './BaseMenuItem' + +interface IMenuProps { + className?: string + slots?: any + slotProps?: any +} + +export const BaseMenu: React.FunctionComponent = props => { + const { slotProps = {}, slots = {}, ...rest } = props + const { item: MenuItem = BaseMenuItem, root: Root = 'div' } = slots + const { root: rootProps = {}, items = [] } = slotProps + const rootClassName = `${rootProps.className || ''}${` ${rest && rest.className}` || ''}` + return ( + + {items.map((item: any) => ( + + ))} + + ) +} diff --git a/packages/react/src/utils/create-component/BaseMenuItem.tsx b/packages/react/src/utils/create-component/BaseMenuItem.tsx new file mode 100644 index 0000000000..22018aabf1 --- /dev/null +++ b/packages/react/src/utils/create-component/BaseMenuItem.tsx @@ -0,0 +1,32 @@ +import * as React from 'react' + +interface IMenuItemProps { + className?: string + slots?: any + slotProps?: any +} + +export const BaseMenuItem: React.FunctionComponent = props => { + const { children, slots = {}, slotProps = {}, ...rest } = props + const { root: Root = 'div', text: Text, icon: Icon, menu: Menu } = slots + const { + root: rootProps = {}, + text: textProps = {}, + icon: iconProps = {}, + menu: menuProps = {}, + } = slotProps + const rootClassName = `${rootProps.className || ''}${` ${rest && rest.className}` || ''}` + const content = children || ( + <> + {Icon && } + {Text && } + {Menu && } + + ) + + return ( + + {content} + + ) +} diff --git a/packages/react/src/utils/create-component/ClassCache.test.ts b/packages/react/src/utils/create-component/ClassCache.test.ts new file mode 100644 index 0000000000..80004a2fbd --- /dev/null +++ b/packages/react/src/utils/create-component/ClassCache.test.ts @@ -0,0 +1,49 @@ +import { ClassCache, VariantBasedCacheKeyStrategy } from './ClassCache' + +describe('ClassCache', () => { + it('allows access via theme and string', () => { + const c = new ClassCache() + const val = {} + const theme = {} + c.set(theme, 'foo-bar-baz', val) + expect(c.get(theme, 'foo-bar-baz')).toBe(val) + }) + + it('allows access via theme and multiple strings', () => { + const c = new ClassCache() + const val = {} + const theme = {} + c.set(theme, 'foo-bar-baz', val) + c.set(theme, 'foo-bar', {}) + expect(c.get(theme, 'foo-bar-baz')).toBe(val) + }) + + it('returns null if entry not found', () => { + const c = new ClassCache() + expect(c.get({}, '')).toBeNull() + }) + + describe('getOrSet', () => { + it('allows for passing in of a default value', () => { + const c = new ClassCache() + const cacheEntry = {} + const theme = {} + const key = '' + const fetchedEntry: any = c.getOrSet(theme, key, cacheEntry) + expect(fetchedEntry).toBe(cacheEntry) + expect(c.get(theme, key)).toBe(cacheEntry) + }) + }) + + describe('with automative cache key computation', () => { + it('handles cache key computation', () => { + const c = new ClassCache() + const val = {} + const theme = {} + c.set(theme, new VariantBasedCacheKeyStrategy(['a', 'b', 'c'], {}).toString(), val) + expect(c.get(theme, new VariantBasedCacheKeyStrategy(['a', 'b', 'c'], {}).toString())).toBe( + val, + ) + }) + }) +}) diff --git a/packages/react/src/utils/create-component/ClassCache.ts b/packages/react/src/utils/create-component/ClassCache.ts new file mode 100644 index 0000000000..a2eee1efbd --- /dev/null +++ b/packages/react/src/utils/create-component/ClassCache.ts @@ -0,0 +1,47 @@ +export class ClassCache { + private cache = new WeakMap() + + public get(theme: {}, arg1: string): any { + const obj = this.cache.get(theme) + if (!obj) { + return null + } + return obj[arg1] || null + } + + public set(theme: {}, arg1: string, val: {}) { + let themeEntry + if (this.cache.get(theme)) { + themeEntry = this.cache.get(theme) + } else { + themeEntry = {} + this.cache.set(theme, themeEntry) + } + themeEntry[arg1] = val + } + + public getOrSet(theme: {}, key: string, cacheEntry: any): any { + const existing = this.get(theme, key) + if (existing !== undefined && existing !== null) { + return existing + } + this.set(theme, key, cacheEntry) + return cacheEntry + } +} + +export class VariantBasedCacheKeyStrategy { + private computed: string + + constructor(private variants: string[] = [], private props: any = {}) {} + + public toString() { + if (this.computed) { + return this.computed + } + const computedRaw: any = {} + this.variants.slice().forEach(v => (computedRaw[v] = this.props[v])) + this.computed = JSON.stringify(computedRaw) + return this.computed + } +} diff --git a/packages/react/src/utils/create-component/FluentButton.tsx b/packages/react/src/utils/create-component/FluentButton.tsx new file mode 100644 index 0000000000..5bb1f64bdf --- /dev/null +++ b/packages/react/src/utils/create-component/FluentButton.tsx @@ -0,0 +1,4 @@ +import { BaseButton } from './BaseButton' +import { compose } from './compose' + +export const FluentButton = compose('FluentButton', BaseButton) diff --git a/packages/react/src/utils/create-component/FluentMenu.tsx b/packages/react/src/utils/create-component/FluentMenu.tsx new file mode 100644 index 0000000000..7ea1eef6d2 --- /dev/null +++ b/packages/react/src/utils/create-component/FluentMenu.tsx @@ -0,0 +1,9 @@ +import { BaseMenu } from './BaseMenu' +import { FluentMenuItem } from './FluentMenuItem' +import { compose } from './compose' + +export const FluentMenu = compose('FluentMenu', BaseMenu, { + slots: { + item: FluentMenuItem, + }, +}) diff --git a/packages/react/src/utils/create-component/FluentMenuItem.tsx b/packages/react/src/utils/create-component/FluentMenuItem.tsx new file mode 100644 index 0000000000..2299bc0f6d --- /dev/null +++ b/packages/react/src/utils/create-component/FluentMenuItem.tsx @@ -0,0 +1,13 @@ +import { BaseMenuItem } from './BaseMenuItem' +import { compose } from './compose' +// import { FluentMenu } from './' + +export const FluentMenuItem = compose( + 'FluentMenuItem', + BaseMenuItem, + // { + // slots: { + // menu: FluentMenu, + // } + // } +) diff --git a/packages/react/src/utils/create-component/FluentTheme.ts b/packages/react/src/utils/create-component/FluentTheme.ts new file mode 100644 index 0000000000..4946eeb682 --- /dev/null +++ b/packages/react/src/utils/create-component/FluentTheme.ts @@ -0,0 +1,99 @@ +import { IFluentThemeShape, ColorRamp } from './FluentThemeShape' + +export const FluentTheme: IFluentThemeShape = { + colors: { + brand: new ColorRamp(['#00f9ff', '#008e91', '#003233']), + neutral: new ColorRamp(['#dedede', '#7c7c7c', '#292929']), + }, + typography: { + ramp: [8, 10, 12, 16, 24, 36, 48, 128], + fontFace: 'Futura', + }, +} + +export const FluentButtonTheme = { + styles: ({ typography, colors }: any) => ({ + root: { + fontFamily: typography.fontFace, + fontSize: typography.ramp[5], + backgroundColor: colors.brand.strongest(), + color: colors.neutral.weakest(), + }, + }), + variants: { + tiny: { + true: { + root: { fontSize: '20%' }, + }, + }, + large: { + true: { + root: { fontSize: '400%' }, + }, + }, + size: { + s: { root: { fontSize: '100%' } }, + m: { root: { fontSize: '200%' } }, + l: { root: { fontSize: '400%' } }, + }, + shadowed: { + true: { root: { fontSize: '77%', boxShadow: '10px 5px 5px purple' } }, + }, + bigIcon: { + true: { + root: { fontSize: '300%' }, + icon: { fontSize: '300%' }, + }, + }, + beautiful: { + true: { + root: { + border: '3px solid pink', + }, + }, + }, + }, +} + +export const PlannerFluentTheme: IFluentThemeShape = { + colors: { + brand: new ColorRamp(['#00f9ff', '#008e91', '#003233']), + neutral: new ColorRamp(['#dedede', '#7c7c7c', '#292929']), + }, + typography: { + ramp: [8, 10, 12, 16, 24, 36, 48, 128], + fontFace: 'Futura', + }, + components: { + FluentButton: FluentButtonTheme, + FluentMenu: { + styles: () => ({ + root: { + border: '1px solid red', + padding: '10px', + }, + }), + variants: { + rounded: { + true: { + root: { borderRadius: '10px' }, + }, + }, + }, + }, + FluentMenuItem: { + styles: () => ({ + root: { + border: '1px solid blue', + }, + }), + variants: { + rounded: { + true: { + root: { borderRadius: '20px' }, // FluentMenu should propagate this prop to the FluentMenuItem... + }, + }, + }, + }, + }, +} diff --git a/packages/react/src/utils/create-component/FluentThemeShape.ts b/packages/react/src/utils/create-component/FluentThemeShape.ts new file mode 100644 index 0000000000..be34eb301b --- /dev/null +++ b/packages/react/src/utils/create-component/FluentThemeShape.ts @@ -0,0 +1,32 @@ +import { IBaseThemeShape } from './ThemeShape' + +export class ColorRamp { + constructor(public colors: string[] = []) {} + + public strongest(): string { + return this.colors[this.colors.length - 1] + } + + public weakest(): string { + return this.colors[0] + } +} + +export interface IFluentThemeShape extends IBaseThemeShape { + colors: { + brand: ColorRamp + neutral: ColorRamp + } + + typography: { + ramp: number[] + fontFace: string + } + + components?: { + [key: string]: { + styles?: any + variants?: any + } + } +} diff --git a/packages/react/src/utils/create-component/Provider.tsx b/packages/react/src/utils/create-component/Provider.tsx new file mode 100644 index 0000000000..48dbee15d4 --- /dev/null +++ b/packages/react/src/utils/create-component/Provider.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' +// import { IBaseThemeShape } from './ThemeShape'; + +/* +interface IProviderProps { + theme: T; +} +*/ + +export const ProviderContext = React.createContext(null) + +export const Provider: React.FunctionComponent = props => { + return {props.children} +} diff --git a/packages/react/src/utils/create-component/ThemeShape.ts b/packages/react/src/utils/create-component/ThemeShape.ts new file mode 100644 index 0000000000..1ef0548f42 --- /dev/null +++ b/packages/react/src/utils/create-component/ThemeShape.ts @@ -0,0 +1,3 @@ +export interface IBaseThemeShape { + components?: any +} diff --git a/packages/react/src/utils/create-component/compose.test.tsx b/packages/react/src/utils/create-component/compose.test.tsx new file mode 100644 index 0000000000..f176f1fb7c --- /dev/null +++ b/packages/react/src/utils/create-component/compose.test.tsx @@ -0,0 +1,250 @@ +import { getClassName } from './compose' +import { ClassCache } from './ClassCache' + +describe('compose', () => { + describe('getClassName', () => { + it('returns nothing in the default case', () => { + expect(getClassName(new ClassCache(), {}, {}, '')).toEqual({}) + }) + + it('returns classNames for a single slot', () => { + expect(getClassName(new ClassCache(), {}, {}, '')).toEqual({}) + }) + + it('returns customized classNames for a single slot', () => { + const cssRenderer = (args: any) => { + if (args.background === '#fff') { + return 'correct' + } + return 'incorrect' + } + expect( + getClassName( + new ClassCache(), + { + components: { + foo: { + variants: { + primary: { + true: { + root: { + background: '#fff', + }, + }, + }, + }, + }, + }, + }, + { primary: true }, + 'foo', + cssRenderer, + ), + ).toEqual({ root: 'correct' }) + }) + + it('returns customized classNames for a single slot when multiple variants are specified', () => { + const cssRenderer = (args: any) => { + if (args.background === '#fff' && args.color === '#000') { + return 'correct' + } + return 'incorrect' + } + expect( + getClassName( + new ClassCache(), + { + components: { + foo: { + variants: { + primary: { + true: { + root: { + background: '#fff', + }, + }, + }, + disabled: { + true: { + root: { + color: '#000', + }, + }, + }, + }, + }, + }, + }, + { primary: true, disabled: true }, + 'foo', + cssRenderer, + ), + ).toEqual({ root: 'correct' }) + }) + + it('returns customized classNames for ennumerated variants', () => { + const cssRenderer = (args: any) => { + if (args.background === '#fff') { + return 'correct' + } + return 'incorrect' + } + expect( + getClassName( + new ClassCache(), + { + components: { + foo: { + variants: { + primary: { + very: { + root: { + background: '#fff', + }, + }, + }, + }, + }, + }, + }, + { primary: 'very' }, + 'foo', + cssRenderer, + ), + ).toEqual({ root: 'correct' }) + }) + }) + + describe('caching', () => { + it('uses the cache for a simple variant', () => { + let counter = 0 + const cssRenderer = (args: any) => `class-${counter++}` + const theme = { + components: { + foo: { + variants: { + primary: { + true: { + root: { + background: '#fff', + }, + }, + }, + }, + }, + }, + } + const cache = new ClassCache() + const originalClassNames = getClassName(cache, theme, { primary: true }, 'foo', cssRenderer) + const nextRenderClassNames = getClassName(cache, theme, { primary: true }, 'foo', cssRenderer) + expect(nextRenderClassNames).toEqual(originalClassNames) + }) + + it('skips the cache for a separate theme', () => { + let counter = 0 + const cssRenderer = (args: any) => `class-${counter++}` + const theme = { + components: { + foo: { + variants: { + primary: { + true: { + root: { + background: '#fff', + }, + }, + }, + }, + }, + }, + } + const anotherTheme = { ...theme } + const cache = new ClassCache() + const originalClassNames = getClassName(cache, theme, { primary: true }, 'foo', cssRenderer) + const nextRenderClassNames = getClassName( + cache, + anotherTheme, + { primary: true }, + 'foo', + cssRenderer, + ) + expect(nextRenderClassNames).not.toEqual(originalClassNames) + }) + }) + + it('correctly merges variants', () => { + const cssRendererImportant = (args: any) => { + if (args.background === 'red') { + return 'correct' + } + return 'incorrect' + } + expect( + getClassName( + new ClassCache(), + { + components: { + foo: { + variants: { + primary: { + true: { + root: { + background: '#fff', + }, + }, + }, + important: { + true: { + root: { + background: 'red', + }, + }, + }, + }, + }, + }, + }, + { primary: true, important: true }, + 'foo', + cssRendererImportant, + ), + ).toEqual({ root: 'correct' }) + + const cssRendererPrimary = (args: any) => { + if (args.background === '#fff') { + return 'correct' + } + return 'incorrect' + } + expect( + getClassName( + new ClassCache(), + { + components: { + foo: { + variants: { + important: { + true: { + root: { + background: 'red', + }, + }, + }, + primary: { + true: { + root: { + background: '#fff', + }, + }, + }, + }, + }, + }, + }, + { important: true, primary: true }, + 'foo', + cssRendererPrimary, + ), + ).toEqual({ root: 'correct' }) + }) +}) diff --git a/packages/react/src/utils/create-component/compose.tsx b/packages/react/src/utils/create-component/compose.tsx new file mode 100644 index 0000000000..fb589589b9 --- /dev/null +++ b/packages/react/src/utils/create-component/compose.tsx @@ -0,0 +1,136 @@ +import * as React from 'react' +import { ProviderContext } from './Provider' +import { mergeCss } from '@uifabric/merge-styles' +import { VariantBasedCacheKeyStrategy, ClassCache } from './ClassCache' + +// TODO: +// 1. how do we know the slots for component? +// 2. how do we tackle enum props (not just booleans) +// 3. final decision on styles living in theme (sync with JD) +// 4. type safety (props which are variants should be typed) +// 5. props which are variants should not be spreaded on the root +// 6. merging multiple variants styles should be predictable - maybe resolved... +// 7. merging is not correct (spreading is not enough, it should be deep merge) +// 8. How would it work for composition (Menu + MenuItem) +// 9. support inline-style calculation based on a prop +// 10. how to cache the styles + +/** + * Solvable: + * 6, 9 + * + * Possible blockers: + * P0: 3, 8, 10 + * P1: 4, 5, 7 + * + * Solved: + * 1, 2 + */ + +const getProps = (cssMap: any, props: any, slots: any = {}) => { + const newProps = { + ...props, + slotProps: props.slotProps || {}, + slots: { ...props.slots, ...slots }, + } + Object.keys(cssMap).forEach(slotName => { + if (!newProps.slotProps[slotName]) { + newProps.slotProps[slotName] = {} + } + newProps.slotProps[slotName].className = `${newProps.slotProps[slotName].className || + ''} ${cssMap[slotName] || ''}` + }) + + return newProps +} + +export const getClassName = ( + cache: ClassCache, + theme: any, + componentProps: any, + componentName: string, + cssRenderer: (args: any) => string = mergeCss, +) => { + const stylesAdditions: any = {} + const variantNames: string[] = [] + + const componentStyles = + theme && + theme.components && + theme.components[componentName] && + theme.components[componentName].styles + ? theme.components[componentName].styles({ + typography: theme.typography, + colors: theme.colors, + }) + : {} + + const slotNames: string[] = Object.keys(componentStyles) + + if ( + theme && + theme.components && + theme.components[componentName] && + theme.components[componentName].variants + ) { + Object.keys(theme.components[componentName].variants).forEach(variantName => { + stylesAdditions[variantName] = {} + variantNames.push(variantName) + Object.keys(theme.components[componentName].variants[variantName]).forEach(enumValue => { + const variant: any = {} + stylesAdditions[variantName][enumValue] = variant + + Object.keys(theme.components[componentName].variants[variantName][enumValue]).forEach( + slotName => { + if (!slotNames.find(s => s === slotName)) { + slotNames.push(slotName) + } + variant[slotName] = + theme.components[componentName].variants[variantName][enumValue][slotName] + }, + ) + }) + }) + } + + const mergedSlotStyles: any = {} + + slotNames.forEach(slotName => { + mergedSlotStyles[slotName] = componentStyles[slotName] || {} + // eslint-disable-next-line array-callback-return + variantNames.map(v => { + if ( + componentProps[v] !== undefined && + stylesAdditions[v] !== undefined && + stylesAdditions[v][componentProps[v]] !== undefined + ) { + mergedSlotStyles[slotName] = { + ...mergedSlotStyles[slotName], + ...stylesAdditions[v][componentProps[v]][slotName], + } + } + }) + }) + + const mutableCacheEntry: any = {} + const cacheKey = new VariantBasedCacheKeyStrategy(variantNames, componentProps) + const cacheEntry = cache.getOrSet(theme, cacheKey.toString(), mutableCacheEntry) + + if (cacheEntry !== mutableCacheEntry) { + return cacheEntry + } + slotNames.forEach(slotName => { + mutableCacheEntry[slotName] = cssRenderer(mergedSlotStyles[slotName]) + }) + return mutableCacheEntry +} + +export const compose = (displayName: string, BaseComponent: any, settings = { slots: {} }) => { + const cache = new ClassCache() + return (props: any) => { + const theme = (React.useContext(ProviderContext) as any)! + const cssMap = getClassName(cache, theme, props, displayName) + const newProps = getProps(cssMap, props, settings.slots) + return + } +} From cc410c2463af51d213fc21c0a102ad4d48c649af Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Tue, 17 Dec 2019 16:50:25 +0100 Subject: [PATCH 02/13] -added prototype for testing --- docs/src/components/Sidebar/Sidebar.tsx | 9 ++ docs/src/prototypes/createComponent/index.tsx | 122 ++++++++++++++++++ docs/src/routes.tsx | 2 + .../src/components/Button}/BaseButton.tsx | 22 ++-- .../src/components/Button/FluentButton.tsx | 4 + .../src/components/Menu}/BaseMenu.tsx | 22 ++-- .../src/components/Menu}/BaseMenuItem.tsx | 22 ++-- .../src/components/Menu/FluentMenu.tsx | 9 ++ .../src/components/Menu/FluentMenuItem.tsx | 13 ++ .../components/ThemeProvider}/Provider.tsx | 8 +- .../src/create-component/ClassCache.test.ts | 49 +++++++ .../src}/create-component/ClassCache.ts | 38 +++--- .../src}/create-component/FluentTheme.ts | 13 +- .../src}/create-component/FluentThemeShape.ts | 26 ++-- .../src}/create-component/ThemeShape.ts | 2 +- .../createComponent.test.tsx} | 108 ++++++++-------- .../src/create-component/createComponent.tsx} | 92 ++++++------- packages/react-theming/src/index.ts | 6 + .../utils/create-component/ClassCache.test.ts | 49 ------- .../utils/create-component/FluentButton.tsx | 4 - .../src/utils/create-component/FluentMenu.tsx | 9 -- .../utils/create-component/FluentMenuItem.tsx | 13 -- 22 files changed, 396 insertions(+), 246 deletions(-) create mode 100644 docs/src/prototypes/createComponent/index.tsx rename packages/{react/src/utils/create-component => react-theming/src/components/Button}/BaseButton.tsx (77%) create mode 100644 packages/react-theming/src/components/Button/FluentButton.tsx rename packages/{react/src/utils/create-component => react-theming/src/components/Menu}/BaseMenu.tsx (57%) rename packages/{react/src/utils/create-component => react-theming/src/components/Menu}/BaseMenuItem.tsx (80%) create mode 100644 packages/react-theming/src/components/Menu/FluentMenu.tsx create mode 100644 packages/react-theming/src/components/Menu/FluentMenuItem.tsx rename packages/{react/src/utils/create-component => react-theming/src/components/ThemeProvider}/Provider.tsx (66%) create mode 100644 packages/react-theming/src/create-component/ClassCache.test.ts rename packages/{react/src/utils => react-theming/src}/create-component/ClassCache.ts (50%) rename packages/{react/src/utils => react-theming/src}/create-component/FluentTheme.ts (95%) rename packages/{react/src/utils => react-theming/src}/create-component/FluentThemeShape.ts (50%) rename packages/{react/src/utils => react-theming/src}/create-component/ThemeShape.ts (64%) rename packages/{react/src/utils/create-component/compose.test.tsx => react-theming/src/create-component/createComponent.test.tsx} (79%) rename packages/{react/src/utils/create-component/compose.tsx => react-theming/src/create-component/createComponent.tsx} (71%) delete mode 100644 packages/react/src/utils/create-component/ClassCache.test.ts delete mode 100644 packages/react/src/utils/create-component/FluentButton.tsx delete mode 100644 packages/react/src/utils/create-component/FluentMenu.tsx delete mode 100644 packages/react/src/utils/create-component/FluentMenuItem.tsx diff --git a/docs/src/components/Sidebar/Sidebar.tsx b/docs/src/components/Sidebar/Sidebar.tsx index 583941f2c1..20f2123060 100644 --- a/docs/src/components/Sidebar/Sidebar.tsx +++ b/docs/src/components/Sidebar/Sidebar.tsx @@ -384,6 +384,15 @@ class Sidebar extends React.Component { }, public: true, }, + { + key: 'prototype-create-component', + title: { + content: 'Create component', + as: NavLink, + to: 'prototype-create-component', + }, + public: false, + }, ] const componentTreeSection = { diff --git a/docs/src/prototypes/createComponent/index.tsx b/docs/src/prototypes/createComponent/index.tsx new file mode 100644 index 0000000000..0e0ec13559 --- /dev/null +++ b/docs/src/prototypes/createComponent/index.tsx @@ -0,0 +1,122 @@ +import * as React from 'react' +import { mergeCss } from '@uifabric/merge-styles' +import { + Provider, + FluentTheme, + PlannerFluentTheme, + FluentButton, + FluentMenu, + FluentMenuItem, +} from '@fluentui/react-theming' + +const oddRedBorder = mergeCss({ border: '10px solid red' }) +const example = mergeCss({ margin: 20 }) + +const MenuItemText = (props: any) => { + return {props.children} +} + +// This is a bad API... :( +const items = [ + { + slots: { text: MenuItemText, menu: FluentMenu }, + slotProps: { + text: { id: 'blabla', children: 'Bla' }, + menu: { + slotProps: { + items: [ + { + slots: { text: MenuItemText }, + slotProps: { text: { id: 'blabla', children: 'Boo' } }, + }, + { + slots: { text: MenuItemText }, + slotProps: { text: { id: 'blabla', children: 'Coo' } }, + }, + ], + }, + }, + }, + rounded: true, + }, + { slots: { text: MenuItemText }, slotProps: { text: { id: 'blabla', children: 'Foo' } } }, +] + +// Much better in my opinion +// const items = [ +// { slots: { text: MenuItemText }, text: { id: 'blabla', children: 'Bla' } }, +// { slots: { text: MenuItemText }, text: { id: 'blabla', children: 'Foo' } } +// ]; + +const Icon: React.FunctionComponent = props => @ +const ButtonThemedExample: React.FunctionComponent<{}> = props => { + const onClick = React.useCallback(() => console.log('clicked button'), []) + const variants = () => { + return ( + <> +
+ tiny +
+
+ large +
+
+ small + medium + large +
+ +
+ shadowed +
+
+ BigIcon }} + /> +
+
+ + shadowed & tiny + +
+
+ + Shadowed tiny bigIcon + +
+
+ + Beautiful + +
+ +
+ + Fluent Button with an odd red border + +
+ + ) + } + return ( +
+

Fluent Theme

+ {variants()} + +

Planner Fluent Theme

+ {variants()} + +

Menu

+ + + + +
+ ) +} + +export default ButtonThemedExample diff --git a/docs/src/routes.tsx b/docs/src/routes.tsx index 853ba94482..1aa1e152ad 100644 --- a/docs/src/routes.tsx +++ b/docs/src/routes.tsx @@ -48,6 +48,7 @@ import ParticipantsListPrototype from './prototypes/ParticipantsList' import CustomScrollbarPrototype from './prototypes/customScrollbar' import EditorToolbarPrototype from './prototypes/EditorToolbar' import HexagonalAvatarPrototype from './prototypes/hexagonalAvatar' +import CreateComponentPrototype from './prototypes/createComponent' const Routes = () => ( @@ -87,6 +88,7 @@ const Routes = () => ( /> + diff --git a/packages/react/src/utils/create-component/BaseButton.tsx b/packages/react-theming/src/components/Button/BaseButton.tsx similarity index 77% rename from packages/react/src/utils/create-component/BaseButton.tsx rename to packages/react-theming/src/components/Button/BaseButton.tsx index 1524590148..95ee7eaae1 100644 --- a/packages/react/src/utils/create-component/BaseButton.tsx +++ b/packages/react-theming/src/components/Button/BaseButton.tsx @@ -1,38 +1,38 @@ -import * as React from 'react' +import * as React from 'react'; /** * TODO: * 1) do we really need slots prop? */ interface IBaseButtonProps extends React.AllHTMLAttributes { - slots?: any - slotProps?: any + slots?: any; + slotProps?: any; } -export const ButtonText: React.FunctionComponent = props => my button +export const ButtonText: React.FunctionComponent = props => my button; export const BaseButton: React.FunctionComponent = props => { - const { slots, children, slotProps, ...rest } = props + const { slots, children, slotProps, ...rest } = props; const { root: Root = 'button', icon: Icon, primaryText: PrimaryText, secondaryText: SecondaryText, - } = slots || {} - const { root = {}, icon = {}, primaryText = {}, secondaryText = {} } = slotProps || {} + } = slots || {}; + const { root = {}, icon = {}, primaryText = {}, secondaryText = {} } = slotProps || {}; - const rootClassName = `${root.className || ''}${` ${rest.className}` || ''}` + const rootClassName = `${root.className || ''}${` ${rest.className}` || ''}`; const content = children || ( <> {Icon && } {PrimaryText && } {SecondaryText && } - ) + ); return ( {content} - ) -} + ); +}; diff --git a/packages/react-theming/src/components/Button/FluentButton.tsx b/packages/react-theming/src/components/Button/FluentButton.tsx new file mode 100644 index 0000000000..5a73d01d51 --- /dev/null +++ b/packages/react-theming/src/components/Button/FluentButton.tsx @@ -0,0 +1,4 @@ +import { BaseButton } from './BaseButton'; +import { createComponent } from '../../create-component/createComponent'; + +export const FluentButton = createComponent('FluentButton', BaseButton); diff --git a/packages/react/src/utils/create-component/BaseMenu.tsx b/packages/react-theming/src/components/Menu/BaseMenu.tsx similarity index 57% rename from packages/react/src/utils/create-component/BaseMenu.tsx rename to packages/react-theming/src/components/Menu/BaseMenu.tsx index 6fdf7ab17f..e2a99858c4 100644 --- a/packages/react/src/utils/create-component/BaseMenu.tsx +++ b/packages/react-theming/src/components/Menu/BaseMenu.tsx @@ -1,22 +1,22 @@ -import * as React from 'react' -import { BaseMenuItem } from './BaseMenuItem' +import * as React from 'react'; +import { BaseMenuItem } from './BaseMenuItem'; interface IMenuProps { - className?: string - slots?: any - slotProps?: any + className?: string; + slots?: any; + slotProps?: any; } export const BaseMenu: React.FunctionComponent = props => { - const { slotProps = {}, slots = {}, ...rest } = props - const { item: MenuItem = BaseMenuItem, root: Root = 'div' } = slots - const { root: rootProps = {}, items = [] } = slotProps - const rootClassName = `${rootProps.className || ''}${` ${rest && rest.className}` || ''}` + const { slotProps = {}, slots = {}, ...rest } = props; + const { item: MenuItem = BaseMenuItem, root: Root = 'div' } = slots; + const { root: rootProps = {}, items = [] } = slotProps; + const rootClassName = `${rootProps.className || ''}${` ${rest && rest.className}` || ''}`; return ( {items.map((item: any) => ( ))} - ) -} + ); +}; diff --git a/packages/react/src/utils/create-component/BaseMenuItem.tsx b/packages/react-theming/src/components/Menu/BaseMenuItem.tsx similarity index 80% rename from packages/react/src/utils/create-component/BaseMenuItem.tsx rename to packages/react-theming/src/components/Menu/BaseMenuItem.tsx index 22018aabf1..65ffcb10de 100644 --- a/packages/react/src/utils/create-component/BaseMenuItem.tsx +++ b/packages/react-theming/src/components/Menu/BaseMenuItem.tsx @@ -1,32 +1,32 @@ -import * as React from 'react' +import * as React from 'react'; interface IMenuItemProps { - className?: string - slots?: any - slotProps?: any + className?: string; + slots?: any; + slotProps?: any; } export const BaseMenuItem: React.FunctionComponent = props => { - const { children, slots = {}, slotProps = {}, ...rest } = props - const { root: Root = 'div', text: Text, icon: Icon, menu: Menu } = slots + const { children, slots = {}, slotProps = {}, ...rest } = props; + const { root: Root = 'div', text: Text, icon: Icon, menu: Menu } = slots; const { root: rootProps = {}, text: textProps = {}, icon: iconProps = {}, menu: menuProps = {}, - } = slotProps - const rootClassName = `${rootProps.className || ''}${` ${rest && rest.className}` || ''}` + } = slotProps; + const rootClassName = `${rootProps.className || ''}${` ${rest && rest.className}` || ''}`; const content = children || ( <> {Icon && } {Text && } {Menu && } - ) + ); return ( {content} - ) -} + ); +}; diff --git a/packages/react-theming/src/components/Menu/FluentMenu.tsx b/packages/react-theming/src/components/Menu/FluentMenu.tsx new file mode 100644 index 0000000000..22537ab7b6 --- /dev/null +++ b/packages/react-theming/src/components/Menu/FluentMenu.tsx @@ -0,0 +1,9 @@ +import { BaseMenu } from './BaseMenu'; +import { FluentMenuItem } from './FluentMenuItem'; +import { createComponent } from '../../create-component/createComponent'; + +export const FluentMenu = createComponent('FluentMenu', BaseMenu, { + slots: { + item: FluentMenuItem, + }, +}); diff --git a/packages/react-theming/src/components/Menu/FluentMenuItem.tsx b/packages/react-theming/src/components/Menu/FluentMenuItem.tsx new file mode 100644 index 0000000000..c674e5c7e5 --- /dev/null +++ b/packages/react-theming/src/components/Menu/FluentMenuItem.tsx @@ -0,0 +1,13 @@ +import { BaseMenuItem } from './BaseMenuItem'; +import { createComponent } from '../../create-component/createComponent'; +// import { FluentMenu } from './' + +export const FluentMenuItem = createComponent( + 'FluentMenuItem', + BaseMenuItem, + // { + // slots: { + // menu: FluentMenu, + // } + // } +); diff --git a/packages/react/src/utils/create-component/Provider.tsx b/packages/react-theming/src/components/ThemeProvider/Provider.tsx similarity index 66% rename from packages/react/src/utils/create-component/Provider.tsx rename to packages/react-theming/src/components/ThemeProvider/Provider.tsx index 48dbee15d4..62258a26c8 100644 --- a/packages/react/src/utils/create-component/Provider.tsx +++ b/packages/react-theming/src/components/ThemeProvider/Provider.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import * as React from 'react'; // import { IBaseThemeShape } from './ThemeShape'; /* @@ -7,8 +7,8 @@ interface IProviderProps { } */ -export const ProviderContext = React.createContext(null) +export const ProviderContext = React.createContext(null); export const Provider: React.FunctionComponent = props => { - return {props.children} -} + return {props.children}; +}; diff --git a/packages/react-theming/src/create-component/ClassCache.test.ts b/packages/react-theming/src/create-component/ClassCache.test.ts new file mode 100644 index 0000000000..4650b563d6 --- /dev/null +++ b/packages/react-theming/src/create-component/ClassCache.test.ts @@ -0,0 +1,49 @@ +import { ClassCache, VariantBasedCacheKeyStrategy } from './ClassCache'; + +describe('ClassCache', () => { + it('allows access via theme and string', () => { + const c = new ClassCache(); + const val = {}; + const theme = {}; + c.set(theme, 'foo-bar-baz', val); + expect(c.get(theme, 'foo-bar-baz')).toBe(val); + }); + + it('allows access via theme and multiple strings', () => { + const c = new ClassCache(); + const val = {}; + const theme = {}; + c.set(theme, 'foo-bar-baz', val); + c.set(theme, 'foo-bar', {}); + expect(c.get(theme, 'foo-bar-baz')).toBe(val); + }); + + it('returns null if entry not found', () => { + const c = new ClassCache(); + expect(c.get({}, '')).toBeNull(); + }); + + describe('getOrSet', () => { + it('allows for passing in of a default value', () => { + const c = new ClassCache(); + const cacheEntry = {}; + const theme = {}; + const key = ''; + const fetchedEntry: any = c.getOrSet(theme, key, cacheEntry); + expect(fetchedEntry).toBe(cacheEntry); + expect(c.get(theme, key)).toBe(cacheEntry); + }); + }); + + describe('with automative cache key computation', () => { + it('handles cache key computation', () => { + const c = new ClassCache(); + const val = {}; + const theme = {}; + c.set(theme, new VariantBasedCacheKeyStrategy(['a', 'b', 'c'], {}).toString(), val); + expect(c.get(theme, new VariantBasedCacheKeyStrategy(['a', 'b', 'c'], {}).toString())).toBe( + val, + ); + }); + }); +}); diff --git a/packages/react/src/utils/create-component/ClassCache.ts b/packages/react-theming/src/create-component/ClassCache.ts similarity index 50% rename from packages/react/src/utils/create-component/ClassCache.ts rename to packages/react-theming/src/create-component/ClassCache.ts index a2eee1efbd..35de57cbd6 100644 --- a/packages/react/src/utils/create-component/ClassCache.ts +++ b/packages/react-theming/src/create-component/ClassCache.ts @@ -1,47 +1,47 @@ export class ClassCache { - private cache = new WeakMap() + private cache = new WeakMap(); public get(theme: {}, arg1: string): any { - const obj = this.cache.get(theme) + const obj = this.cache.get(theme); if (!obj) { - return null + return null; } - return obj[arg1] || null + return obj[arg1] || null; } public set(theme: {}, arg1: string, val: {}) { - let themeEntry + let themeEntry; if (this.cache.get(theme)) { - themeEntry = this.cache.get(theme) + themeEntry = this.cache.get(theme); } else { - themeEntry = {} - this.cache.set(theme, themeEntry) + themeEntry = {}; + this.cache.set(theme, themeEntry); } - themeEntry[arg1] = val + themeEntry[arg1] = val; } public getOrSet(theme: {}, key: string, cacheEntry: any): any { - const existing = this.get(theme, key) + const existing = this.get(theme, key); if (existing !== undefined && existing !== null) { - return existing + return existing; } - this.set(theme, key, cacheEntry) - return cacheEntry + this.set(theme, key, cacheEntry); + return cacheEntry; } } export class VariantBasedCacheKeyStrategy { - private computed: string + private computed: string; constructor(private variants: string[] = [], private props: any = {}) {} public toString() { if (this.computed) { - return this.computed + return this.computed; } - const computedRaw: any = {} - this.variants.slice().forEach(v => (computedRaw[v] = this.props[v])) - this.computed = JSON.stringify(computedRaw) - return this.computed + const computedRaw: any = {}; + this.variants.slice().forEach(v => (computedRaw[v] = this.props[v])); + this.computed = JSON.stringify(computedRaw); + return this.computed; } } diff --git a/packages/react/src/utils/create-component/FluentTheme.ts b/packages/react-theming/src/create-component/FluentTheme.ts similarity index 95% rename from packages/react/src/utils/create-component/FluentTheme.ts rename to packages/react-theming/src/create-component/FluentTheme.ts index 4946eeb682..0b4eda4fed 100644 --- a/packages/react/src/utils/create-component/FluentTheme.ts +++ b/packages/react-theming/src/create-component/FluentTheme.ts @@ -1,4 +1,4 @@ -import { IFluentThemeShape, ColorRamp } from './FluentThemeShape' +import { IFluentThemeShape, ColorRamp } from './FluentThemeShape'; export const FluentTheme: IFluentThemeShape = { colors: { @@ -9,7 +9,7 @@ export const FluentTheme: IFluentThemeShape = { ramp: [8, 10, 12, 16, 24, 36, 48, 128], fontFace: 'Futura', }, -} +}; export const FluentButtonTheme = { styles: ({ typography, colors }: any) => ({ @@ -46,14 +46,15 @@ export const FluentButtonTheme = { }, }, beautiful: { - true: { + true: props => ({ + // bigIcon: true, size: m, shadowed: false root: { border: '3px solid pink', }, - }, + }), }, }, -} +}; export const PlannerFluentTheme: IFluentThemeShape = { colors: { @@ -96,4 +97,4 @@ export const PlannerFluentTheme: IFluentThemeShape = { }, }, }, -} +}; diff --git a/packages/react/src/utils/create-component/FluentThemeShape.ts b/packages/react-theming/src/create-component/FluentThemeShape.ts similarity index 50% rename from packages/react/src/utils/create-component/FluentThemeShape.ts rename to packages/react-theming/src/create-component/FluentThemeShape.ts index be34eb301b..451d0f4cf8 100644 --- a/packages/react/src/utils/create-component/FluentThemeShape.ts +++ b/packages/react-theming/src/create-component/FluentThemeShape.ts @@ -1,32 +1,32 @@ -import { IBaseThemeShape } from './ThemeShape' +import { IBaseThemeShape } from './ThemeShape'; export class ColorRamp { constructor(public colors: string[] = []) {} public strongest(): string { - return this.colors[this.colors.length - 1] + return this.colors[this.colors.length - 1]; } public weakest(): string { - return this.colors[0] + return this.colors[0]; } } export interface IFluentThemeShape extends IBaseThemeShape { colors: { - brand: ColorRamp - neutral: ColorRamp - } + brand: ColorRamp; + neutral: ColorRamp; + }; typography: { - ramp: number[] - fontFace: string - } + ramp: number[]; + fontFace: string; + }; components?: { [key: string]: { - styles?: any - variants?: any - } - } + styles?: any; + variants?: any; + }; + }; } diff --git a/packages/react/src/utils/create-component/ThemeShape.ts b/packages/react-theming/src/create-component/ThemeShape.ts similarity index 64% rename from packages/react/src/utils/create-component/ThemeShape.ts rename to packages/react-theming/src/create-component/ThemeShape.ts index 1ef0548f42..438df2a3f2 100644 --- a/packages/react/src/utils/create-component/ThemeShape.ts +++ b/packages/react-theming/src/create-component/ThemeShape.ts @@ -1,3 +1,3 @@ export interface IBaseThemeShape { - components?: any + components?: any; } diff --git a/packages/react/src/utils/create-component/compose.test.tsx b/packages/react-theming/src/create-component/createComponent.test.tsx similarity index 79% rename from packages/react/src/utils/create-component/compose.test.tsx rename to packages/react-theming/src/create-component/createComponent.test.tsx index f176f1fb7c..6590ad2594 100644 --- a/packages/react/src/utils/create-component/compose.test.tsx +++ b/packages/react-theming/src/create-component/createComponent.test.tsx @@ -1,23 +1,23 @@ -import { getClassName } from './compose' -import { ClassCache } from './ClassCache' +import { getClassName } from './createComponent'; +import { ClassCache } from './ClassCache'; -describe('compose', () => { +describe('createComponent', () => { describe('getClassName', () => { it('returns nothing in the default case', () => { - expect(getClassName(new ClassCache(), {}, {}, '')).toEqual({}) - }) + expect(getClassName(new ClassCache(), {}, {}, '')).toEqual({}); + }); it('returns classNames for a single slot', () => { - expect(getClassName(new ClassCache(), {}, {}, '')).toEqual({}) - }) + expect(getClassName(new ClassCache(), {}, {}, '')).toEqual({}); + }); it('returns customized classNames for a single slot', () => { const cssRenderer = (args: any) => { if (args.background === '#fff') { - return 'correct' + return 'correct'; } - return 'incorrect' - } + return 'incorrect'; + }; expect( getClassName( new ClassCache(), @@ -40,16 +40,16 @@ describe('compose', () => { 'foo', cssRenderer, ), - ).toEqual({ root: 'correct' }) - }) + ).toEqual({ root: 'correct' }); + }); it('returns customized classNames for a single slot when multiple variants are specified', () => { const cssRenderer = (args: any) => { if (args.background === '#fff' && args.color === '#000') { - return 'correct' + return 'correct'; } - return 'incorrect' - } + return 'incorrect'; + }; expect( getClassName( new ClassCache(), @@ -79,16 +79,16 @@ describe('compose', () => { 'foo', cssRenderer, ), - ).toEqual({ root: 'correct' }) - }) + ).toEqual({ root: 'correct' }); + }); it('returns customized classNames for ennumerated variants', () => { const cssRenderer = (args: any) => { if (args.background === '#fff') { - return 'correct' + return 'correct'; } - return 'incorrect' - } + return 'incorrect'; + }; expect( getClassName( new ClassCache(), @@ -111,14 +111,14 @@ describe('compose', () => { 'foo', cssRenderer, ), - ).toEqual({ root: 'correct' }) - }) - }) + ).toEqual({ root: 'correct' }); + }); + }); describe('caching', () => { it('uses the cache for a simple variant', () => { - let counter = 0 - const cssRenderer = (args: any) => `class-${counter++}` + let counter = 0; + const cssRenderer = (args: any) => `class-${counter++}`; const theme = { components: { foo: { @@ -133,16 +133,22 @@ describe('compose', () => { }, }, }, - } - const cache = new ClassCache() - const originalClassNames = getClassName(cache, theme, { primary: true }, 'foo', cssRenderer) - const nextRenderClassNames = getClassName(cache, theme, { primary: true }, 'foo', cssRenderer) - expect(nextRenderClassNames).toEqual(originalClassNames) - }) + }; + const cache = new ClassCache(); + const originalClassNames = getClassName(cache, theme, { primary: true }, 'foo', cssRenderer); + const nextRenderClassNames = getClassName( + cache, + theme, + { primary: true }, + 'foo', + cssRenderer, + ); + expect(nextRenderClassNames).toEqual(originalClassNames); + }); it('skips the cache for a separate theme', () => { - let counter = 0 - const cssRenderer = (args: any) => `class-${counter++}` + let counter = 0; + const cssRenderer = (args: any) => `class-${counter++}`; const theme = { components: { foo: { @@ -157,28 +163,28 @@ describe('compose', () => { }, }, }, - } - const anotherTheme = { ...theme } - const cache = new ClassCache() - const originalClassNames = getClassName(cache, theme, { primary: true }, 'foo', cssRenderer) + }; + const anotherTheme = { ...theme }; + const cache = new ClassCache(); + const originalClassNames = getClassName(cache, theme, { primary: true }, 'foo', cssRenderer); const nextRenderClassNames = getClassName( cache, anotherTheme, { primary: true }, 'foo', cssRenderer, - ) - expect(nextRenderClassNames).not.toEqual(originalClassNames) - }) - }) + ); + expect(nextRenderClassNames).not.toEqual(originalClassNames); + }); + }); it('correctly merges variants', () => { const cssRendererImportant = (args: any) => { if (args.background === 'red') { - return 'correct' + return 'correct'; } - return 'incorrect' - } + return 'incorrect'; + }; expect( getClassName( new ClassCache(), @@ -208,14 +214,14 @@ describe('compose', () => { 'foo', cssRendererImportant, ), - ).toEqual({ root: 'correct' }) + ).toEqual({ root: 'correct' }); const cssRendererPrimary = (args: any) => { if (args.background === '#fff') { - return 'correct' + return 'correct'; } - return 'incorrect' - } + return 'incorrect'; + }; expect( getClassName( new ClassCache(), @@ -245,6 +251,6 @@ describe('compose', () => { 'foo', cssRendererPrimary, ), - ).toEqual({ root: 'correct' }) - }) -}) + ).toEqual({ root: 'correct' }); + }); +}); diff --git a/packages/react/src/utils/create-component/compose.tsx b/packages/react-theming/src/create-component/createComponent.tsx similarity index 71% rename from packages/react/src/utils/create-component/compose.tsx rename to packages/react-theming/src/create-component/createComponent.tsx index fb589589b9..2c80349225 100644 --- a/packages/react/src/utils/create-component/compose.tsx +++ b/packages/react-theming/src/create-component/createComponent.tsx @@ -1,7 +1,7 @@ -import * as React from 'react' -import { ProviderContext } from './Provider' -import { mergeCss } from '@uifabric/merge-styles' -import { VariantBasedCacheKeyStrategy, ClassCache } from './ClassCache' +import * as React from 'react'; +import { ProviderContext } from '../components/ThemeProvider/Provider'; +import { mergeCss } from '@uifabric/merge-styles'; +import { VariantBasedCacheKeyStrategy, ClassCache } from './ClassCache'; // TODO: // 1. how do we know the slots for component? @@ -32,17 +32,17 @@ const getProps = (cssMap: any, props: any, slots: any = {}) => { ...props, slotProps: props.slotProps || {}, slots: { ...props.slots, ...slots }, - } + }; Object.keys(cssMap).forEach(slotName => { if (!newProps.slotProps[slotName]) { - newProps.slotProps[slotName] = {} + newProps.slotProps[slotName] = {}; } newProps.slotProps[slotName].className = `${newProps.slotProps[slotName].className || - ''} ${cssMap[slotName] || ''}` - }) + ''} ${cssMap[slotName] || ''}`; + }); - return newProps -} + return newProps; +}; export const getClassName = ( cache: ClassCache, @@ -51,8 +51,8 @@ export const getClassName = ( componentName: string, cssRenderer: (args: any) => string = mergeCss, ) => { - const stylesAdditions: any = {} - const variantNames: string[] = [] + const stylesAdditions: any = {}; + const variantNames: string[] = []; const componentStyles = theme && @@ -63,9 +63,9 @@ export const getClassName = ( typography: theme.typography, colors: theme.colors, }) - : {} + : {}; - const slotNames: string[] = Object.keys(componentStyles) + const slotNames: string[] = Object.keys(componentStyles); if ( theme && @@ -74,29 +74,29 @@ export const getClassName = ( theme.components[componentName].variants ) { Object.keys(theme.components[componentName].variants).forEach(variantName => { - stylesAdditions[variantName] = {} - variantNames.push(variantName) + stylesAdditions[variantName] = {}; + variantNames.push(variantName); Object.keys(theme.components[componentName].variants[variantName]).forEach(enumValue => { - const variant: any = {} - stylesAdditions[variantName][enumValue] = variant + const variant: any = {}; + stylesAdditions[variantName][enumValue] = variant; Object.keys(theme.components[componentName].variants[variantName][enumValue]).forEach( slotName => { if (!slotNames.find(s => s === slotName)) { - slotNames.push(slotName) + slotNames.push(slotName); } variant[slotName] = - theme.components[componentName].variants[variantName][enumValue][slotName] + theme.components[componentName].variants[variantName][enumValue][slotName]; }, - ) - }) - }) + ); + }); + }); } - const mergedSlotStyles: any = {} + const mergedSlotStyles: any = {}; slotNames.forEach(slotName => { - mergedSlotStyles[slotName] = componentStyles[slotName] || {} + mergedSlotStyles[slotName] = componentStyles[slotName] || {}; // eslint-disable-next-line array-callback-return variantNames.map(v => { if ( @@ -107,30 +107,34 @@ export const getClassName = ( mergedSlotStyles[slotName] = { ...mergedSlotStyles[slotName], ...stylesAdditions[v][componentProps[v]][slotName], - } + }; } - }) - }) + }); + }); - const mutableCacheEntry: any = {} - const cacheKey = new VariantBasedCacheKeyStrategy(variantNames, componentProps) - const cacheEntry = cache.getOrSet(theme, cacheKey.toString(), mutableCacheEntry) + const mutableCacheEntry: any = {}; + const cacheKey = new VariantBasedCacheKeyStrategy(variantNames, componentProps); + const cacheEntry = cache.getOrSet(theme, cacheKey.toString(), mutableCacheEntry); if (cacheEntry !== mutableCacheEntry) { - return cacheEntry + return cacheEntry; } slotNames.forEach(slotName => { - mutableCacheEntry[slotName] = cssRenderer(mergedSlotStyles[slotName]) - }) - return mutableCacheEntry -} + mutableCacheEntry[slotName] = cssRenderer(mergedSlotStyles[slotName]); + }); + return mutableCacheEntry; +}; -export const compose = (displayName: string, BaseComponent: any, settings = { slots: {} }) => { - const cache = new ClassCache() +export const createComponent = ( + displayName: string, + BaseComponent: any, + settings = { slots: {} }, +) => { + const cache = new ClassCache(); return (props: any) => { - const theme = (React.useContext(ProviderContext) as any)! - const cssMap = getClassName(cache, theme, props, displayName) - const newProps = getProps(cssMap, props, settings.slots) - return - } -} + const theme = (React.useContext(ProviderContext) as any)!; + const cssMap = getClassName(cache, theme, props, displayName); + const newProps = getProps(cssMap, props, settings.slots); + return ; + }; +}; diff --git a/packages/react-theming/src/index.ts b/packages/react-theming/src/index.ts index f27630efab..6fab5c0978 100644 --- a/packages/react-theming/src/index.ts +++ b/packages/react-theming/src/index.ts @@ -26,4 +26,10 @@ export { ThemeProvider } from './components/ThemeProvider/ThemeProvider'; export { Box } from './components/Box/Box'; export { createTheme } from './utilities/createTheme'; +export { FluentButton } from './components/Button/FluentButton'; +export { FluentMenu } from './components/Menu/FluentMenu'; +export { FluentMenuItem } from './components/Menu/FluentMenuItem'; +export { Provider } from './components/ThemeProvider/Provider'; +export { FluentTheme, PlannerFluentTheme } from './create-component/FluentTheme'; + jss.setup(preset()); diff --git a/packages/react/src/utils/create-component/ClassCache.test.ts b/packages/react/src/utils/create-component/ClassCache.test.ts deleted file mode 100644 index 80004a2fbd..0000000000 --- a/packages/react/src/utils/create-component/ClassCache.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ClassCache, VariantBasedCacheKeyStrategy } from './ClassCache' - -describe('ClassCache', () => { - it('allows access via theme and string', () => { - const c = new ClassCache() - const val = {} - const theme = {} - c.set(theme, 'foo-bar-baz', val) - expect(c.get(theme, 'foo-bar-baz')).toBe(val) - }) - - it('allows access via theme and multiple strings', () => { - const c = new ClassCache() - const val = {} - const theme = {} - c.set(theme, 'foo-bar-baz', val) - c.set(theme, 'foo-bar', {}) - expect(c.get(theme, 'foo-bar-baz')).toBe(val) - }) - - it('returns null if entry not found', () => { - const c = new ClassCache() - expect(c.get({}, '')).toBeNull() - }) - - describe('getOrSet', () => { - it('allows for passing in of a default value', () => { - const c = new ClassCache() - const cacheEntry = {} - const theme = {} - const key = '' - const fetchedEntry: any = c.getOrSet(theme, key, cacheEntry) - expect(fetchedEntry).toBe(cacheEntry) - expect(c.get(theme, key)).toBe(cacheEntry) - }) - }) - - describe('with automative cache key computation', () => { - it('handles cache key computation', () => { - const c = new ClassCache() - const val = {} - const theme = {} - c.set(theme, new VariantBasedCacheKeyStrategy(['a', 'b', 'c'], {}).toString(), val) - expect(c.get(theme, new VariantBasedCacheKeyStrategy(['a', 'b', 'c'], {}).toString())).toBe( - val, - ) - }) - }) -}) diff --git a/packages/react/src/utils/create-component/FluentButton.tsx b/packages/react/src/utils/create-component/FluentButton.tsx deleted file mode 100644 index 5bb1f64bdf..0000000000 --- a/packages/react/src/utils/create-component/FluentButton.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { BaseButton } from './BaseButton' -import { compose } from './compose' - -export const FluentButton = compose('FluentButton', BaseButton) diff --git a/packages/react/src/utils/create-component/FluentMenu.tsx b/packages/react/src/utils/create-component/FluentMenu.tsx deleted file mode 100644 index 7ea1eef6d2..0000000000 --- a/packages/react/src/utils/create-component/FluentMenu.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { BaseMenu } from './BaseMenu' -import { FluentMenuItem } from './FluentMenuItem' -import { compose } from './compose' - -export const FluentMenu = compose('FluentMenu', BaseMenu, { - slots: { - item: FluentMenuItem, - }, -}) diff --git a/packages/react/src/utils/create-component/FluentMenuItem.tsx b/packages/react/src/utils/create-component/FluentMenuItem.tsx deleted file mode 100644 index 2299bc0f6d..0000000000 --- a/packages/react/src/utils/create-component/FluentMenuItem.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { BaseMenuItem } from './BaseMenuItem' -import { compose } from './compose' -// import { FluentMenu } from './' - -export const FluentMenuItem = compose( - 'FluentMenuItem', - BaseMenuItem, - // { - // slots: { - // menu: FluentMenu, - // } - // } -) From e1d33ce20397f7f6523103c475200f725d8852f5 Mon Sep 17 00:00:00 2001 From: Levi Thomason Date: Mon, 6 Jan 2020 16:08:27 -0800 Subject: [PATCH 03/13] add clarification comment and perf gain todo --- .../react-theming/src/create-component/createComponent.tsx | 4 ++++ packages/react/src/utils/renderComponent.tsx | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/react-theming/src/create-component/createComponent.tsx b/packages/react-theming/src/create-component/createComponent.tsx index 2c80349225..68f30423a2 100644 --- a/packages/react-theming/src/create-component/createComponent.tsx +++ b/packages/react-theming/src/create-component/createComponent.tsx @@ -67,6 +67,10 @@ export const getClassName = ( const slotNames: string[] = Object.keys(componentStyles); + // We need to merge the slot names defined in the styles with the slot names + // defined in the variants + // styles = { [slot]: { css in js } + // variants = { [enumValue]: { [slot]: { css in js } } } if ( theme && theme.components && diff --git a/packages/react/src/utils/renderComponent.tsx b/packages/react/src/utils/renderComponent.tsx index b7bda3bbd9..576169d385 100644 --- a/packages/react/src/utils/renderComponent.tsx +++ b/packages/react/src/utils/renderComponent.tsx @@ -208,6 +208,8 @@ const renderComponent =

( : {} // Resolve styles using resolved variables, merge results, allow props.styles to override + // TODO: update mergeComponentStyles to cache its results based on theme object and prop combinations. + // together with the already existent resolveStylesAndClasses caching, this should skip all style calculations on re-render. const mergedStyles: ComponentSlotStylesPrepared = mergeComponentStyles( theme.componentStyles[displayName], withDebugId({ root: props.design }, 'props.design'), From 458898fda6d51a82a6fe36438e8fe10d3c797d44 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Tue, 7 Jan 2020 15:44:52 +0100 Subject: [PATCH 04/13] -added mergeComponentStyles cache --- packages/react/package.json | 1 + packages/react/src/utils/factories.ts | 19 +++++++++++------ packages/react/src/utils/mergeThemes.ts | 19 +++++++++++++++++ packages/react/src/utils/renderComponent.tsx | 22 ++++++++++++++------ yarn.lock | 5 +++++ 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index 31093910e8..60bc9200bc 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -23,6 +23,7 @@ "fela-plugin-rtl": "^10.6.1", "keyboard-key": "^1.0.1", "lodash": "^4.17.15", + "object-hash": "^2.0.1", "popper.js": "^1.15.0", "prop-types": "^15.7.2", "react-fela": "^10.6.1", diff --git a/packages/react/src/utils/factories.ts b/packages/react/src/utils/factories.ts index 8c0f4855c8..0163e4fed8 100644 --- a/packages/react/src/utils/factories.ts +++ b/packages/react/src/utils/factories.ts @@ -9,7 +9,7 @@ import { ShorthandRenderFunction, ShorthandRenderer, } from '../types' -import { mergeStyles } from './mergeThemes' +// import { mergeStyles } from './mergeThemes' type HTMLTag = 'iframe' | 'img' | 'input' type ShorthandProp = 'children' | 'src' | 'type' @@ -250,11 +250,18 @@ function createShorthandFromValue

({ // Merge styles if (defaultProps.styles || overrideProps.styles || usersProps.styles) { - ;(props as any).styles = mergeStyles( - defaultProps.styles, - usersProps.styles, - overrideProps.styles, - ) + // As long as we allow the styles prop to be a function, I don't see how we can cache it, as the stirng of the function will always be mergeStyles.. + // ;(props as any).styles = mergeStyles( + // defaultProps.styles, + // usersProps.styles, + // overrideProps.styles, + // ) + // Just for hacking, relaying that it will always be an object, so we can cache... + ;(props as any).styles = { + ...(defaultProps.styles || {}), + ...(usersProps.styles || {}), + ...(overrideProps.styles || {}), + } } // ---------------------------------------- diff --git a/packages/react/src/utils/mergeThemes.ts b/packages/react/src/utils/mergeThemes.ts index c9f74626d1..20aa4b4da3 100644 --- a/packages/react/src/utils/mergeThemes.ts +++ b/packages/react/src/utils/mergeThemes.ts @@ -22,6 +22,7 @@ import { import toCompactArray from './toCompactArray' import deepmerge from './deepmerge' import objectKeyToValues from './objectKeysToValues' +import hash from 'object-hash' import { isEnabled as isDebugEnabled } from './debug/debugEnabled' import withDebugId from './withDebugId' @@ -42,6 +43,24 @@ export const emptyTheme: ThemePrepared = { // Component level merge functions // ---------------------------------------- +const mergeComponentStylesCache = {} + +export const mergeComponentStylesWithCache = ( + hashObj, + sources: (ComponentSlotStylesInput | null | undefined)[], +) => { + try { + const hashVal = hash(hashObj) + + if (!mergeComponentStylesCache[hashVal]) { + mergeComponentStylesCache[hashVal] = mergeComponentStyles(...sources) + } + + return mergeComponentStylesCache[hashVal] + } catch (e) { + return mergeComponentStyles(...sources) + } +} /** * Merges a single component's styles (keyed by component part) with another component's styles. */ diff --git a/packages/react/src/utils/renderComponent.tsx b/packages/react/src/utils/renderComponent.tsx index 576169d385..e6577e3a7c 100644 --- a/packages/react/src/utils/renderComponent.tsx +++ b/packages/react/src/utils/renderComponent.tsx @@ -29,7 +29,7 @@ import { import { Props, ProviderContextPrepared } from '../types' import { ReactAccessibilityBehavior, AccessibilityActionHandlers } from './accessibility/reactTypes' import getKeyDownHandlers from './getKeyDownHandlers' -import { emptyTheme, mergeComponentStyles, mergeComponentVariables } from './mergeThemes' +import { emptyTheme, mergeComponentVariables, mergeComponentStylesWithCache } from './mergeThemes' import createAnimationStyles from './createAnimationStyles' import { isEnabled as isDebugEnabled } from './debug/debugEnabled' import { DebugData } from './debug/debugData' @@ -210,11 +210,21 @@ const renderComponent =

( // Resolve styles using resolved variables, merge results, allow props.styles to override // TODO: update mergeComponentStyles to cache its results based on theme object and prop combinations. // together with the already existent resolveStylesAndClasses caching, this should skip all style calculations on re-render. - const mergedStyles: ComponentSlotStylesPrepared = mergeComponentStyles( - theme.componentStyles[displayName], - withDebugId({ root: props.design }, 'props.design'), - withDebugId({ root: props.styles }, 'props.styles'), - withDebugId({ root: animationCSSProp }, 'props.animation'), + const mergedStyles: ComponentSlotStylesPrepared = mergeComponentStylesWithCache( + { + displayName, + design: props.design, + styles: props.styles, + animation: props.animation, + themeStyles: + theme.componentStyles[displayName] && theme.componentStyles[displayName].toString(), + }, + [ + theme.componentStyles[displayName], + withDebugId({ root: props.design }, 'props.design'), + withDebugId({ root: props.styles }, 'props.styles'), + withDebugId({ root: animationCSSProp }, 'props.animation'), + ], ) const accessibility: ReactAccessibilityBehavior = getAccessibility( diff --git a/yarn.lock b/yarn.lock index c076cab20a..091c88a08b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15360,6 +15360,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.1.tgz#cef18a0c940cc60aa27965ecf49b782cbf101d96" + integrity sha512-HgcGMooY4JC2PBt9sdUdJ6PMzpin+YtY3r/7wg0uTifP+HJWW8rammseSEHuyt0UeShI183UGssCJqm1bJR7QA== + object-inspect@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" From 132181e4c3285566c345c69c6996e7e735eecc69 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Tue, 7 Jan 2020 17:41:31 +0100 Subject: [PATCH 05/13] -changed hash fn --- packages/react/src/utils/mergeThemes.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react/src/utils/mergeThemes.ts b/packages/react/src/utils/mergeThemes.ts index 20aa4b4da3..4c9df179a8 100644 --- a/packages/react/src/utils/mergeThemes.ts +++ b/packages/react/src/utils/mergeThemes.ts @@ -22,7 +22,7 @@ import { import toCompactArray from './toCompactArray' import deepmerge from './deepmerge' import objectKeyToValues from './objectKeysToValues' -import hash from 'object-hash' +// import hash from 'object-hash' import { isEnabled as isDebugEnabled } from './debug/debugEnabled' import withDebugId from './withDebugId' @@ -50,7 +50,9 @@ export const mergeComponentStylesWithCache = ( sources: (ComponentSlotStylesInput | null | undefined)[], ) => { try { - const hashVal = hash(hashObj) + // using the hash fn is costly... is stringify valid? [I previosly convert all possible fn to string...] + // const hashVal = hash(hashObj) + const hashVal = JSON.stringify(hashObj) if (!mergeComponentStylesCache[hashVal]) { mergeComponentStylesCache[hashVal] = mergeComponentStyles(...sources) From 67aad10b4eb602c501e751e788fb19d1d3703990 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Wed, 8 Jan 2020 10:49:33 +0100 Subject: [PATCH 06/13] -fixed bug --- packages/react/src/utils/mergeThemes.ts | 9 ++++++++- packages/react/src/utils/renderComponent.tsx | 3 +-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/react/src/utils/mergeThemes.ts b/packages/react/src/utils/mergeThemes.ts index 4c9df179a8..ee3947093a 100644 --- a/packages/react/src/utils/mergeThemes.ts +++ b/packages/react/src/utils/mergeThemes.ts @@ -44,15 +44,22 @@ export const emptyTheme: ThemePrepared = { // ---------------------------------------- const mergeComponentStylesCache = {} +const mergeComponentStylesThemeCache = {} export const mergeComponentStylesWithCache = ( + theme, hashObj, sources: (ComponentSlotStylesInput | null | undefined)[], ) => { try { + const themeHash = JSON.stringify(theme) + if (!mergeComponentStylesThemeCache[themeHash]) { + mergeComponentStylesThemeCache[themeHash] = Object.keys(mergeComponentStylesThemeCache).length + } + // using the hash fn is costly... is stringify valid? [I previosly convert all possible fn to string...] // const hashVal = hash(hashObj) - const hashVal = JSON.stringify(hashObj) + const hashVal = JSON.stringify({ ...hashObj, theme: mergeComponentStylesThemeCache[themeHash] }) if (!mergeComponentStylesCache[hashVal]) { mergeComponentStylesCache[hashVal] = mergeComponentStyles(...sources) diff --git a/packages/react/src/utils/renderComponent.tsx b/packages/react/src/utils/renderComponent.tsx index e6577e3a7c..33530b69f3 100644 --- a/packages/react/src/utils/renderComponent.tsx +++ b/packages/react/src/utils/renderComponent.tsx @@ -211,13 +211,12 @@ const renderComponent =

( // TODO: update mergeComponentStyles to cache its results based on theme object and prop combinations. // together with the already existent resolveStylesAndClasses caching, this should skip all style calculations on re-render. const mergedStyles: ComponentSlotStylesPrepared = mergeComponentStylesWithCache( + theme, { displayName, design: props.design, styles: props.styles, animation: props.animation, - themeStyles: - theme.componentStyles[displayName] && theme.componentStyles[displayName].toString(), }, [ theme.componentStyles[displayName], From ec188cf9d434820f7409cd5dd95e3bd47b0fec6b Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Wed, 8 Jan 2020 12:04:26 +0100 Subject: [PATCH 07/13] -more fixes -added theme hash --- .../examples/components/Animation/Types/AnimationExample.tsx | 2 +- .../components/Animation/Types/AnimationExampleDelay.tsx | 2 +- .../components/Animation/Types/AnimationExampleDirection.tsx | 2 +- .../components/Animation/Types/AnimationExampleDuration.tsx | 2 +- .../components/Animation/Types/AnimationExampleFillMode.tsx | 2 +- .../Animation/Types/AnimationExampleIterationCount.tsx | 2 +- .../components/Animation/Types/AnimationExamplePlayState.tsx | 2 +- .../Animation/Types/AnimationExampleTimingFunction.tsx | 2 +- .../Chat/Types/ChatMessageExampleStyled.shorthand.tsx | 1 + .../components/Provider/Types/ProviderExample.shorthand.tsx | 2 +- packages/react/src/components/Flex/FlexItem.tsx | 4 ++-- packages/react/src/themes/teams-dark/index.ts | 1 + packages/react/src/themes/teams-high-contrast/index.ts | 1 + packages/react/src/themes/teams/index.tsx | 1 + packages/react/src/themes/types.ts | 2 ++ packages/react/src/utils/mergeThemes.ts | 4 +++- 16 files changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/src/examples/components/Animation/Types/AnimationExample.tsx b/docs/src/examples/components/Animation/Types/AnimationExample.tsx index f08f15f3b8..65a57f9b28 100644 --- a/docs/src/examples/components/Animation/Types/AnimationExample.tsx +++ b/docs/src/examples/components/Animation/Types/AnimationExample.tsx @@ -15,7 +15,7 @@ const spinner = { } const AnimationExample = () => ( - + diff --git a/docs/src/examples/components/Animation/Types/AnimationExampleDelay.tsx b/docs/src/examples/components/Animation/Types/AnimationExampleDelay.tsx index aea561f531..9fd4ef9de4 100644 --- a/docs/src/examples/components/Animation/Types/AnimationExampleDelay.tsx +++ b/docs/src/examples/components/Animation/Types/AnimationExampleDelay.tsx @@ -18,7 +18,7 @@ const AnimationExampleDelay = () => (

This animation will start after 5 seconds
- + diff --git a/docs/src/examples/components/Animation/Types/AnimationExampleDirection.tsx b/docs/src/examples/components/Animation/Types/AnimationExampleDirection.tsx index 83ba4a24b9..31460191a1 100644 --- a/docs/src/examples/components/Animation/Types/AnimationExampleDirection.tsx +++ b/docs/src/examples/components/Animation/Types/AnimationExampleDirection.tsx @@ -15,7 +15,7 @@ const spinner = { } const AnimationExampleDirection = () => ( - + diff --git a/docs/src/examples/components/Animation/Types/AnimationExampleDuration.tsx b/docs/src/examples/components/Animation/Types/AnimationExampleDuration.tsx index a32dafa30c..a8cec3f3d6 100644 --- a/docs/src/examples/components/Animation/Types/AnimationExampleDuration.tsx +++ b/docs/src/examples/components/Animation/Types/AnimationExampleDuration.tsx @@ -15,7 +15,7 @@ const spinner = { } const AnimationExampleDuration = () => ( - + diff --git a/docs/src/examples/components/Animation/Types/AnimationExampleFillMode.tsx b/docs/src/examples/components/Animation/Types/AnimationExampleFillMode.tsx index cb33cce563..a58b85f132 100644 --- a/docs/src/examples/components/Animation/Types/AnimationExampleFillMode.tsx +++ b/docs/src/examples/components/Animation/Types/AnimationExampleFillMode.tsx @@ -11,7 +11,7 @@ const colorChanger = { } const AnimationExampleFillMode = () => ( - + diff --git a/docs/src/examples/components/Animation/Types/AnimationExampleIterationCount.tsx b/docs/src/examples/components/Animation/Types/AnimationExampleIterationCount.tsx index 9b9f456d1e..cab1d201c0 100644 --- a/docs/src/examples/components/Animation/Types/AnimationExampleIterationCount.tsx +++ b/docs/src/examples/components/Animation/Types/AnimationExampleIterationCount.tsx @@ -15,7 +15,7 @@ const spinner = { } const AnimationExampleIterationCount = () => ( - + diff --git a/docs/src/examples/components/Animation/Types/AnimationExamplePlayState.tsx b/docs/src/examples/components/Animation/Types/AnimationExamplePlayState.tsx index 19437d1d91..7df21986ce 100644 --- a/docs/src/examples/components/Animation/Types/AnimationExamplePlayState.tsx +++ b/docs/src/examples/components/Animation/Types/AnimationExamplePlayState.tsx @@ -27,7 +27,7 @@ class AnimationExamplePlayState extends React.Component { render() { return ( - +