diff --git a/modules/react/common/README.md b/modules/react/common/README.md index 9b6851ebdb..81c96eec23 100644 --- a/modules/react/common/README.md +++ b/modules/react/common/README.md @@ -14,28 +14,11 @@ Includes: - [Installation](#installation) - [CanvasProvider](#canvasprovider) - [Storybook Decorator](#storybook-decorator) - - [Input Provider](#input-provider) - - [Definitions](#definitions) - - [Usage](#usage) - - [Static Properties](#static-properties) - - [`InputTypes`](#inputtypes) - - [Component Props](#component-props) - - [Required](#required) - - [Optional](#optional) - - [`provideIntent: boolean`](#provideintent-boolean) - - [`container: HTMLElement | React.RefObject`](#container-htmlelement--reactrefobjecthtmlelement) - - [Storybook Decorator](#storybook-decorator-1) + - [Focus rings](#focus-rings) + - [RTL and bidirectionality](#rtl-and-bidirectionality) - [Theming](#theming) - - [Bidirectionality](#bidirectionality) - - [Component Functions](#component-functions) - - [`createComponent`](#createcomponent) - - [`ExtractProps`](#extractprops) - - [Common Hooks](#common-hooks) - - [useUniqueId](#useuniqueid) - - [Utility Functions](#utility-functions) - - [generateUniqueId](#generateuniqueid) - - [setUniqueSeed](#setuniqueseed) - - [resetUniqueIdCount](#resetuniqueidcount) + - [Component functions](#component-functions) + - [Unique IDs](#unique-ids) ## CanvasProvider @@ -46,8 +29,8 @@ can find more information in the [theming documentation](./lib/theming/README.md **We strongly encourage you to use this in your application to wrap all Canvas components.** ```tsx -import * as React from 'react'; -import {CanvasProvider} from '@workday/canvas-kit-react/common'; +import * as React from "react"; +import { CanvasProvider } from "@workday/canvas-kit-react/common"; {/* All your components containing any Canvas components */}; ``` @@ -60,7 +43,7 @@ stories in a `CanvasProvider` automatically. Add this decorator to your `/.storybook/preview.js` configuration file to apply to all stories: ```js -import {CanvasProviderDecorator} from '../utils/storybook'; +import { CanvasProviderDecorator } from "../utils/storybook"; export const decorators = [CanvasProviderDecorator]; ``` @@ -68,10 +51,10 @@ export const decorators = [CanvasProviderDecorator]; Or, add it to stories individually: ```js -import {CanvasProviderDecorator} from '../../../../utils/storybook'; +import { CanvasProviderDecorator } from "../../../../utils/storybook"; export default { - title: 'MyComponent', + title: "MyComponent", component: MyComponent, decorators: [CanvasProviderDecorator], }; @@ -88,253 +71,35 @@ MyStory.decorators = [CanvasProviderDecorator]; Use [:focus-visible](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible), which will match when the user triggers focus via -## RTL +## RTL and bidirectionality Use -[CSS Logical Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values) -to support RTL in styling. +[CSS logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values) +for styling, and `isElementRTL` for keyboard handlers that need the content direction of an element. + +See +[Hooks and Utilities / Bidirectionality](https://workday.github.io/canvas-kit/?path=/docs/hooks-and-utilities-bidirectionality--docs) +in Storybook. --- ## Theming -Theming documentation has its own README. You can find it [here](./lib/theming/README.md). +Theming documentation has its own README. You can find it [here](./lib/theming/README.md), and the +guide is in +[Features / Theming / Overview](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs). --- -## Bidirectionality - -We strongly encourage the use of -[CSS logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties) -for styling. For keyboard events, the right arrow should still move a cursor to the right of the -cursor's current position. For left-to-right languages, this means "move to the next item" while in -right-to-left languages, this means "move to the previous item". The directionality of an element -can be retrieved from the [isElementRTL](#iselementrtl) function. This should be done inside an -event handler. - -## `useElementRTL` - -Returns `true` if the element has a content direction of `rtl`. This is most useful in event -handlers like an `onKeyDown`. - -```ts -onKeyDown(event: React.KeyboardEvent) { - const isRTL = isElementRTL(event.currentTarget); - - if (event.key === 'ArrowRight') { - if (isRTL) { - // Right arrow moves to the right even on RTL languages, but "right" doesn't mean - //"advance" in RTL. Previous is to the right of the current in RTL. - model.events.goToPreviousRow(); - } else { - model.events.goToNextRow(); - } - } -} -``` - -## Component Functions - -### `createComponent` - -`createComponent` is a convenience function that makes it easier to create components that -[forward refs](https://reactjs.org/docs/forwarding-refs.html) and take an `as` prop. The `as` prop -allows you to change the default element rendered by the component. - -```tsx -interface MyComponentProps { - prop1: string; - prop2: number; - children: React.ReactNode; -} - -export const MyComponent = createComponent('div')({ - displayName: 'MyComponent', // the component's name in the React dev tools - Component: (({ prop1, prop2, children, ...elemProps }: MyComponentProps), ref, as) => { - return {children} - }) -}) -``` - -Using a component created via `createComponent`: - -```tsx - - Content - -``` - -`MyComponent` is returning a type that looks like `ElementComponent<'div', MyComponentProps>`. The -`'div'` tells you what the default element is of the element component. - -Notice that `MyComponentProps` does not extend `React.HTMLAttributes`. The `as` prop -changes the interface of the component to match that of the `aside` element which is a -`React.HTMLAttributes`. For this reason, the interface cannot be known statically ahead -of time. Components using `createComponent` are starting to not export the prop interface. For this -reason, [ExtractProps](#extractprops) was created. - -The first parameter of `createComponent` might not be defined. If this is the case, the type -returned is a `Component` instead of `ElementComponent`. `Component`s do not have any HTML -attributes associated with them. This usually means the component doesn't return a real element. -This is used for compound components that do not require an element for accessibility reasons and -only provide a common model for the subcomponents. - -### `ExtractProps` - -v5 introduces components that could take an `as` prop, which means the underlying element that is -rendered can change. Component props that use the `as` prop no longer extend an HTML attribute -interface because the interface is determined by the value of the `as` prop. +## Component functions -`ExtractProps` can be used in place of importing a prop interface directly. If you wish to extend a -Canvas Kit component by extending it's props, use `ExtractProps`. - -```tsx -interface MyNewComponentProps extends ExtractProps { - foo: string; -} - -const MyNewComponent = ({foo, ...props}: MyNewComponentProps) => { - return My Content; -}; -``` - -`ExtractProps` will return an type that includes the props of the component passed to it as well as -any HTML attributes associated with the component. `ExtractProps` takes an optional second argument. -If the second argument is omitted, like in the example above, the HTML attributes will be extracted -from the first argument of `createComponent`, which is the default element. In the case of -`MyComponent`, that would be `'div'`. You can tell by hovering over the `MyComponent` which will -show the type as `ElementComponent<'div', MyComponentProps>`. - -The second argument should be used if you plan to change the base element of the component using the -`as` prop. This is required because Typescript has no way of knowing what the props will be outside -an `as` declaration. Here's an example of the `MyNewComponent` that renders `MyComponent` as an -`aside`. - -```tsx -interface MyNewComponentProps extends ExtractProps { - foo: string; -} - -const MyNewComponent = ({foo, ...props}: MyNewComponentProps) => { - return ( - - My Content - - ); -}; -``` - -Note the `aside` used in both the props interface as well as the `as` in the JSX. This is useful if -your component uses props that come from the HTML attribute interface of a component, like `onClick` -that isn't explicitly declared in the original component prop interface like `MyComponentProps`. In -this example, `onClick` doesn't exist on `MyComponent`, but since `MyComponent` is an -`ElementComponent`, the `onClick` will be forwarded to the underlying `div` element. - -If we inspect the `MyNewComponentProps` type, it will return the following: - -```tsx -MyComponentProps & React.HTMLAttributes; -``` +`createComponent`, `ExtractProps`, `createModelHook`, `createContainer`, `createSubcomponent` and +`createElemPropsHook` are documented in +[Hooks and Utilities / Compound Components](https://workday.github.io/canvas-kit/?path=/docs/hooks-and-utilities-compound-components--docs). -Inside Canvas Kit components, we use `ExtractProps` when we need to extend from the interfaces of -other components, but since our components are also dynamic based on the `as` props, we cannot -declare our HTML attribute interface yet. `ExtractProps` can take `never` as the second parameter -for this case, which means to only take the declared props of a component and not include the HTML -attributes. - -```tsx -interface MyNewComponentProps extends ExtractProps { - foo: string; -} - -const MyNewComponent = createComponent('aside')( - displayName: 'MyNewComponent', - Component: (({foo, ...props}: MyNewComponentProps, ref, as) => { - return ( - - My Content - - ); - }; -}); -``` - -If the component is a `Component` and not an `ElementComponent`, only the prop interface will ever -be returned since there is not HTML attribute interface associated with `Component`. - -## Common Hooks - -### useUniqueId - -A hook to generate a unique identifier for an element – most commonly used for accessibility. The -hook will generate a unique id the first render and always return the same id every render. This -uses [generateUniqueId](#generateuniqueid) internally. You should use `useUniqueId` when you need to -create a unique ID within a component's render loop. Otherwise, you should opt for -`generateUniqueId`. - -```tsx -const MyComponent = () => { - const id = useUniqueId(); - - return
Hello!
; -}; -``` - -If you wish to support user-defined ids, `useUniqueId` allows an optional id to override. This -provides a safe and easy way of handling id overrides without conditional hooks. - -```tsx -const MyComponent = ({id}) => { - const localId = useUniqueId(id); - - return
Hello!
; -}; -``` - -## Utility Functions - -### generateUniqueId - -Generates a unique and HTML5 compliant identifier every time it is called. Internally it uses a 4 -character random seed starting with a letter. This seed is unique to each instance of this package -meaning different versions of Canvas Kit on the page will have a different seed. Each call will use -a Base 36 string (10 numbers + 26 letters) based on an incremented number. The incremented number -always starts at 0 and can be reset for testing purposes using -[resetUniqueIdCount](#resetuniqueidcount). [setUniqueSeed](#setuniqueseed) can also be used for -testing or server side rendering to get the same results during hydration. - -```ts -const id1 = generateUniqueId(); // vi1e0 -const id2 = generateUniqueId(); // vi1e1 -``` - -### setUniqueSeed - -Update the seed used by the id generator. This is useful for snapshot tests to help stabilize ids -generated each run. This could also be used for server-side hydration - if you choose the same seed -for server and set that on the client before components are rendered, the ids generated will be the -same. - -For snapshot testing, this will help stabilize snapshot tests. Use in conjunction with -[resetUniqueIdCount](#resetuniqueidcount). - -```ts -// set in a script tag from the server -setUniqueSeed(window.__ID_SEED); // set in a script tag from the server - -// jest setup -before(() => { - setUniqueSeed('a'); -}); -``` - -### resetUniqueIdCount +--- -This should only be called for tests in `beforeEach` for snapshot tests. Use in conjunction with -[setUniqueSeed](#setuniqueseed). +## Unique IDs -```ts -beforeEach(() => { - resetUniqueIdCount(); -}); -``` +`useUniqueId`, `generateUniqueId`, `setUniqueSeed` and `resetUniqueIdCount` are documented in +[Hooks and Utilities / Unique IDs](https://workday.github.io/canvas-kit/?path=/docs/hooks-and-utilities-unique-ids--docs). diff --git a/modules/react/common/stories/mdx/Bidirectionality.mdx b/modules/react/common/stories/mdx/Bidirectionality.mdx new file mode 100644 index 0000000000..58096ceea2 --- /dev/null +++ b/modules/react/common/stories/mdx/Bidirectionality.mdx @@ -0,0 +1,66 @@ +import { Meta } from "@storybook/blocks"; +import { ExampleCodeBlock } from "@workday/canvas-kit-docs"; + +import { RTL } from "./examples/RTL"; + + + +# Bidirectionality + +Canvas Kit supports right-to-left (RTL) content direction. Set the direction once on +`CanvasProvider` and style with logical properties, so the same markup works in both directions. + +## Installation + +```sh +yarn add @workday/canvas-kit-react +``` + +## Styling + +Use +[CSS logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties) +rather than physical ones: `paddingInlineStart` instead of `paddingLeft`, `marginInlineEnd` instead +of `marginRight`. The browser resolves them against the content direction, so no direction-specific +stylesheet is needed. + +Where a style has to differ by direction, such as mirroring a directional icon, use the +[`:dir()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:dir) pseudo-class rather than branching +in JavaScript. + + + +## Keyboard events + +Logical properties cover styling, but they do not cover key handling. The right arrow key should +always move the cursor to the right of its current position. In a left-to-right language that means +"move to the next item"; in a right-to-left language it means "move to the previous one". So a +keyboard handler has to know the direction of the element it is on. + +### isElementRTL + +Returns `true` when an element has a content direction of `rtl`. It reads the computed style of the +element it is given, so it is meant for event handlers rather than render functions. + +```tsx +import {isElementRTL} from '@workday/canvas-kit-react/common'; + +onKeyDown(event: React.KeyboardEvent) { + const isRTL = isElementRTL(event.currentTarget); + + if (event.key === 'ArrowRight') { + if (isRTL) { + // Right arrow still moves to the right, but "right" is not "advance" in + // RTL: the previous item is to the right of the current one. + model.events.goToPrevious(); + } else { + model.events.goToNext(); + } + } +} +``` + +
+ +> **Note:** `useIsRTL` is deprecated and will be removed in a future major version. Use +> `isElementRTL`, or the `:dir()` pseudo-class where the decision is a styling one. diff --git a/modules/react/common/stories/mdx/CompoundComponentUtilities.mdx b/modules/react/common/stories/mdx/CompoundComponentUtilities.mdx index 15cdead929..73baf5544b 100644 --- a/modules/react/common/stories/mdx/CompoundComponentUtilities.mdx +++ b/modules/react/common/stories/mdx/CompoundComponentUtilities.mdx @@ -35,6 +35,68 @@ up components with `ref` forwarding. +# ExtractProps + +Components that accept an `as` prop do not extend an HTML attribute interface directly, because the +interface depends on what `as` is set to. `ExtractProps` recovers that type, so a component can be +extended without importing its prop interface. + +```tsx +interface MyNewComponentProps extends ExtractProps { + foo: string; +} + +const MyNewComponent = ({foo, ...props}: MyNewComponentProps) => { + return My Content; +}; +``` + +With one argument, the HTML attributes are taken from the default element given to +`createComponent`. For a component typed `ElementComponent<'div', MyComponentProps>`, that is +`'div'`. + +## Changing the element + +Pass the element as a second argument when the component will be rendered as something else. +TypeScript cannot infer it from the `as` prop alone, so the element has to appear in both places. + +```tsx +interface MyNewComponentProps extends ExtractProps { + foo: string; +} + +const MyNewComponent = ({foo, ...props}: MyNewComponentProps) => { + return ( + + My Content + + ); +}; +``` + +This matters for props that come from the HTML attribute interface rather than the component's own, +such as `onClick`. `onClick` is not declared on `MyComponentProps`, but because `MyComponent` is an +`ElementComponent` it is forwarded to the underlying element. The resulting type is: + +```tsx +MyComponentProps & React.HTMLAttributes; +``` + +## Declared props only + +Pass `never` as the second argument to take only a component's declared props, with no HTML +attributes. Canvas Kit uses this internally when extending one dynamic component from another, where +the attribute interface is not yet known. + +```tsx +interface MyNewComponentProps extends ExtractProps { + foo: string; +} +``` + +For a `Component` rather than an `ElementComponent`, only the prop interface is ever returned, since +there is no HTML attribute interface associated with it. + # createModelHook When building compound components that might have some internal state or events, you typically want diff --git a/modules/react/common/stories/mdx/UniqueIds.mdx b/modules/react/common/stories/mdx/UniqueIds.mdx new file mode 100644 index 0000000000..e80e3219b2 --- /dev/null +++ b/modules/react/common/stories/mdx/UniqueIds.mdx @@ -0,0 +1,100 @@ +import { Meta } from "@storybook/blocks"; + + + +# Unique IDs + +Accessible markup needs IDs to connect elements to one another: a label to its field, a heading to +the region it names, a control to the element it describes. Those IDs have to be unique on the page +and stable across renders. Canvas Kit generates them for you. + +## Installation + +```sh +yarn add @workday/canvas-kit-react +``` + +## useUniqueId + +Generates a unique, HTML5-compliant ID that is stable across renders, and uses `React.useId()` where +it is available. It takes an optional ID: pass one in and it is returned unchanged, so a component +can accept an ID from a consumer and fall back to a generated one without branching. + +```tsx +import {useUniqueId} from '@workday/canvas-kit-react/common'; + +// Pairing a heading with the table it names, where `` is not wanted +const tableHeadingId = useUniqueId(); + +Pizza Toppings +{/* table rows */}
+``` + +Because it accepts an ID, it is safe to call unconditionally: + +```tsx +const MyField = ({ id, ...elemProps }) => { + // uses `id` when given, generates one when not + const fieldId = useUniqueId(id); + return ; +}; +``` + +> **Note:** In React 18, `useId()` generates IDs containing colons, such as `:r0:`, which are not +> valid in CSS selectors. `useUniqueId` rewrites them with guillemets, `«r0»`, matching the format +> React is moving to. + +## generateUniqueId + +Returns a new ID string each time it is called. It is the generator behind `useUniqueId`, exported +for the cases where an ID is needed outside a render, where hook rules do not apply. + +```tsx +import { generateUniqueId } from "@workday/canvas-kit-react/common"; + +const id = generateUniqueId(); +``` + +Each ID is a four-character random seed followed by an incrementing Base 36 counter. The seed is +unique per copy of the package, so two versions of Canvas Kit on one page will not collide. + +Prefer `useUniqueId` inside components: `generateUniqueId` is not stable across renders. + +## setUniqueSeed + +Replaces the random seed with one of your choosing. Two uses: + +- **Snapshot tests**, so the IDs in a snapshot are the same on every run. +- **Server-side rendering**, so IDs generated on the server match the ones generated on the client + during hydration. The seed is only half of it: IDs are the seed plus an incrementing counter, so + the two sides must also start from the same count and generate in the same order. Prefer + `useUniqueId` inside components, which uses `React.useId()` where it is available and does not + depend on either. + +```tsx +import { setUniqueSeed } from "@workday/canvas-kit-react/common"; + +// from a script tag written by the server +setUniqueSeed(window.__ID_SEED); +``` + +## resetUniqueIdCount + +Resets the counter to zero. Intended for tests, in a `beforeEach`, alongside `setUniqueSeed`. + +```tsx +import { setUniqueSeed, resetUniqueIdCount } from "@workday/canvas-kit-react/common"; + +beforeEach(() => { + setUniqueSeed("a"); // a static seed + resetUniqueIdCount(); // reset the counter before every test +}); +``` + +See [Guides / Testing](https://workday.github.io/canvas-kit/?path=/docs/guides-testing--docs) for +how this fits into a test setup, and +[Guides / Accessibility / Overview](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-overview--docs) +for where generated IDs are needed. + +> **Note:** `uniqueId` is deprecated and will be removed in a future major version. Use +> `useUniqueId` instead.