@workday/canvas-kit-docs
Version:
Documentation components of Canvas Kit components
287 lines (205 loc) • 11.1 kB
text/mdx
import {
ExampleCodeBlock,
SymbolDoc,
Specifications,
StorybookStatusIndicator,
} from '@workday/canvas-kit-docs';
import Basic from './examples/Basic';
import Heading from './examples/Heading';
import AlternatePanel from './examples/Variant';
import ExternalControl from './examples/ExternalControl';
import RightOrigin from './examples/RightOrigin';
import AlwaysOpen from './examples/AlwaysOpen';
import OnStateTransition from './examples/OnStateTransition';
# Canvas Kit Side Panel <StorybookStatusIndicator type="new" />
`SidePanel` is a collapsible container that anchors to the left or right side of the screen. It uses
the model pattern for state management and is fully accessible.
[> Workday Design Reference](https://design.workday.com/components/containers/side-panel)
## Installation
```sh
yarn add @workday/canvas-kit-labs-react
```
## Migrating from Preview
If you're migrating from `@workday/canvas-kit-preview-react/side-panel`, here are the key API
changes:
### Import Changes
```tsx
// Before (preview-react)
import {SidePanel, useSidePanel} from '@workday/canvas-kit-preview-react/side-panel';
// After (labs-react)
import {SidePanel, useSidePanelModel} from '@workday/canvas-kit-labs-react/side-panel';
```
### Hook API Changes
| Preview (`useSidePanel`) | Labs (`useSidePanelModel`) |
| -------------------------------- | ------------------------------------------------------- |
| `initialExpanded: boolean` | `initialTransitionState: 'expanded' \| 'collapsed'` |
| `origin: 'left' \| 'right'` | `origin: 'start' \| 'end'` |
| Returns `expanded: boolean` | Returns `model.state.transitionState` |
| Returns `setExpanded(bool)` | Use `model.events.expand()` / `model.events.collapse()` |
| Returns `panelProps` to spread | Props applied automatically via `elemPropsHook` |
| Returns `labelProps` to spread | Use `id={model.state.labelId}` on label element |
| Returns `controlProps` to spread | Props applied automatically to `SidePanel.ToggleButton` |
### Component API Changes
| Preview | Labs |
| ---------------------------------------------- | ------------------------------------------------- |
| `<SidePanel {...panelProps}>` | `<SidePanel model={model}>` or just `<SidePanel>` |
| `<SidePanel.ToggleButton {...controlProps} />` | `<SidePanel.ToggleButton />` |
| `<Heading {...labelProps}>` | `<Heading id={model.state.labelId}>` |
| `expanded` prop on SidePanel | Managed by model's `transitionState` |
| `touched` prop on SidePanel | Managed internally |
| `onExpandedChange` callback | Use `onStateTransition` and derive expanded state |
| `onStateTransition` on component | `onStateTransition` on model config |
### Code Migration Example
```tsx
// Before (preview-react)
const {expanded, panelProps, labelProps, controlProps} = useSidePanel({
initialExpanded: false,
});
<SidePanel {...panelProps} origin="right" onExpandedChange={exp => console.log(exp)}>
<SidePanel.ToggleButton {...controlProps} />
<Heading {...labelProps}>Panel Title</Heading>
{expanded && <Content />}
</SidePanel>;
// After (labs-react)
const model = useSidePanelModel({
initialTransitionState: 'collapsed',
origin: 'end',
onStateTransition: state => {
const isExpanded = state === 'expanded' || state === 'expanding';
console.log(isExpanded);
},
});
<SidePanel model={model}>
<SidePanel.ToggleButton />
<Heading id={model.state.labelId}>Panel Title</Heading>
{model.state.transitionState === 'expanded' && <Content />}
</SidePanel>;
```
### Checking Expanded State
```tsx
// Before (preview-react)
if (expanded) {
/* ... */
}
// After (labs-react) - for exact state
if (model.state.transitionState === 'expanded') {
/* ... */
}
// After (labs-react) - including animation states
const isExpanded =
model.state.transitionState === 'expanded' || model.state.transitionState === 'expanding';
```
## Usage
### Basic Example
`SidePanel` is composed of three parts:
- The panel container (with an optional `model` prop)
- A heading (`SidePanel.Heading`) for the panel that is visually hidden when the panel is collapsed
- A toggle button (`SidePanel.ToggleButton`) to control the expand / collapse states
Bidirectional support is built into `SidePanel`. As seen in the example below, CSS Flexbox flips the
page layout and the panel's contents. `SidePanel` also has logic to flip the position and direction
of the `ToggleButton` as well as the direction of the expand / collapse animation. If you're using
CSS Flexbox for layouts and using the provided components, you shouldn't have to provide any custom
logic or styling for bidirectional support.
<ExampleCodeBlock code={Basic} />
### Hidden Name
`SidePanel`'s `<section>` element container should always have an accessible name to help screen
reader users understand the purpose of the panel. For this reason, we recommend using the
`SidePanel.Heading` component and setting the `hidden` prop to `true`. This will visually hide the
heading while keeping it accessible to screen readers.
<ExampleCodeBlock code={Heading} />
### Alternate Variant
`SidePanel` has one variant, `alternate`, which you can supply as a top-level prop. Default depth of
`alternate` variant is 5, if `alternate` SidePanel has an overlay behavior the depth 6 should be
used (this case is covered in the Examples section).
<ExampleCodeBlock code={AlternatePanel} />
### External Control
Sometimes you'll want to control `SidePanel`'s expand / collapse behavior from outside the
component. You can use the model's events (`model.events.expand()` and `model.events.collapse()`) to
programmatically control the panel.
#### Notes about accessibility
When using external controls, be mindful of accessibility:
- Use `aria-pressed` on toggle buttons to indicate the current state
- The `SidePanel.ToggleButton` inside the panel automatically receives the correct ARIA attributes
- External buttons should have their own accessible labels (don't rely on `aria-labelledby` pointing
to the panel's label)
In the following example, we use the model's `transitionState` to determine the button's pressed
state and call `model.events.expand()` or `model.events.collapse()` on click.
<ExampleCodeBlock code={ExternalControl} />
### Right Origin
By default, `SidePanel` uses a `start` origin (left in LTR, right in RTL). This sets the
`ToggleButton`'s position and direction as well as the direction of the animation. You can set the
origin to `"end"` to flip these. The origin uses logical properties (`start`/`end`) for proper
bidirectional support.
<ExampleCodeBlock code={RightOrigin} />
### Always Open
If you do not need `SidePanel`'s expand / collapse behavior, you can simply omit the `ToggleButton`.
<ExampleCodeBlock code={AlwaysOpen} />
### Deriving Expanded State
If you need a simple boolean `expanded` state (similar to the preview-react `onExpandedChange`
callback), you can derive it from the `transitionState` using the `onStateTransition` callback on
the model.
### onStateTransition
The `onStateTransition` callback is called whenever the panel's transition state changes. This
includes all four states: `expanding`, `expanded`, `collapsing`, and `collapsed`. You can pass this
callback directly to the `SidePanel` component or to the `useSidePanelModel` hook.
The transition flow is:
1. **Collapsing**: `expanded` → `collapsing` → `collapsed`
2. **Expanding**: `collapsed` → `expanding` → `expanded`
This is useful for:
- Triggering side effects when the panel state changes
- Syncing the panel state with external state management
- Animating child components based on the transition state
<ExampleCodeBlock code={OnStateTransition} />
### Accessibility
`SidePanel` renders a `<section>` element with an accessible name provided by `aria-labelledby`,
which references the `SidePanel.Heading` component. This ensures screen reader users understand the
purpose of the panel.
#### Panel and Heading
- The `SidePanel.Heading` provides the accessible name for the panel via `aria-labelledby`
- When the panel is collapsed, the heading is automatically hidden visually but remains accessible
to screen readers
- Use the `hidden` prop on `SidePanel.Heading` if you want the heading always visually hidden
#### Toggle Button
- `SidePanel.ToggleButton` automatically includes `aria-controls` (references the panel's `id`),
`aria-pressed` (indicates current state), and `aria-describedby` (references the panel's heading)
- Developers must provide a static `aria-label` string on `SidePanel.ToggleButton` to describe the
button's purpose (e.g., "Collapse View"). Avoid using ambiguous terms like "Toggle" in the label.
Since `aria-pressed` communicates the state, avoid dynamically updating `aria-label`
- The button includes a Tooltip with customizable text via `tooltipTextExpand` and
`tooltipTextCollapse` props (defaults: "Expand View" and "Collapse View")
- For optimal keyboard navigation, place `SidePanel.ToggleButton` as the first focusable element in
the panel
## Component API
<SymbolDoc name="SidePanel" fileName="/labs-react/" />
## Hooks
### useSidePanelModel
The `useSidePanelModel` hook creates a model for managing the SidePanel's state and events. You can
pass this model to the `SidePanel` component, or let the component create one internally.
```tsx
import {useSidePanelModel} from '@workday/canvas-kit-labs-react/side-panel';
// Create a model with custom configuration
const model = useSidePanelModel({
initialTransitionState: 'collapsed',
origin: 'end',
onStateTransition: state => console.log('State:', state),
});
// Access state
model.state.transitionState; // 'expanded' | 'expanding' | 'collapsed' | 'collapsing'
model.state.panelId; // unique ID for the panel
model.state.labelId; // unique ID for the label
// Trigger events
model.events.expand(); // Set to expanded (no animation)
model.events.collapse(); // Set to collapsed (no animation)
model.events.handleAnimationStart(); // Start expand/collapse animation
```
<SymbolDoc name="useSidePanelModel" fileName="/labs-react/" />
### useSidePanelContainer
The `useSidePanelContainer` elemProps hook provides the necessary props for the SidePanel container
element, including `id`, `aria-labelledby`, and `onTransitionEnd`.
<SymbolDoc name="useSidePanelContainer" fileName="/labs-react/" />
### useSidePanelToggleButtonElemProps
The `useSidePanelToggleButtonElemProps` elemProps hook provides ARIA attributes for the toggle
button, including `aria-controls`, `aria-expanded`, and `aria-labelledby`.
<SymbolDoc name="useSidePanelToggleButtonElemProps" fileName="/labs-react/" />
## Specifications
<Specifications file="SidePanelLabs.spec.ts" name="Side Panel" />