--- tags: guides --- # Consolidating components between suites The intent of this guide is to help developers consolidate components between Fabric and Stardust components. # Problem Given a component we have 2 or more copies of the same thing and should have 1. ## End goal * Single package to host the component(s) * :warning: Package can depend on other sub-packages but it must not depend on a suite package! This would create circular dependencies. * Within the package, we have 1 or more components for the which meet our conformance criteria * Styles written in static css * rtl friendly * auto-prefixed * css modules for encapsulation * child-window friendly * ssr friendly * Both v0 and vNext packages export it * It's OK to ship one first as long as the goal is for the other to move to it. * The documentation website has clear usage guidance for customers * The component is tested within multiple themes. * All available tests pass (converge tests) # Convergence checklist 1. Do the research 2. Scaffold package 3. Scaffold component 4. Bring the component types from v0 and vNext; vet out the design. 5. Export in v0. a. v0 docs updated 7. Export in vNext. b. vNext docs updated # Steps ## 1a. Know what you're doing (research and docs) While doing research and writing specs keep the following in mind: ### Principles 1. Favor the minimum amount of API surface, it's easy to add more later. 2. Single use applies to props. Each property should have one well defined use. 3. Maximize configurability by favoring callbacks/functions vs static objects * Slots are a great example of this. 4. Support a controlled mode. Users should be able to pass in a defaultValue as well as a controlled value for the primary data prop. 5. If multiple properties can be set in such a way to achieve a specific design, consider making that a varient rather have that be controlled by 1 boolean prop. > Open questions: > --- 1. Create open-ui research page * More details to come about exact steps * Take screenshots of many states/property combinations of components 2. Synthesize knowledge gained from research in a template of [this hackmd](https://hackmd.io/wZPEGjRuSXa0ZILrgTgL8g?both) 3. Write a spec in a template of [this hackmd](https://hackmd.io/Ovj2OEdWRR6PcoFBvFWNvg?both=) 4. Request reviews on both the spec and synthesis. Note: This research and design should take a non-trivial amount of time. Often it can pay to receive feedback multiple times, from multiple different people about your designs. Don't be afraid to not think about your spec for a day or two and then come back to re-edit. > Open questions: > * Is there an example spec we can link? > * Add breaking changes guideline here; if a change breaks, what are the steps required to communicate this to our users? > * Styles prop for example > * Where are specs stored - next to the component or in a universal place? > * Is there a draft vs release process? Internal vs open ui? > * We have a ton of APIs; could we ship the API updates as a separate step to simplify convergence? > * Create backwards compat guide to show patterns of reimplementing ## 1b. Prepare convergence package 1. Pick a package name. See [Package naming principles](#Package-naming-principles) for guidance. 2. At the root of the repo: ```bash yarn create-package <package name> ``` > Open questions: > * Make sure this `create-package` conforms to current standards. > * Would be great to have `create-component` as well. ## Scaffold the component It is ideal to start with an existing component from either v0 or v7, whichever is closer to the goal, and to refactor the existing component over a few iterations into the following organizational structure for conformance and consistency. ### File structure A component package should have the following files: | Filename | Description | | - | - | | {Component}Base.tsx | A base, unstyled component. | {Component}.types.tsx | Public types for the component. | use{Component}.ts | A hook encapsulating the state management. | {Component}.scss | Default styling for the styled component. | {Component}.tsx | A recomposed version of the base component. | {Component}.tests.tsx | Jest unit tests. | {Component}.stories.tsx | Stories for internal development loop. > Open questions: > * Conventions > * Should the base component be ComponentBase.tsx or Component.base.tsx? > * If we want multiple themes for the component, how are these delivered? Does the component own the themes for itself, or is that part of a theme package? > * Is component schema generated or hand crafted or both? > * Where do docs go? ### State State management belongs in the component hook. Each component hook returns an object containing the following props: | Value | Description | |-|-| | `state` | The final resolved props + state object. | | `slots` | The slots to be used for the component. | | `slotProps` | The props to be distributed for each slot. | To calculate the state, use hooks from the `@fluentui/react-hooks` library in addition to standard hooks provided by React. If you see an opportunity to reuse a hook, please add to the library. Every component should use the `mergeProps` helper within the component to standardize how `slots` and `slotProps` are parsed from the given state. ```tsx export const useImage = ( props: ImageProps, options: ImageOptions ) => { const { ...state } = props; // compute any additional state values mergeProps(state, options); } ``` > Open questions: > * While accessibility could be manually defined here for each slotProp, is there a better way? Potentially factor the accessibility helpers to more easily merge in? ### Accessibility Accessibility is a concern to be managed within the state hook. Accessibility concerns of components should follow an accessibility guide > Open questions: > * Can we use the `accessibility` helpers, or should we augment their > WRT `accessibility` props - can we have a better way to ensure the right accessibility model for a component? > ### Styling Styling the styled component involves importing a scss module and mixing the `classes` map and `stylesheet` prop into `compose`: ```tsx import { classes, stylesheet } from './Image.scss'; export const Image = compose(ImageBase, { classes, stylesheet }); ``` > Open questions: > * Should we have slot styles prefixed with parent class to ensure specificity? Load order problem ### Content of the scss file The Fluent UI scss pipeline manages a few things for you: * Autoprefixing * CSS modules * RTL and nested RTL support ### Defining classes Each class in the scss file represents one of these types: | Type | Example | Description | |-|-|-| | Slot | `root` | A class which is mixed on the slot each time | | Modifer | `primary` | A class mixed into the root slot only when the matching prop is found in the component state. | | Enum | `size_large` | A class mixed into the root slot oly when the matching prop (e.g. `size`) is found in the component state holding the given value (e.g. `large`.) By ensuring we follow this convention, it makes it possible to abstract the logic of class name distribution to the appropriate slotProp's `className`. This is currently done in `mergeProps`. Targeting slots of a component when modifiers are present is simply a matter of using the right selectors: ```tsx .primary .child { ... } ``` * Try to minimize specificity * Avoid media queries which drastically change the look and feel of the component in different screen widths. These only work predictably when the component spans full screen width. * Avoid targeting sub component slots and instead try to keep each component responsible for its own styling. > Open questions: ### Tokens > NOTE: This guide is a WIP as we discover features and limitations with CSS variables. CSS variables (tokens) represent the inputs into our stylesheets. They will be used to inject values originating from the `ThemeProvider` theme or inline. Most of your css file should avoid hardcoded definitions and expose values through a variable. Guide on naming format for component variables: `--{component/concept}-{recipe or variant}-{prop}-{state}` Common naming for concepts: |Concept |Recommended wording | Examples | |-|-|-| | background color | `fill` | `--button-fill` | | foreground color | `{thing}Color` | `--button-textColor`, `iconColor`, `borderColor` | | rest state | (blank, it's implied) | `--button-fill` | | hover state | `hovered` | | active state | `active` | `--button-fill-active` | | disabled state | `disabled` | | checked or selected state | `checked` | | multi state (checked and disabled) | `checkedDisabled` | Examples: `--avatar-size-smallest` > Open questions: * Multi state order convention ## Step 4. Ensure conformance TODO: we need to standardize conformance tests in a reusable testing package. > Open questions: ## Step 5. Replace existing component in one repo ## Step 6. Build upgrade notes and scripts ### Package naming principles **1. If the package is React specific, prefix with `react-`** > Good: `@fluentui/react-avatar` > Bad: `@fluentui/avatar` Why: Clear distinction between framework specific vs agnostic packages. **2. 1 component per package unless there are clear overlaps or relations.** > Good: `react-focus` > Bad: `react-form-components` > > Good: `react-slider` > Bad: `react-form-components` Why: Reduces ambiguity about what goes in which packages. ### Component conformance An unstyled component should never have a graph edge to a styled component. You should be able to render a primitive or composite component with no CSS opinions. ### Documentation conformance TODO # References ### Shorthand notation How it works: The component defines a shorthand static function `create`, which takes in shorthand info and renders the component with it. This is done with a helper `createShorthandFactory`. For example, the `Status` component would define its `create` function like so: ```tsx Status.create = createShorthandFactory({ Component: Status, // Component to render mappedProp: 'state' // Default prop for literals }); ``` The responsibility of `create` functions is to take in short hand props and return appropriate JSX. When parent components render children, they use the factory function to translate shorthand into JSX. For example, the `Avatar` would render the `Status` component like so: ```tsx const Avatar = (props) => { const { status } = props; return ( <...> { Status.create(status)} </...> ); }; ``` This model allows the `Status` component to be the owner of parsing the input and rendering the right content. The `status` prop can be any of the following values: #### Literal value Each shorthand can translate literals into a default prop value. In the `Status` case, a `string` would be interpretted as the `state` prop of the component. Example: ```tsx <Avatar status="warning"/> ``` #### Props object A props object would be passed to the `Status` instance: ```tsx <Avatar status={{ state: 'warning' }} /> ``` #### Props object with children function If the `children` prop is a function, it is called with the default component (`Status`) and the props which would be passed to it. The result will replace the `Status` component in the rendered hierarchy. This is useful for wrapping components in a tooltip, or replacing them entirely: ```tsx <Avatar status={{ children: (C, p) => ( <Tooltip><C {...p}/></Tooltip> }} /> ) }} /> ``` Not sure why just having a function like so wouldn't be better: ```tsx <Avatar status={{ (C, p) => ( <Tooltip><C {...p}/></Tooltip> }} /> )}} /> ``` ### Problems shorthand creates * It creates a hard dependency to another component. This is a problem for unstyled base components. How do we recompose a styled component to reference a styled child? * Dev ergonomics, it would be nicer to render children using JSX, but not critical. * Missing inline JSX. For example: ```tsx /* Provide a new component for status */ return ( <Avatar status={ <NewStatus /> } /> ); /* Provide a JSX icon for the status */ return ( <Avatar status={{ icon: <AwayIcon /> }} /> ); ``` ### Solution We are going to move away from the `create` statics and instead provide the benefits of shorthand through a mechanism that allows the components to be defined without hard dependencies on which components are used to render child components.