@data-slot/drawer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +283 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +178 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.js +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# @data-slot/drawer
|
|
2
|
+
|
|
3
|
+
Accessible, unstyled drawers for vanilla JavaScript. The component supports modal and non-modal behavior, swipe-to-dismiss, a single snap point, detached triggers, and nested drawers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @data-slot/drawer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<div
|
|
15
|
+
id="filters-drawer"
|
|
16
|
+
data-slot="drawer"
|
|
17
|
+
data-swipe-direction="down"
|
|
18
|
+
data-snap-point="0.6"
|
|
19
|
+
>
|
|
20
|
+
<button data-slot="drawer-trigger">Edit filters</button>
|
|
21
|
+
|
|
22
|
+
<div data-slot="drawer-portal">
|
|
23
|
+
<div data-slot="drawer-backdrop" hidden></div>
|
|
24
|
+
<div data-slot="drawer-viewport" hidden>
|
|
25
|
+
<div data-slot="drawer-popup" hidden>
|
|
26
|
+
<div class="drawer-handle" aria-hidden="true"></div>
|
|
27
|
+
<div data-slot="drawer-content">
|
|
28
|
+
<h2 data-slot="drawer-title">Filters</h2>
|
|
29
|
+
<p data-slot="drawer-description">
|
|
30
|
+
Narrow the results without leaving this page.
|
|
31
|
+
</p>
|
|
32
|
+
<button data-slot="drawer-close">Apply filters</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<script type="module">
|
|
40
|
+
import { create } from "@data-slot/drawer";
|
|
41
|
+
|
|
42
|
+
create();
|
|
43
|
+
</script>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`drawer-popup` is the dialog surface. Set its full height or width with CSS. A single optional snap point sets how much of that surface is visible when open. `drawer-content` is an optional inner content or scroll container.
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### Initialization
|
|
51
|
+
|
|
52
|
+
#### `create(scope?)`
|
|
53
|
+
|
|
54
|
+
Find and bind every `[data-slot="drawer"]` in `scope` (the document by default).
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { create } from "@data-slot/drawer";
|
|
58
|
+
|
|
59
|
+
const drawers = create();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### `createDrawer(root, options?)`
|
|
63
|
+
|
|
64
|
+
Create one drawer controller. JavaScript options take precedence over data attributes.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { createDrawer } from "@data-slot/drawer";
|
|
68
|
+
|
|
69
|
+
const drawer = createDrawer(element, {
|
|
70
|
+
modal: true,
|
|
71
|
+
swipeDirection: "down",
|
|
72
|
+
snapPoint: 0.6,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
drawer.open();
|
|
76
|
+
drawer.setSnapPoint("320px");
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Slots
|
|
80
|
+
|
|
81
|
+
#### Runtime Slots
|
|
82
|
+
|
|
83
|
+
- `drawer` - Root element that manages the open state and receives drawer events.
|
|
84
|
+
- `drawer-trigger` - Optional button that opens or toggles the drawer; can target a root from outside it with `data-drawer-target`.
|
|
85
|
+
- `drawer-portal` - Optional wrapper moved to the portal destination while the drawer is open.
|
|
86
|
+
- `drawer-backdrop` - Optional backdrop that receives open and swipe state for styling.
|
|
87
|
+
- `drawer-viewport` - Optional interaction and measurement wrapper around the popup, typically styled as a fixed viewport.
|
|
88
|
+
- `drawer-popup` - Required dialog surface that receives focus management, swipe gestures, and snap-point positioning.
|
|
89
|
+
- `drawer-title` - Optional title used for the popup's `aria-labelledby`.
|
|
90
|
+
- `drawer-description` - Optional description used for the popup's `aria-describedby`.
|
|
91
|
+
- `drawer-close` - Optional button that closes the drawer.
|
|
92
|
+
- `drawer-provider` - Optional ancestor wrapper that coordinates the page indentation state for related drawers.
|
|
93
|
+
- `drawer-indent` - Optional page surface inside a provider; receives active state and drawer size/swipe variables for a receding-page effect.
|
|
94
|
+
- `drawer-indent-background` - Optional background behind that page surface; receives the same provider state and variables.
|
|
95
|
+
- `drawer-virtual-keyboard-provider` - Optional wrapper that enables the drawer viewport to adapt to the on-screen keyboard.
|
|
96
|
+
|
|
97
|
+
#### Style-only Slots
|
|
98
|
+
|
|
99
|
+
- `drawer-content` - Optional inner wrapper for content layout or scrolling inside the popup.
|
|
100
|
+
- `drawer-handle` - Optional visual drag handle. Swipe behavior belongs to the popup and does not require this slot.
|
|
101
|
+
|
|
102
|
+
#### Detached Triggers
|
|
103
|
+
|
|
104
|
+
Detached triggers can live outside the root when the root has an ID:
|
|
105
|
+
|
|
106
|
+
```html
|
|
107
|
+
<button
|
|
108
|
+
data-slot="drawer-trigger"
|
|
109
|
+
data-drawer-target="filters-drawer"
|
|
110
|
+
data-payload='{"source":"toolbar"}'
|
|
111
|
+
>
|
|
112
|
+
Filters
|
|
113
|
+
</button>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`data-payload` accepts JSON or a plain string. The parsed value is forwarded in change event details.
|
|
117
|
+
|
|
118
|
+
Use native `<button>` elements for triggers and close controls. Native `disabled`, `data-disabled`, and `aria-disabled="true"` block their activation.
|
|
119
|
+
|
|
120
|
+
#### Composition Slots
|
|
121
|
+
|
|
122
|
+
The package also recognizes `drawer-provider`, `drawer-indent`, `drawer-indent-background`, and `drawer-virtual-keyboard-provider`. They are ordinary DOM wrappers rather than React context providers.
|
|
123
|
+
|
|
124
|
+
Wrap related drawers in a provider. Put the page surface that should visually recede inside `drawer-indent` and its background behind that surface. A nested drawer remains a complete `drawer` root inside the parent popup.
|
|
125
|
+
|
|
126
|
+
```html
|
|
127
|
+
<div data-slot="drawer-provider">
|
|
128
|
+
<div data-slot="drawer-indent-background"></div>
|
|
129
|
+
<main data-slot="drawer-indent">...</main>
|
|
130
|
+
|
|
131
|
+
<div id="parent" data-slot="drawer">
|
|
132
|
+
...
|
|
133
|
+
<div data-slot="drawer-popup">
|
|
134
|
+
<div id="child" data-slot="drawer">
|
|
135
|
+
<button data-slot="drawer-trigger">Open details</button>
|
|
136
|
+
...
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Wrap the viewport inside `drawer-virtual-keyboard-provider` when form controls in that drawer need the viewport to follow the on-screen keyboard:
|
|
144
|
+
|
|
145
|
+
```html
|
|
146
|
+
<div data-slot="drawer">
|
|
147
|
+
<div data-slot="drawer-virtual-keyboard-provider">
|
|
148
|
+
<div data-slot="drawer-viewport">
|
|
149
|
+
<div data-slot="drawer-popup">...</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Data Attributes
|
|
156
|
+
|
|
157
|
+
Root attributes and their JavaScript equivalents are listed in [Options](#options).
|
|
158
|
+
|
|
159
|
+
Popup-specific attributes:
|
|
160
|
+
|
|
161
|
+
| Attribute | Type | Default | Description |
|
|
162
|
+
| --- | --- | --- | --- |
|
|
163
|
+
| `data-initial-focus` | boolean or selector | popup | Use `true` for the first focusable element, a selector for an explicit target, or `false` to skip automatic focus. |
|
|
164
|
+
| `data-final-focus` | boolean or selector | `true` | Choose the focus target after close, or set `false` to skip focus restoration. |
|
|
165
|
+
|
|
166
|
+
Portal-specific attributes:
|
|
167
|
+
|
|
168
|
+
| Attribute | Type | Default | Description |
|
|
169
|
+
| --- | --- | --- | --- |
|
|
170
|
+
| `data-container` | selector | `body` | Element that receives the portal. |
|
|
171
|
+
| `data-keep-mounted` | boolean | `false` | Keep portal content mounted while closed. |
|
|
172
|
+
|
|
173
|
+
### Options
|
|
174
|
+
|
|
175
|
+
| Option | Data attribute | Type | Default | Description |
|
|
176
|
+
| --- | --- | --- | --- | --- |
|
|
177
|
+
| `open` / `defaultOpen` | `data-default-open` | `boolean` | `false` | Initial open state. `open` and the attribute are initialization values, not controlled state. |
|
|
178
|
+
| `modal` | `data-modal` | `boolean \| "trap-focus"` | `true` | `true` makes outside content inert and locks scroll; `"trap-focus"` traps focus without making outside content inert. |
|
|
179
|
+
| `disablePointerDismissal` | `data-disable-pointer-dismissal` | `boolean` | `false` | Ignore outside pointer presses. |
|
|
180
|
+
| `closeOnEscape` | `data-close-on-escape` | `boolean` | `true` | Close when `Escape` is pressed. |
|
|
181
|
+
| `swipeDirection` | `data-swipe-direction` | `"down" \| "up" \| "left" \| "right"` | `"down"` | Direction used to dismiss the drawer. |
|
|
182
|
+
| `snapPoint` / `defaultSnapPoint` | `data-snap-point` / `data-default-snap-point` | `DrawerSnapPoint \| null` | `null` | Initial single open position. `snapPoint` takes precedence over the default. `null` uses the full CSS size. |
|
|
183
|
+
| `triggerId` / `defaultTriggerId` | `data-trigger-id` / `data-default-trigger-id` | `string \| null` | — | Initial detached trigger identifier. |
|
|
184
|
+
| `initialFocus` | popup `data-initial-focus` | `boolean \| string \| HTMLElement` | popup | Choose focus when the drawer opens. |
|
|
185
|
+
| `finalFocus` | popup `data-final-focus` | `boolean \| string \| HTMLElement` | trigger or previous focus | Choose focus when the drawer closes. |
|
|
186
|
+
| `keepMounted` | portal `data-keep-mounted` | `boolean` | `false` | Keep portal content mounted while closed. |
|
|
187
|
+
| `container` | portal `data-container` | `string \| HTMLElement` | `document.body` | Portal destination. |
|
|
188
|
+
| `onOpenChange` | — | `(open, details) => void` | — | Called before committing an open-state change. Call `details.cancel()` to stop it. |
|
|
189
|
+
| `onSnapPointChange` | — | `(snapPoint, details) => void` | — | Called before replacing the single snap point. Call `details.cancel()` to stop it. |
|
|
190
|
+
| `onOpenChangeComplete` | — | `(open) => void` | — | Called when the opening or closing transition completes. |
|
|
191
|
+
|
|
192
|
+
A snap point is a viewport fraction (`0` to `1`), a pixel number greater than `1`, or a string in `px` or `rem`. It is capped at the popup's CSS size. Only one open position is supported; arrays and sequential snap points are not supported. A swipe returns to that position or dismisses the drawer. Use `setSnapPoint()` or `drawer:set` to replace it at runtime; the value persists across close/open cycles. Options and data attributes are read at initialization.
|
|
193
|
+
|
|
194
|
+
### Controller
|
|
195
|
+
|
|
196
|
+
| Method or property | Description |
|
|
197
|
+
| --- | --- |
|
|
198
|
+
| `open(triggerId?)` | Open the drawer, optionally associating an identified trigger. |
|
|
199
|
+
| `close()` | Close the drawer. |
|
|
200
|
+
| `toggle()` | Toggle the open state. |
|
|
201
|
+
| `setSnapPoint(value)` | Replace the single open position; `null` restores the full CSS size. |
|
|
202
|
+
| `snapPoint` | Current snap point (readonly). |
|
|
203
|
+
| `unmount()` | Unmount closed portal content. |
|
|
204
|
+
| `isOpen` | Current open state (readonly). |
|
|
205
|
+
| `triggerId` | Trigger associated with the current open cycle (readonly). |
|
|
206
|
+
| `destroy()` | Remove listeners and restore DOM state. |
|
|
207
|
+
|
|
208
|
+
### Events
|
|
209
|
+
|
|
210
|
+
Serializable data attributes and DOM events make the component usable directly from Astro markup and across framework boundaries. JavaScript consumers can use the matching callback options.
|
|
211
|
+
|
|
212
|
+
#### Outbound Events
|
|
213
|
+
|
|
214
|
+
| Event | Detail | Cancelable | Description |
|
|
215
|
+
| --- | --- | --- | --- |
|
|
216
|
+
| `drawer:beforechange` | `{ open, reason, trigger, payload, originalEvent?, cancel(), preventUnmountOnClose() }` | yes | Fires before the open state changes. Call `event.preventDefault()` or `detail.cancel()` to cancel it. |
|
|
217
|
+
| `drawer:change` | `{ open, reason, trigger, payload, originalEvent?, cancel(), preventUnmountOnClose() }` | no | Fires after the open state changes. The methods have no effect after the change commits. |
|
|
218
|
+
| `drawer:change-complete` | `{ open }` | no | Fires after the opening or closing transition completes. |
|
|
219
|
+
| `drawer:beforesnapchange` | `{ snapPoint, reason, originalEvent?, cancel() }` | yes | Fires before replacing the single snap point. Cancel with `event.preventDefault()` or `detail.cancel()`. |
|
|
220
|
+
| `drawer:snapchange` | `{ snapPoint, reason, originalEvent?, cancel() }` | no | Fires after the snap point changes. |
|
|
221
|
+
|
|
222
|
+
Reasons are `trigger-press`, `close-press`, `outside-press`, `escape-key`, `focus-out`, `imperative-action`, `swipe`, or `none`.
|
|
223
|
+
|
|
224
|
+
Open-state requests made inside change listeners or `onOpenChange` run after the current change finishes. If a callback makes several requests, the latest state and trigger win. Requesting the same state and trigger as the current change does not repeat its callbacks; use `details.cancel()` to cancel that change.
|
|
225
|
+
|
|
226
|
+
`preventUnmountOnClose()` on `drawer:beforechange` keeps the portal mounted for the current close. This is useful when an application needs to coordinate its own exit animation.
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
root.addEventListener("drawer:beforechange", (event) => {
|
|
230
|
+
if (!event.detail.open && formIsDirty) event.preventDefault();
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### Inbound Events
|
|
235
|
+
|
|
236
|
+
| Event | Detail | Description |
|
|
237
|
+
| --- | --- | --- |
|
|
238
|
+
| `drawer:set` | `{ open?, snapPoint?, triggerId? }` | Set one or more state values. |
|
|
239
|
+
| `drawer:open` | — | Open the drawer. |
|
|
240
|
+
| `drawer:close` | — | Close the drawer. |
|
|
241
|
+
| `drawer:toggle` | — | Toggle the drawer. |
|
|
242
|
+
| `drawer:unmount` | — | Unmount closed portal content. |
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
root.dispatchEvent(new CustomEvent("drawer:set", {
|
|
246
|
+
detail: { open: true, snapPoint: 0.6, triggerId: "toolbar" },
|
|
247
|
+
}));
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Styling
|
|
251
|
+
|
|
252
|
+
Root, portal, backdrop, viewport, and popup expose `data-state="open|closed"`, `data-open`, `data-closed`, and `data-swipe-direction`. The active trigger gets `data-popup-open`. During gestures the viewport, backdrop, and popup expose `data-swiping`. Nested popups additionally expose `data-nested`, `data-nested-drawer-open`, and `data-nested-swiping`; indentation parts expose `data-active` or `data-inactive`. A keyboard-aware viewport exposes `data-keyboard-open` while the on-screen keyboard overlaps it.
|
|
253
|
+
|
|
254
|
+
Popup and backdrop expose `data-starting-style` and `data-ending-style` for entry and exit transitions. The runtime waits for exit transitions before hiding or restoring the portal.
|
|
255
|
+
|
|
256
|
+
CSS variables include:
|
|
257
|
+
|
|
258
|
+
- `--drawer-height` and `--drawer-width`
|
|
259
|
+
- `--drawer-snap-point-offset` (signed offset for the configured direction)
|
|
260
|
+
- `--drawer-swipe-amount-x` and `--drawer-swipe-amount-y`
|
|
261
|
+
- `--drawer-swipe-progress` and release `--drawer-swipe-strength` (`0.1` to `1`)
|
|
262
|
+
- `--nested-drawers` and `--drawer-front-height`
|
|
263
|
+
- `--drawer-keyboard-inset` on `drawer-viewport`
|
|
264
|
+
|
|
265
|
+
```css
|
|
266
|
+
[data-slot="drawer-backdrop"] {
|
|
267
|
+
opacity: calc(1 - var(--drawer-swipe-progress, 0));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
[data-slot="drawer-popup"] {
|
|
271
|
+
transform: translateY(calc(var(--drawer-snap-point-offset, 0px) + var(--drawer-swipe-amount-y, 0px)));
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Accessibility
|
|
276
|
+
|
|
277
|
+
The runtime sets `role="dialog"`, `aria-labelledby`, and `aria-describedby` on the popup, adds `aria-modal="true"` in fully modal mode, and maintains `aria-expanded`, `aria-controls`, and `aria-haspopup="dialog"` on triggers. Modal and `"trap-focus"` drawers trap focus and restore it after close; fully modal drawers also make outside content inert and lock body scroll. `Escape`, close controls, allowed outside presses, and a swipe in the configured direction dismiss the drawer.
|
|
278
|
+
|
|
279
|
+
### Behavior
|
|
280
|
+
|
|
281
|
+
#### Base UI Adaptation
|
|
282
|
+
|
|
283
|
+
The DOM parts and interaction model follow Base UI Drawer where they translate to static HTML. React-only facilities such as JSX render props, React context, controlled `open` values, and React animation internals are represented by ordinary wrappers, data attributes, native pointer gestures, controller methods, callbacks, and cancelable custom events. Initial values in HTML are read once; dispatch `drawer:set` or call the controller to update live state.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@data-slot/core`);function t(e){if(typeof e==`number`)return Number.isFinite(e)&&e>=0?e:null;if(typeof e!=`string`||!e.trim())return null;let t=e.trim();if(/^\d*\.?\d+(px|rem)$/.test(t))return t;let n=Number(t);return Number.isFinite(n)&&n>=0?n:null}function n(e,t,n){return typeof e==`number`?e<=1?e*t:e:parseFloat(e)*(e.endsWith(`rem`)?n:1)}function r(e,t,n,r){for(let i=e;i&&t.contains(i);i=i.parentElement){let e=i,t=e.ownerDocument.defaultView.getComputedStyle(e),a=n?t.overflowX:t.overflowY;if(!/(auto|scroll)/.test(a))continue;let o=n?e.scrollLeft:e.scrollTop,s=n?e.scrollWidth-e.clientWidth:e.scrollHeight-e.clientHeight;if(s>1&&(r>0?o>0:o<s-1))return!0}return!1}function i(t){let{popup:n,direction:i}=t,a=i===`left`||i===`right`,o=i===`up`||i===`left`?-1:1,s=e=>a?e.deltaX:e.deltaY;return(0,e.createSwipeGesture)({element:n,axes:[a?`x`:`y`],start:(e,r)=>!t.enabled()||r.closest(`[data-swipe-ignore],input,textarea,select,button,a,[contenteditable="true"]`)?null:r.closest(`[data-slot="drawer-popup"]`)===n?n:null,lock:(e,t,i)=>!r(i,n,a,s(t)),active:t.enabled,move:(e,n)=>t.move(s(n)*o,n.event),release:(e,n)=>{let r=s(n)*o;t.release(r,r/Math.max(1,n.duration),n.event),t.reset()},reset:(e,n)=>t.reset(n)}).destroy}function a(t){let n=t.ownerDocument,r=n.defaultView,i=r.visualViewport,a=t.style.getPropertyValue(`--drawer-keyboard-inset`),o=t.hasAttribute(`data-keyboard-open`),s=0,c=null,l=()=>{if(!c)return;let{element:e,paddingBottom:t,scrollPaddingBottom:n,overflowAnchor:r}=c;Object.assign(e.style,{paddingBottom:t,scrollPaddingBottom:n,overflowAnchor:r}),c=null},u=()=>{s=0;let e=n.activeElement,a=e?.matches(`textarea, input:not([type]), input[type="text"], input[type="email"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="url"], [contenteditable]:not([contenteditable="false"])`),o=!!i&&(i.scale===void 0||i.scale===1)&&r.innerHeight-i.height>60&&!!a&&t.contains(e),u=i?Math.min(r.innerHeight,i.height+i.offsetTop):r.innerHeight;if(t.style.setProperty(`--drawer-keyboard-inset`,`${o?Math.max(0,r.innerHeight-u):0}px`),t.toggleAttribute(`data-keyboard-open`,o),!o||!e){l();return}let d=e.parentElement;for(;d&&t.contains(d)&&!/(auto|scroll)/.test(r.getComputedStyle(d).overflowY);)d=d.parentElement;if(!d||!t.contains(d)){l();return}c?.element!==d&&l();let f=e.getBoundingClientRect(),p=d.getBoundingClientRect(),m=Math.min(u,p.bottom)-16,h=Math.max(i?.offsetTop??0,p.top)+16,g=f.bottom-m;g>0?(c||={element:d,paddingBottom:d.style.paddingBottom,scrollPaddingBottom:d.style.scrollPaddingBottom,overflowAnchor:d.style.overflowAnchor,basePadding:parseFloat(r.getComputedStyle(d).paddingBottom)||0},d.style.overflowAnchor=`none`,d.style.paddingBottom=`${c.basePadding+Math.max(0,p.bottom-u)+f.height+48}px`,d.style.scrollPaddingBottom=`16px`,d.scrollTop+=g):f.top<h&&(d.scrollTop-=h-f.top)},d=()=>{s||=r.requestAnimationFrame(u)};u();let f=[(0,e.on)(n,`focusin`,d),(0,e.on)(n,`focusout`,d),(0,e.on)(r,`resize`,d)];return i&&f.push((0,e.on)(i,`resize`,d),(0,e.on)(i,`scroll`,d)),()=>{f.forEach(e=>e()),r.cancelAnimationFrame(s),l(),a?t.style.setProperty(`--drawer-keyboard-inset`,a):t.style.removeProperty(`--drawer-keyboard-inset`),t.toggleAttribute(`data-keyboard-open`,o)}}const o=new WeakMap;function s(e){let t=null;for(let n of e)n.open&&(!t||n.order>t.order)&&(t=n);return t}function c(e){let{popup:t,swipe:n}=e,r=e.front??e;t.toggleAttribute(`data-nested-swiping`,e.descendants.some(e=>e.open&&e.swipe!==null));for(let i of[t,e.parts.backdrop,e.parts.viewport])i&&(i.toggleAttribute(`data-swiping`,n!==null),i.style.setProperty(`--drawer-swipe-progress`,String((i===t?r.swipe:n)?.progress??0)));t.style.setProperty(`--drawer-swipe-amount-x`,`${n?.x??0}px`),t.style.setProperty(`--drawer-swipe-amount-y`,`${n?.y??0}px`)}function l(e){e.popup.toggleAttribute(`data-nested`,!!e.parent),e.popup.toggleAttribute(`data-nested-drawer-open`,e.front!==null),e.popup.style.setProperty(`--nested-drawers`,String(e.descendants.filter(e=>e.open).length)),e.popup.style.setProperty(`--drawer-front-height`,`${(e.front??e).height}px`),c(e)}function u(e){let{front:t}=e;for(let n of e.indents)n.toggleAttribute(`data-active`,!!t),n.toggleAttribute(`data-inactive`,!t),n.style.setProperty(`--drawer-swipe-progress`,String(t?.swipe?.progress??0)),n.style.setProperty(`--drawer-height`,`${t?.height??0}px`)}function d(e){e.front=s(e.entries),e.indents=Array.from(e.element.querySelectorAll(`[data-slot="drawer-indent"],[data-slot="drawer-indent-background"]`)).filter(t=>t.closest(`[data-slot="drawer-provider"]`)===e.element),u(e)}function f(e){for(let t of e.entries.values())t.ancestors=[],t.descendants=[];for(let t of e.entries.values())for(let n=t.parent&&e.entries.get(t.parent);n;n=n.parent&&e.entries.get(n.parent))t.ancestors.push(n),n.descendants.push(t);for(let t of e.entries.values())t.front=s(t.descendants),l(t);for(let[t,n]of e.providers)d(n),n.entries.size||e.providers.delete(t)}function p(t,n,r={}){let i=t.ownerDocument,a=o.get(i);a||(a={entries:new Map,providers:new Map,openOrder:0},o.set(i,a));let p=null;for(let n of a.entries.values())(0,e.containsWithPortals)(n.popup,t)&&(!p||(0,e.containsWithPortals)(p.popup,n.root))&&(p=n);let m=t.closest(`[data-slot="drawer-provider"]`)??p?.provider?.element,h=null;m&&(h=a.providers.get(m)??{element:m,entries:new Set,indents:[],front:null},a.providers.set(m,h));let g={root:t,popup:n,parts:r,parent:t.parentElement?.closest(`[data-slot="drawer"]`)??p?.root??null,ancestors:[],descendants:[],provider:h,open:!1,swipe:null,order:0,height:n.getBoundingClientRect().height,front:null};a.entries.set(t,g),h?.entries.add(g),f(a);let _=()=>{l(g),g.ancestors.forEach(l)},v=!1;return{get parent(){return g.parent},setOpen(e){if(!(v||e===g.open)){e&&(g.order=++a.openOrder),g.open=e,e||(g.swipe=null);for(let e of g.ancestors)e.front=s(e.descendants);_(),h&&d(h)}},setSwipe(e){if(!v&&(g.swipe=e,c(g),g.ancestors.forEach(c),h?.front===g))for(let t of h.indents)t.style.setProperty(`--drawer-swipe-progress`,String(e?.progress??0))},refresh(e){v||g.height===e||(g.height=e,_(),h?.front===g&&u(h))},destroy(){v||(v=!0,a.entries.delete(t),h?.entries.delete(g),f(a),a.entries.size||o.delete(i))}}}const m=`@data-slot/drawer`,h=t=>t.matches(`:disabled`)||(0,e.getDataBool)(t,`disabled`)===!0||t.getAttribute(`aria-disabled`)===`true`,g=(e,t)=>Array.from(e.querySelectorAll(`[data-slot="drawer-${t}"]`)).filter(t=>t.closest(`[data-slot="drawer"]`)===e),_=(e,t)=>{try{return e.querySelector(t)}catch{return null}},v=(t,n)=>{let r=(0,e.getDataString)(t,n);return r===`false`?!1:r===`true`||r===``?!0:r},ee=e=>{let t=e?.getAttribute(`data-payload`);if(t!=null)try{return JSON.parse(t)}catch{return t}};function y(r,o={}){let s=(0,e.reuseRootBinding)(r,m,`[@data-slot/drawer] Drawer already initialized; returning its controller.`);if(s)return s;let c=g(r,`popup`)[0];if(!c)throw Error(`Drawer requires drawer-popup slot`);let l=r.ownerDocument,u=l.defaultView,d=g(r,`portal`)[0],f=g(r,`backdrop`)[0],y=g(r,`viewport`)[0],te=g(r,`title`)[0],ne=g(r,`description`)[0],b=g(r,`trigger`);if(r.id)for(let e of l.querySelectorAll(`[data-drawer-target]`))e.getAttribute(`data-drawer-target`)?.replace(/^#/,``)===r.id&&e.dataset.slot===`drawer-trigger`&&!b.includes(e)&&b.push(e);let re=(0,e.getDataString)(r,`modal`),x=o.modal??(re===`trap-focus`?`trap-focus`:re!==`false`),ie=!(o.disablePointerDismissal??(0,e.getDataBool)(r,`disablePointerDismissal`)??!1),ae=o.closeOnEscape??(0,e.getDataBool)(r,`closeOnEscape`)??!0,oe=o.swipeDirection??(0,e.getDataString)(r,`swipeDirection`),S=[`up`,`down`,`left`,`right`].includes(oe??``)?oe:`down`,C=S===`left`||S===`right`,w=S===`left`||S===`up`?-1:1,T=t(o.snapPoint===void 0?o.defaultSnapPoint===void 0?(0,e.getDataString)(r,`snapPoint`)??(0,e.getDataString)(r,`defaultSnapPoint`):o.defaultSnapPoint:o.snapPoint),E=o.initialFocus??v(c,`initialFocus`),se=o.finalFocus??v(c,`finalFocus`),ce=o.keepMounted??(d?(0,e.getDataBool)(d,`keepMounted`):void 0)??!1,le=o.container??(d?(0,e.getDataString)(d,`container`):void 0),ue=typeof le==`string`?_(l,le):le,de=d??y??c,D=(0,e.createPortalLifecycle)({content:de,root:r}),O=[],k=!1,A=!1,fe=!1,j=!1,M=!1,N,pe=y&&(r.closest(`[data-slot="drawer-virtual-keyboard-provider"]`)||g(r,`virtual-keyboard-provider`).length),P=null,me=o.triggerId??o.defaultTriggerId??(0,e.getDataString)(r,`triggerId`)??(0,e.getDataString)(r,`defaultTriggerId`),F=b.find(e=>e.id===me)??null,I=0,L=-1,he=0,R=new Set,z=e=>{let t=u.requestAnimationFrame(()=>{R.delete(t),A||e()});R.add(t)},ge=[r,d,f,y,c].filter(e=>!!e),_e=c.getAttribute(`tabindex`);_e===null&&(c.tabIndex=-1),c.setAttribute(`role`,`dialog`),x===!0?(0,e.setAria)(c,`modal`,!0):c.removeAttribute(`aria-modal`),(0,e.ensureId)(c,`drawer-popup`),(0,e.linkLabelledBy)(c,te??null,ne??null),f&&(f.setAttribute(`aria-hidden`,`true`),f.setAttribute(`role`,`presentation`));for(let e of[...b,...g(r,`close`)])e.tagName===`BUTTON`&&!e.hasAttribute(`type`)&&e.setAttribute(`type`,`button`);for(let t of b)(0,e.ensureId)(t,`drawer-trigger`),t.setAttribute(`aria-haspopup`,`dialog`),t.setAttribute(`aria-controls`,c.id),(0,e.setAria)(t,`expanded`,!1);let B=p(r,c,{backdrop:f,viewport:y}),V=()=>{for(let e of ge)e.setAttribute(`data-state`,k?`open`:`closed`),e.toggleAttribute(`data-open`,k),e.toggleAttribute(`data-closed`,!k),e.setAttribute(`data-swipe-direction`,S);for(let t of b)(0,e.setAria)(t,`expanded`,k&&t===F),t.toggleAttribute(`data-popup-open`,k&&t===F);B.setOpen(k)},H=()=>C?c.getBoundingClientRect().width||u.innerWidth:c.getBoundingClientRect().height||u.innerHeight,U=()=>T===null?H():Math.min(H(),n(T,C?y?.clientWidth||l.documentElement.clientWidth||u.innerWidth:y?.clientHeight||l.documentElement.clientHeight||u.innerHeight,parseFloat(u.getComputedStyle(l.documentElement).fontSize)||16)),ve=()=>Math.max(0,H()-U()),W=()=>{let e=c.getBoundingClientRect();c.style.setProperty(`--drawer-height`,`${e.height}px`),c.style.setProperty(`--drawer-width`,`${e.width}px`),c.style.setProperty(`--drawer-snap-point-offset`,`${ve()*w}px`),T===null?r.removeAttribute(`data-snap-point`):r.setAttribute(`data-snap-point`,String(T)),B.refresh(e.height)},G=t=>(0,e.containsWithPortals)(c,t),ye=e=>typeof e==`string`?_(l,e):typeof e==`object`?e:null,be=()=>{!k||!J.isTopmost||E===!1||(0,e.focusElement)(ye(E)??(E===!0?(0,e.getAutofocusOrFirstFocusable)(c):null)??c)},xe=()=>{let t=ye(se)??F??P;P=null,se!==!1&&t?.isConnected&&(0,e.focusElement)(t)},Se=t=>{(0,e.emit)(r,`drawer:change-complete`,{open:t}),o.onOpenChangeComplete?.(t)},Ce=e=>{if(!k||I!==e||L===e)return;let t=Te.flatMap(e=>typeof e.getAnimations==`function`?e.getAnimations():[]),n=()=>{!A&&k&&I===e&&L!==e&&(L=e,Se(!0))};t.length?Promise.allSettled(t.map(e=>e.finished)).then(n):n()},K=()=>{k||M||A||(c.hidden=!0,f&&(f.hidden=!0),y&&(y.hidden=!0),d&&(d.hidden=!0),ce||(D.restore(),fe=!1))},we=()=>{k||A||--he>0||(K(),Se(!1))},Te=[c,f].filter(e=>!!e),q=Te.map(t=>(0,e.createPresenceLifecycle)({element:t,win:u,onExitComplete:we})),Ee=(n,i=`imperative-action`,a)=>{let s=t(n);if(A||n!==null&&s===null||s===T)return;let c=!1,l={snapPoint:s,reason:i,originalEvent:a,cancel:()=>{c=!0}},u=new CustomEvent(`drawer:beforesnapchange`,{bubbles:!0,cancelable:!0,detail:l});r.dispatchEvent(u),!(u.defaultPrevented||c||A)&&(o.onSnapPointChange?.(s,l),!(c||A)&&(T=s,W(),(0,e.emit)(r,`drawer:snapchange`,l)))},J=(0,e.createModalStackItem)({content:c,overlay:f,cssVarPrefix:`drawer`,isolateOutside:x===!0,onTabKeydown:t=>{if(x===!1||!k)return;let n=(0,e.getTabbables)(c),r=l.activeElement;if(G(r)&&!c.contains(r))return;if(!n.length){t.preventDefault(),(0,e.focusElement)(c);return}let i=n[0],a=n.at(-1);(!n.includes(r)||!t.shiftKey&&r===a||t.shiftKey&&r===i)&&(t.preventDefault(),(0,e.focusElement)(t.shiftKey?a:i))}}),De=({next:t,reason:n,originalEvent:i,trigger:s})=>{if(A)return;let u=k&&t&&s!==F;if(k===t&&!u)return;let p=!1,m=!1,h={open:t,reason:n,trigger:s,payload:ee(s),originalEvent:i,cancel:()=>{p=!0},preventUnmountOnClose:()=>{m=!0}},g=new CustomEvent(`drawer:beforechange`,{bubbles:!0,cancelable:!0,detail:h});if(r.dispatchEvent(g),g.defaultPrevented||p||A||(o.onOpenChange?.(t,h),p||A))return;if(u){F=s,V(),(0,e.emit)(r,`drawer:change`,h);return}let _=++I;if(F=s,k=t,M=!t&&m,t){P=l.activeElement,q.forEach(e=>e.enter()),fe||=(D.mount(),ue&&ue.appendChild(de),!0);for(let e of[d,y,f,c])e&&(e.hidden=!1);J.open(),x===!0&&((0,e.lockScroll)(),j=!0),pe&&y&&(N=a(y)),V(),W(),z(be),z(()=>z(()=>Ce(_)))}else{J.close(),N?.(),N=void 0,j&&=((0,e.unlockScroll)(),!1),V(),he=q.length,q.forEach(e=>e.exit());let t=i?.target;se===void 0&&(n===`focus-out`||n===`outside-press`&&x===!1&&t?.closest?.(`button,input,textarea,select,a[href],[tabindex]`))?P=null:xe()}(0,e.emit)(r,`drawer:change`,h)},Y=null,X=null,Z=(e,t=`imperative-action`,n,r=F)=>{if(!A){if(Y?.next===e&&Y.trigger===r){X=null;return}if(X={next:e,reason:t,originalEvent:n,trigger:r},!Y)try{for(;X&&!A;)Y=X,X=null,De(Y)}finally{Y=null,X=null}}};O.push((0,e.createDismissLayer)({root:r,isOpen:()=>k,closeOnEscape:ae,closeOnClickOutside:ie,isInside:e=>G(e)||b.some(t=>t.contains(e)),onDismiss:({reason:e,originalEvent:t})=>{e===`outside-press`&&t.button!==0||Z(!1,e,t)}}));for(let t of b)O.push((0,e.on)(t,`click`,e=>{e.defaultPrevented||h(t)||Z(!k||F!==t,`trigger-press`,e,t)}));for(let t of g(r,`close`))O.push((0,e.on)(t,`click`,e=>{!e.defaultPrevented&&!h(t)&&Z(!1,`close-press`,e)}));O.push((0,e.on)(l,`focusin`,t=>{!k||!J.isTopmost||G(t.target)||(x===!1?ie&&!b.includes(t.target)&&Z(!1,`focus-out`,t):(0,e.focusElement)((0,e.getAutofocusOrFirstFocusable)(c)??c))}));let Oe=()=>B.setSwipe(null);if(O.push(i({popup:c,direction:S,enabled:()=>k&&J.isTopmost,move:e=>{let t=Math.max(-ve(),Math.min(U(),e)),n=Math.min(1,Math.max(0,t/Math.max(1,U())));B.setSwipe({progress:n,x:C?t*w:0,y:C?0:t*w})},release:(e,t,n)=>{c.style.setProperty(`--drawer-swipe-strength`,String(Math.max(.1,Math.min(1,1/Math.max(1,Math.abs(t)))))),(e>U()*.35||t>.5&&e>12)&&Z(!1,`swipe`,n)},reset:Oe})),O.push((0,e.on)(u,`resize`,W)),u.ResizeObserver!==void 0){let e=new u.ResizeObserver(W);e.observe(c),O.push(()=>e.disconnect())}let Q=(t,n)=>O.push((0,e.on)(r,t,e=>{e.target===r&&n(e)}));Q(`drawer:set`,e=>{let t=e.detail;if(!t||typeof t!=`object`)return;let n=typeof t.triggerId==`string`?b.find(e=>e.id===t.triggerId)??F:F;`snapPoint`in t&&Ee(t.snapPoint,`imperative-action`,e),typeof t.open==`boolean`&&Z(t.open,`imperative-action`,e,n)}),Q(`drawer:open`,e=>Z(!0,`imperative-action`,e)),Q(`drawer:close`,e=>Z(!1,`imperative-action`,e)),Q(`drawer:toggle`,e=>Z(!k,`imperative-action`,e)),Q(`drawer:unmount`,()=>{M=!1,K()}),c.style.setProperty(`--drawer-swipe-strength`,`1`),V(),W(),Oe(),K();let $={open:e=>Z(!0,`imperative-action`,void 0,b.find(t=>t.id===e)??F),close:()=>Z(!1),toggle:()=>Z(!k),setSnapPoint:e=>Ee(e),unmount:()=>{M=!1,K()},get isOpen(){return k},get snapPoint(){return T},get triggerId(){return F?.id??null},destroy(){if(A)return;let t=k&&G(l.activeElement);A=!0,k=!1,I++,R.forEach(e=>u.cancelAnimationFrame(e)),R.clear(),q.forEach(e=>e.cleanup()),J.destroy(),N?.(),j&&=((0,e.unlockScroll)(),!1),O.forEach(e=>e()),V(),B.destroy();for(let e of[c,f,y,d])e&&(e.hidden=!0);D.cleanup(),_e===null&&c.removeAttribute(`tabindex`),t&&xe(),(0,e.clearRootBinding)(r,m,$)}};return(0,e.setRootBinding)(r,m,$),(o.open??o.defaultOpen??(0,e.getDataBool)(r,`defaultOpen`)??!1)&&Z(!0,`none`),$}function te(t=document){return(0,e.getRoots)(t,`drawer`).filter(t=>!(0,e.hasRootBinding)(t,m)).map(e=>y(e))}exports.create=te,exports.createDrawer=y;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
//#region src/gestures.d.ts
|
|
2
|
+
/** Direction in which a swipe dismisses the drawer. */
|
|
3
|
+
type DrawerSwipeDirection = 'down' | 'up' | 'left' | 'right';
|
|
4
|
+
/**
|
|
5
|
+
* A single visible open size: a viewport fraction from `0` to `1` (inclusive),
|
|
6
|
+
* a pixel number greater than `1`, or a non-negative `px`/`rem` string.
|
|
7
|
+
* Numeric strings are normalized to numbers. The visible size is capped at the
|
|
8
|
+
* popup's CSS size; pass `null` to the options/controller to use its full size.
|
|
9
|
+
*/
|
|
10
|
+
type DrawerSnapPoint = number | string;
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/index.d.ts
|
|
13
|
+
/** Source of a state change. Initial opening uses `none`; controller methods use `imperative-action`. */
|
|
14
|
+
type DrawerChangeReason = 'trigger-press' | 'close-press' | 'outside-press' | 'escape-key' | 'focus-out' | 'imperative-action' | 'swipe' | 'none';
|
|
15
|
+
/** Details shared by `onOpenChange`, `drawer:beforechange`, and `drawer:change`. */
|
|
16
|
+
interface DrawerChangeDetails {
|
|
17
|
+
/** Requested open state; available before the controller commits the change. */
|
|
18
|
+
open: boolean;
|
|
19
|
+
/** Interaction or API call that requested the change. */
|
|
20
|
+
reason: DrawerChangeReason;
|
|
21
|
+
/** Trigger associated with this request, or `null` when none has been selected. */
|
|
22
|
+
trigger: HTMLElement | null;
|
|
23
|
+
/** Trigger's `data-payload`, parsed as JSON when valid, otherwise a string; absent values are `undefined`. */
|
|
24
|
+
payload: unknown;
|
|
25
|
+
/** DOM event that initiated the request, if any. */
|
|
26
|
+
originalEvent?: Event;
|
|
27
|
+
/** Cancel synchronously in `drawer:beforechange` or `onOpenChange`; has no effect after commit. */
|
|
28
|
+
cancel(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Keep the closing content mounted and unhidden until `controller.unmount()`.
|
|
31
|
+
* Call synchronously in `drawer:beforechange` or `onOpenChange` for a close request.
|
|
32
|
+
* The drawer still closes logically and releases its modal effects and focus.
|
|
33
|
+
*/
|
|
34
|
+
preventUnmountOnClose(): void;
|
|
35
|
+
}
|
|
36
|
+
/** Details shared by `onSnapPointChange`, `drawer:beforesnapchange`, and `drawer:snapchange`. */
|
|
37
|
+
interface DrawerSnapChangeDetails {
|
|
38
|
+
/** Requested open position, or `null` to use the popup's full CSS size. */
|
|
39
|
+
snapPoint: DrawerSnapPoint | null;
|
|
40
|
+
/** Source of the snap-point request. */
|
|
41
|
+
reason: DrawerChangeReason;
|
|
42
|
+
/** DOM event that initiated the request, if any. */
|
|
43
|
+
originalEvent?: Event;
|
|
44
|
+
/** Cancel synchronously in `drawer:beforesnapchange` or `onSnapPointChange`; has no effect after commit. */
|
|
45
|
+
cancel(): void;
|
|
46
|
+
}
|
|
47
|
+
/** Initialization options. JavaScript values take precedence over corresponding data attributes. */
|
|
48
|
+
interface DrawerOptions {
|
|
49
|
+
/** Initial open state, overriding `defaultOpen`. Read once; use the controller for later changes. */
|
|
50
|
+
open?: boolean;
|
|
51
|
+
/** Initial open state when `open` is omitted (default: `data-default-open`, then `false`). */
|
|
52
|
+
defaultOpen?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* `true` traps focus, makes outside content inert, and locks scrolling (default).
|
|
55
|
+
* `"trap-focus"` traps focus without inertness or scroll locking; `false` is non-modal.
|
|
56
|
+
* Also configurable with root `data-modal`.
|
|
57
|
+
*/
|
|
58
|
+
modal?: boolean | 'trap-focus';
|
|
59
|
+
/** Ignore outside pointer presses and non-modal focus-out dismissal (default: `false`). */
|
|
60
|
+
disablePointerDismissal?: boolean;
|
|
61
|
+
/** Allow Escape to dismiss the drawer (default: `true`). */
|
|
62
|
+
closeOnEscape?: boolean;
|
|
63
|
+
/** Direction of the dismissal swipe (default: `"down"`). */
|
|
64
|
+
swipeDirection?: DrawerSwipeDirection;
|
|
65
|
+
/** Initial single open position, overriding `defaultSnapPoint`; `null` uses the full CSS size. */
|
|
66
|
+
snapPoint?: DrawerSnapPoint | null;
|
|
67
|
+
/** Fallback initial position, before root snap-point attributes (default: `null`). Read once. */
|
|
68
|
+
defaultSnapPoint?: DrawerSnapPoint | null;
|
|
69
|
+
/** Initial associated trigger ID, taking precedence over `defaultTriggerId` when non-null. */
|
|
70
|
+
triggerId?: string | null;
|
|
71
|
+
/** Fallback initial trigger ID, before root trigger-ID attributes. Defaults to no associated trigger. */
|
|
72
|
+
defaultTriggerId?: string | null;
|
|
73
|
+
/**
|
|
74
|
+
* Focus target on open: an element or document selector, `true` for autofocus/first
|
|
75
|
+
* focusable content, or `false` to skip initial focus. Defaults to the popup itself.
|
|
76
|
+
* Also configurable with popup `data-initial-focus`.
|
|
77
|
+
*/
|
|
78
|
+
initialFocus?: boolean | string | HTMLElement;
|
|
79
|
+
/**
|
|
80
|
+
* Focus target on close: an element or document selector, `true` for the associated
|
|
81
|
+
* trigger/previous focus, or `false` to skip restoration. When omitted, defaults to
|
|
82
|
+
* trigger/previous focus but preserves outside focus after focus-out dismissal or
|
|
83
|
+
* a non-modal outside press on a focusable control. Also uses popup `data-final-focus`.
|
|
84
|
+
*/
|
|
85
|
+
finalFocus?: boolean | string | HTMLElement;
|
|
86
|
+
/** Keep content at its portal destination while closed and hidden (default: `false`; portal `data-keep-mounted`). */
|
|
87
|
+
keepMounted?: boolean;
|
|
88
|
+
/** Portal destination element or document selector (default: `document.body`; portal `data-container`). */
|
|
89
|
+
container?: string | HTMLElement;
|
|
90
|
+
/**
|
|
91
|
+
* Called before committing an open-state or active-trigger change, after an uncanceled
|
|
92
|
+
* `drawer:beforechange`. Call `details.cancel()` to stop the change.
|
|
93
|
+
*/
|
|
94
|
+
onOpenChange?: (open: boolean, details: DrawerChangeDetails) => void;
|
|
95
|
+
/** Called after the opening/closing transition finishes; also emits `drawer:change-complete`. */
|
|
96
|
+
onOpenChangeComplete?: (open: boolean) => void;
|
|
97
|
+
/** Called before replacing the snap point, after an uncanceled `drawer:beforesnapchange`; cancel with `details.cancel()`. */
|
|
98
|
+
onSnapPointChange?: (snapPoint: DrawerSnapPoint | null, details: DrawerSnapChangeDetails) => void;
|
|
99
|
+
}
|
|
100
|
+
/** Imperative drawer state and lifecycle controls. State-change requests can be canceled by listeners. */
|
|
101
|
+
interface DrawerController {
|
|
102
|
+
/** Open or switch the active trigger. An omitted or unknown trigger ID retains the current trigger. */
|
|
103
|
+
open(triggerId?: string): void;
|
|
104
|
+
/** Request closing; content stays visible until its exit transition finishes. */
|
|
105
|
+
close(): void;
|
|
106
|
+
/** Toggle the open state while retaining the associated trigger. */
|
|
107
|
+
toggle(): void;
|
|
108
|
+
/** Replace the single open position across close/open cycles; `null` restores full CSS size. Invalid values are ignored. */
|
|
109
|
+
setSnapPoint(point: DrawerSnapPoint | null): void;
|
|
110
|
+
/** Finish a close held by `preventUnmountOnClose()`. Honors `keepMounted` and does not close an open drawer. */
|
|
111
|
+
unmount(): void;
|
|
112
|
+
/** Current committed open state, independent of enter/exit animation progress. */
|
|
113
|
+
readonly isOpen: boolean;
|
|
114
|
+
/** Current normalized snap point, or `null` for the popup's full CSS size. */
|
|
115
|
+
readonly snapPoint: DrawerSnapPoint | null;
|
|
116
|
+
/** Associated trigger ID, retained after close, or `null` when none is selected. */
|
|
117
|
+
readonly triggerId: string | null;
|
|
118
|
+
/** Remove listeners and observers, release modal effects, hide content, and restore portal placement. Allows rebinding. */
|
|
119
|
+
destroy(): void;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Create a drawer controller for a root element.
|
|
123
|
+
*
|
|
124
|
+
* Canonical markup (supply CSS for positioning, sizing, and transitions):
|
|
125
|
+
* ```html
|
|
126
|
+
* <div id="filters-drawer" data-slot="drawer" data-swipe-direction="down">
|
|
127
|
+
* <button data-slot="drawer-trigger">Edit filters</button>
|
|
128
|
+
* <div data-slot="drawer-portal">
|
|
129
|
+
* <div data-slot="drawer-backdrop" hidden></div>
|
|
130
|
+
* <div data-slot="drawer-viewport" hidden>
|
|
131
|
+
* <div data-slot="drawer-popup" hidden>
|
|
132
|
+
* <h2 data-slot="drawer-title">Filters</h2>
|
|
133
|
+
* <p data-slot="drawer-description">Narrow the results.</p>
|
|
134
|
+
* <button data-slot="drawer-close">Apply filters</button>
|
|
135
|
+
* </div>
|
|
136
|
+
* </div>
|
|
137
|
+
* </div>
|
|
138
|
+
* </div>
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* Only `drawer-popup` is required inside the root. Optional title and description
|
|
142
|
+
* slots supply accessible labels. Detached triggers use `data-drawer-target` with
|
|
143
|
+
* the root's ID; parts inside nested drawer roots belong to their own controllers.
|
|
144
|
+
*
|
|
145
|
+
* Options and data attributes are read at initialization. Use controller methods
|
|
146
|
+
* or `drawer:set` events to update live state. Open and snap changes emit cancellable
|
|
147
|
+
* `drawer:beforechange` / `drawer:beforesnapchange` events before their callbacks,
|
|
148
|
+
* followed by `drawer:change` / `drawer:snapchange` after commit.
|
|
149
|
+
*
|
|
150
|
+
* @param root - Element containing this drawer's slots.
|
|
151
|
+
* @param options - Initial configuration, overriding corresponding data attributes.
|
|
152
|
+
* @returns A drawer controller, reusing the existing one if already bound. Destroy it before rebinding with new options.
|
|
153
|
+
* @throws If the root has no `drawer-popup` belonging to this drawer.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* const root = document.getElementById('filters-drawer')!;
|
|
158
|
+
* const drawer = createDrawer(root, { snapPoint: 0.6 });
|
|
159
|
+
* drawer.open();
|
|
160
|
+
* drawer.setSnapPoint('320px');
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
declare function createDrawer(root: Element, options?: DrawerOptions): DrawerController;
|
|
164
|
+
/**
|
|
165
|
+
* Bind uninitialized `[data-slot="drawer"]` descendants of a scope.
|
|
166
|
+
* The scope itself and already-bound roots are excluded.
|
|
167
|
+
*
|
|
168
|
+
* @param scope - DOM subtree to scan (default: `document`).
|
|
169
|
+
* @returns Controllers created by this call.
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* import { create } from '@data-slot/drawer';
|
|
173
|
+
* const drawers = create();
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
declare function create(scope?: ParentNode): DrawerController[];
|
|
177
|
+
//#endregion
|
|
178
|
+
export { DrawerChangeDetails, DrawerChangeReason, DrawerController, DrawerOptions, DrawerSnapChangeDetails, type DrawerSnapPoint, type DrawerSwipeDirection, create, createDrawer };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
//#region src/gestures.d.ts
|
|
2
|
+
/** Direction in which a swipe dismisses the drawer. */
|
|
3
|
+
type DrawerSwipeDirection = 'down' | 'up' | 'left' | 'right';
|
|
4
|
+
/**
|
|
5
|
+
* A single visible open size: a viewport fraction from `0` to `1` (inclusive),
|
|
6
|
+
* a pixel number greater than `1`, or a non-negative `px`/`rem` string.
|
|
7
|
+
* Numeric strings are normalized to numbers. The visible size is capped at the
|
|
8
|
+
* popup's CSS size; pass `null` to the options/controller to use its full size.
|
|
9
|
+
*/
|
|
10
|
+
type DrawerSnapPoint = number | string;
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/index.d.ts
|
|
13
|
+
/** Source of a state change. Initial opening uses `none`; controller methods use `imperative-action`. */
|
|
14
|
+
type DrawerChangeReason = 'trigger-press' | 'close-press' | 'outside-press' | 'escape-key' | 'focus-out' | 'imperative-action' | 'swipe' | 'none';
|
|
15
|
+
/** Details shared by `onOpenChange`, `drawer:beforechange`, and `drawer:change`. */
|
|
16
|
+
interface DrawerChangeDetails {
|
|
17
|
+
/** Requested open state; available before the controller commits the change. */
|
|
18
|
+
open: boolean;
|
|
19
|
+
/** Interaction or API call that requested the change. */
|
|
20
|
+
reason: DrawerChangeReason;
|
|
21
|
+
/** Trigger associated with this request, or `null` when none has been selected. */
|
|
22
|
+
trigger: HTMLElement | null;
|
|
23
|
+
/** Trigger's `data-payload`, parsed as JSON when valid, otherwise a string; absent values are `undefined`. */
|
|
24
|
+
payload: unknown;
|
|
25
|
+
/** DOM event that initiated the request, if any. */
|
|
26
|
+
originalEvent?: Event;
|
|
27
|
+
/** Cancel synchronously in `drawer:beforechange` or `onOpenChange`; has no effect after commit. */
|
|
28
|
+
cancel(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Keep the closing content mounted and unhidden until `controller.unmount()`.
|
|
31
|
+
* Call synchronously in `drawer:beforechange` or `onOpenChange` for a close request.
|
|
32
|
+
* The drawer still closes logically and releases its modal effects and focus.
|
|
33
|
+
*/
|
|
34
|
+
preventUnmountOnClose(): void;
|
|
35
|
+
}
|
|
36
|
+
/** Details shared by `onSnapPointChange`, `drawer:beforesnapchange`, and `drawer:snapchange`. */
|
|
37
|
+
interface DrawerSnapChangeDetails {
|
|
38
|
+
/** Requested open position, or `null` to use the popup's full CSS size. */
|
|
39
|
+
snapPoint: DrawerSnapPoint | null;
|
|
40
|
+
/** Source of the snap-point request. */
|
|
41
|
+
reason: DrawerChangeReason;
|
|
42
|
+
/** DOM event that initiated the request, if any. */
|
|
43
|
+
originalEvent?: Event;
|
|
44
|
+
/** Cancel synchronously in `drawer:beforesnapchange` or `onSnapPointChange`; has no effect after commit. */
|
|
45
|
+
cancel(): void;
|
|
46
|
+
}
|
|
47
|
+
/** Initialization options. JavaScript values take precedence over corresponding data attributes. */
|
|
48
|
+
interface DrawerOptions {
|
|
49
|
+
/** Initial open state, overriding `defaultOpen`. Read once; use the controller for later changes. */
|
|
50
|
+
open?: boolean;
|
|
51
|
+
/** Initial open state when `open` is omitted (default: `data-default-open`, then `false`). */
|
|
52
|
+
defaultOpen?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* `true` traps focus, makes outside content inert, and locks scrolling (default).
|
|
55
|
+
* `"trap-focus"` traps focus without inertness or scroll locking; `false` is non-modal.
|
|
56
|
+
* Also configurable with root `data-modal`.
|
|
57
|
+
*/
|
|
58
|
+
modal?: boolean | 'trap-focus';
|
|
59
|
+
/** Ignore outside pointer presses and non-modal focus-out dismissal (default: `false`). */
|
|
60
|
+
disablePointerDismissal?: boolean;
|
|
61
|
+
/** Allow Escape to dismiss the drawer (default: `true`). */
|
|
62
|
+
closeOnEscape?: boolean;
|
|
63
|
+
/** Direction of the dismissal swipe (default: `"down"`). */
|
|
64
|
+
swipeDirection?: DrawerSwipeDirection;
|
|
65
|
+
/** Initial single open position, overriding `defaultSnapPoint`; `null` uses the full CSS size. */
|
|
66
|
+
snapPoint?: DrawerSnapPoint | null;
|
|
67
|
+
/** Fallback initial position, before root snap-point attributes (default: `null`). Read once. */
|
|
68
|
+
defaultSnapPoint?: DrawerSnapPoint | null;
|
|
69
|
+
/** Initial associated trigger ID, taking precedence over `defaultTriggerId` when non-null. */
|
|
70
|
+
triggerId?: string | null;
|
|
71
|
+
/** Fallback initial trigger ID, before root trigger-ID attributes. Defaults to no associated trigger. */
|
|
72
|
+
defaultTriggerId?: string | null;
|
|
73
|
+
/**
|
|
74
|
+
* Focus target on open: an element or document selector, `true` for autofocus/first
|
|
75
|
+
* focusable content, or `false` to skip initial focus. Defaults to the popup itself.
|
|
76
|
+
* Also configurable with popup `data-initial-focus`.
|
|
77
|
+
*/
|
|
78
|
+
initialFocus?: boolean | string | HTMLElement;
|
|
79
|
+
/**
|
|
80
|
+
* Focus target on close: an element or document selector, `true` for the associated
|
|
81
|
+
* trigger/previous focus, or `false` to skip restoration. When omitted, defaults to
|
|
82
|
+
* trigger/previous focus but preserves outside focus after focus-out dismissal or
|
|
83
|
+
* a non-modal outside press on a focusable control. Also uses popup `data-final-focus`.
|
|
84
|
+
*/
|
|
85
|
+
finalFocus?: boolean | string | HTMLElement;
|
|
86
|
+
/** Keep content at its portal destination while closed and hidden (default: `false`; portal `data-keep-mounted`). */
|
|
87
|
+
keepMounted?: boolean;
|
|
88
|
+
/** Portal destination element or document selector (default: `document.body`; portal `data-container`). */
|
|
89
|
+
container?: string | HTMLElement;
|
|
90
|
+
/**
|
|
91
|
+
* Called before committing an open-state or active-trigger change, after an uncanceled
|
|
92
|
+
* `drawer:beforechange`. Call `details.cancel()` to stop the change.
|
|
93
|
+
*/
|
|
94
|
+
onOpenChange?: (open: boolean, details: DrawerChangeDetails) => void;
|
|
95
|
+
/** Called after the opening/closing transition finishes; also emits `drawer:change-complete`. */
|
|
96
|
+
onOpenChangeComplete?: (open: boolean) => void;
|
|
97
|
+
/** Called before replacing the snap point, after an uncanceled `drawer:beforesnapchange`; cancel with `details.cancel()`. */
|
|
98
|
+
onSnapPointChange?: (snapPoint: DrawerSnapPoint | null, details: DrawerSnapChangeDetails) => void;
|
|
99
|
+
}
|
|
100
|
+
/** Imperative drawer state and lifecycle controls. State-change requests can be canceled by listeners. */
|
|
101
|
+
interface DrawerController {
|
|
102
|
+
/** Open or switch the active trigger. An omitted or unknown trigger ID retains the current trigger. */
|
|
103
|
+
open(triggerId?: string): void;
|
|
104
|
+
/** Request closing; content stays visible until its exit transition finishes. */
|
|
105
|
+
close(): void;
|
|
106
|
+
/** Toggle the open state while retaining the associated trigger. */
|
|
107
|
+
toggle(): void;
|
|
108
|
+
/** Replace the single open position across close/open cycles; `null` restores full CSS size. Invalid values are ignored. */
|
|
109
|
+
setSnapPoint(point: DrawerSnapPoint | null): void;
|
|
110
|
+
/** Finish a close held by `preventUnmountOnClose()`. Honors `keepMounted` and does not close an open drawer. */
|
|
111
|
+
unmount(): void;
|
|
112
|
+
/** Current committed open state, independent of enter/exit animation progress. */
|
|
113
|
+
readonly isOpen: boolean;
|
|
114
|
+
/** Current normalized snap point, or `null` for the popup's full CSS size. */
|
|
115
|
+
readonly snapPoint: DrawerSnapPoint | null;
|
|
116
|
+
/** Associated trigger ID, retained after close, or `null` when none is selected. */
|
|
117
|
+
readonly triggerId: string | null;
|
|
118
|
+
/** Remove listeners and observers, release modal effects, hide content, and restore portal placement. Allows rebinding. */
|
|
119
|
+
destroy(): void;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Create a drawer controller for a root element.
|
|
123
|
+
*
|
|
124
|
+
* Canonical markup (supply CSS for positioning, sizing, and transitions):
|
|
125
|
+
* ```html
|
|
126
|
+
* <div id="filters-drawer" data-slot="drawer" data-swipe-direction="down">
|
|
127
|
+
* <button data-slot="drawer-trigger">Edit filters</button>
|
|
128
|
+
* <div data-slot="drawer-portal">
|
|
129
|
+
* <div data-slot="drawer-backdrop" hidden></div>
|
|
130
|
+
* <div data-slot="drawer-viewport" hidden>
|
|
131
|
+
* <div data-slot="drawer-popup" hidden>
|
|
132
|
+
* <h2 data-slot="drawer-title">Filters</h2>
|
|
133
|
+
* <p data-slot="drawer-description">Narrow the results.</p>
|
|
134
|
+
* <button data-slot="drawer-close">Apply filters</button>
|
|
135
|
+
* </div>
|
|
136
|
+
* </div>
|
|
137
|
+
* </div>
|
|
138
|
+
* </div>
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* Only `drawer-popup` is required inside the root. Optional title and description
|
|
142
|
+
* slots supply accessible labels. Detached triggers use `data-drawer-target` with
|
|
143
|
+
* the root's ID; parts inside nested drawer roots belong to their own controllers.
|
|
144
|
+
*
|
|
145
|
+
* Options and data attributes are read at initialization. Use controller methods
|
|
146
|
+
* or `drawer:set` events to update live state. Open and snap changes emit cancellable
|
|
147
|
+
* `drawer:beforechange` / `drawer:beforesnapchange` events before their callbacks,
|
|
148
|
+
* followed by `drawer:change` / `drawer:snapchange` after commit.
|
|
149
|
+
*
|
|
150
|
+
* @param root - Element containing this drawer's slots.
|
|
151
|
+
* @param options - Initial configuration, overriding corresponding data attributes.
|
|
152
|
+
* @returns A drawer controller, reusing the existing one if already bound. Destroy it before rebinding with new options.
|
|
153
|
+
* @throws If the root has no `drawer-popup` belonging to this drawer.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* const root = document.getElementById('filters-drawer')!;
|
|
158
|
+
* const drawer = createDrawer(root, { snapPoint: 0.6 });
|
|
159
|
+
* drawer.open();
|
|
160
|
+
* drawer.setSnapPoint('320px');
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
declare function createDrawer(root: Element, options?: DrawerOptions): DrawerController;
|
|
164
|
+
/**
|
|
165
|
+
* Bind uninitialized `[data-slot="drawer"]` descendants of a scope.
|
|
166
|
+
* The scope itself and already-bound roots are excluded.
|
|
167
|
+
*
|
|
168
|
+
* @param scope - DOM subtree to scan (default: `document`).
|
|
169
|
+
* @returns Controllers created by this call.
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* import { create } from '@data-slot/drawer';
|
|
173
|
+
* const drawers = create();
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
declare function create(scope?: ParentNode): DrawerController[];
|
|
177
|
+
//#endregion
|
|
178
|
+
export { DrawerChangeDetails, DrawerChangeReason, DrawerController, DrawerOptions, DrawerSnapChangeDetails, type DrawerSnapPoint, type DrawerSwipeDirection, create, createDrawer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{clearRootBinding as e,containsWithPortals as t,createDismissLayer as n,createModalStackItem as r,createPortalLifecycle as i,createPresenceLifecycle as a,createSwipeGesture as o,emit as s,ensureId as c,focusElement as l,getAutofocusOrFirstFocusable as u,getDataBool as d,getDataString as f,getRoots as p,getTabbables as ee,hasRootBinding as m,linkLabelledBy as te,lockScroll as ne,on as h,reuseRootBinding as re,setAria as g,setRootBinding as ie,unlockScroll as ae}from"@data-slot/core";function oe(e){if(typeof e==`number`)return Number.isFinite(e)&&e>=0?e:null;if(typeof e!=`string`||!e.trim())return null;let t=e.trim();if(/^\d*\.?\d+(px|rem)$/.test(t))return t;let n=Number(t);return Number.isFinite(n)&&n>=0?n:null}function se(e,t,n){return typeof e==`number`?e<=1?e*t:e:parseFloat(e)*(e.endsWith(`rem`)?n:1)}function _(e,t,n,r){for(let i=e;i&&t.contains(i);i=i.parentElement){let e=i,t=e.ownerDocument.defaultView.getComputedStyle(e),a=n?t.overflowX:t.overflowY;if(!/(auto|scroll)/.test(a))continue;let o=n?e.scrollLeft:e.scrollTop,s=n?e.scrollWidth-e.clientWidth:e.scrollHeight-e.clientHeight;if(s>1&&(r>0?o>0:o<s-1))return!0}return!1}function ce(e){let{popup:t,direction:n}=e,r=n===`left`||n===`right`,i=n===`up`||n===`left`?-1:1,a=e=>r?e.deltaX:e.deltaY;return o({element:t,axes:[r?`x`:`y`],start:(n,r)=>!e.enabled()||r.closest(`[data-swipe-ignore],input,textarea,select,button,a,[contenteditable="true"]`)?null:r.closest(`[data-slot="drawer-popup"]`)===t?t:null,lock:(e,n,i)=>!_(i,t,r,a(n)),active:e.enabled,move:(t,n)=>e.move(a(n)*i,n.event),release:(t,n)=>{let r=a(n)*i;e.release(r,r/Math.max(1,n.duration),n.event),e.reset()},reset:(t,n)=>e.reset(n)}).destroy}function le(e){let t=e.ownerDocument,n=t.defaultView,r=n.visualViewport,i=e.style.getPropertyValue(`--drawer-keyboard-inset`),a=e.hasAttribute(`data-keyboard-open`),o=0,s=null,c=()=>{if(!s)return;let{element:e,paddingBottom:t,scrollPaddingBottom:n,overflowAnchor:r}=s;Object.assign(e.style,{paddingBottom:t,scrollPaddingBottom:n,overflowAnchor:r}),s=null},l=()=>{o=0;let i=t.activeElement,a=i?.matches(`textarea, input:not([type]), input[type="text"], input[type="email"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="url"], [contenteditable]:not([contenteditable="false"])`),l=!!r&&(r.scale===void 0||r.scale===1)&&n.innerHeight-r.height>60&&!!a&&e.contains(i),u=r?Math.min(n.innerHeight,r.height+r.offsetTop):n.innerHeight;if(e.style.setProperty(`--drawer-keyboard-inset`,`${l?Math.max(0,n.innerHeight-u):0}px`),e.toggleAttribute(`data-keyboard-open`,l),!l||!i){c();return}let d=i.parentElement;for(;d&&e.contains(d)&&!/(auto|scroll)/.test(n.getComputedStyle(d).overflowY);)d=d.parentElement;if(!d||!e.contains(d)){c();return}s?.element!==d&&c();let f=i.getBoundingClientRect(),p=d.getBoundingClientRect(),ee=Math.min(u,p.bottom)-16,m=Math.max(r?.offsetTop??0,p.top)+16,te=f.bottom-ee;te>0?(s||={element:d,paddingBottom:d.style.paddingBottom,scrollPaddingBottom:d.style.scrollPaddingBottom,overflowAnchor:d.style.overflowAnchor,basePadding:parseFloat(n.getComputedStyle(d).paddingBottom)||0},d.style.overflowAnchor=`none`,d.style.paddingBottom=`${s.basePadding+Math.max(0,p.bottom-u)+f.height+48}px`,d.style.scrollPaddingBottom=`16px`,d.scrollTop+=te):f.top<m&&(d.scrollTop-=m-f.top)},u=()=>{o||=n.requestAnimationFrame(l)};l();let d=[h(t,`focusin`,u),h(t,`focusout`,u),h(n,`resize`,u)];return r&&d.push(h(r,`resize`,u),h(r,`scroll`,u)),()=>{d.forEach(e=>e()),n.cancelAnimationFrame(o),c(),i?e.style.setProperty(`--drawer-keyboard-inset`,i):e.style.removeProperty(`--drawer-keyboard-inset`),e.toggleAttribute(`data-keyboard-open`,a)}}const v=new WeakMap;function y(e){let t=null;for(let n of e)n.open&&(!t||n.order>t.order)&&(t=n);return t}function b(e){let{popup:t,swipe:n}=e,r=e.front??e;t.toggleAttribute(`data-nested-swiping`,e.descendants.some(e=>e.open&&e.swipe!==null));for(let i of[t,e.parts.backdrop,e.parts.viewport])i&&(i.toggleAttribute(`data-swiping`,n!==null),i.style.setProperty(`--drawer-swipe-progress`,String((i===t?r.swipe:n)?.progress??0)));t.style.setProperty(`--drawer-swipe-amount-x`,`${n?.x??0}px`),t.style.setProperty(`--drawer-swipe-amount-y`,`${n?.y??0}px`)}function x(e){e.popup.toggleAttribute(`data-nested`,!!e.parent),e.popup.toggleAttribute(`data-nested-drawer-open`,e.front!==null),e.popup.style.setProperty(`--nested-drawers`,String(e.descendants.filter(e=>e.open).length)),e.popup.style.setProperty(`--drawer-front-height`,`${(e.front??e).height}px`),b(e)}function S(e){let{front:t}=e;for(let n of e.indents)n.toggleAttribute(`data-active`,!!t),n.toggleAttribute(`data-inactive`,!t),n.style.setProperty(`--drawer-swipe-progress`,String(t?.swipe?.progress??0)),n.style.setProperty(`--drawer-height`,`${t?.height??0}px`)}function ue(e){e.front=y(e.entries),e.indents=Array.from(e.element.querySelectorAll(`[data-slot="drawer-indent"],[data-slot="drawer-indent-background"]`)).filter(t=>t.closest(`[data-slot="drawer-provider"]`)===e.element),S(e)}function de(e){for(let t of e.entries.values())t.ancestors=[],t.descendants=[];for(let t of e.entries.values())for(let n=t.parent&&e.entries.get(t.parent);n;n=n.parent&&e.entries.get(n.parent))t.ancestors.push(n),n.descendants.push(t);for(let t of e.entries.values())t.front=y(t.descendants),x(t);for(let[t,n]of e.providers)ue(n),n.entries.size||e.providers.delete(t)}function fe(e,n,r={}){let i=e.ownerDocument,a=v.get(i);a||(a={entries:new Map,providers:new Map,openOrder:0},v.set(i,a));let o=null;for(let n of a.entries.values())t(n.popup,e)&&(!o||t(o.popup,n.root))&&(o=n);let s=e.closest(`[data-slot="drawer-provider"]`)??o?.provider?.element,c=null;s&&(c=a.providers.get(s)??{element:s,entries:new Set,indents:[],front:null},a.providers.set(s,c));let l={root:e,popup:n,parts:r,parent:e.parentElement?.closest(`[data-slot="drawer"]`)??o?.root??null,ancestors:[],descendants:[],provider:c,open:!1,swipe:null,order:0,height:n.getBoundingClientRect().height,front:null};a.entries.set(e,l),c?.entries.add(l),de(a);let u=()=>{x(l),l.ancestors.forEach(x)},d=!1;return{get parent(){return l.parent},setOpen(e){if(!(d||e===l.open)){e&&(l.order=++a.openOrder),l.open=e,e||(l.swipe=null);for(let e of l.ancestors)e.front=y(e.descendants);u(),c&&ue(c)}},setSwipe(e){if(!d&&(l.swipe=e,b(l),l.ancestors.forEach(b),c?.front===l))for(let t of c.indents)t.style.setProperty(`--drawer-swipe-progress`,String(e?.progress??0))},refresh(e){d||l.height===e||(l.height=e,u(),c?.front===l&&S(c))},destroy(){d||(d=!0,a.entries.delete(e),c?.entries.delete(l),de(a),a.entries.size||v.delete(i))}}}const C=`@data-slot/drawer`,pe=e=>e.matches(`:disabled`)||d(e,`disabled`)===!0||e.getAttribute(`aria-disabled`)===`true`,w=(e,t)=>Array.from(e.querySelectorAll(`[data-slot="drawer-${t}"]`)).filter(t=>t.closest(`[data-slot="drawer"]`)===e),me=(e,t)=>{try{return e.querySelector(t)}catch{return null}},he=(e,t)=>{let n=f(e,t);return n===`false`?!1:n===`true`||n===``?!0:n},ge=e=>{let t=e?.getAttribute(`data-payload`);if(t!=null)try{return JSON.parse(t)}catch{return t}};function T(o,p={}){let m=re(o,C,`[@data-slot/drawer] Drawer already initialized; returning its controller.`);if(m)return m;let _=w(o,`popup`)[0];if(!_)throw Error(`Drawer requires drawer-popup slot`);let v=o.ownerDocument,y=v.defaultView,b=w(o,`portal`)[0],x=w(o,`backdrop`)[0],S=w(o,`viewport`)[0],ue=w(o,`title`)[0],de=w(o,`description`)[0],T=w(o,`trigger`);if(o.id)for(let e of v.querySelectorAll(`[data-drawer-target]`))e.getAttribute(`data-drawer-target`)?.replace(/^#/,``)===o.id&&e.dataset.slot===`drawer-trigger`&&!T.includes(e)&&T.push(e);let _e=f(o,`modal`),E=p.modal??(_e===`trap-focus`?`trap-focus`:_e!==`false`),ve=!(p.disablePointerDismissal??d(o,`disablePointerDismissal`)??!1),ye=p.closeOnEscape??d(o,`closeOnEscape`)??!0,be=p.swipeDirection??f(o,`swipeDirection`),D=[`up`,`down`,`left`,`right`].includes(be??``)?be:`down`,O=D===`left`||D===`right`,xe=D===`left`||D===`up`?-1:1,k=oe(p.snapPoint===void 0?p.defaultSnapPoint===void 0?f(o,`snapPoint`)??f(o,`defaultSnapPoint`):p.defaultSnapPoint:p.snapPoint),Se=p.initialFocus??he(_,`initialFocus`),A=p.finalFocus??he(_,`finalFocus`),Ce=p.keepMounted??(b?d(b,`keepMounted`):void 0)??!1,j=p.container??(b?f(b,`container`):void 0),we=typeof j==`string`?me(v,j):j,Te=b??S??_,M=i({content:Te,root:o}),N=[],P=!1,F=!1,Ee=!1,De=!1,I=!1,L,Oe=S&&(o.closest(`[data-slot="drawer-virtual-keyboard-provider"]`)||w(o,`virtual-keyboard-provider`).length),R=null,ke=p.triggerId??p.defaultTriggerId??f(o,`triggerId`)??f(o,`defaultTriggerId`),z=T.find(e=>e.id===ke)??null,B=0,Ae=-1,je=0,V=new Set,Me=e=>{let t=y.requestAnimationFrame(()=>{V.delete(t),F||e()});V.add(t)},Ne=[o,b,x,S,_].filter(e=>!!e),Pe=_.getAttribute(`tabindex`);Pe===null&&(_.tabIndex=-1),_.setAttribute(`role`,`dialog`),E===!0?g(_,`modal`,!0):_.removeAttribute(`aria-modal`),c(_,`drawer-popup`),te(_,ue??null,de??null),x&&(x.setAttribute(`aria-hidden`,`true`),x.setAttribute(`role`,`presentation`));for(let e of[...T,...w(o,`close`)])e.tagName===`BUTTON`&&!e.hasAttribute(`type`)&&e.setAttribute(`type`,`button`);for(let e of T)c(e,`drawer-trigger`),e.setAttribute(`aria-haspopup`,`dialog`),e.setAttribute(`aria-controls`,_.id),g(e,`expanded`,!1);let H=fe(o,_,{backdrop:x,viewport:S}),U=()=>{for(let e of Ne)e.setAttribute(`data-state`,P?`open`:`closed`),e.toggleAttribute(`data-open`,P),e.toggleAttribute(`data-closed`,!P),e.setAttribute(`data-swipe-direction`,D);for(let e of T)g(e,`expanded`,P&&e===z),e.toggleAttribute(`data-popup-open`,P&&e===z);H.setOpen(P)},Fe=()=>O?_.getBoundingClientRect().width||y.innerWidth:_.getBoundingClientRect().height||y.innerHeight,W=()=>k===null?Fe():Math.min(Fe(),se(k,O?S?.clientWidth||v.documentElement.clientWidth||y.innerWidth:S?.clientHeight||v.documentElement.clientHeight||y.innerHeight,parseFloat(y.getComputedStyle(v.documentElement).fontSize)||16)),Ie=()=>Math.max(0,Fe()-W()),G=()=>{let e=_.getBoundingClientRect();_.style.setProperty(`--drawer-height`,`${e.height}px`),_.style.setProperty(`--drawer-width`,`${e.width}px`),_.style.setProperty(`--drawer-snap-point-offset`,`${Ie()*xe}px`),k===null?o.removeAttribute(`data-snap-point`):o.setAttribute(`data-snap-point`,String(k)),H.refresh(e.height)},K=e=>t(_,e),Le=e=>typeof e==`string`?me(v,e):typeof e==`object`?e:null,Re=()=>{!P||!Y.isTopmost||Se===!1||l(Le(Se)??(Se===!0?u(_):null)??_)},ze=()=>{let e=Le(A)??z??R;R=null,A!==!1&&e?.isConnected&&l(e)},Be=e=>{s(o,`drawer:change-complete`,{open:e}),p.onOpenChangeComplete?.(e)},Ve=e=>{if(!P||B!==e||Ae===e)return;let t=Ue.flatMap(e=>typeof e.getAnimations==`function`?e.getAnimations():[]),n=()=>{!F&&P&&B===e&&Ae!==e&&(Ae=e,Be(!0))};t.length?Promise.allSettled(t.map(e=>e.finished)).then(n):n()},q=()=>{P||I||F||(_.hidden=!0,x&&(x.hidden=!0),S&&(S.hidden=!0),b&&(b.hidden=!0),Ce||(M.restore(),Ee=!1))},He=()=>{P||F||--je>0||(q(),Be(!1))},Ue=[_,x].filter(e=>!!e),J=Ue.map(e=>a({element:e,win:y,onExitComplete:He})),We=(e,t=`imperative-action`,n)=>{let r=oe(e);if(F||e!==null&&r===null||r===k)return;let i=!1,a={snapPoint:r,reason:t,originalEvent:n,cancel:()=>{i=!0}},c=new CustomEvent(`drawer:beforesnapchange`,{bubbles:!0,cancelable:!0,detail:a});o.dispatchEvent(c),!(c.defaultPrevented||i||F)&&(p.onSnapPointChange?.(r,a),!(i||F)&&(k=r,G(),s(o,`drawer:snapchange`,a)))},Y=r({content:_,overlay:x,cssVarPrefix:`drawer`,isolateOutside:E===!0,onTabKeydown:e=>{if(E===!1||!P)return;let t=ee(_),n=v.activeElement;if(K(n)&&!_.contains(n))return;if(!t.length){e.preventDefault(),l(_);return}let r=t[0],i=t.at(-1);(!t.includes(n)||!e.shiftKey&&n===i||e.shiftKey&&n===r)&&(e.preventDefault(),l(e.shiftKey?i:r))}}),Ge=({next:e,reason:t,originalEvent:n,trigger:r})=>{if(F)return;let i=P&&e&&r!==z;if(P===e&&!i)return;let a=!1,c=!1,l={open:e,reason:t,trigger:r,payload:ge(r),originalEvent:n,cancel:()=>{a=!0},preventUnmountOnClose:()=>{c=!0}},u=new CustomEvent(`drawer:beforechange`,{bubbles:!0,cancelable:!0,detail:l});if(o.dispatchEvent(u),u.defaultPrevented||a||F||(p.onOpenChange?.(e,l),a||F))return;if(i){z=r,U(),s(o,`drawer:change`,l);return}let d=++B;if(z=r,P=e,I=!e&&c,e){R=v.activeElement,J.forEach(e=>e.enter()),Ee||=(M.mount(),we&&we.appendChild(Te),!0);for(let e of[b,S,x,_])e&&(e.hidden=!1);Y.open(),E===!0&&(ne(),De=!0),Oe&&S&&(L=le(S)),U(),G(),Me(Re),Me(()=>Me(()=>Ve(d)))}else{Y.close(),L?.(),L=void 0,De&&=(ae(),!1),U(),je=J.length,J.forEach(e=>e.exit());let e=n?.target;A===void 0&&(t===`focus-out`||t===`outside-press`&&E===!1&&e?.closest?.(`button,input,textarea,select,a[href],[tabindex]`))?R=null:ze()}s(o,`drawer:change`,l)},X=null,Z=null,Q=(e,t=`imperative-action`,n,r=z)=>{if(!F){if(X?.next===e&&X.trigger===r){Z=null;return}if(Z={next:e,reason:t,originalEvent:n,trigger:r},!X)try{for(;Z&&!F;)X=Z,Z=null,Ge(X)}finally{X=null,Z=null}}};N.push(n({root:o,isOpen:()=>P,closeOnEscape:ye,closeOnClickOutside:ve,isInside:e=>K(e)||T.some(t=>t.contains(e)),onDismiss:({reason:e,originalEvent:t})=>{e===`outside-press`&&t.button!==0||Q(!1,e,t)}}));for(let e of T)N.push(h(e,`click`,t=>{t.defaultPrevented||pe(e)||Q(!P||z!==e,`trigger-press`,t,e)}));for(let e of w(o,`close`))N.push(h(e,`click`,t=>{!t.defaultPrevented&&!pe(e)&&Q(!1,`close-press`,t)}));N.push(h(v,`focusin`,e=>{!P||!Y.isTopmost||K(e.target)||(E===!1?ve&&!T.includes(e.target)&&Q(!1,`focus-out`,e):l(u(_)??_))}));let Ke=()=>H.setSwipe(null);if(N.push(ce({popup:_,direction:D,enabled:()=>P&&Y.isTopmost,move:e=>{let t=Math.max(-Ie(),Math.min(W(),e)),n=Math.min(1,Math.max(0,t/Math.max(1,W())));H.setSwipe({progress:n,x:O?t*xe:0,y:O?0:t*xe})},release:(e,t,n)=>{_.style.setProperty(`--drawer-swipe-strength`,String(Math.max(.1,Math.min(1,1/Math.max(1,Math.abs(t)))))),(e>W()*.35||t>.5&&e>12)&&Q(!1,`swipe`,n)},reset:Ke})),N.push(h(y,`resize`,G)),y.ResizeObserver!==void 0){let e=new y.ResizeObserver(G);e.observe(_),N.push(()=>e.disconnect())}let $=(e,t)=>N.push(h(o,e,e=>{e.target===o&&t(e)}));$(`drawer:set`,e=>{let t=e.detail;if(!t||typeof t!=`object`)return;let n=typeof t.triggerId==`string`?T.find(e=>e.id===t.triggerId)??z:z;`snapPoint`in t&&We(t.snapPoint,`imperative-action`,e),typeof t.open==`boolean`&&Q(t.open,`imperative-action`,e,n)}),$(`drawer:open`,e=>Q(!0,`imperative-action`,e)),$(`drawer:close`,e=>Q(!1,`imperative-action`,e)),$(`drawer:toggle`,e=>Q(!P,`imperative-action`,e)),$(`drawer:unmount`,()=>{I=!1,q()}),_.style.setProperty(`--drawer-swipe-strength`,`1`),U(),G(),Ke(),q();let qe={open:e=>Q(!0,`imperative-action`,void 0,T.find(t=>t.id===e)??z),close:()=>Q(!1),toggle:()=>Q(!P),setSnapPoint:e=>We(e),unmount:()=>{I=!1,q()},get isOpen(){return P},get snapPoint(){return k},get triggerId(){return z?.id??null},destroy(){if(F)return;let t=P&&K(v.activeElement);F=!0,P=!1,B++,V.forEach(e=>y.cancelAnimationFrame(e)),V.clear(),J.forEach(e=>e.cleanup()),Y.destroy(),L?.(),De&&=(ae(),!1),N.forEach(e=>e()),U(),H.destroy();for(let e of[_,x,S,b])e&&(e.hidden=!0);M.cleanup(),Pe===null&&_.removeAttribute(`tabindex`),t&&ze(),e(o,C,qe)}};return ie(o,C,qe),(p.open??p.defaultOpen??d(o,`defaultOpen`)??!1)&&Q(!0,`none`),qe}function _e(e=document){return p(e,`drawer`).filter(e=>!m(e,C)).map(e=>T(e))}export{_e as create,T as createDrawer};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@data-slot/drawer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsdown"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/bejamas/data-slot.git",
|
|
26
|
+
"directory": "packages/drawer"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"headless",
|
|
30
|
+
"ui",
|
|
31
|
+
"drawer",
|
|
32
|
+
"modal",
|
|
33
|
+
"vanilla",
|
|
34
|
+
"data-slot"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@data-slot/core": "1.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|