For Developers

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

  • 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 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.

interface ButtonProps {
size: 'small' | 'medium' | 'large';
}
// use
<PrimaryButton size="medium" />;

JavaScript objects can be used as enums without the downsides:

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.
  • 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.
[`[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.

    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 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.
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.
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
  • 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.
// 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 standards for our prop type definitions.

The base pattern for prop descriptions is: The <property> of the <component>. For example:

/**
* 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:

/**
* The text of the Checkbox label.
*/
label?: string;

Feel free to provide additional detail in the description:

/**
* 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:

/**
* 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:

/**
* 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:

/**
* 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:

/**
* 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:

/**
* 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:

/**
* The HTML `id` of the underlying checkbox input element.
*/
id?: string;

Can't Find What You Need?

Check out our FAQ section which may help you find the information you're looking for.

FAQ Section