@workday/canvas-kit-docs
Version:
Documentation components of Canvas Kit components
421 lines (331 loc) • 14 kB
text/mdx
# 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;
```