Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions docs/stories/04-components/Drawer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Meta, Canvas, ArgTypes } from '@storybook/addon-docs/blocks';
import { Drawer } from '@strapi/design-system';
import * as Dialog from '@radix-ui/react-dialog';

import * as DrawerStories from './Drawer.stories';

<Meta of={DrawerStories} />

# Drawer

- [Overview](#overview)
- [Usage](#usage)
- [Props](#props)
- [Positions](#positions)
- [Header visible](#header-visible)
- [Accessibility](#accessibility)

## Overview

A dismissible drawer that slides in from an edge of the viewport. Built on Radix UI Dialog for accessibility and keyboard/outside-click dismissal.

<ViewSource path="components/Drawer" />

<Canvas of={DrawerStories.Base} />

## Usage

```js
import { Drawer } from '@strapi/design-system';
```

## Props

### Root

Shares Radix Dialog Root component parameters (with an additional `headerVisible`):
<ArgTypes of={Drawer.Root & Dialog.Root} />

### Trigger

Shares Radix Dialog Trigger component parameters.

The `Trigger` component uses `asChild` — it renders the child and merges drawer open/close behaviour onto it.

### Content

<ArgTypes of={Drawer.Content} />
Also forwards Radix Dialog Content props (`onOpenAutoFocus`, `onCloseAutoFocus`, `onEscapeKeyDown`, `onPointerDownOutside`, `onInteractOutside`, etc.).

### Close

Shares Radix Dialog Close component parameters.
Uses `asChild` — wrap a button (or other focusable element) to close the drawer on activation.

Example:
```js
import { Drawer } from '@strapi/design-system';

<Drawer.Close>
<button>Cancel</button>
</Drawer.Close>
```

### Header

<ArgTypes of={Drawer.Header} />

### Title

Use for the drawer heading. Renders as `h2` for accessibility.

### Body

Optional. Scrollable content area of the drawer (hidden by default when `defaultOpen` on Drawer.Content is `false`).

### Footer

Optional. Flex container for custom actions.

## Positions

The drawer can be positioned on any edge via the `side` prop.

## Header visible

With `headerVisible` on Root, when `open` is false only the header is visible and the overlay is hidden. A toggle button in the header opens and closes the drawer.

## Accessibility

Uses [Radix UI Dialog](https://www.radix-ui.com/primitives/docs/components/dialog), which implements the [Dialog WAI-ARIA pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal). The drawer is dismissible via:

- **Escape** key
- **Click outside** (on the overlay)
- **Close** button in the header (when using `Drawer.Header`)
219 changes: 219 additions & 0 deletions docs/stories/04-components/Drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import * as React from 'react';

import { Meta, StoryObj } from '@storybook/react-vite';
import { Button, Drawer, Field, Flex } from '@strapi/design-system';
import { outdent } from 'outdent';
import { fn } from 'storybook/test';

interface DrawerArgs
extends Drawer.Props,
Pick<
Drawer.ContentProps,
| 'side'
| 'width'
| 'height'
| 'maxWidth'
| 'maxHeight'
| 'padding'
| 'onOpenAutoFocus'
| 'onCloseAutoFocus'
| 'onEscapeKeyDown'
| 'onPointerDownOutside'
| 'onInteractOutside'
> {
headerVisible?: boolean;
overlayVisible?: boolean;
}

const meta: Meta<DrawerArgs> = {
title: 'Components/Drawer',
component: Drawer.Root,
decorators: [
(Story) => (
<Flex width="100%" height="100%" justifyContent="center">
<Story />
</Flex>
),
],
parameters: {
docs: {
source: {
code: outdent`
<Drawer.Root>
<Drawer.Trigger>
<Button>Open drawer</Button>
</Drawer.Trigger>
<Drawer.Content side="right">
<Drawer.Header>
<Drawer.Title>Drawer title</Drawer.Title>
</Drawer.Header>
<Drawer.Body>
<p>Drawer content goes here.</p>
</Drawer.Body>
<Drawer.Footer>
<Drawer.Close>
<Button variant="tertiary">Cancel</Button>
</Drawer.Close>
<Button>Confirm</Button>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>
`,
},
},
chromatic: { disableSnapshot: false },
},
args: {
defaultOpen: false,
side: 'right',
headerVisible: false,
overlayVisible: true,
onOpenChange: fn(),
onOpenAutoFocus: fn(),
onCloseAutoFocus: fn(),
onEscapeKeyDown: fn(),
},
argTypes: {
side: {
control: 'select',
options: ['top', 'right', 'bottom', 'left'],
},
},
render: ({
side,
width,
height,
maxWidth,
maxHeight,
padding,
headerVisible,
overlayVisible,
onOpenAutoFocus,
onCloseAutoFocus,
onEscapeKeyDown,
...args
}) => {
return (
<Drawer.Root {...args} headerVisible={headerVisible} overlayVisible={overlayVisible}>
{!headerVisible && (
<Drawer.Trigger>
<Button>Open drawer</Button>
</Drawer.Trigger>
)}
<Drawer.Content
side={side}
width={width}
{...(height !== undefined && { height })}
{...(maxWidth !== undefined && { maxWidth })}
{...(maxHeight !== undefined && { maxHeight })}
{...(padding !== undefined && { padding })}
onOpenAutoFocus={onOpenAutoFocus}
onCloseAutoFocus={onCloseAutoFocus}
onEscapeKeyDown={onEscapeKeyDown}
>
<Drawer.Header>
<Drawer.Title>Drawer title</Drawer.Title>
</Drawer.Header>
<Drawer.Body>
<Field.Root name="example">
<Field.Label>Example field</Field.Label>
<Field.Input placeholder="Type something…" />
</Field.Root>
</Drawer.Body>
<Drawer.Footer>
<Drawer.Close>
<Button variant="tertiary">Cancel</Button>
</Drawer.Close>
<Button>Confirm</Button>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>
);
},
};

export default meta;

type Story = StoryObj<DrawerArgs>;

export const Base = {
args: {
defaultOpen: false,
},

name: 'base',
} satisfies Story;

export const DefaultOpen = {
args: {
defaultOpen: true,
},
name: 'default open',
} satisfies Story;

export const SideRight = {
args: {
defaultOpen: true,
side: 'right',
},
name: 'side right',
} satisfies Story;

export const SideLeft = {
args: {
defaultOpen: true,
side: 'left',
},
name: 'side left',
} satisfies Story;

export const SideTop = {
args: {
defaultOpen: true,
side: 'top',
},
name: 'side top',
} satisfies Story;

export const SideBottom = {
args: {
defaultOpen: true,
side: 'bottom',
},
name: 'side bottom',
} satisfies Story;

export const HeaderVisible = {
parameters: {
docs: {
source: {
code: outdent`
<Drawer.Root headerVisible defaultOpen={false}>
<Drawer.Content side="bottom" width="100%" padding={0}>
<Drawer.Header>
<Drawer.Title>Drawer title</Drawer.Title>
</Drawer.Header>
<Drawer.Body>
<p>Toggle to expand and see content + overlay.</p>
</Drawer.Body>
<Drawer.Footer>
<Drawer.Close>
<Button variant="tertiary">Cancel</Button>
</Drawer.Close>
<Button>Confirm</Button>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>
`,
},
},
},
args: {
defaultOpen: false,
headerVisible: true,
side: 'bottom',
width: '100%',
padding: 0,
},
name: 'header visible',
} satisfies Story;
4 changes: 2 additions & 2 deletions packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@
"@radix-ui/react-use-callback-ref": "1.0.1",
"@strapi/ui-primitives": "2.1.2",
"@uiw/react-codemirror": "4.22.2",
"lodash": "4.17.21",
"lodash": "4.17.23",
"react-remove-scroll": "2.5.10"
},
"devDependencies": {
"@strapi/icons": "2.1.2",
"@types/lodash": "^4.17.15",
"@types/lodash": "^4.17.23",
"@vitejs/plugin-react-swc": "^3.7.0",
"jest": "29.7.0",
"react": "18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const Overlay = styled(AlertDialog.Overlay)`
will-change: opacity;

@media (prefers-reduced-motion: no-preference) {
animation: ${ANIMATIONS.overlayFadeIn} ${(props) => props.theme.motion.timings['200']}
animation: ${ANIMATIONS.fadeIn} ${(props) => props.theme.motion.timings['200']}
${(props) => props.theme.motion.easings.authenticMotion};
}
`;
Expand Down
Loading
Loading