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

feat(variants): Add variants to compose. #2281

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/react-theming/.storybook/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { configure } from '@storybook/react';
import { withInfo } from '@storybook/addon-info';
import { addDecorator } from '@storybook/react';

addDecorator(withInfo());

const req = require.context('../src', true, /\.stories\.tsx$/);

function loadStories() {
return req.keys().map(req);
}

configure(loadStories, module);
2 changes: 2 additions & 0 deletions packages/react-theming/.storybook/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const webpackConfig = require('@fluentui/scripts/config/storybook/webpack.config');
module.exports = webpackConfig;
1 change: 1 addition & 0 deletions packages/react-theming/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"build": "just-scripts build",
"clean": "just-scripts clean",
"just": "just-scripts",
"start": "just-scripts start",
"start-test": "just-scripts test:watch",
"test": "just-scripts test",
"update-snapshots": "just-scripts jest:snapshots"
Expand Down
43 changes: 34 additions & 9 deletions packages/react-theming/src/compose.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { useTheme } from './themeContext';
import { resolveTokens } from './resolveTokens';
import jss from 'jss';

import { resolveTokens } from './resolveTokens';
import { ITheme } from './theme.types';
import { useTheme } from './themeContext';
import { Variant } from './variant';

type Options = ComposeOptions[];
type SlotsAssignment = any;
type Variants = { [variantName: string]: Variant };

interface ComposeOptions {
name?: string;
slots?: any;
tokens?: any;
styles?: any;
variants?: Variants;
}

