Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
96 changes: 96 additions & 0 deletions docs/stories/04-components/Drawer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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 `direction` prop (can be `left`, `right`, `top` or `bottom`).

The width will be forced at 100% on mobile devices.

## 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`)
196 changes: 196 additions & 0 deletions docs/stories/04-components/Drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
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';

interface DrawerArgs
extends Drawer.Props,
Pick<Drawer.ContentProps, 'direction' | 'width' | 'height' | 'maxWidth' | 'maxHeight' | 'padding'>,
Pick<Drawer.HeaderProps, 'hasToggle' | 'hasClose'> {
headerVisible?: 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.Portal>
<Drawer.Overlay />
<Drawer.Content direction="right">
<Drawer.Header hasClose={hasClose} hasToggle={hasToggle}>
<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.Portal>
</Drawer.Root>
`,
},
},
chromatic: { disableSnapshot: false },
},
args: {
defaultOpen: false,
direction: 'right',
headerVisible: false,
hasClose: true,
hasToggle: true,
},
argTypes: {
direction: {
control: 'select',
options: ['top', 'right', 'bottom', 'left'],
},
},
render: ({ direction, width, height, maxWidth, maxHeight, padding, headerVisible, hasClose, hasToggle, ...args }) => {
return (
<Drawer.Root {...args} headerVisible={headerVisible}>
{!headerVisible && (
<Drawer.Trigger>
<Button>Open drawer</Button>
</Drawer.Trigger>
)}
<Drawer.Portal>
<Drawer.Overlay />
<Drawer.Content
direction={direction}
width={width}
{...(height !== undefined && { height })}
{...(maxWidth !== undefined && { maxWidth })}
{...(maxHeight !== undefined && { maxHeight })}
{...(padding !== undefined && { padding })}
>
<Drawer.Header hasClose={hasClose} hasToggle={hasToggle}>
<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.Portal>
</Drawer.Root>
);
},
};

export default meta;

type Story = StoryObj<DrawerArgs>;

export const Base = {
args: {
defaultOpen: true,
headerVisible: true,
},

name: 'base',
} satisfies Story;

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

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

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

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

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

export const HeaderVisible = {
parameters: {
docs: {
source: {
code: outdent`
<Drawer.Root headerVisible defaultOpen={false}>
<Drawer.Portal>
<Drawer.Overlay />
<Drawer.Content direction="bottom" width="100%" padding={0}>
<Drawer.Header hasToggle={false}>
<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.Portal>
</Drawer.Root>
`,
},
},
},
args: {
defaultOpen: false,
headerVisible: true,
direction: 'bottom',
width: '100%',
padding: 0,
},
name: 'header visible',
} satisfies Story;
Loading
Loading