Skip to content

Commit

Permalink
feat(Drawer): new Drawer component
Browse files Browse the repository at this point in the history
  • Loading branch information
Chlenix committed Aug 6, 2024
1 parent 6160469 commit 4a4495e
Show file tree
Hide file tree
Showing 8 changed files with 732 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"dependencies": {
"@floating-ui/react": "0.26.12",
"@navikt/aksel-icons": "^5.12.2",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-slot": "^1.0.2",
"@tanstack/react-virtual": "^3.2.0"
},
Expand Down
32 changes: 32 additions & 0 deletions packages/react/src/components/Drawer/Drawer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Meta, Canvas, Controls, Primary } from '@storybook/blocks';

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

<Meta of={DrawerStories} />

# Drawer

Med `Drawer` kan du vise tilleggsinformasjon eller kontroller uten å forlate den nåværende siden. Den er ideell for å presentere sekundært innhold som ikke trenger å være synlig hele tiden.

**Vær oppmerksom på:**

- Drawer bør ikke inneholde kritisk informasjon som brukeren må se umiddelbart.
- Innholdet i Drawer bør være fokusert og relevant for den aktuelle konteksten.

<br />

<Primary />
<Controls />

## Bruk

```tsx
import { Drawer } from '@digdir/designsystemet-react';

<Drawer
trigger={<Button>Åpne Drawer</Button>}
title='Drawer Tittel'
>
<p>Drawer innhold</p>
</Drawer>;
```
118 changes: 118 additions & 0 deletions packages/react/src/components/Drawer/Drawer.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
.overlay {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
inset: 0;
animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
}

.content {
background-color: white;
box-shadow:
hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
position: fixed;
top: 0;
bottom: 0;
width: 90%;
max-width: 450px;
padding: 25px;
animation-duration: 150ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}

.content:focus {
outline: none;
}

.right {
right: 0;
animation-name: contentShowRight;
}

.left {
left: 0;
animation-name: contentShowLeft;
}

.top {
top: 0;
left: 0;
right: 0;
bottom: auto;
width: 100%;
max-width: 100%;
height: 50%;
max-height: 300px;
animation-name: contentShowTop;
}

.closeButton {
position: fixed;
right: 0;
margin-right: var(--fds-spacing-4);
}

.bottom {
top: auto;
bottom: 0;
left: 0;
right: 0;
width: 100%;
max-width: 100%;
height: 50%;
max-height: 300px;
animation-name: contentShowBottom;
}