export interface Composeable {
Expand All @@ -26,6 +30,7 @@ export type ForwardRefComponent<TProps, TElement> = React.FunctionComponent<
interface ComposedFunctionComponent<TProps> extends React.FunctionComponent<TProps> {
__optionsSet?: ComposeOptions[];
__directRender?: React.FunctionComponent<TProps>;
variants?: Variants;

// Needed for components using forwardRef (See https://github.com/facebook/react/issues/12453).
render?: React.FunctionComponent<TProps>;
Expand Down Expand Up @@ -60,7 +65,7 @@ export const _composeFactory = (useThemeHook: any = useTheme) => {

return renderFn({
...props,
classes: _getClasses(componentName, theme, classNamesCache, optionsSet),
classes: _getClasses(componentName, theme, classNamesCache, optionsSet, props),
slots,
});
};
Expand All @@ -70,6 +75,7 @@ export const _composeFactory = (useThemeHook: any = useTheme) => {
}

Component.propTypes = baseComponent.propTypes;
Component.variants = options.variants;

Component.__optionsSet = optionsSet;
Component.__directRender = renderFn;
Expand Down Expand Up @@ -130,20 +136,39 @@ const _mergeOptions = (options: ComposeOptions, baseOptions?: Options): Options
return optionsSet;
};

/**
* _tokensFromOptions returns the accurate set of tokens
* based on the currently rendered props.
*
* @internal
*
* @param options A set of options
* @param props Props for this render
*/
export const _tokensFromOptions = (options: any[], props: any) => {
let result = options.reduce((acc, option) => {
return { ...acc, ...(option.tokens || {}) };
}, {});
options.forEach(option => {
Object.keys(option.variants || {}).forEach(variantName => {
const v: Variant = option.variants[variantName];
result = { ...result, ...v.tokens(props[variantName]) };
});
});
return result;
};

const _getClasses = (
name: string | undefined,
theme: ITheme,
classNamesCache: WeakMap<any, any>,
optionsSet: any[],
props: any,
) => {
let classes = classNamesCache.get(theme);
let classes = null; // classNamesCache.get(theme);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this was intentional.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now it is, as this PR doesn't yet implement correct caching based on props. Will be in a following PR once we ratify the design of variants. Good callout though!


if (!classes) {
const tokens = resolveTokens(
name,
theme,
optionsSet.map(o => o.tokens || {}),
);
const tokens = resolveTokens(name, theme, _tokensFromOptions(optionsSet, props));
let styles: any = {};

optionsSet.forEach((options: any) => {
Expand Down
101 changes: 101 additions & 0 deletions packages/react-theming/src/examples/compose/complex.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from 'react';

import { ThemeProvider } from './../../components/ThemeProvider/ThemeProvider';
import { compose } from './../../compose';
import { Variant } from './../../variant';
import { theme } from './../theme';

export default {
component: 'complex compose',
title: 'Slightly More Complex Compose Demos',
};

const BaseDisplay: React.FunctionComponent<{
classes: any;
slots: any;
slotProps: any;
title: string;
}> = props => {
return (
<div className={props.classes.root}>
<props.slots.header className={props.classes.header}>{props.title}</props.slots.header>
{props.children}
</div>
);
};

const ComposedDisplay = compose(BaseDisplay as any, {
slots: {
header: 'h2',
},
tokens: {
fontWeight: 300,
borderRadius: 0,
disabled: false,
},
styles: (tokens: any) => {
const style: any = {
root: {
background: 'red',
borderRadius: tokens.borderRadius,
margin: 10,
padding: 10,
},
header: {
fontWeight: tokens.fontWeight,
},
};
if (!tokens.disabled) {
style.root['&:hover'] = {
background: 'blue',
};
style.header['&:hover'] = {
textDecoration: 'underline',
};
} else {
style.root.background = '#ddd';
style.root.color = '#333';
}
return style;
},
variants: {
disabled: Variant.boolean({ disabled: true }),
strong: Variant.boolean({ fontWeight: 700 }),
rounded: Variant.boolean({ borderRadius: 30 }),
},
});

export const composedDemo = () => (
<ThemeProvider theme={theme}>
<ComposedDisplay>I am children</ComposedDisplay>
</ThemeProvider>
);

export const variantDemo = () => {
return (
<ThemeProvider theme={theme}>
<ComposedDisplay title="Lorem Ipsum">Default control</ComposedDisplay>
<ComposedDisplay title="Lorem Ipsum" strong>
Strong variant
</ComposedDisplay>
<ComposedDisplay title="Lorem Ipsum" rounded>
Rounded variant
</ComposedDisplay>
<ComposedDisplay title="Lorem Ipsum" strong rounded>
Strong &amp; rounded variants
</ComposedDisplay>
<ComposedDisplay title="Lorem Ipsum" disabled>
Disabled variant
</ComposedDisplay>
<ComposedDisplay title="Lorem Ipsum" strong disabled>
Strong &amp; Disabled variant
</ComposedDisplay>
<ComposedDisplay title="Lorem Ipsum" rounded disabled>
Rounded &amp; Disabled variant
</ComposedDisplay>
<ComposedDisplay title="Lorem Ipsum" strong rounded disabled>
Strong &amp; Rounded &amp; Disabled variants
</ComposedDisplay>
</ThemeProvider>
);
};
56 changes: 56 additions & 0 deletions packages/react-theming/src/examples/compose/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';

import { ThemeProvider } from './../../components/ThemeProvider/ThemeProvider';
import { compose } from './../../compose';
import { Variant } from './../../variant';
import { theme } from './../theme';

export default {
component: 'compose',
title: 'Compose Demos',
};

const BaseDiv: React.FunctionComponent<{ classes: any }> = props => {
return <div className={props.classes.root}>{props.children}</div>;
};

const ComposedDiv = compose(BaseDiv as any, {
tokens: {
fontWeight: 300,
borderRadius: 0,
},
styles: (tokens: any) => {
return {
root: {
background: 'red',
fontWeight: tokens.fontWeight,
borderRadius: tokens.borderRadius,
margin: 10,
padding: 10,
},
};
},
variants: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see that this is inspired from the work we have done on #2200 however after playing a bit with it, I have some doubts about how it would work when you will need to combine variants together. More over, usually for some variants like disable, you want to override everything that was previously defined (like no hover styles for example), would lieke to hear from you how you envision these to work. Generally, I think it would be useful to see more complex styles examples that will require the above mention scenarios.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added another scenario with a disabled prop. The styles implementation isn't ideal (yet), but shows what is possible.

strong: Variant.boolean({ fontWeight: 700 }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks interesting, however sometimes in the definition of the styles we will want to specify something if some variant is not defined, how would that work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For lack of a variant, the styles will default to whatever was defined in the base set of tokens. It would be easy to define another style of variant (eg booleanAlwaysEvaluated) that gets a chance to affect the tokens even when not called.

rounded: Variant.boolean({ borderRadius: 30 }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to reiterate some verbal discussion we had, I think it'd be neat if variants could also support slot implementations at component creation time rather than have to be passed in at render time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty good. One thought I have - do we have a perf test validating the compose examples? Could we maybe hook one up to Checkbox proto or Slider or something?

},
});

export const composedDemo = () => (
<ThemeProvider theme={theme}>
<ComposedDiv>I am children</ComposedDiv>
</ThemeProvider>
);

export const variantDemo = () => {
return (
<ThemeProvider theme={theme}>
<ComposedDiv>Default control</ComposedDiv>
<ComposedDiv strong>Strong variant</ComposedDiv>
<ComposedDiv rounded>Rounded variant</ComposedDiv>
<ComposedDiv strong rounded>
Strong &amp; rounded variants
</ComposedDiv>
</ThemeProvider>
);
};
47 changes: 47 additions & 0 deletions packages/react-theming/src/examples/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { initializeStyling, IColorRamp, ITheme } from './../index';

initializeStyling();

const emptyRamp: IColorRamp = { values: [], index: -1 };
export const theme: ITheme = {
components: {},
colors: {
background: 'white',
bodyText: 'black',
subText: 'black',
disabledText: 'green',
brand: emptyRamp,
accent: emptyRamp,
neutral: emptyRamp,
success: emptyRamp,
warning: emptyRamp,
danger: emptyRamp,
},
fonts: {
default: '',
userContent: '',
mono: '',
},
fontSizes: {
base: 1,
scale: 1,
unit: 'rem',
},
animations: {
fadeIn: {},
fadeOut: {},
},
direction: 'ltr',
spacing: {
base: 0,
scale: 0,
unit: 'rem',
},
radius: {
base: 0,
scale: 0,
unit: 'rem',
},
icons: {},
schemes: {},
};
2 changes: 1 addition & 1 deletion packages/react-theming/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ export { ThemeProvider } from './components/ThemeProvider/ThemeProvider';
export { Box } from './components/Box/Box';
export { createTheme } from './utilities/createTheme';

jss.setup(preset());
export const initializeStyling = () => jss.setup(preset());
Loading