UNPKG

@workday/canvas-kit-docs

Version:

Documentation components of Canvas Kit components

421 lines (331 loc) • 14 kB
# API & Pattern Guidelines Note: This repo hasn't seen a full audit, so you may find examples that contradict these guidelines. Some of the below rules are inspired by painpoints we've encountered in this project. - [Canvas](#canvas) - [Naming](#naming) - [Props](#props) - [T-shirt Sizes](#t-shirt-sizes) - [Theme Types](#theme-types) - [Event Handlers](#event-handlers) - [Enums](#enums) - [Patterns](#patterns) - [Event Handlers](#event-handlers-1) - [Grow Interface](#grow-interface) - [Input Provider](#input-provider) - [Prop Spread Behavior](#prop-spread-behavior) - [Controlled Components](#controlled-components) - [Accessibility](#accessibility) - [Child Mapping](#child-mapping) - [Logic Flow](#logic-flow) - [Server Side Rendering](#server-side-rendering) - [Code Style](#code-style) - [Default Props](#default-props) - [Element Choice](#element-choice) - [Styled Components](#styled-components) - [Exports](#exports) - [Documentation](#documentation) - [Readmes](#readmes) - [Storybook Structure](#storybook-structure) - [Prop Descriptions](#prop-descriptions) ## Canvas - Ensure you're always using the Canvas primitives and enums wherever possible for things like: - Space (e.g. `canvas.space.s`) - Depth (e.g. `canvas.depth[2]`) - Type (e.g. `...canvas.type.h1`). Always start from a type hierarchy level and override if needed. - Use the provided types (e.g. `CanvasSpaceValues`, `CanvasSpaceNumbers`, etc.) to restrict prop values - Check out the [`@workday/canvas-kit-react/tokens` README](https://github.com/Workday/canvas-kit/blob/master/modules/react/tokens/README.md) for the latest and greatest Canvas helpers. ## Naming #### Props - Prop names should never include the component name (e.g. `type`, not `buttonType`) - Use the same props for the same concepts across components - Avoid names that reference color, position, and size. For example: - `blueIcon` can be bad because it may not be blue to everyone and changing colors or making colors variable is a breaking change. - `leftIcon` can be bad because we can change the position with RTL or add something to the left of that, then it wouldn't make sense anymore. - `mediumIcon` can be bad if we add another size in between... then which one is medium? Is it mediumLarge now? #### T-shirt Sizes - Always use the shortest enumeration (`xs, s, m, l, xl`, etc.) - **Do not** use longer versions (e.g. `sm`) #### Theme Types - Default - normal state/color for use on light background - Inverse - inverted colors for use on a dark background - _Note:_ If you encounter somewhere you need another theme type, please let us know so we can document it #### Event Handlers - Always use standard `on{Descriptor}{Event}` naming (`onClick`, `onChange`, `onBreakpointChange`, etc.) - Only use a descriptor if: - You need more context - There is already a handler for that type of event (e.g. `onChange`, `onValidColorChange`) #### Enums Use disjoint string unions instead of enums. Enums have issues with overloading or extending. They are also more difficult to use and create a different experience from JavaScript. These union types also have a better autocomplete experience. ```tsx interface ButtonProps { size: 'small' | 'medium' | 'large'; } // use <PrimaryButton size="medium" />; ``` JavaScript objects can be used as enums without the downsides: ```tsx const ButtonType = { Primary: 'primary', Secondary: 'secondary' } as const // as const locks object values as literal interface ButtonProps { type: typeof ButtonType[keyof typeof ButtonType] // returns 'primary' | 'secondary' } Button.Type = ButtonType // use <Button type={Button.Type.Primary} /> <Button type="primary" /> ``` If objects are desired, the keys follow these rules: - Singular - PascalCase - Include component name unless it's a generic enum shared across components. Since we export our enums, this prevents naming clashes - Exclude component name in static variables (`Button.Type` vs. `Button.ButtonType`): ## Patterns #### Event Handlers - Use standard browser events wherever possible - All event handlers should receive an event unless there's a good reason otherwise. This is for consumer predictability. In other words, always opt for `onChange: e => void` over `onChange: () => void` or `onChange: value => void`, etc. #### Grow Interface - If your component needs to grow to fill to it's container, extend `GrowthInterface` (e.g. `export interface MyComponentProps extends GrowthBehavior`) - Then use the `grow` boolean prop in your styles to achieve the desired effect (e.g. `width: grow ? '100%' : undefined`) #### Input Provider - All Canvas Kit components should support an `InputProvider` component to provide the cleanest experience for mouse users. Read the docs [here](https://github.com/Workday/canvas-kit/tree/master/modules/core/react#input-provider). - Do not use `InputProvider` within your components. It is meant to be used only once in your application. It does not require wrapping any children - Make sure you provide fully accessible styling by default, and only override for mouse usage. - ```tsx [`[data-whatinput='mouse'] &:focus, [data-whatinput='touch'] &:focus, [data-whatinput='pointer'] &:focus`]: { outline: 'none', border: 'none', }, ``` #### Prop Spread Behavior - Extend the interface of the primary element/component in your component (e.g. `export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>`) - Intentionally destructure your props so that every prop is assigned. This allows you to use spread the way it was intended. ```tsx interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { type: ButtonType, size: ButtonSize, icon: CanvasIcon } // ...somewhere in your button render() const { type, size, icon, ...elemProps } = this.props <ButtonContainer type={type} size={size} icon={icon} {...elemProps} /> ``` - Only spread props on one element/component #### Controlled Components - We opt for controlled components wherever possible. - We aim to manage the least amount of state within our components as possible. - For input type components: - Always stick with the default `value` and `onChange` if you can - Deviate where it makes sense and/or is required (e.g. `checked` and `onChange` for checkboxes). #### Accessibility - Use aria labels where required - Ensure full keyboard navigation - Check whether tabbing is enough or whether additional keyboard navigation is required (e.g. arrow keys) - When in doubt, ask an expert! #### Children - We often add or augment props to React children within our components. Use `React.Children.map` along with `React.cloneElement()` - Use `React.isValidElement()` if you want to make sure it's a React component and not a regular DOM node. - If you're adding any event handlers to the children, make sure you also support existing ones #### Logic Flow - If vs. Switch: use switch statements when code branching is determined by the value of a single variable or expression. - Nested Ternaries: maximum two levels and only if it's very obvious. If you have two or more levels, try rewriting it as if/else statements and compare the complexity & scanability. - Opt for [pure functions](https://medium.com/@jamesjefferyuk/javascript-what-are-pure-functions-4d4d5392d49c) wherever possible. They make unit testing easier and always behave as expected. Because React can be a bit of a magic black box, sometimes `this.x` values are not what you expect. ```ts foo(number, bar) => { return number * bar } foo(this.number, this.bar); // is a much better option than foo() => { return this.number * this.bar } foo(); ``` #### Server Side Rendering - In order to support SSR, we cannot reference global objects (`window`, `document`, etc.) before a component is hydrated/mounted. - Generally, it is only safe to use these freely within `componentDidMount`, `useEffect` and `useLayoutEffect`. - This means that any reference to `window` or `document` should be avoided wherever possible within the global scope, constructors, and render methods. - If you need to reference these variables in these avoided places, you must check whether it's undefined first (e.g. `typeof window !== 'undefined'`) - Be particularly careful when initializing default props or state with something stored on the `window`/`document` objects. These initializations will have to be skipped for SSR contexts (assign `undefined` or `null`) and updated upon mounting. ## Code Style #### Default Props - Use `defaultProps` whenever you find yourself checking for the existence of something before executing branching logic. It significantly reduces conditionals, facilitating easier testing and less bugs. - We prefer to colocate our default props and destructure them which allows consumers to rename our components on import. - Note: If you assign a default value to a prop, make sure to make the prop as optional in the interface. ```jsx const someInterface { /** * If true, sets the Checkbox checked to true * @default false */ checked?: boolean; /** * If true, set the Checkbox to the disabled state. * @default false */ disabled?: boolean; /** * The value of the Checkbox. */ value?: string; } //... const {checked = false, disabled = false, value} = this.props; ``` #### Element Choice - Use the correct native element wherever possible. This enables us to get as much behavior for free from the browser. - For example, if something peforms an action on a click, it should generally use a `button` to get keypress handling for free. #### Styled Components - Always initialize styled components outside of your render function. Failing to do this will result in a big performance hit. - When specifying the props a styled component can accept, it is up to you do define how restrictive you should be. You can accept any prop that the component accepts (e.g. `styled('div')<ComponentProps>`) or only accept a subset (e.g. `styled('div')<Pick<ComponentProps, 'someProp' | 'anotherProp'>>`) - We generally prefer the use of `styled` components over using the `css` function. However, `css` can be handy for some basic styling. #### Exports - [Avoid default exports](https://basarat.gitbook.io/typescript/main-1/defaultisbad) - Export everything else as a named export (`export * from ...`). Consider the naming of the things you're exporting (interfaces, enums, etc.) so you don't encounter any clashes. ```ts // inside MyComponent/index.ts export * from './lib/MyComponent'; export * from './lib/AnotherComponent'; ``` ## Documentation #### Readmes - Follow our README template - Outline static properties (e.g. `Button.Type`), required props, and optional props - Usage example should be as standalone as possible. As long as it's not too complex, this snippet should be a working implementation so consumers can copy/paste #### Storybook Structure - Always opt for the most referenceable code in your stories. Storybook helps us test, but many consumers use it as an example of how to implement components. - Avoid helper functions to reduce duplication that make it harder to parse. - Avoid sharing wrappers, components, etc. from other story files. - Essentially, try to keep each example as standalone and referencable as possible. #### Prop Descriptions We use [JSDoc](https://devdocs.io/jsdoc/) standards for our prop type definitions. The base pattern for prop descriptions is: `The <property> of the <component>.` For example: ```js /** * The value of the Checkbox. */ value?: string; ``` Be as specific as possible. For example, suppose there is a `label` prop for `Checkbox` which specifies the text of the label. Rather than describe `label` as `The label of the Checkbox`, the following is preferable: ```js /** * The text of the Checkbox label. */ label?: string; ``` Feel free to provide additional detail in the description: ```js /** * The value of the Slider. Goes to 11. */ value: number; ``` Be sure to specify a proper `@default` for enum props. Listing the named values which are accepted by the enum prop is encouraged: ```js /** * The side from which the SidePanel opens. Accepts `Left` or `Right`. * @default SidePanelOpenDirection.Left */ openDirection?: SidePanelOpenDirection; ``` Use a modified pattern for function props: `The function called when <something happens>.` For example: ```js /** * The function called when the Checkbox state changes. */ onChange?: (e: React.ChangeEvent) => void; ``` The pattern for booleans is also different: `If true, <do something>.` For standard 2-state booleans, set `@default false` in the description. For example: ```js /** * If true, set the Checkbox to the disabled state. * @default false */ disabled?: boolean; ``` Provide additional detail for 2-state booleans where the `false` outcome cannot be inferred: ```js /** * If true, center the Header navigation. If false, right-align the Header navigation. * @default false */ centeredNav?: boolean; ``` For 3-state booleans, you will need to describe all 3 cases: `If true <do something>. If false <do something else>. If undefined <do yet another thing>.` We also recommend the following pattern for errors: ```js /** * The type of error associated with the Checkbox (if applicable). */ error?: ErrorType; ``` Occasionally, you may encounter props which don't play nicely with the suggested guidelines. Rather than following the patterns to the letter, adjust them to provide a better description if necessary. For example, rather than ambiguously describing `id` as `The id of the Checkbox`, provide a more explicit description: ```js /** * The HTML `id` of the underlying checkbox input element. */ id?: string; ```