# Summary `react-theming` provides a set of tools for creating themable components. The `createComponent` tool extends unstyled components by applying given options and contextual overrides provided through React context, computes injected props including `classes`, `slots`, and `slotProps`. Styles are computed only when encountering unique theme objects per component, resulting in optimized performance. Example usage: ```jsx import { SliderBase } from 'implementation'; import { createComponent } from '@fluentui/react-theming'; export const Slider = createComponent(SliderBase, { // the themable name of the component name: 'Slider' // the set of replacement tokens to be injected into the styles tokens, // The css styling to be injected as a `classes` prop styles // A dictionary of React components representing the actual subcomponents to be used by the Slider slots }); // Created components can be further extended to alter look and feel. export const RedSlider = createComponent(Slider, { tokens: { railColor: 'red' } }); ``` Theming is achieved by using the `Provider` component (also provided): ```tsx // Create a theme with a token override for the Slider. const theme = createTheme({ components: { Slider: { tokens: { railColor: 'green', }, }, }, }); // Render the slider using the theme. const App = () => ( <Provider theme={theme}> <Slider /> </Provider> ); ``` ## Principles Key principles of `react-theming`: - allow any base component following the contract (explained in "createComponent API surface") to be extended (and therefore styled). - should be future proof, and not coupled to a specific styling/css implementation ## Who is `createComponent` for? ### Component authors Components created with `createComponent` are styleable by end user applications, and provide a separation of concerns when adressing multiple design systems or styling implementations. ### Application authors Application authors should find value using `createComponent`, as existing library components will be easily extendable, and new components can be effortlessly created that tie in to existing themes. ## When should I use `createComponent`? - When creating a component that should be themeable. - When overriding major details of an existing component. (Explained later in "Overriding (tokens|slots) with a new component".) ## What's in a theme? > TODO: document theme # Using components created with `createComponent` Components created with `createComponent` should exist in the context of a `Provider`. All other patterns common to developing with React controls should be standard. ## Styling components with a theme > TODO: document theme ## Customizing with tokens Frequently, design needs of a specific product conflict with that of the base design system. To accomodate necessary changes, `Tokens` exist to allow easy modification of most all aspects of look and feel. A token is a key that corresponds to a value, usually from from the applied theme. Examples of tokens might be `fontSize`, `fontFamily`, `borderRadius`, `animationDuration`, and `labelHoveredBackground`. ### Setting tokens with a theme To understand the set of tokens that a specific component understands, refer to the documentation of that component. For this example, we will assume that a `Button` component exists that supports the following tokens: - `backgroundColor` - `fontSize` - `backgroundHoverColor` To override any (or all) of the Button's tokens, an object should be provided within the theme under: ```{.json} { "components": { "Button": { "tokens": { "values": "here..." } } } } ``` Tokens are represented by the following: 1. Function A functional token is the preferred method of adjusting look and feel. Functional tokens reference values in the applied theme. ```javascript { "components": { "Button": { "tokens": { "fontSize": t => t.fonts.base, // pull some value from the theme } } } } ``` 2. Literal value A literal value allows a token to be hard-coded. It is considered the least desirable (as it will never be affected by other changes in the theme). A literal token in practice looks like: ```{.json} { "components": { "Button": { "tokens": { "fontSize": 12 } } } } ``` 3. Dependent value There are several cases where the value of a token is based on a calculation of another value. For instance, the background hover color of a button might be desired to be a shade lighter than the default background color of the button. (In order to specify this, assume we have a `lighten()` function available.) ```javascript { "components": { "Button": { "tokens": { "backgroundHoverColor": { dependsOn: ['backgroundColor'], value: ([backgroundColor: Color]) => invert(backgroundColor) } } } } } ``` ### Customizing components with variants `createComponent` supports variants, where a variant represents a new prop that implies a set of styles. Examples of expected variants for a button might be `circle`, `size`, or `primary`. Variants are specified in the call to `createComponent` along with additional tokens and styles. Variant arguments represent the keys in the defintion passed to `createComponent`. ```javascript const DeluxeButton = createComponent(Button, { variants: { size: { small: { // used via <DeluxeButton size="small" /> tokens: t => { return { fontSize: t.fonts.smallest } } }, large: { // used via <DeluxeButton size="large" /> tokens: t => { return { fontSize: t.fonts.largest } } }, }, primary: { true: { // used via <DeluxeButton primary /> tokens: t => { return { shadowColor: t.accents[1]; } }, styles: tokens => { return { root: { boxShadow: "5px 5px #{tokens.shadowColor}" } } } } } }, }); ``` ## Customizing with slots While tokens affect the look and feel of rendered elements, `slots` provides a way to make more significant adjustments to a component\'s structure and behavior. A slot is a rendered DOM element or higher level control that can be replaced at runtime. As an example, a `Checkbox` might choose to render a `label` element to hold descriptive text. If a use-case called for a proprietary `<MyLabel />` control instead of a `label`, that slot could be targeted for replacement. ### Overriding slots with a theme To override a slot from a theme, specify a reference to the component in the theme. ```javascript import { MyLabel } from 'my-library'; { "components": { "Checkbox": { "slots": { "label": MyLabel } } } } ``` ### Overriding slots with a new component `createComponent` can also specify slot assignments directly. (This is the expected usage for component libraries.) ```javascript import { MyLabel } from 'my-library'; const MyCheckbox = compse(Checkbox, { slots: { label: MyLabel } }); ``` # Creating a base component A base component is an unstyled component that contains desired behaviors. A base component should anticipate 2 special `props` and behave accordingly. ## `props.slots` `slots` defines the control that should be rendered in each slot. The outermost component should always be named `root`, and other child components should be given predictable names and documented. ## `props.slotProps` There will be an entry in `slotProps` for each accompanying `slot`. The base component is responsible for merging user-provided props with `slotProps`, then passing them to the according `slot` for rendering. # Theming a component In order to apply a theme to a component, each base component should be wrapped by `createComponent` along with accompanying styling. ## Understanding tokens Tokens are the exclusive means of getting data from a theme into a component. Tokens should be specified for every aspect of a control\'s look and feel. Tokens should be named according to the following anatomy: {slot (or none for root)}{property}{state (or none for default)} Examples: - `thumbSizeHovered` - `backgroundColor` - `labelBorderDisabled` ## Understanding styles After evaluating tokens, the tokens are passed to a `style` function. The `style` function should return an object which can be rendered by the CSS renderer chosen for the design system. Likely CSS renderers include `Fela`, `mergeStyles`, and `JSS`. Example targeting `JSS`: ```javascript const styles = (tokens: MyComponentTokens) => { return { root: { backgroundColor: tokens.backgroundColor, '&:hover': { backgroundColor: tokens.backgroundHoverColor } }, widget: { borderColor: tokens.borderColor } }; } ``` ## Understanding slots Components should define a set of logical elements that are reasonable to replace. Additionally, sensible defaults should be provided. Slots provide an opportunity for callers to late-bind sections for replacement. TODO: examples of more slots ## Writing the base component Any functional component can be used with `createComponent`. However, there are several conventions that should be respected in order to make the user experience predictable. A good base component deviates from a run-of-the-mill component in 3 ways: - It should have no built-in opinion of styling. When styled via `createComponent`, class names will be passed in via `slotProps` to provide styling. - It accepts a prop named `slots`, which define the component to use for subcomponents. - It accepts a prop named `slotProps`, which will be handed off to subcomponents. ### Slots TODO: Describe how to interact with slots ### Slot Props TODO: Describe how to interact with slotProps ### Building in practice A simple base component that renders a button might look like the following: ```javascript interface Props { slots; slotProps; children; onClick; } const BaseButton: React.FunctionComponent<Props> = (props: Props) => { // First, define the slots // define `Root` as a const which renders the root. // Default to a button element. const { root: Root = 'button' } = props.slots || {}; // Break out slot props to be passed to various components. // Mix in the props specified directly in props. const { root: rootProps } = props.slotProps || {}; const resolvedRootProps = { ...rootProps, onClick: props.onClick }; // Finally, render the component return <Root {...resolvedRootProps}>{props.children}</Root> } ``` As components grow and become more complex, it is expected that hooks will be developed to resolve state and intelligently merge `props` into `slotProps`. ## Conformance TODO: Describe how to run conformance tests to make sure that base components appropriately react to theme changes.