Skip to content

Commit 7f95fa0

Browse files
committed
[drawer] Add CloseWatcher support and swipe area
1 parent f6a19a8 commit 7f95fa0

File tree

29 files changed

+1283
-212
lines changed

29 files changed

+1283
-212
lines changed

docs/reference/generated/drawer-root.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
},
6969
"swipeDirection": {
7070
"type": "DrawerSwipeDirection",
71-
"default": "'right'",
71+
"default": "'down'",
7272
"description": "The swipe direction used to dismiss the drawer.",
7373
"detailedType": "'up' | 'down' | 'left' | 'right' | undefined"
7474
},
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "DrawerSwipeArea",
3+
"description": "An invisible area that listens for swipe gestures to open the drawer.\nRenders a `<div>` element.",
4+
"props": {
5+
"swipeDirection": {
6+
"type": "DrawerSwipeDirection",
7+
"description": "The swipe direction that opens the drawer.\nDefaults to the opposite of `Drawer.Root` `swipeDirection`.",
8+
"detailedType": "'up' | 'down' | 'left' | 'right' | undefined"
9+
},
10+
"disabled": {
11+
"type": "boolean",
12+
"default": "false",
13+
"description": "Whether the swipe area is disabled.",
14+
"detailedType": "boolean | undefined"
15+
},
16+
"className": {
17+
"type": "string | ((state: Drawer.SwipeArea.State) => string | undefined)",
18+
"description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state.",
19+
"detailedType": "| string\n| ((state: Drawer.SwipeArea.State) => string | undefined)"
20+
},
21+
"style": {
22+
"type": "CSSProperties | ((state: Drawer.SwipeArea.State) => CSSProperties | undefined)",
23+
"detailedType": "| React.CSSProperties\n| ((\n state: Drawer.SwipeArea.State,\n ) => CSSProperties | undefined)\n| undefined"
24+
},
25+
"render": {
26+
"type": "ReactElement | ((props: HTMLProps, state: Drawer.SwipeArea.State) => ReactElement)",
27+
"description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.",
28+
"detailedType": "| ReactElement\n| ((\n props: HTMLProps,\n state: Drawer.SwipeArea.State,\n ) => ReactElement)"
29+
}
30+
},
31+
"dataAttributes": {},
32+
"cssVariables": {}
33+
}

docs/src/app/(docs)/react/components/drawer/demos/nested/css-modules/index.module.css

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,6 @@
104104

105105
&[data-ending-style] {
106106
pointer-events: none;
107-
}
108-
109-
&[data-ending-style] {
110107
transition-duration: calc(var(--drawer-swipe-strength, 1) * 400ms);
111108
}
112109
}
@@ -209,9 +206,6 @@
209206

