Drawer
Drawer (드로어)
Drawer 컴포넌트는 화면의 어느 한쪽에서 오버레이 영역을 표시하는 컴포넌트예요. left(기본), top, right, bottom 위치에 패널을 열 수 있어요.
출처: 문서
본문
Drawer는 opened와 onClose prop으로 상태를 제어해요.
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Drawer opened={opened} onClose={close} title="Authentication">
{/* Drawer content */}
</Drawer>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
위치 (Position)
Drawer는 left(기본), top, right, bottom에 배치할 수 있어요. position prop으로 위치를 제어해요.
오프셋 (Offset)
offset prop으로 뷰포트 가장자리로부터의 드로어 오프셋을 바꿀 수 있어요.
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Drawer offset={8} radius="md" opened={opened} onClose={close} title="Authentication">
{/* Drawer content */}
</Drawer>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
오버레이 커스터마이즈 (Customize overlay)
Drawer는 Overlay 컴포넌트를 사용해요. Overlay가 지원하는 props를 overlayProps로 설정할 수 있어요.
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Drawer
opened={opened}
onClose={close}
title="Authentication"
overlayProps={{ backgroundOpacity: 0.5, blur: 4 }}
>
{/* Drawer content */}
</Drawer>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
크기 (Sizes)
size prop에 미리 정의된 크기나 유효한 너비 값(예: size="55%" 또는 size={200})을 설정하면 드로어의 너비/높이(position에 따라 다름)를 바꿀 수 있어요.
import { Drawer } from '@mantine/core';
function Demo() {
return (
<Drawer position="right" size="xl" opened onClose={() => {}}>
{/* Drawer content */}
</Drawer>
);
}
xs, sm, md, lg, xl, 100%, 40rem, 25% 크기를 지원해요.
헤더 제거하기 (Remove header)
헤더를 제거하려면 withCloseButton={false}를 설정해요.
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Drawer opened={opened} onClose={close} withCloseButton={false}>
Drawer without header, press escape or click on overlay to close
</Drawer>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
스크롤이 있는 Drawer
다음 예시처럼 콘텐츠가 길어도 헤더는 고정(sticky) 상태로 유지돼요.
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
const content = Array(100)
.fill(0)
.map((_, index) => <p key={index}>Drawer with scroll</p>);
return (
<>
<Drawer opened={opened} onClose={close} title="Header is sticky">
{content}
</Drawer>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
ScrollArea와 함께 사용하기
scrollAreaComponent prop으로 스크롤 영역 컴포넌트를 지정할 수 있어요.
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button, ScrollArea } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
const content = Array(100)
.fill(0)
.map((_, index) => <p key={index}>Drawer with scroll</p>);
return (
<>
<Drawer
opened={opened}
onClose={close}
title="Header is sticky"
scrollAreaComponent={ScrollArea.Autosize}
>
{content}
</Drawer>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
트랜지션 바꾸기 (Change transition)
Drawer는 Transition 컴포넌트로 만들어졌어요. Transition 속성을 커스터마이즈하려면 transitionProps prop을 사용해요.
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Drawer
opened={opened}
onClose={close}
title="Authentication"
transitionProps={{ transition: 'rotate-left', duration: 150, timingFunction: 'linear' }}
>
{/* Drawer content */}
</Drawer>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
onExitTransitionEnd와 onEnterTransitionEnd
onExitTransitionEnd와 onEnterTransitionEnd prop으로 닫힘/열림 트랜지션이 끝난 후 코드를 실행할 수 있어요. 예를 들어 드로어가 닫힌 후 데이터를 지우고 싶을 때 유용해요.
import { useState } from 'react';
import { Button, Group, Drawer } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [firstOpened, firstHandlers] = useDisclosure(false);
const [secondOpened, secondHandlers] = useDisclosure(false);
const [drawerData, setDrawerData] = useState({
title: '',
message: '',
});
return (
<>
<Drawer
opened={firstOpened}
onClose={() => {
firstHandlers.close();
setDrawerData({ title: '', message: '' });
}}
title={drawerData.title}
>
{drawerData.message}
</Drawer>
<Drawer
opened={secondOpened}
onClose={secondHandlers.close}
onExitTransitionEnd={() => setDrawerData({ title: '', message: '' })}
title={drawerData.title}
>
{drawerData.message}
</Drawer>
<Group>
<Button
onClick={() => {
firstHandlers.open();
setDrawerData({ title: 'Edit your profile', message: 'Imagine a form here' });
}}
>
Clear data in onClose
</Button>
<Button
onClick={() => {
secondHandlers.open();
setDrawerData({ title: 'Edit your profile', message: 'Imagine a form here' });
}}
>
Clear data in onExitTransitionEnd
</Button>
</Group>
</>
);
}
초기 포커스 (Initial focus)
Drawer는 포커스를 가두기 위해 FocusTrap을 사용해요. 초기 포커스를 받을 요소에 data-autofocus 속성을 추가해요.
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button, TextInput } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Drawer opened={opened} onClose={close} title="Focus demo">
<TextInput label="First input" placeholder="First input" />
<TextInput
data-autofocus
label="Input with initial focus"
placeholder="It has data-autofocus attribute"
mt="md"
/>
</Drawer>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
드로어가 열렸을 때 어떤 요소에도 포커스를 주지 않으려면 FocusTrap.InitialFocus 컴포넌트로 시각적으로 숨겨진 초기 포커스 요소를 만들어요.
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button, TextInput, FocusTrap } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Drawer opened={opened} onClose={close} title="Focus demo">
<FocusTrap.InitialFocus />
<TextInput label="First input" placeholder="First input" />
<TextInput
data-autofocus
label="Input with initial focus"
placeholder="It has data-autofocus attribute"
mt="md"
/>
</Drawer>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
data-autofocus 속성을 추가하지 않고 FocusTrap.InitialFocus도 사용하지 않으면, 드로어는 내부의 첫 번째 포커스 가능한 요소(보통 닫기 버튼)에 포커스를 줘요.
동작 제어 (Control behavior)
다음 props로 Drawer 동작을 제어할 수 있어요. 대부분의 경우 이러한 기능을 끄는 것은 권장되지 않아요. 컴포넌트가 덜 접근 가능해질 수 있어요.
trapFocus– 포커스를 드로어 안에 가둘지 결정해요closeOnEscape–Escape키를 누르면 드로어를 닫을지 결정해요closeOnClickOutside– 사용자가 오버레이를 클릭하면 드로어를 닫을지 결정해요returnFocus– 드로어가 열리기 전에 포커스되었던 요소로 포커스를 되돌릴지 결정해요
react-remove-scroll 설정
Drawer는 스크롤을 잠그기 위해 react-remove-scroll 패키지를 사용해요. removeScrollProps로 RemoveScroll 컴포넌트에 props를 전달할 수 있어요.
import { Drawer } from '@mantine/core';
function Demo() {
return (
<Drawer
removeScrollProps={{ allowPinchZoom: true }}
opened
onClose={() => {}}
>
{/* Drawer content */}
</Drawer>
);
}
닫기 아이콘 바꾸기 (Change close icon)
closeButtonProps로 닫기 버튼을 커스터마이즈할 수 있어요.
import { XCircleIcon } from '@phosphor-icons/react';
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Drawer
opened={opened}
onClose={close}
title="Authentication"
closeButtonProps={{
icon: <XCircleIcon size={20} />,
}}
>
{/* Drawer content */}
</Drawer>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
복합 컴포넌트 (Compound components)
Drawer 렌더링을 완전히 제어하려면 다음 복합 컴포넌트를 사용할 수 있어요.
Drawer.Root– 컨텍스트 프로바이더Drawer.Overlay– Overlay 렌더링Drawer.Content– 메인 드로어 요소, 모든 드로어 콘텐츠를 포함해야 해요Drawer.Header– 고정(sticky) 헤더, 보통Drawer.Title과Drawer.CloseButton포함Drawer.Title–h2요소,Drawer.Content의aria-labelledby가 이 요소를 가리켜요. 보통Drawer.Header안에 렌더링돼요Drawer.CloseButton– 닫기 버튼, 보통Drawer.Header안에 렌더링돼요Drawer.Body– 메인 콘텐츠 자리,Drawer.Content의aria-describedby가 이 요소를 가리켜요
import { useDisclosure } from '@mantine/hooks';
import { Drawer, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Drawer.Root opened={opened} onClose={close}>
<Drawer.Overlay />
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Drawer title</Drawer.Title>
<Drawer.CloseButton />
</Drawer.Header>
<Drawer.Body>Drawer content</Drawer.Body>
</Drawer.Content>
</Drawer.Root>
<Button variant="default" onClick={open}>
Open Drawer
</Button>
</>
);
}
Drawer.Stack
Drawer.Stack 컴포넌트로 여러 드로어를 동시에 렌더링할 수 있어요. Drawer.Stack은 열린 드로어를 추적하고 z-index 값을 관리하며 포커스 트랩과 closeOnEscape 동작을 관리해요. Drawer.Stack은 useDrawersStack 훅과 함께 사용하도록 설계되었어요.
여러 Drawer 컴포넌트를 사용하는 것과의 차이점:
Drawer.Stack은 z-index 값을 관리해요. 나중에 열린 드로어는 DOM 순서와 무관하게 항상 더 높은 z-index 값을 가져요Drawer.Stack은 현재 열린 드로어를 제외한 모든 드로어에 대해 포커스 트랩과Escape키 처리를 비활성화해요- 현재 열려 있지 않은 드로어는 DOM에 존재하지만
opacity: 0과pointer-events: none으로 숨겨져요 - 한 번에 하나의 오버레이만 렌더링돼요
import { Button, Group, Drawer, useDrawersStack } from '@mantine/core';
function Demo() {
const stack = useDrawersStack(['delete-page', 'confirm-action', 'really-confirm-action']);
return (
<>
<Drawer.Stack>
<Drawer {...stack.register('delete-page')} title="Delete this page?">
Are you sure you want to delete this page? This action cannot be undone.
<Group mt="lg" justify="flex-end">
<Button onClick={stack.closeAll} variant="default">
Cancel
</Button>
<Button onClick={() => stack.open('confirm-action')} color="red">
Delete
</Button>
</Group>
</Drawer>
<Drawer {...stack.register('confirm-action')} title="Confirm action">
Are you sure you want to perform this action? This action cannot be undone. If you are
sure, press confirm button below.
<Group mt="lg" justify="flex-end">
<Button onClick={stack.closeAll} variant="default">
Cancel
</Button>
<Button onClick={() => stack.open('really-confirm-action')} color="red">
Confirm
</Button>
</Group>
</Drawer>
<Drawer {...stack.register('really-confirm-action')} title="Really confirm action">
Jokes aside. You have confirmed this action. This is your last chance to cancel it. After
you press confirm button below, action will be performed and cannot be undone. For real
this time. Are you sure you want to proceed?
<Group mt="lg" justify="flex-end">
<Button onClick={stack.closeAll} variant="default">
Cancel
</Button>
<Button onClick={stack.closeAll} color="red">
Confirm
</Button>
</Group>
</Drawer>
</Drawer.Stack>
<Button variant="default" onClick={() => stack.open('delete-page')}>
Open drawer
</Button>
</>
);
}
주의: Drawer.Stack은 Drawer 컴포넌트와만 사용할 수 있어요. Drawer.Root와 다른 복합 컴포넌트로 만든 컴포넌트는 Drawer.Stack과 호환되지 않아요.
useDrawersStack 훅
useDrawersStack 훅은 여러 드로어를 동시에 제어하는 쉬운 방법을 제공해요. 고유한 드로어 id 배열을 받아 다음 속성을 가진 객체를 반환해요.
interface UseDrawersStackReturnType<T extends string> {
// Current opened state of each drawer
state: Record<T, boolean>;
// Opens drawer with the given id
open: (id: T) => void;
// Closes drawer with the given id
close: (id: T) => void;
// Toggles drawer with the given id
toggle: (id: T) => void;
// Closes all drawers within the stack
closeAll: () => void;
// Returns props for drawer with the given id
register: (id: T) => {
opened: boolean;
onClose: () => void;
stackId: T;
};
}
Drawer 컴포넌트와 함께 useDrawersStack을 사용하는 예시:
import { Drawer, useDrawersStack } from '@mantine/core';
function Demo() {
const stack = useDrawersStack(['first', 'second']);
return (
<>
<Drawer {...stack.register('first')}>First</Drawer>
<Drawer {...stack.register('second')}>Second</Drawer>
<Button onClick={() => stack.open('first')}>Open first</Button>
</>
);
}
고정 요소 오프셋 (Fixed elements offset)
Drawer 컴포넌트는 스크롤을 잠그기 위해 react-remove-scroll 패키지를 사용해요. 이러한 position: fixed 요소를 올바르게 크기 조정하려면 요소에 className을 추가해요(문서).
import { RemoveScroll } from '@mantine/core';
function Demo() {
return (
<>
<RemoveScroll>
<div>width: 100%</div>
<div>right: 0</div>
</RemoveScroll>
</>
);
}
접근성 (Accessibility)
Drawer 컴포넌트는 WAI-ARIA 권장사항을 따르며 접근성을 지원해요.
title prop을 설정하면 컴포넌트를 접근 가능하게 만들고, 콘텐츠 요소에 aria-labelledby를 추가해요.
import { Drawer } from '@mantine/core';
function Demo() {
return <Drawer title="Drawer label" opened onClose={() => {}} />;
}
닫기 버튼의 aria-label을 설정하려면 closeButtonProps를 사용해요.
import { Drawer } from '@mantine/core';
function Demo() {
return (
<Drawer
closeButtonProps={{ 'aria-label': 'Close drawer' }}
opened
onClose={() => {}}
/>
);
}