diff --git a/docs/src/components/Sidebar/Sidebar.tsx b/docs/src/components/Sidebar/Sidebar.tsx index 9e3e3f5fb1..94e708c66c 100644 --- a/docs/src/components/Sidebar/Sidebar.tsx +++ b/docs/src/components/Sidebar/Sidebar.tsx @@ -393,6 +393,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 f835a590f6..14a0f9abd0 100644 --- a/docs/src/routes.tsx +++ b/docs/src/routes.tsx @@ -49,6 +49,7 @@ import CustomScrollbarPrototype from './prototypes/customScrollbar' import EditorToolbarPrototype from './prototypes/EditorToolbar' import HexagonalAvatarPrototype from './prototypes/hexagonalAvatar' import TablePrototype from './prototypes/table' +import CreateComponentPrototype from './prototypes/createComponent' const Routes = () => ( @@ -89,6 +90,7 @@ const Routes = () => ( /> + diff --git a/packages/react-theming/src/components/Button/BaseButton.tsx b/packages/react-theming/src/components/Button/BaseButton.tsx new file mode 100644 index 0000000000..95ee7eaae1 --- /dev/null +++ b/packages/react-theming/src/components/Button/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-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-theming/src/components/Menu/BaseMenu.tsx b/packages/react-theming/src/components/Menu/BaseMenu.tsx new file mode 100644 index 0000000000..e2a99858c4 --- /dev/null +++ b/packages/react-theming/src/components/Menu/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-theming/src/components/Menu/BaseMenuItem.tsx b/packages/react-theming/src/components/Menu/BaseMenuItem.tsx new file mode 100644 index 0000000000..65ffcb10de --- /dev/null +++ b/packages/react-theming/src/components/Menu/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-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-theming/src/components/ThemeProvider/Provider.tsx b/packages/react-theming/src/components/ThemeProvider/Provider.tsx new file mode 100644 index 0000000000..62258a26c8 --- /dev/null +++ b/packages/react-theming/src/components/ThemeProvider/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-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-theming/src/create-component/ClassCache.ts b/packages/react-theming/src/create-component/ClassCache.ts new file mode 100644 index 0000000000..35de57cbd6 --- /dev/null +++ b/packages/react-theming/src/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-theming/src/create-component/FluentTheme.ts b/packages/react-theming/src/create-component/FluentTheme.ts new file mode 100644 index 0000000000..0b4eda4fed --- /dev/null +++ b/packages/react-theming/src/create-component/FluentTheme.ts @@ -0,0 +1,100 @@ +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: props => ({ + // bigIcon: true, size: m, shadowed: false + 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-theming/src/create-component/FluentThemeShape.ts b/packages/react-theming/src/create-component/FluentThemeShape.ts new file mode 100644 index 0000000000..451d0f4cf8 --- /dev/null +++ b/packages/react-theming/src/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-theming/src/create-component/ThemeShape.ts b/packages/react-theming/src/create-component/ThemeShape.ts new file mode 100644 index 0000000000..438df2a3f2 --- /dev/null +++ b/packages/react-theming/src/create-component/ThemeShape.ts @@ -0,0 +1,3 @@ +export interface IBaseThemeShape { + components?: any; +} diff --git a/packages/react-theming/src/create-component/createComponent.test.tsx b/packages/react-theming/src/create-component/createComponent.test.tsx new file mode 100644 index 0000000000..6590ad2594 --- /dev/null +++ b/packages/react-theming/src/create-component/createComponent.test.tsx @@ -0,0 +1,256 @@ +import { getClassName } from './createComponent'; +import { ClassCache } from './ClassCache'; + +describe('createComponent', () => { + 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-theming/src/create-component/createComponent.tsx b/packages/react-theming/src/create-component/createComponent.tsx new file mode 100644 index 0000000000..68f30423a2 --- /dev/null +++ b/packages/react-theming/src/create-component/createComponent.tsx @@ -0,0 +1,144 @@ +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? +// 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); + + // 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 && + 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 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 ; + }; +}; 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/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'),