-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
732 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
143
packages/react/src/components/Drawer/Drawer.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import React, { useRef, useState } from 'react'; | ||
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> | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
import { Drawer, DrawerProps } from './Drawer'; | ||
|
||
const TRIGGER_TEXT = 'Open Drawer'; | ||
const DRAWER_CONTENT = 'Drawer Content'; | ||
const DRAWER_TITLE = 'Drawer Title'; | ||
|
||
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(); | ||
}); | ||
}); |
Oops, something went wrong.