210207
&[data-ending-style] {
211208
box-shadow: 0 2px 10px rgb(0 0 0 / 0);
212-
}
213-
214-
&[data-ending-style] {
215209
transition-duration: calc(var(--drawer-swipe-strength, 1) * 400ms);
216210
}
217211
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
.Root {
2+
position: relative;
3+
width: 100%;
4+
min-height: 320px;
5+
overflow: hidden;
6+
border: 1px solid var(--color-gray-200);
7+
background-color: var(--color-gray-50);
8+
color: var(--color-gray-900);
9+
}
10+
11+
.Center {
12+
min-height: 320px;
13+
display: flex;
14+
align-items: center;
15+
justify-content: center;
16+
padding: 1rem;
17+
}
18+
19+
.Instructions {
20+
display: flex;
21+
flex-direction: column;
22+
align-items: center;
23+
gap: 0.75rem;
24+
text-align: center;
25+
}
26+
27+
.Hint {
28+
margin: 0;
29+
font-size: 1rem;
30+
line-height: 1.5rem;
31+
color: var(--color-gray-600);
32+
text-align: center;
33+
padding-right: 3rem;
34+
}
35+
36+
.SwipeArea {
37+
position: absolute;
38+
top: 0;
39+
right: 0;
40+
bottom: 0;
41+
width: 2.5rem;
42+
z-index: 1;
43+
box-sizing: border-box;
44+
border-left: 2px dashed var(--color-blue);
45+
background-color: color-mix(in oklch, var(--color-blue), transparent 90%);
46+
}
47+
48+
.SwipeLabel {
49+
position: absolute;
50+
right: 0;
51+
top: 50%;
52+
margin-right: 0.5rem;
53+
transform: translateY(-50%) rotate(-90deg);
54+
transform-origin: center;
55+
z-index: 0;
56+
font-size: 0.75rem;
57+
font-weight: 600;
58+
letter-spacing: 0.12em;
59+
text-transform: uppercase;
60+
color: var(--color-blue);
61+
white-space: nowrap;
62+
pointer-events: none;
63+
}
64+
65+
.Button {
66+
box-sizing: border-box;
67+
display: flex;
68+
align-items: center;
69+
justify-content: center;
70+
height: 2.5rem;
71+
padding: 0 0.875rem;
72+
margin: 0;
73+
outline: 0;
74+
border: 1px solid var(--color-gray-200);
75+
border-radius: 0.375rem;
76+
background-color: var(--color-gray-50);
77+
font-family: inherit;
78+
font-size: 1rem;
79+
font-weight: 500;
80+
line-height: 1.5rem;
81+
color: var(--color-gray-900);
82+
user-select: none;
83+
84+
@media (hover: hover) {
85+
&:hover {
86+
background-color: var(--color-gray-100);
87+
}
88+
}
89+
90+
&:active {
91+
background-color: var(--color-gray-100);
92+
}
93+
94+
&:focus-visible {
95+
outline: 2px solid var(--color-blue);
96+
outline-offset: -1px;
97+
}
98+
}
99+
100+
.Backdrop {
101+
--backdrop-opacity: 0.2;
102+
--bleed: 3rem;
103+
position: absolute;
104+
min-height: 100dvh;
105+
inset: 0;
106+
background-color: black;
107+
opacity: calc(var(--backdrop-opacity) * (1 - var(--drawer-swipe-progress, 0)));
108+
transition-duration: 450ms;
109+
transition-property: opacity;
110+
transition-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
111+
112+
@media (prefers-color-scheme: dark) {
113+
--backdrop-opacity: 0.7;
114+
}
115+
116+
&[data-starting-style],
117+
&[data-ending-style] {
118+
opacity: 0;
119+
}
120+
121+
&[data-swiping] {
122+
transition-duration: 0ms;
123+
}
124+
125+
&[data-ending-style] {
126+
transition-duration: calc(var(--drawer-swipe-strength, 1) * 400ms);
127+
}
128+
}
129+
130+
.Viewport {
131+
--viewport-padding: 0px;
132+
position: absolute;
133+
inset: 0;
134+
display: flex;
135+
justify-content: flex-end;
136+
padding: var(--viewport-padding);
137+
z-index: 2;
138+
139+
@supports (-webkit-touch-callout: none) {
140+
--viewport-padding: 0.625rem;
141+
}
142+
}
143+
144+
.Popup {
145+
--bleed: 3rem;
146+
box-sizing: border-box;
147+
width: calc(20rem + var(--bleed));
148+
max-width: calc(100vw - 3rem + var(--bleed));
149+
height: 100%;
150+
padding: 1.5rem;
151+
padding-right: calc(1.5rem + var(--bleed));
152+
margin-right: calc(-1 * var(--bleed));
153+
outline: 1px solid var(--color-gray-200);
154+
background-color: var(--color-gray-50);
155+
color: var(--color-gray-900);
156+
overflow-y: auto;
157+
overscroll-behavior: contain;
158+
touch-action: auto;
159+
transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1);
160+
will-change: transform;
161+
transform: translateX(var(--drawer-swipe-movement-x));
162+
163+
&[data-swiping] {
164+
user-select: none;
165+
}
166+
167+
@media (prefers-color-scheme: dark) {
168+
outline: 1px solid var(--color-gray-300);
169+
}
170+
171+
&[data-starting-style],
172+
&[data-ending-style] {
173+
transform: translateX(calc(100% - var(--bleed) + var(--viewport-padding)));
174+
}
175+
176+
&[data-ending-style] {
177+
transition-duration: calc(var(--drawer-swipe-strength, 1) * 400ms);
178+
}
179+
180+
@supports (-webkit-touch-callout: none) {
181+
--bleed: 0px;
182+
margin-right: 0;
183+
border-radius: 10px;
184+
}
185+
}
186+
187+
.Content {
188+
width: 100%;
189+
max-width: 32rem;
190+
margin: 0 auto;
191+
}
192+
193+
.Title {
194+
margin-top: -0.375rem;
195+
margin-bottom: 0.25rem;
196+
font-size: 1.125rem;
197+
line-height: 1.75rem;
198+
letter-spacing: -0.0025em;
199+
font-weight: 500;
200+
}
201+
202+
.Description {
203+
margin: 0 0 1.5rem;
204+
font-size: 1rem;
205+
line-height: 1.5rem;
206+
color: var(--color-gray-600);
207+
}
208+
209+
.Actions {
210+
display: flex;
211+
justify-content: flex-end;
212+
gap: 1rem;
213+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client';
2+
import * as React from 'react';
3+
import { Drawer } from '@base-ui/react/drawer';
4+
import styles from './index.module.css';
5+
6+
export default function ExampleDrawerSwipeArea() {
7+
const [portalContainer, setPortalContainer] = React.useState<HTMLDivElement | null>(null);
8+
9+
return (
10+
<div className={styles.Root} ref={setPortalContainer}>
11+
<Drawer.Root swipeDirection="right" modal={false}>
12+
<Drawer.SwipeArea className={styles.SwipeArea}>
13+
<span className={styles.SwipeLabel}>Swipe here</span>
14+
</Drawer.SwipeArea>
15+
<div className={styles.Center}>
16+
<div className={styles.Instructions}>
17+
<p className={styles.Hint}>Swipe from the right edge to open the drawer.</p>
18+
</div>
19+
</div>
20+
<Drawer.Portal container={portalContainer}>
21+
<Drawer.Backdrop className={styles.Backdrop} />
22+
<Drawer.Viewport className={styles.Viewport}>
23+
<Drawer.Popup className={styles.Popup}>
24+
<Drawer.Content className={styles.Content}>
25+
<Drawer.Title className={styles.Title}>Library</Drawer.Title>
26+
<Drawer.Description className={styles.Description}>
27+
Swipe from the edge whenever you want to jump back into your playlists.
28+
</Drawer.Description>
29+
<div className={styles.Actions}>
30+
<Drawer.Close className={styles.Button}>Close</Drawer.Close>
31+
</div>
32+
</Drawer.Content>
33+
</Drawer.Popup>
34+
</Drawer.Viewport>
35+
</Drawer.Portal>
36+
</Drawer.Root>
37+
</div>
38+
);
39+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createDemoWithVariants } from 'docs/src/utils/createDemo';
2+
import CssModules from './css-modules';
3+
import Tailwind from './tailwind';
4+
5+
export const DemoDrawerSwipeArea = createDemoWithVariants(import.meta.url, {
6+
CssModules,
7+
Tailwind,
8+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use client';
2+
import * as React from 'react';
3+
import { Drawer } from '@base-ui/react/drawer';
4+
5+
export default function ExampleDrawerSwipeArea() {
6+
const [portalContainer, setPortalContainer] = React.useState<HTMLDivElement | null>(null);
7+
8+
return (
9+
<div
10+
ref={setPortalContainer}
11+
className="relative min-h-[320px] w-full overflow-hidden border border-gray-200 bg-gray-50 text-gray-900"
12+
>
13+
<Drawer.Root swipeDirection="right" modal={false}>
14+
<Drawer.SwipeArea className="absolute inset-y-0 right-0 z-10 w-10 border-l-2 border-dashed border-blue-500 bg-blue-500/10 box-border">
15+
<span className="pointer-events-none absolute right-0 top-1/2 mr-2 -translate-y-1/2 -rotate-90 origin-center whitespace-nowrap text-xs font-semibold tracking-[0.12em] text-blue-600 uppercase">
16+
Swipe here
17+
</span>
18+
</Drawer.SwipeArea>
19+
<div className="flex min-h-[320px] flex-col items-center justify-center gap-3 px-4 text-center">
20+
<p className="text-base text-gray-600 text-center pr-12">
21+
Swipe from the right edge to open the drawer.
22+
</p>
23+
</div>
24+
<Drawer.Portal container={portalContainer}>
25+
<Drawer.Backdrop className="[--backdrop-opacity:0.2] [--bleed:3rem] dark:[--backdrop-opacity:0.7] absolute inset-0 min-h-dvh bg-black opacity-[calc(var(--backdrop-opacity)*(1-var(--drawer-swipe-progress,0)))] transition-opacity duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] data-[swiping]:duration-0 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 data-[ending-style]:duration-[calc(var(--drawer-swipe-strength,1)*400ms)] supports-[-webkit-touch-callout:none]:absolute" />
26+
<Drawer.Viewport className="[--viewport-padding:0px] supports-[-webkit-touch-callout:none]:[--viewport-padding:0.625rem] absolute inset-0 z-20 flex items-stretch justify-end p-[var(--viewport-padding)]">
27+
<Drawer.Popup className="[--bleed:3rem] supports-[-webkit-touch-callout:none]:[--bleed:0px] h-full w-[calc(20rem+3rem)] max-w-[calc(100vw-3rem+3rem)] -mr-[3rem] bg-gray-50 p-6 pr-[calc(1.5rem+3rem)] text-gray-900 outline outline-1 outline-gray-200 overflow-y-auto overscroll-contain touch-auto [transform:translateX(var(--drawer-swipe-movement-x))] transition-transform duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] data-[swiping]:select-none data-[ending-style]:[transform:translateX(calc(100%-var(--bleed)+var(--viewport-padding)))] data-[starting-style]:[transform:translateX(calc(100%-var(--bleed)+var(--viewport-padding)))] data-[ending-style]:duration-[calc(var(--drawer-swipe-strength,1)*400ms)] supports-[-webkit-touch-callout:none]:mr-0 supports-[-webkit-touch-callout:none]:w-[20rem] supports-[-webkit-touch-callout:none]:max-w-[calc(100vw-20px)] supports-[-webkit-touch-callout:none]:rounded-[10px] supports-[-webkit-touch-callout:none]:pr-6 dark:outline-gray-300">
28+
<Drawer.Content className="mx-auto w-full max-w-[32rem]">
29+
<Drawer.Title className="-mt-1.5 mb-1 text-lg font-medium">Library</Drawer.Title>
30+
<Drawer.Description className="mb-6 text-base text-gray-600">
31+
Swipe from the edge whenever you want to jump back into your playlists.
32+
</Drawer.Description>
33+
<div className="flex justify-end gap-4">
34+
<Drawer.Close className="flex h-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100">
35+
Close
36+
</Drawer.Close>
37+
</div>
38+
</Drawer.Content>
39+
</Drawer.Popup>
40+
</Drawer.Viewport>
41+
</Drawer.Portal>
42+
</Drawer.Root>
43+
</div>
44+
);
45+
}

0 commit comments

Comments
 (0)