@keyframes overlayShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes contentShowRight {
from {
opacity: 0;
transform: translate3d(100%, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}

@keyframes contentShowLeft {
from {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}

@keyframes contentShowTop {
from {
opacity: 0;
transform: translate3d(0, -100%, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}

@keyframes contentShowBottom {
from {
opacity: 0;
transform: translate3d(0, 100%, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
143 changes: 143 additions & 0 deletions packages/react/src/components/Drawer/Drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { useRef, useState } from 'react';

Check failure on line 1 in packages/react/src/components/Drawer/Drawer.stories.tsx

View workflow job for this annotation

GitHub Actions / Builds, lints and tests code

'React' is defined but never used
import type { Meta, StoryFn } from '@storybook/react';

import { Button } from '../Button';
import { Textfield } from '../form/Textfield';
import { Paragraph } from '../Typography';
import { Divider } from '../Divider';
import { Combobox } from '..';

import { Drawer, type DrawerRef } from './Drawer';

export default {
title: 'Komponenter/Drawer',
component: Drawer,
} as Meta;

export const Preview: StoryFn<typeof Drawer> = (args) => {
const drawerRef = useRef<DrawerRef>(null);

return (
<>
<Button onClick={() => drawerRef.current?.open()}>Open Drawer</Button>
<Drawer
ref={drawerRef}
{...args}
>
<h2>Drawer Content</h2>
<Paragraph>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis
doloremque obcaecati assumenda odio ducimus sunt et.
</Paragraph>
</Drawer>
</>
);
};

export const WithBuiltInTrigger: StoryFn<typeof Drawer> = (args) => {
return (
<Drawer
trigger={<Button>Open Drawer</Button>}
{...args}
>
<h2>Drawer with Built-in Trigger</h2>
<Paragraph>This drawer uses the built-in trigger prop.</Paragraph>
</Drawer>
);
};

export const DifferentPositions: StoryFn<typeof Drawer> = () => {
const drawerRef = useRef<DrawerRef>(null);
const [position, setPosition] = useState<'left' | 'right' | 'top' | 'bottom'>(
'right',
);

return (
<>
<Button onClick={() => drawerRef.current?.open()}>Open Drawer</Button>
<select
onChange={(e) => setPosition(e.target.value as typeof position)}
value={position}
style={{ marginLeft: '10px' }}
>
<option value='left'>Left</option>
<option value='right'>Right</option>
<option value='top'>Top</option>
<option value='bottom'>Bottom</option>
</select>
<Drawer
ref={drawerRef}
position={position}
>
<h2>Drawer from {position}</h2>
<Paragraph>This drawer opens from the {position}.</Paragraph>
</Drawer>
</>
);
};

export const DrawerWithForm: StoryFn<typeof Drawer> = () => {
const drawerRef = useRef<DrawerRef>(null);
const [input, setInput] = useState('');

return (
<>
<Button onClick={() => drawerRef.current?.open()}>
Open Drawer with Form
</Button>
<Drawer ref={drawerRef}>
<h2>Drawer with Form</h2>
<Textfield
label='Name'
placeholder='John Doe'
value={input}
autoFocus
onChange={(e) => setInput(e.target.value)}
/>
<Button
onClick={() => {
window.alert(`You submitted the form with name: ${input}`);
setInput('');
drawerRef.current?.close();
}}
>
Submit Form
</Button>
</Drawer>
</>
);
};

export const DrawerWithDivider: StoryFn<typeof Drawer> = () => (
<Drawer trigger={<Button>Open Drawer with Divider</Button>}>
<h2>Drawer with Divider</h2>
<Divider color='subtle' />
<Paragraph>Content between dividers</Paragraph>
<Divider color='subtle' />
</Drawer>
);

export const DrawerWithSelect: StoryFn<typeof Drawer> = () => {
const drawerRef = useRef<DrawerRef>(null);

return (
<>
<Button onClick={() => drawerRef.current?.open()}>
Open Drawer with Select
</Button>
<Drawer
ref={drawerRef}
style={{ overflow: 'visible' }}
>
<h2>Drawer with Select</h2>
<Combobox portal={false}>
<Combobox.Empty>No results found</Combobox.Empty>
<Combobox.Option value='oslo'>Oslo</Combobox.Option>
<Combobox.Option value='bergen'>Bergen</Combobox.Option>
<Combobox.Option value='trondheim'>Trondheim</Combobox.Option>
<Combobox.Option value='stavanger'>Stavanger</Combobox.Option>
</Combobox>
</Drawer>
</>
);
};
74 changes: 74 additions & 0 deletions packages/react/src/components/Drawer/Drawer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { act, render as renderRtl, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

Check warning on line 2 in packages/react/src/components/Drawer/Drawer.test.tsx

View workflow job for this annotation

GitHub Actions / Builds, lints and tests code

There should be at least one empty line between import groups
import { Drawer, DrawerProps } from './Drawer';

Check warning on line 3 in packages/react/src/components/Drawer/Drawer.test.tsx

View workflow job for this annotation

GitHub Actions / Builds, lints and tests code

Import "DrawerProps" is only used as types

const TRIGGER_TEXT = 'Open Drawer';
const DRAWER_CONTENT = 'Drawer Content';
const DRAWER_TITLE = 'Drawer Title';

Check failure on line 7 in packages/react/src/components/Drawer/Drawer.test.tsx

View workflow job for this annotation

GitHub Actions / Builds, lints and tests code

'DRAWER_TITLE' is assigned a value but never used

const TestComponent = (props: Partial<DrawerProps>) => (
<Drawer
trigger={<button>{TRIGGER_TEXT}</button>}
{...props}
>
{DRAWER_CONTENT}
</Drawer>
);

const render = async (props: Partial<DrawerProps> = {}) => {
await act(async () => {});
const user = userEvent.setup();
return {
user,
...renderRtl(<TestComponent {...props} />),
};
};

describe('Drawer', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('should render the trigger', async () => {
await render();
expect(screen.getByText(TRIGGER_TEXT)).toBeInTheDocument();
});

it('should open the drawer', async () => {
const { user } = await render();
const trigger = screen.getByText(TRIGGER_TEXT);
await user.click(trigger);
expect(screen.getByText(DRAWER_CONTENT)).toBeInTheDocument();
});

it('should close the drawer', async () => {
const { user } = await render();
const trigger = screen.getByText(TRIGGER_TEXT);
await user.click(trigger);
const closeButton = screen.getByRole('button', { name: /close/i });
await user.click(closeButton);
expect(screen.queryByText(DRAWER_CONTENT)).not.toBeInTheDocument();
});

it('should use custom aria-label for close button', async () => {
const customLabel = 'Custom Close Label';
await render({ arialabelCloseDrawer: customLabel });
const trigger = screen.getByText(TRIGGER_TEXT);
await userEvent.click(trigger);
expect(screen.getByLabelText(customLabel)).toBeInTheDocument();
});

it('should not render trigger when not provided', async () => {
await render({ trigger: undefined });
expect(screen.queryByText(TRIGGER_TEXT)).not.toBeInTheDocument();
});

it('should autofocus on close button when opened', async () => {
const { user } = await render();
const trigger = screen.getByText(TRIGGER_TEXT);
await user.click(trigger);

const closeButton = screen.getByRole('button', { name: /close/i });
expect(closeButton).toHaveFocus();
});
});
Loading

0 comments on commit 4a4495e

Please sign in to comment.