css-drawer 0.3.1 → 0.4.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 +52 -0
- package/dist/index.cjs +2 -3
- package/dist/index.d.cts +2 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -3
- package/dist/index.mjs.map +1 -1
- package/dist/observer-BZAUqGHC.cjs +2 -0
- package/dist/observer-DEB7SwSL.mjs +3 -0
- package/dist/observer-DEB7SwSL.mjs.map +1 -0
- package/dist/react.cjs +1 -2
- package/dist/react.d.cts +14 -7
- package/dist/react.d.cts.map +1 -1
- package/dist/react.d.mts +14 -7
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +1 -2
- package/dist/react.mjs.map +1 -1
- package/dist/{observer-CtfG4nrq.css → style.css} +91 -128
- package/package.json +11 -7
- package/src/drawer.css +6 -1
- package/dist/observer-B-WjvsFU.cjs +0 -1
- package/dist/observer-DIOx0VCj.mjs +0 -2
- package/dist/observer-DIOx0VCj.mjs.map +0 -1
- package/dist/observer-QD6bSdOu.css +0 -334
- package/dist/observer-QD6bSdOu.css.map +0 -1
package/README.md
CHANGED
|
@@ -198,6 +198,7 @@ The `onOpenChange` callback fires when:
|
|
|
198
198
|
| `open` | `boolean` | - | Controlled open state |
|
|
199
199
|
| `onOpenChange` | `(open: boolean) => void` | - | Called when open state changes |
|
|
200
200
|
| `closeOnOutsideClick` | `boolean` | `true` | Close when clicking outside the drawer |
|
|
201
|
+
| `scrollLock` | `boolean` | `true` | Lock body scroll while open. See [Body Scroll Lock](#body-scroll-lock). |
|
|
201
202
|
| `className` | `string` | - | Additional CSS classes |
|
|
202
203
|
| `...props` | `DialogHTMLAttributes` | - | All native dialog props |
|
|
203
204
|
|
|
@@ -386,6 +387,7 @@ open(drawer)
|
|
|
386
387
|
| `handle` | `boolean` | `true` | Include drag handle |
|
|
387
388
|
| `className` | `string` | `''` | Additional CSS classes |
|
|
388
389
|
| `closeOnOutsideClick` | `boolean` | `true` | Close when clicking outside |
|
|
390
|
+
| `scrollLock` | `boolean` | `true` | Lock body scroll while open. See [Body Scroll Lock](#body-scroll-lock). |
|
|
389
391
|
|
|
390
392
|
**Returns:** `HTMLDialogElement`
|
|
391
393
|
|
|
@@ -575,6 +577,56 @@ No setup required.
|
|
|
575
577
|
|
|
576
578
|
---
|
|
577
579
|
|
|
580
|
+
## Body Scroll Lock
|
|
581
|
+
|
|
582
|
+
While a drawer is open, the underlying page is prevented from scrolling. The lock is applied as `position: fixed; top: -<scrollY>px; width: 100%` on `<body>` (the same technique Vaul, Radix Dialog, and the archived `body-scroll-lock` package use), and on close the styles are reverted and `window.scrollTo({ top: <savedScrollY>, behavior: 'instant' })` restores the scroll position. A CSS-only `overflow: hidden` rule stays as defense-in-depth but isn't load-bearing.
|
|
583
|
+
|
|
584
|
+
The lock is **counter-based**: incremented per opt-in drawer that's open, decremented when one closes. Body styles are applied only on the `0 → positive` transition and restored only on the `positive → 0` transition. That property is what makes the next section work.
|
|
585
|
+
|
|
586
|
+
### Drawer → another drawer is handled automatically
|
|
587
|
+
|
|
588
|
+
The common multi-modal flow — a mobile menu drawer closes and a checkout drawer opens in the same synchronous handler — is the case consumers hit. Because the counter goes `1 → 1` across the swap, **the lock is never released mid-transition**, so the body stays pinned and scroll position is preserved.
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
function handleSignIn() {
|
|
592
|
+
setMenuOpen(false) // close one css-drawer
|
|
593
|
+
setCheckoutOpen(true) // open another in the same tick
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Verified in Chromium mobile emulation (`e2e/scroll-lock.spec.ts`):
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
during swap: body.position=fixed body.top=-800px ← lock NEVER released
|
|
601
|
+
final scrollY after close = 800 ← restored on last close
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**If you're upgrading from 0.3.x and have your own consumer-side `useEffect` scroll lock, remove it.** The bug where the page jumps to the wrong scroll position during a drawer-to-drawer transition is almost certainly that consumer `useEffect` running its cleanup mid-swap. With v0.4.0 owning the lock, the counter holds it continuously.
|
|
605
|
+
|
|
606
|
+
### Opting out (`scrollLock={false}`)
|
|
607
|
+
|
|
608
|
+
For the rare side-panel-allows-scroll case:
|
|
609
|
+
|
|
610
|
+
```tsx
|
|
611
|
+
<Drawer.Content ref={ref} scrollLock={false}>...</Drawer.Content>
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
// Vanilla
|
|
616
|
+
create({ id: 'side-panel', scrollLock: false })
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
```html
|
|
620
|
+
<!-- HTML -->
|
|
621
|
+
<dialog class="drawer" data-scroll-lock="false">...</dialog>
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
The CSS defense-in-depth (`overflow: hidden` via `:has()`) also respects this opt-out.
|
|
625
|
+
|
|
626
|
+
All three are exported from both `css-drawer` and `css-drawer/react`.
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
578
630
|
## Preventing Outside Click Close
|
|
579
631
|
|
|
580
632
|
By default, clicking the backdrop closes the drawer. Disable this for forms or when accidental dismissal could cause data loss.
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
require(
|
|
2
|
-
const e=require(`./observer-B-WjvsFU.cjs`);function t(e){return e?typeof e==`string`?document.getElementById(e):e:null}function n(e){t(e)?.showModal()}function r(e){t(e)?.close()}function i(){Array.from(document.querySelectorAll(`dialog.drawer[open]`)).reverse().forEach(e=>e.close())}function a(e){return t(e)?.open??!1}function o(){return Array.from(document.querySelectorAll(`dialog.drawer[open]`))}function s(){let e=o();return e[e.length-1]??null}function c(e={}){let{id:t,content:n=``,direction:r,handle:i=!0,className:a=``,closeOnOutsideClick:o=!0}=e,s=document.createElement(`dialog`);return s.className=`drawer ${a}`.trim(),t&&(s.id=t),r&&(s.dataset.direction=r),o||(s.dataset.closeOnOutsideClick=`false`),s.innerHTML=`
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./observer-BZAUqGHC.cjs`);function t(e){return e?typeof e==`string`?document.getElementById(e):e:null}function n(e){t(e)?.showModal()}function r(e){t(e)?.close()}function i(){Array.from(document.querySelectorAll(`dialog.drawer[open]`)).reverse().forEach(e=>e.close())}function a(e){return t(e)?.open??!1}function o(){return Array.from(document.querySelectorAll(`dialog.drawer[open]`))}function s(){let e=o();return e[e.length-1]??null}function c(e={}){let{id:t,content:n=``,direction:r,handle:i=!0,className:a=``,closeOnOutsideClick:o=!0,scrollLock:s=!0}=e,c=document.createElement(`dialog`);return c.className=`drawer ${a}`.trim(),t&&(c.id=t),r&&(c.dataset.direction=r),o||(c.dataset.closeOnOutsideClick=`false`),s||(c.dataset.scrollLock=`false`),c.innerHTML=`
|
|
3
2
|
${i?`<div class="drawer-handle"></div>`:``}
|
|
4
3
|
<div class="drawer-content">${n}</div>
|
|
5
|
-
`,
|
|
4
|
+
`,c.addEventListener(`click`,e=>{e.target===c&&c.dataset.closeOnOutsideClick!==`false`&&c.close()}),c}function l(e){return document.body.appendChild(e),e}function u(e){t(e)?.remove()}function d(){let e=e=>{let t=e.target;t.matches(`dialog.drawer`)&&t.dataset.closeOnOutsideClick!==`false`&&t.close()};return document.addEventListener(`click`,e),()=>document.removeEventListener(`click`,e)}function f(n,r){let i=t(n);if(!i)return()=>{};let{onOpen:a,onClose:o,onCancel:s}=r,c=()=>o?.(),l=()=>s?.(),u=e=>{e.detail?.target===i&&i.open&&a?.()};return i.addEventListener(`close`,c),i.addEventListener(`cancel`,l),window.addEventListener(e.t,u),()=>{i.removeEventListener(`close`,c),i.removeEventListener(`cancel`,l),window.removeEventListener(e.t,u)}}function p(e,t){let n=t?.closeOnOutsideClick??!0;return{id:e,className:`drawer`,"data-close-on-outside-click":n?void 0:`false`,onClick:e=>{n&&e.target===e.currentTarget&&e.currentTarget.close()}}}exports.close=r,exports.closeAll=i,exports.create=c,exports.getOpen=o,exports.getTop=s,exports.init=d,exports.isOpen=a,exports.mount=l,exports.open=n,exports.props=p,exports.subscribe=f,exports.unmount=u;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
//#region src/index.d.ts
|
|
2
|
-
|
|
3
2
|
type DrawerElement = HTMLDialogElement;
|
|
4
3
|
type DrawerRef = string | DrawerElement | null | undefined;
|
|
5
4
|
type DrawerDirection = 'bottom' | 'top' | 'left' | 'right' | 'modal';
|
|
@@ -16,6 +15,8 @@ interface CreateDrawerOptions {
|
|
|
16
15
|
className?: string;
|
|
17
16
|
/** Close when clicking outside (default: true) */
|
|
18
17
|
closeOnOutsideClick?: boolean;
|
|
18
|
+
/** Lock body scroll while open (default: true) */
|
|
19
|
+
scrollLock?: boolean;
|
|
19
20
|
}
|
|
20
21
|
interface DrawerEventHandlers {
|
|
21
22
|
/** Called when drawer opens */
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";KAOY,aAAA,GAAgB,iBAAA;AAAA,KAEhB,SAAA,YAAqB,aAAA;AAAA,KAErB,eAAA;AAAA,UAEK,mBAAA;EAJgB;EAM/B,EAAA;EAJU;EAMV,OAAA;;EAEA,SAAA,GAAY,eAAA;EARa;EAUzB,MAAA;EARkC;EAUlC,SAAA;EAJ2B;EAM3B,mBAAA;EARA;EAUA,UAAA;AAAA;AAAA,UAGe,mBAAA;EAPf;EASA,MAAA;EALA;EAOA,OAAA;EAPU;EASV,QAAA;AAAA;;;;iBAiBc,IAAA,CAAK,MAAA,EAAQ,SAAA;;;;iBAQb,KAAA,CAAM,MAAA,EAAQ,SAAA;;;;iBAQd,QAAA,CAAA;AARhB;;;AAAA,iBAgBgB,MAAA,CAAO,MAAA,EAAQ,SAAA;;AAR/B;;iBAgBgB,OAAA,CAAA,GAAW,aAAA;;;AAR3B;iBAegB,MAAA,CAAA,GAAU,aAAA;;;;iBAQV,MAAA,CAAO,OAAA,GAAS,mBAAA,GAA2B,aAAA;;;;iBAgC3C,KAAA,CAAM,MAAA,EAAQ,aAAA,GAAgB,aAAA;AAxC9C;;;AAAA,iBAgDgB,OAAA,CAAQ,MAAA,EAAQ,SAAA;;AAxChC;;;;iBAkDgB,IAAA,CAAA;;;;;iBAgBA,SAAA,CACd,MAAA,EAAQ,SAAA,EACR,QAAA,EAAU,mBAAA;;;;;iBAiCI,KAAA,CAAM,EAAA,UAAY,OAAA;EAAY,mBAAA;AAAA;EAAA;;;wBAM7B,UAAA;AAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
//#region src/index.d.ts
|
|
2
|
-
|
|
3
2
|
type DrawerElement = HTMLDialogElement;
|
|
4
3
|
type DrawerRef = string | DrawerElement | null | undefined;
|
|
5
4
|
type DrawerDirection = 'bottom' | 'top' | 'left' | 'right' | 'modal';
|
|
@@ -16,6 +15,8 @@ interface CreateDrawerOptions {
|
|
|
16
15
|
className?: string;
|
|
17
16
|
/** Close when clicking outside (default: true) */
|
|
18
17
|
closeOnOutsideClick?: boolean;
|
|
18
|
+
/** Lock body scroll while open (default: true) */
|
|
19
|
+
scrollLock?: boolean;
|
|
19
20
|
}
|
|
20
21
|
interface DrawerEventHandlers {
|
|
21
22
|
/** Called when drawer opens */
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";KAOY,aAAA,GAAgB,iBAAA;AAAA,KAEhB,SAAA,YAAqB,aAAA;AAAA,KAErB,eAAA;AAAA,UAEK,mBAAA;EAJgB;EAM/B,EAAA;EAJU;EAMV,OAAA;;EAEA,SAAA,GAAY,eAAA;EARa;EAUzB,MAAA;EARkC;EAUlC,SAAA;EAJ2B;EAM3B,mBAAA;EARA;EAUA,UAAA;AAAA;AAAA,UAGe,mBAAA;EAPf;EASA,MAAA;EALA;EAOA,OAAA;EAPU;EASV,QAAA;AAAA;;;;iBAiBc,IAAA,CAAK,MAAA,EAAQ,SAAA;;;;iBAQb,KAAA,CAAM,MAAA,EAAQ,SAAA;;;;iBAQd,QAAA,CAAA;AARhB;;;AAAA,iBAgBgB,MAAA,CAAO,MAAA,EAAQ,SAAA;;AAR/B;;iBAgBgB,OAAA,CAAA,GAAW,aAAA;;;AAR3B;iBAegB,MAAA,CAAA,GAAU,aAAA;;;;iBAQV,MAAA,CAAO,OAAA,GAAS,mBAAA,GAA2B,aAAA;;;;iBAgC3C,KAAA,CAAM,MAAA,EAAQ,aAAA,GAAgB,aAAA;AAxC9C;;;AAAA,iBAgDgB,OAAA,CAAQ,MAAA,EAAQ,SAAA;;AAxChC;;;;iBAkDgB,IAAA,CAAA;;;;;iBAgBA,SAAA,CACd,MAAA,EAAQ,SAAA,EACR,QAAA,EAAU,mBAAA;;;;;iBAiCI,KAAA,CAAM,EAAA,UAAY,OAAA;EAAY,mBAAA;AAAA;EAAA;;;wBAM7B,UAAA;AAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import{t as e}from"./observer-
|
|
2
|
-
function t(e){return e?typeof e==`string`?document.getElementById(e):e:null}function n(e){t(e)?.showModal()}function r(e){t(e)?.close()}function i(){Array.from(document.querySelectorAll(`dialog.drawer[open]`)).reverse().forEach(e=>e.close())}function a(e){return t(e)?.open??!1}function o(){return Array.from(document.querySelectorAll(`dialog.drawer[open]`))}function s(){let e=o();return e[e.length-1]??null}function c(e={}){let{id:t,content:n=``,direction:r,handle:i=!0,className:a=``,closeOnOutsideClick:o=!0}=e,s=document.createElement(`dialog`);return s.className=`drawer ${a}`.trim(),t&&(s.id=t),r&&(s.dataset.direction=r),o||(s.dataset.closeOnOutsideClick=`false`),s.innerHTML=`
|
|
1
|
+
import{t as e}from"./observer-DEB7SwSL.mjs";function t(e){return e?typeof e==`string`?document.getElementById(e):e:null}function n(e){t(e)?.showModal()}function r(e){t(e)?.close()}function i(){Array.from(document.querySelectorAll(`dialog.drawer[open]`)).reverse().forEach(e=>e.close())}function a(e){return t(e)?.open??!1}function o(){return Array.from(document.querySelectorAll(`dialog.drawer[open]`))}function s(){let e=o();return e[e.length-1]??null}function c(e={}){let{id:t,content:n=``,direction:r,handle:i=!0,className:a=``,closeOnOutsideClick:o=!0,scrollLock:s=!0}=e,c=document.createElement(`dialog`);return c.className=`drawer ${a}`.trim(),t&&(c.id=t),r&&(c.dataset.direction=r),o||(c.dataset.closeOnOutsideClick=`false`),s||(c.dataset.scrollLock=`false`),c.innerHTML=`
|
|
3
2
|
${i?`<div class="drawer-handle"></div>`:``}
|
|
4
3
|
<div class="drawer-content">${n}</div>
|
|
5
|
-
`,
|
|
4
|
+
`,c.addEventListener(`click`,e=>{e.target===c&&c.dataset.closeOnOutsideClick!==`false`&&c.close()}),c}function l(e){return document.body.appendChild(e),e}function u(e){t(e)?.remove()}function d(){let e=e=>{let t=e.target;t.matches(`dialog.drawer`)&&t.dataset.closeOnOutsideClick!==`false`&&t.close()};return document.addEventListener(`click`,e),()=>document.removeEventListener(`click`,e)}function f(n,r){let i=t(n);if(!i)return()=>{};let{onOpen:a,onClose:o,onCancel:s}=r,c=()=>o?.(),l=()=>s?.(),u=e=>{e.detail?.target===i&&i.open&&a?.()};return i.addEventListener(`close`,c),i.addEventListener(`cancel`,l),window.addEventListener(e,u),()=>{i.removeEventListener(`close`,c),i.removeEventListener(`cancel`,l),window.removeEventListener(e,u)}}function p(e,t){let n=t?.closeOnOutsideClick??!0;return{id:e,className:`drawer`,"data-close-on-outside-click":n?void 0:`false`,onClick:e=>{n&&e.target===e.currentTarget&&e.currentTarget.close()}}}export{r as close,i as closeAll,c as create,o as getOpen,s as getTop,d as init,a as isOpen,l as mount,n as open,p as props,f as subscribe,u as unmount};
|
|
6
5
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * CSS Drawer - Headless drawer component\n * Works with any framework: React, Vue, Svelte, vanilla JS\n */\nimport './drawer.css'\nimport { DRAWER_STATE_CHANGE } from './observer'\n\nexport type DrawerElement = HTMLDialogElement\n\nexport type DrawerRef = string | DrawerElement | null | undefined\n\nexport type DrawerDirection = 'bottom' | 'top' | 'left' | 'right' | 'modal'\n\nexport interface CreateDrawerOptions {\n /** Drawer ID */\n id?: string\n /** HTML content for the drawer */\n content?: string\n /** Direction the drawer opens from (default: 'bottom') */\n direction?: DrawerDirection\n /** Include drag handle (default: true) */\n handle?: boolean\n /** Additional CSS classes */\n className?: string\n /** Close when clicking outside (default: true) */\n closeOnOutsideClick?: boolean\n /** Lock body scroll while open (default: true) */\n scrollLock?: boolean\n}\n\nexport interface DrawerEventHandlers {\n /** Called when drawer opens */\n onOpen?: () => void\n /** Called when drawer closes */\n onClose?: () => void\n /** Called when drawer is cancelled (backdrop click or Escape) */\n onCancel?: () => void\n}\n\n/**\n * Resolve a drawer reference to an element\n */\nfunction resolve(drawer: DrawerRef): DrawerElement | null {\n if (!drawer) return null\n if (typeof drawer === 'string') {\n return document.getElementById(drawer) as DrawerElement | null\n }\n return drawer\n}\n\n/**\n * Open a drawer by ID or element reference\n */\nexport function open(drawer: DrawerRef): void {\n const el = resolve(drawer)\n el?.showModal()\n}\n\n/**\n * Close a drawer by ID or element reference\n */\nexport function close(drawer: DrawerRef): void {\n const el = resolve(drawer)\n el?.close()\n}\n\n/**\n * Close all open drawers (in reverse DOM order for proper animation)\n */\nexport function closeAll(): void {\n const drawers = Array.from(document.querySelectorAll<DrawerElement>('dialog.drawer[open]'))\n drawers.reverse().forEach((d) => d.close())\n}\n\n/**\n * Check if a drawer is open\n */\nexport function isOpen(drawer: DrawerRef): boolean {\n const el = resolve(drawer)\n return el?.open ?? false\n}\n\n/**\n * Get all open drawers\n */\nexport function getOpen(): DrawerElement[] {\n return Array.from(document.querySelectorAll<DrawerElement>('dialog.drawer[open]'))\n}\n\n/**\n * Get the topmost open drawer\n */\nexport function getTop(): DrawerElement | null {\n const open = getOpen()\n return open[open.length - 1] ?? null\n}\n\n/**\n * Create a drawer element programmatically\n */\nexport function create(options: CreateDrawerOptions = {}): DrawerElement {\n const { id, content = '', direction, handle = true, className = '', closeOnOutsideClick = true, scrollLock = true } = options\n\n const dialog = document.createElement('dialog') as DrawerElement\n dialog.className = `drawer ${className}`.trim()\n if (id) dialog.id = id\n if (direction) dialog.dataset.direction = direction\n if (!closeOnOutsideClick) {\n dialog.dataset.closeOnOutsideClick = 'false'\n }\n if (!scrollLock) {\n dialog.dataset.scrollLock = 'false'\n }\n\n dialog.innerHTML = `\n ${handle ? '<div class=\"drawer-handle\"></div>' : ''}\n <div class=\"drawer-content\">${content}</div>\n `\n\n // Backdrop click to close (respects data attribute)\n dialog.addEventListener('click', (e) => {\n if (e.target === dialog && dialog.dataset.closeOnOutsideClick !== 'false') {\n dialog.close()\n }\n })\n\n return dialog\n}\n\n/**\n * Mount a drawer to the DOM (appends to body)\n */\nexport function mount(drawer: DrawerElement): DrawerElement {\n document.body.appendChild(drawer)\n return drawer\n}\n\n/**\n * Unmount a drawer from the DOM\n */\nexport function unmount(drawer: DrawerRef): void {\n const el = resolve(drawer)\n el?.remove()\n}\n\n/**\n * Initialize global backdrop-click-to-close behavior\n * Alternative to adding onclick to each drawer\n * Respects data-close-on-outside-click=\"false\" attribute\n */\nexport function init(): () => void {\n const handler = (e: MouseEvent) => {\n const target = e.target as HTMLElement\n if (target.matches('dialog.drawer') && (target as DrawerElement).dataset.closeOnOutsideClick !== 'false') {\n ;(target as DrawerElement).close()\n }\n }\n\n document.addEventListener('click', handler)\n return () => document.removeEventListener('click', handler)\n}\n\n/**\n * Subscribe to drawer events\n * @returns Cleanup function\n */\nexport function subscribe(\n drawer: DrawerRef,\n handlers: DrawerEventHandlers\n): () => void {\n const el = resolve(drawer)\n if (!el) return () => {}\n\n const { onOpen, onClose, onCancel } = handlers\n\n const handleClose = () => onClose?.()\n const handleCancel = () => onCancel?.()\n\n // Listen to shared drawer state change event (no per-subscription observer)\n const handleStateChange = (e: Event) => {\n const detail = (e as CustomEvent).detail\n if (detail?.target === el && el.open) {\n onOpen?.()\n }\n }\n\n el.addEventListener('close', handleClose)\n el.addEventListener('cancel', handleCancel)\n window.addEventListener(DRAWER_STATE_CHANGE, handleStateChange)\n\n return () => {\n el.removeEventListener('close', handleClose)\n el.removeEventListener('cancel', handleCancel)\n window.removeEventListener(DRAWER_STATE_CHANGE, handleStateChange)\n }\n}\n\n/**\n * React-friendly hook helper - returns props to spread on dialog\n * Usage: <dialog {...drawer.props('my-drawer')} />\n */\nexport function props(id: string, options?: { closeOnOutsideClick?: boolean }) {\n const closeOnOutsideClick = options?.closeOnOutsideClick ?? true\n return {\n id,\n className: 'drawer',\n 'data-close-on-outside-click': closeOnOutsideClick ? undefined : 'false',\n onClick: (e: MouseEvent) => {\n if (closeOnOutsideClick && e.target === e.currentTarget) {\n ;(e.currentTarget as DrawerElement).close()\n }\n },\n } as const\n}\n\n"],"mappings":"4CA0CA,SAAS,EAAQ,EAAyC,CAKxD,OAJK,EACD,OAAO,GAAW,SACb,SAAS,eAAe,EAAO,CAEjC,EAJa,KAUtB,SAAgB,EAAK,EAAyB,CACjC,EAAQ,EACjB,EAAE,WAAW,CAMjB,SAAgB,EAAM,EAAyB,CAClC,EAAQ,EACjB,EAAE,OAAO,CAMb,SAAgB,GAAiB,CACf,MAAM,KAAK,SAAS,iBAAgC,sBAAsB,CACnF,CAAC,SAAS,CAAC,QAAS,GAAM,EAAE,OAAO,CAAC,CAM7C,SAAgB,EAAO,EAA4B,CAEjD,OADW,EAAQ,EACV,EAAE,MAAQ,GAMrB,SAAgB,GAA2B,CACzC,OAAO,MAAM,KAAK,SAAS,iBAAgC,sBAAsB,CAAC,CAMpF,SAAgB,GAA+B,CAC7C,IAAM,EAAO,GAAS,CACtB,OAAO,EAAK,EAAK,OAAS,IAAM,KAMlC,SAAgB,EAAO,EAA+B,EAAE,CAAiB,CACvE,GAAM,CAAE,KAAI,UAAU,GAAI,YAAW,SAAS,GAAM,YAAY,GAAI,sBAAsB,GAAM,aAAa,IAAS,EAEhH,EAAS,SAAS,cAAc,SAAS,CAuB/C,MAtBA,GAAO,UAAY,UAAU,IAAY,MAAM,CAC3C,IAAI,EAAO,GAAK,GAChB,IAAW,EAAO,QAAQ,UAAY,GACrC,IACH,EAAO,QAAQ,oBAAsB,SAElC,IACH,EAAO,QAAQ,WAAa,SAG9B,EAAO,UAAY;MACf,EAAS,oCAAsC,GAAG;kCACtB,EAAQ;IAIxC,EAAO,iBAAiB,QAAU,GAAM,CAClC,EAAE,SAAW,GAAU,EAAO,QAAQ,sBAAwB,SAChE,EAAO,OAAO,EAEhB,CAEK,EAMT,SAAgB,EAAM,EAAsC,CAE1D,OADA,SAAS,KAAK,YAAY,EAAO,CAC1B,EAMT,SAAgB,EAAQ,EAAyB,CACpC,EAAQ,EACjB,EAAE,QAAQ,CAQd,SAAgB,GAAmB,CACjC,IAAM,EAAW,GAAkB,CACjC,IAAM,EAAS,EAAE,OACb,EAAO,QAAQ,gBAAgB,EAAK,EAAyB,QAAQ,sBAAwB,SAC7F,EAAyB,OAAO,EAKtC,OADA,SAAS,iBAAiB,QAAS,EAAQ,KAC9B,SAAS,oBAAoB,QAAS,EAAQ,CAO7D,SAAgB,EACd,EACA,EACY,CACZ,IAAM,EAAK,EAAQ,EAAO,CAC1B,GAAI,CAAC,EAAI,UAAa,GAEtB,GAAM,CAAE,SAAQ,UAAS,YAAa,EAEhC,MAAoB,KAAW,CAC/B,MAAqB,KAAY,CAGjC,EAAqB,GAAa,CACtB,EAAkB,QACtB,SAAW,GAAM,EAAG,MAC9B,KAAU,EAQd,OAJA,EAAG,iBAAiB,QAAS,EAAY,CACzC,EAAG,iBAAiB,SAAU,EAAa,CAC3C,OAAO,iBAAiB,EAAqB,EAAkB,KAElD,CACX,EAAG,oBAAoB,QAAS,EAAY,CAC5C,EAAG,oBAAoB,SAAU,EAAa,CAC9C,OAAO,oBAAoB,EAAqB,EAAkB,EAQtE,SAAgB,EAAM,EAAY,EAA6C,CAC7E,IAAM,EAAsB,GAAS,qBAAuB,GAC5D,MAAO,CACL,KACA,UAAW,SACX,8BAA+B,EAAsB,IAAA,GAAY,QACjE,QAAU,GAAkB,CACtB,GAAuB,EAAE,SAAW,EAAE,eACtC,EAAE,cAAgC,OAAO,EAGhD"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import './style.css';
|
|
2
|
+
let e=null,t=0;function n(){if(e!==null||typeof window>`u`||typeof document>`u`)return;let t=document.body,n=document.documentElement,r=window.scrollY,i=window.scrollX;e={scrollX:i,scrollY:r,bodyPosition:t.style.position,bodyTop:t.style.top,bodyLeft:t.style.left,bodyRight:t.style.right,bodyWidth:t.style.width,htmlScrollBehavior:n.style.scrollBehavior},t.style.position=`fixed`,t.style.top=`-${r}px`,t.style.left=`-${i}px`,t.style.right=`0`,t.style.width=`100%`,n.style.scrollBehavior=`auto`}function r(){if(e===null||typeof window>`u`||typeof document>`u`)return;let{scrollX:t,scrollY:n,bodyPosition:r,bodyTop:i,bodyLeft:a,bodyRight:o,bodyWidth:s,htmlScrollBehavior:c}=e,l=document.body,u=document.documentElement;e=null,l.style.position=r,l.style.top=i,l.style.left=a,l.style.right=o,l.style.width=s,u.style.scrollBehavior=c,window.scrollTo({top:n,left:t,behavior:`instant`})}function i(){if(typeof document>`u`)return;let e=document.querySelectorAll(`dialog.drawer[open]:not([data-scroll-lock="false"])`).length;t===0&&e>0?n():t>0&&e===0&&r(),t=e}const a=`drawer:statechange`;let o=!1;function s(){if(typeof window>`u`||o)return;o=!0;let e=()=>{let e=Array.from(document.querySelectorAll(`dialog.drawer[open]`));e.forEach((t,n)=>{n===e.length-1?t.removeAttribute(`inert`):t.setAttribute(`inert`,``)})},t=t=>{e(),i(),window.dispatchEvent(new CustomEvent(a,{detail:{target:t}}))},n=e=>e.nodeType===1&&e.matches?.(`dialog.drawer`),r=e=>{if(e.nodeType!==1)return!1;let t=e;return t.matches?.(`dialog.drawer[open]`)?!0:!!t.querySelector?.(`dialog.drawer[open]`)};new MutationObserver(e=>{for(let i of e){if(i.type===`attributes`&&i.attributeName===`open`&&i.target.classList?.contains(`drawer`)){t(i.target);return}if(i.type===`childList`){for(let e of i.removedNodes)if(n(e)||r(e)){t(e);return}}}}).observe(document.body,{subtree:!0,attributes:!0,attributeFilter:[`open`],childList:!0})}s(),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return a}});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import './style.css';
|
|
2
|
+
let e=null,t=0;function n(){if(e!==null||typeof window>`u`||typeof document>`u`)return;let t=document.body,n=document.documentElement,r=window.scrollY,i=window.scrollX;e={scrollX:i,scrollY:r,bodyPosition:t.style.position,bodyTop:t.style.top,bodyLeft:t.style.left,bodyRight:t.style.right,bodyWidth:t.style.width,htmlScrollBehavior:n.style.scrollBehavior},t.style.position=`fixed`,t.style.top=`-${r}px`,t.style.left=`-${i}px`,t.style.right=`0`,t.style.width=`100%`,n.style.scrollBehavior=`auto`}function r(){if(e===null||typeof window>`u`||typeof document>`u`)return;let{scrollX:t,scrollY:n,bodyPosition:r,bodyTop:i,bodyLeft:a,bodyRight:o,bodyWidth:s,htmlScrollBehavior:c}=e,l=document.body,u=document.documentElement;e=null,l.style.position=r,l.style.top=i,l.style.left=a,l.style.right=o,l.style.width=s,u.style.scrollBehavior=c,window.scrollTo({top:n,left:t,behavior:`instant`})}function i(){if(typeof document>`u`)return;let e=document.querySelectorAll(`dialog.drawer[open]:not([data-scroll-lock="false"])`).length;t===0&&e>0?n():t>0&&e===0&&r(),t=e}const a=`drawer:statechange`;let o=!1;function s(){if(typeof window>`u`||o)return;o=!0;let e=()=>{let e=Array.from(document.querySelectorAll(`dialog.drawer[open]`));e.forEach((t,n)=>{n===e.length-1?t.removeAttribute(`inert`):t.setAttribute(`inert`,``)})},t=t=>{e(),i(),window.dispatchEvent(new CustomEvent(a,{detail:{target:t}}))},n=e=>e.nodeType===1&&e.matches?.(`dialog.drawer`),r=e=>{if(e.nodeType!==1)return!1;let t=e;return t.matches?.(`dialog.drawer[open]`)?!0:!!t.querySelector?.(`dialog.drawer[open]`)};new MutationObserver(e=>{for(let i of e){if(i.type===`attributes`&&i.attributeName===`open`&&i.target.classList?.contains(`drawer`)){t(i.target);return}if(i.type===`childList`){for(let e of i.removedNodes)if(n(e)||r(e)){t(e);return}}}}).observe(document.body,{subtree:!0,attributes:!0,attributeFilter:[`open`],childList:!0})}s();export{a as t};
|
|
3
|
+
//# sourceMappingURL=observer-DEB7SwSL.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer-DEB7SwSL.mjs","names":[],"sources":["../src/scroll-lock.ts","../src/observer.ts"],"sourcesContent":["/**\n * Body scroll lock for drawers.\n *\n * Uses the position:fixed + negative-top technique (same as Vaul / Radix\n * Dialog / body-scroll-lock). The lock is owned by this module and driven\n * by the shared MutationObserver in `./observer.ts`, which calls\n * `updateScrollLockFromDrawers()` on every drawer open/close.\n *\n * The lock is counter-based — incremented per opt-in drawer that's open,\n * decremented when one closes. The body styles are only applied on the\n * 0 → positive transition and only restored on the positive → 0 transition.\n * That means a same-tick close-A + open-B keeps the count at 1 → 1 and the\n * lock is never released mid-transition. This is the fix for the\n * drawer-to-drawer scroll race that consumers used to work around with\n * their own `useEffect`-based locks.\n *\n * Drawers can opt out with `data-scroll-lock=\"false\"`.\n */\n\ninterface Snapshot {\n scrollX: number\n scrollY: number\n bodyPosition: string\n bodyTop: string\n bodyLeft: string\n bodyRight: string\n bodyWidth: string\n htmlScrollBehavior: string\n}\n\nlet snapshot: Snapshot | null = null\nlet prevDrawerCount = 0\n\nfunction applyLock(): void {\n if (snapshot !== null) return\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n\n const body = document.body\n const html = document.documentElement\n const scrollY = window.scrollY\n const scrollX = window.scrollX\n\n snapshot = {\n scrollX,\n scrollY,\n bodyPosition: body.style.position,\n bodyTop: body.style.top,\n bodyLeft: body.style.left,\n bodyRight: body.style.right,\n bodyWidth: body.style.width,\n htmlScrollBehavior: html.style.scrollBehavior,\n }\n\n // `width: 100%` is required — without it the body collapses to content\n // width and the page visibly jumps horizontally on lock/unlock.\n body.style.position = 'fixed'\n body.style.top = `-${scrollY}px`\n body.style.left = `-${scrollX}px`\n body.style.right = '0'\n body.style.width = '100%'\n\n // Force instant scroll on release — a site-wide `scroll-behavior: smooth`\n // on <html> would otherwise animate the scrollTo restore.\n html.style.scrollBehavior = 'auto'\n}\n\nfunction releaseLock(): void {\n if (snapshot === null) return\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n\n const { scrollX, scrollY, bodyPosition, bodyTop, bodyLeft, bodyRight, bodyWidth, htmlScrollBehavior } = snapshot\n const body = document.body\n const html = document.documentElement\n\n snapshot = null\n\n // Order matters: clear styles BEFORE scrollTo. scrollTo while\n // `position: fixed` is still set is a no-op.\n body.style.position = bodyPosition\n body.style.top = bodyTop\n body.style.left = bodyLeft\n body.style.right = bodyRight\n body.style.width = bodyWidth\n html.style.scrollBehavior = htmlScrollBehavior\n\n window.scrollTo({ top: scrollY, left: scrollX, behavior: 'instant' as ScrollBehavior })\n}\n\n/**\n * @internal\n * Called by the shared drawer observer on every open/close. Recomputes the\n * count of currently-open drawers that haven't opted out, and toggles the\n * lock on 0 → positive / positive → 0 transitions.\n */\nexport function updateScrollLockFromDrawers(): void {\n if (typeof document === 'undefined') return\n const count = document.querySelectorAll(\n 'dialog.drawer[open]:not([data-scroll-lock=\"false\"])'\n ).length\n\n if (prevDrawerCount === 0 && count > 0) {\n applyLock()\n } else if (prevDrawerCount > 0 && count === 0) {\n releaseLock()\n }\n prevDrawerCount = count\n}\n","/**\n * Shared drawer state observer\n * Single MutationObserver that dispatches events for all drawer state changes\n */\nimport { updateScrollLockFromDrawers } from './scroll-lock'\n\nexport const DRAWER_STATE_CHANGE = 'drawer:statechange'\n\nlet initialized = false\n\nexport function initDrawerObserver(): void {\n if (typeof window === 'undefined' || initialized) return\n initialized = true\n\n const updateInertState = () => {\n const openDrawers = Array.from(\n document.querySelectorAll<HTMLDialogElement>('dialog.drawer[open]')\n )\n openDrawers.forEach((drawer, index) => {\n if (index === openDrawers.length - 1) {\n drawer.removeAttribute('inert')\n } else {\n drawer.setAttribute('inert', '')\n }\n })\n }\n\n const handleStateChange = (target: Node) => {\n updateInertState()\n updateScrollLockFromDrawers()\n window.dispatchEvent(new CustomEvent(DRAWER_STATE_CHANGE, {\n detail: { target }\n }))\n }\n\n const isDrawerElement = (node: Node): node is HTMLElement =>\n node.nodeType === 1 && (node as HTMLElement).matches?.('dialog.drawer')\n\n const containsOpenDrawer = (node: Node): boolean => {\n if (node.nodeType !== 1) return false\n const el = node as HTMLElement\n if (el.matches?.('dialog.drawer[open]')) return true\n return !!el.querySelector?.('dialog.drawer[open]')\n }\n\n const observer = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n if (\n mutation.type === 'attributes' &&\n mutation.attributeName === 'open' &&\n (mutation.target as HTMLElement).classList?.contains('drawer')\n ) {\n handleStateChange(mutation.target)\n return\n }\n\n if (mutation.type === 'childList') {\n // Catch the case where an open drawer is removed from the DOM\n // (e.g. React unmount without explicit close) — the `open` attribute\n // change never fires, so the scroll lock would otherwise leak.\n for (const node of mutation.removedNodes) {\n if (isDrawerElement(node) || containsOpenDrawer(node)) {\n handleStateChange(node)\n return\n }\n }\n }\n }\n })\n\n observer.observe(document.body, {\n subtree: true,\n attributes: true,\n attributeFilter: ['open'],\n childList: true,\n })\n}\n\n// Auto-initialize on import\ninitDrawerObserver()\n"],"mappings":"AA8BA,IAAI,EAA4B,KAC5B,EAAkB,EAEtB,SAAS,GAAkB,CAEzB,GADI,IAAa,MACb,OAAO,OAAW,KAAe,OAAO,SAAa,IAAa,OAEtE,IAAM,EAAO,SAAS,KAChB,EAAO,SAAS,gBAChB,EAAU,OAAO,QACjB,EAAU,OAAO,QAEvB,EAAW,CACT,UACA,UACA,aAAc,EAAK,MAAM,SACzB,QAAS,EAAK,MAAM,IACpB,SAAU,EAAK,MAAM,KACrB,UAAW,EAAK,MAAM,MACtB,UAAW,EAAK,MAAM,MACtB,mBAAoB,EAAK,MAAM,eAChC,CAID,EAAK,MAAM,SAAW,QACtB,EAAK,MAAM,IAAM,IAAI,EAAQ,IAC7B,EAAK,MAAM,KAAO,IAAI,EAAQ,IAC9B,EAAK,MAAM,MAAQ,IACnB,EAAK,MAAM,MAAQ,OAInB,EAAK,MAAM,eAAiB,OAG9B,SAAS,GAAoB,CAE3B,GADI,IAAa,MACb,OAAO,OAAW,KAAe,OAAO,SAAa,IAAa,OAEtE,GAAM,CAAE,UAAS,UAAS,eAAc,UAAS,WAAU,YAAW,YAAW,sBAAuB,EAClG,EAAO,SAAS,KAChB,EAAO,SAAS,gBAEtB,EAAW,KAIX,EAAK,MAAM,SAAW,EACtB,EAAK,MAAM,IAAM,EACjB,EAAK,MAAM,KAAO,EAClB,EAAK,MAAM,MAAQ,EACnB,EAAK,MAAM,MAAQ,EACnB,EAAK,MAAM,eAAiB,EAE5B,OAAO,SAAS,CAAE,IAAK,EAAS,KAAM,EAAS,SAAU,UAA6B,CAAC,CASzF,SAAgB,GAAoC,CAClD,GAAI,OAAO,SAAa,IAAa,OACrC,IAAM,EAAQ,SAAS,iBACrB,sDACD,CAAC,OAEE,IAAoB,GAAK,EAAQ,EACnC,GAAW,CACF,EAAkB,GAAK,IAAU,GAC1C,GAAa,CAEf,EAAkB,ECnGpB,MAAa,EAAsB,qBAEnC,IAAI,EAAc,GAElB,SAAgB,GAA2B,CACzC,GAAI,OAAO,OAAW,KAAe,EAAa,OAClD,EAAc,GAEd,IAAM,MAAyB,CAC7B,IAAM,EAAc,MAAM,KACxB,SAAS,iBAAoC,sBAAsB,CACpE,CACD,EAAY,SAAS,EAAQ,IAAU,CACjC,IAAU,EAAY,OAAS,EACjC,EAAO,gBAAgB,QAAQ,CAE/B,EAAO,aAAa,QAAS,GAAG,EAElC,EAGE,EAAqB,GAAiB,CAC1C,GAAkB,CAClB,GAA6B,CAC7B,OAAO,cAAc,IAAI,YAAY,EAAqB,CACxD,OAAQ,CAAE,SAAQ,CACnB,CAAC,CAAC,EAGC,EAAmB,GACvB,EAAK,WAAa,GAAM,EAAqB,UAAU,gBAAgB,CAEnE,EAAsB,GAAwB,CAClD,GAAI,EAAK,WAAa,EAAG,MAAO,GAChC,IAAM,EAAK,EAEX,OADI,EAAG,UAAU,sBAAsB,CAAS,GACzC,CAAC,CAAC,EAAG,gBAAgB,sBAAsB,EA4BpD,IAzBqB,iBAAkB,GAAc,CACnD,IAAK,IAAM,KAAY,EAAW,CAChC,GACE,EAAS,OAAS,cAClB,EAAS,gBAAkB,QAC1B,EAAS,OAAuB,WAAW,SAAS,SAAS,CAC9D,CACA,EAAkB,EAAS,OAAO,CAClC,OAGF,GAAI,EAAS,OAAS,iBAIf,IAAM,KAAQ,EAAS,aAC1B,GAAI,EAAgB,EAAK,EAAI,EAAmB,EAAK,CAAE,CACrD,EAAkB,EAAK,CACvB,WAOF,CAAC,QAAQ,SAAS,KAAM,CAC9B,QAAS,GACT,WAAY,GACZ,gBAAiB,CAAC,OAAO,CACzB,UAAW,GACZ,CAAC,CAIJ,GAAoB"}
|
package/dist/react.cjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
require(
|
|
2
|
-
const e=require(`./observer-B-WjvsFU.cjs`);let t=require(`react`),n=require(`react/jsx-runtime`);const r=(0,t.createContext)({direction:void 0});function i(){return(0,t.useContext)(r)}function a(){return typeof window>`u`?[]:Array.from(document.querySelectorAll(`dialog.drawer[open]`))}function o(){let e=a();return e[e.length-1]??null}function s(n){let[r,i]=(0,t.useState)(!1);return(0,t.useEffect)(()=>{let t=()=>{let e=o();i(n.current!==null&&n.current===e)};return t(),window.addEventListener(e.t,t),()=>window.removeEventListener(e.t,t)},[n]),r}function c({children:e,direction:t}){return(0,n.jsx)(r.Provider,{value:{direction:t},children:e})}const l=(0,t.forwardRef)(({children:e,className:r,open:a,onOpenChange:o,closeOnOutsideClick:s=!0,...c},l)=>{let{direction:u}=i(),d=(0,t.useRef)(null),f=l||d;return(0,t.useEffect)(()=>{let e=f.current;e&&(a&&!e.open?(e.showModal(),o?.(!0)):a===!1&&e.open&&e.close())},[a]),(0,n.jsx)(`dialog`,{ref:f,className:`drawer ${r??``}`.trim(),"data-direction":u,onClose:e=>{e.target===e.currentTarget&&(c.onClose?.(e),o?.(!1))},onClick:e=>{c.onClick?.(e),s&&e.target===e.currentTarget&&e.currentTarget.close()},...c,children:e})});l.displayName=`Drawer.Content`;const u=(0,t.forwardRef)(({className:e,...t},r)=>(0,n.jsx)(`div`,{ref:r,className:`drawer-handle ${e??``}`.trim(),"aria-hidden":`true`,...t}));u.displayName=`Drawer.Handle`;const d=(0,t.forwardRef)((e,t)=>(0,n.jsx)(`h2`,{ref:t,...e}));d.displayName=`Drawer.Title`;const f=(0,t.forwardRef)((e,t)=>(0,n.jsx)(`p`,{ref:t,...e}));f.displayName=`Drawer.Description`;const p={Root:c,Content:l,Handle:u,Title:d,Description:f};exports.Drawer=p,exports.getTopDrawer=o,exports.useIsTopDrawer=s;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./observer-BZAUqGHC.cjs`);let t=require(`react`),n=require(`react/jsx-runtime`);const r=(0,t.createContext)({direction:void 0});function i(){return(0,t.useContext)(r)}function a(){return typeof window>`u`?[]:Array.from(document.querySelectorAll(`dialog.drawer[open]`))}function o(){let e=a();return e[e.length-1]??null}function s(n){let[r,i]=(0,t.useState)(!1);return(0,t.useEffect)(()=>{let t=()=>{let e=o();i(n.current!==null&&n.current===e)};return t(),window.addEventListener(e.t,t),()=>window.removeEventListener(e.t,t)},[n]),r}function c({children:e,direction:t}){return(0,n.jsx)(r.Provider,{value:{direction:t},children:e})}const l=(0,t.forwardRef)(({children:e,className:r,open:a,onOpenChange:o,closeOnOutsideClick:s=!0,scrollLock:c=!0,...l},u)=>{let{direction:d}=i(),f=(0,t.useRef)(null),p=u||f;return(0,t.useEffect)(()=>{let e=p.current;e&&(a&&!e.open?(e.showModal(),o?.(!0)):a===!1&&e.open&&e.close())},[a]),(0,n.jsx)(`dialog`,{ref:p,className:`drawer ${r??``}`.trim(),"data-direction":d,"data-scroll-lock":c?void 0:`false`,onClose:e=>{e.target===e.currentTarget&&(l.onClose?.(e),o?.(!1))},onClick:e=>{l.onClick?.(e),s&&e.target===e.currentTarget&&e.currentTarget.close()},...l,children:e})});l.displayName=`Drawer.Content`;const u=(0,t.forwardRef)(({className:e,...t},r)=>(0,n.jsx)(`div`,{ref:r,className:`drawer-handle ${e??``}`.trim(),"aria-hidden":`true`,...t}));u.displayName=`Drawer.Handle`;const d=(0,t.forwardRef)((e,t)=>(0,n.jsx)(`h2`,{ref:t,...e}));d.displayName=`Drawer.Title`;const f=(0,t.forwardRef)((e,t)=>(0,n.jsx)(`p`,{ref:t,...e}));f.displayName=`Drawer.Description`;const p={Root:c,Content:l,Handle:u,Title:d,Description:f};exports.Drawer=p,exports.getTopDrawer=o,exports.useIsTopDrawer=s;
|
package/dist/react.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
-
import * as
|
|
1
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
import * as _$react from "react";
|
|
3
3
|
import { ComponentPropsWithoutRef, ReactNode, RefObject } from "react";
|
|
4
4
|
|
|
5
5
|
//#region src/react.d.ts
|
|
@@ -21,7 +21,7 @@ interface RootProps {
|
|
|
21
21
|
declare function Root({
|
|
22
22
|
children,
|
|
23
23
|
direction
|
|
24
|
-
}: RootProps): react_jsx_runtime0.JSX.Element;
|
|
24
|
+
}: RootProps): _$react_jsx_runtime0.JSX.Element;
|
|
25
25
|
interface ContentProps extends Omit<ComponentPropsWithoutRef<'dialog'>, 'open'> {
|
|
26
26
|
/** Controlled open state */
|
|
27
27
|
open?: boolean;
|
|
@@ -29,16 +29,23 @@ interface ContentProps extends Omit<ComponentPropsWithoutRef<'dialog'>, 'open'>
|
|
|
29
29
|
onOpenChange?: (open: boolean) => void;
|
|
30
30
|
/** Close when clicking outside the drawer (default: true) */
|
|
31
31
|
closeOnOutsideClick?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Lock body scroll while this drawer is open (default: true).
|
|
34
|
+
*
|
|
35
|
+
* Set to `false` for the rare side-panel-allows-scroll case. The CSS-only
|
|
36
|
+
* fallback (`overflow: hidden`) is also disabled for opted-out drawers.
|
|
37
|
+
*/
|
|
38
|
+
scrollLock?: boolean;
|
|
32
39
|
}
|
|
33
40
|
interface HandleProps extends ComponentPropsWithoutRef<'div'> {}
|
|
34
41
|
interface TitleProps extends ComponentPropsWithoutRef<'h2'> {}
|
|
35
42
|
interface DescriptionProps extends ComponentPropsWithoutRef<'p'> {}
|
|
36
43
|
declare const Drawer: {
|
|
37
44
|
Root: typeof Root;
|
|
38
|
-
Content:
|
|
39
|
-
Handle:
|
|
40
|
-
Title:
|
|
41
|
-
Description:
|
|
45
|
+
Content: _$react.ForwardRefExoticComponent<ContentProps & _$react.RefAttributes<HTMLDialogElement>>;
|
|
46
|
+
Handle: _$react.ForwardRefExoticComponent<HandleProps & _$react.RefAttributes<HTMLDivElement>>;
|
|
47
|
+
Title: _$react.ForwardRefExoticComponent<TitleProps & _$react.RefAttributes<HTMLHeadingElement>>;
|
|
48
|
+
Description: _$react.ForwardRefExoticComponent<DescriptionProps & _$react.RefAttributes<HTMLParagraphElement>>;
|
|
42
49
|
};
|
|
43
50
|
//#endregion
|
|
44
51
|
export { Drawer, type ContentProps as DrawerContentProps, type DescriptionProps as DrawerDescriptionProps, type Direction as DrawerDirection, type HandleProps as DrawerHandleProps, type RootProps as DrawerRootProps, type TitleProps as DrawerTitleProps, getTopDrawer, useIsTopDrawer };
|
package/dist/react.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.cts","names":[],"sources":["../src/react.tsx"],"
|
|
1
|
+
{"version":3,"file":"react.d.cts","names":[],"sources":["../src/react.tsx"],"mappings":";;;;;KAeK,SAAA;;;AAJgB;iBA8BL,YAAA,CAAA,GAAgB,iBAAA;;;;AAAhC;iBASgB,cAAA,CAAe,GAAA,EAAK,SAAA,CAAU,iBAAA;AAAA,UAqBpC,SAAA;EACR,QAAA,EAAU,SAAA;EA/BqC;EAiC/C,SAAA,GAAY,SAAA;AAAA;AAAA,iBAGL,IAAA,CAAA;EAAO,QAAA;EAAU;AAAA,GAAa,SAAA,GAAS,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,UAStC,YAAA,SAAqB,IAAA,CAAK,wBAAA;EApCA;EAsClC,IAAA;EAtC6B;EAwC7B,YAAA,IAAgB,IAAA;EAxCqD;EA0CrE,mBAAA;EArBiB;;;;;;EA4BjB,UAAA;AAAA;AAAA,UAoDQ,WAAA,SAAoB,wBAAA;AAAA,UAapB,UAAA,SAAmB,wBAAA;AAAA,UAQnB,gBAAA,SAAyB,wBAAA;AAAA,cAQtB,MAAA"}
|
package/dist/react.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as _$react from "react";
|
|
2
2
|
import { ComponentPropsWithoutRef, ReactNode, RefObject } from "react";
|
|
3
|
-
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/react.d.ts
|
|
6
6
|
type Direction = 'bottom' | 'top' | 'left' | 'right' | 'modal';
|
|
@@ -21,7 +21,7 @@ interface RootProps {
|
|
|
21
21
|
declare function Root({
|
|
22
22
|
children,
|
|
23
23
|
direction
|
|
24
|
-
}: RootProps): react_jsx_runtime0.JSX.Element;
|
|
24
|
+
}: RootProps): _$react_jsx_runtime0.JSX.Element;
|
|
25
25
|
interface ContentProps extends Omit<ComponentPropsWithoutRef<'dialog'>, 'open'> {
|
|
26
26
|
/** Controlled open state */
|
|
27
27
|
open?: boolean;
|
|
@@ -29,16 +29,23 @@ interface ContentProps extends Omit<ComponentPropsWithoutRef<'dialog'>, 'open'>
|
|
|
29
29
|
onOpenChange?: (open: boolean) => void;
|
|
30
30
|
/** Close when clicking outside the drawer (default: true) */
|
|
31
31
|
closeOnOutsideClick?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Lock body scroll while this drawer is open (default: true).
|
|
34
|
+
*
|
|
35
|
+
* Set to `false` for the rare side-panel-allows-scroll case. The CSS-only
|
|
36
|
+
* fallback (`overflow: hidden`) is also disabled for opted-out drawers.
|
|
37
|
+
*/
|
|
38
|
+
scrollLock?: boolean;
|
|
32
39
|
}
|
|
33
40
|
interface HandleProps extends ComponentPropsWithoutRef<'div'> {}
|
|
34
41
|
interface TitleProps extends ComponentPropsWithoutRef<'h2'> {}
|
|
35
42
|
interface DescriptionProps extends ComponentPropsWithoutRef<'p'> {}
|
|
36
43
|
declare const Drawer: {
|
|
37
44
|
Root: typeof Root;
|
|
38
|
-
Content:
|
|
39
|
-
Handle:
|
|
40
|
-
Title:
|
|
41
|
-
Description:
|
|
45
|
+
Content: _$react.ForwardRefExoticComponent<ContentProps & _$react.RefAttributes<HTMLDialogElement>>;
|
|
46
|
+
Handle: _$react.ForwardRefExoticComponent<HandleProps & _$react.RefAttributes<HTMLDivElement>>;
|
|
47
|
+
Title: _$react.ForwardRefExoticComponent<TitleProps & _$react.RefAttributes<HTMLHeadingElement>>;
|
|
48
|
+
Description: _$react.ForwardRefExoticComponent<DescriptionProps & _$react.RefAttributes<HTMLParagraphElement>>;
|
|
42
49
|
};
|
|
43
50
|
//#endregion
|
|
44
51
|
export { Drawer, type ContentProps as DrawerContentProps, type DescriptionProps as DrawerDescriptionProps, type Direction as DrawerDirection, type HandleProps as DrawerHandleProps, type RootProps as DrawerRootProps, type TitleProps as DrawerTitleProps, getTopDrawer, useIsTopDrawer };
|
package/dist/react.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.mts","names":[],"sources":["../src/react.tsx"],"
|
|
1
|
+
{"version":3,"file":"react.d.mts","names":[],"sources":["../src/react.tsx"],"mappings":";;;;;KAeK,SAAA;;;AAJgB;iBA8BL,YAAA,CAAA,GAAgB,iBAAA;;;;AAAhC;iBASgB,cAAA,CAAe,GAAA,EAAK,SAAA,CAAU,iBAAA;AAAA,UAqBpC,SAAA;EACR,QAAA,EAAU,SAAA;EA/BqC;EAiC/C,SAAA,GAAY,SAAA;AAAA;AAAA,iBAGL,IAAA,CAAA;EAAO,QAAA;EAAU;AAAA,GAAa,SAAA,GAAS,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,UAStC,YAAA,SAAqB,IAAA,CAAK,wBAAA;EApCA;EAsClC,IAAA;EAtC6B;EAwC7B,YAAA,IAAgB,IAAA;EAxCqD;EA0CrE,mBAAA;EArBiB;;;;;;EA4BjB,UAAA;AAAA;AAAA,UAoDQ,WAAA,SAAoB,wBAAA;AAAA,UAapB,UAAA,SAAmB,wBAAA;AAAA,UAQnB,gBAAA,SAAyB,wBAAA;AAAA,cAQtB,MAAA"}
|
package/dist/react.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./observer-
|
|
2
|
-
const c=t({direction:void 0});function l(){return r(c)}function u(){return typeof window>`u`?[]:Array.from(document.querySelectorAll(`dialog.drawer[open]`))}function d(){let e=u();return e[e.length-1]??null}function f(t){let[n,r]=o(!1);return i(()=>{let n=()=>{let e=d();r(t.current!==null&&t.current===e)};return n(),window.addEventListener(e,n),()=>window.removeEventListener(e,n)},[t]),n}function p({children:e,direction:t}){return s(c.Provider,{value:{direction:t},children:e})}const m=n(({children:e,className:t,open:n,onOpenChange:r,closeOnOutsideClick:o=!0,...c},u)=>{let{direction:d}=l(),f=a(null),p=u||f;return i(()=>{let e=p.current;e&&(n&&!e.open?(e.showModal(),r?.(!0)):n===!1&&e.open&&e.close())},[n]),s(`dialog`,{ref:p,className:`drawer ${t??``}`.trim(),"data-direction":d,onClose:e=>{e.target===e.currentTarget&&(c.onClose?.(e),r?.(!1))},onClick:e=>{c.onClick?.(e),o&&e.target===e.currentTarget&&e.currentTarget.close()},...c,children:e})});m.displayName=`Drawer.Content`;const h=n(({className:e,...t},n)=>s(`div`,{ref:n,className:`drawer-handle ${e??``}`.trim(),"aria-hidden":`true`,...t}));h.displayName=`Drawer.Handle`;const g=n((e,t)=>s(`h2`,{ref:t,...e}));g.displayName=`Drawer.Title`;const _=n((e,t)=>s(`p`,{ref:t,...e}));_.displayName=`Drawer.Description`;const v={Root:p,Content:m,Handle:h,Title:g,Description:_};export{v as Drawer,d as getTopDrawer,f as useIsTopDrawer};
|
|
1
|
+
import{t as e}from"./observer-DEB7SwSL.mjs";import{createContext as t,forwardRef as n,useContext as r,useEffect as i,useRef as a,useState as o}from"react";import{jsx as s}from"react/jsx-runtime";const c=t({direction:void 0});function l(){return r(c)}function u(){return typeof window>`u`?[]:Array.from(document.querySelectorAll(`dialog.drawer[open]`))}function d(){let e=u();return e[e.length-1]??null}function f(t){let[n,r]=o(!1);return i(()=>{let n=()=>{let e=d();r(t.current!==null&&t.current===e)};return n(),window.addEventListener(e,n),()=>window.removeEventListener(e,n)},[t]),n}function p({children:e,direction:t}){return s(c.Provider,{value:{direction:t},children:e})}const m=n(({children:e,className:t,open:n,onOpenChange:r,closeOnOutsideClick:o=!0,scrollLock:c=!0,...u},d)=>{let{direction:f}=l(),p=a(null),m=d||p;return i(()=>{let e=m.current;e&&(n&&!e.open?(e.showModal(),r?.(!0)):n===!1&&e.open&&e.close())},[n]),s(`dialog`,{ref:m,className:`drawer ${t??``}`.trim(),"data-direction":f,"data-scroll-lock":c?void 0:`false`,onClose:e=>{e.target===e.currentTarget&&(u.onClose?.(e),r?.(!1))},onClick:e=>{u.onClick?.(e),o&&e.target===e.currentTarget&&e.currentTarget.close()},...u,children:e})});m.displayName=`Drawer.Content`;const h=n(({className:e,...t},n)=>s(`div`,{ref:n,className:`drawer-handle ${e??``}`.trim(),"aria-hidden":`true`,...t}));h.displayName=`Drawer.Handle`;const g=n((e,t)=>s(`h2`,{ref:t,...e}));g.displayName=`Drawer.Title`;const _=n((e,t)=>s(`p`,{ref:t,...e}));_.displayName=`Drawer.Description`;const v={Root:p,Content:m,Handle:h,Title:g,Description:_};export{v as Drawer,d as getTopDrawer,f as useIsTopDrawer};
|
|
3
2
|
//# sourceMappingURL=react.mjs.map
|
package/dist/react.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.mjs","names":[],"sources":["../src/react.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n forwardRef,\n useEffect,\n useRef,\n useState,\n type ReactNode,\n type ComponentPropsWithoutRef,\n type RefObject,\n} from 'react'\nimport './drawer.css'\nimport { DRAWER_STATE_CHANGE } from './observer'\n\n/* ===== Types ===== */\ntype Direction = 'bottom' | 'top' | 'left' | 'right' | 'modal'\n\ninterface DrawerContextValue {\n direction?: Direction\n}\n\n/* ===== Context ===== */\nconst DrawerContext = createContext<DrawerContextValue>({ direction: undefined })\n\nfunction useDrawerContext() {\n return useContext(DrawerContext)\n}\n\n/* ===== Utilities ===== */\n\n/**\n * Get all open drawers\n */\nfunction getOpenDrawers(): HTMLDialogElement[] {\n if (typeof window === 'undefined') return []\n return Array.from(document.querySelectorAll<HTMLDialogElement>('dialog.drawer[open]'))\n}\n\n/**\n * Get the topmost open drawer\n */\nexport function getTopDrawer(): HTMLDialogElement | null {\n const open = getOpenDrawers()\n return open[open.length - 1] ?? null\n}\n\n/**\n * Hook to check if a drawer is the topmost open drawer\n * Useful for conditionally rendering content only in the top drawer\n */\nexport function useIsTopDrawer(ref: RefObject<HTMLDialogElement | null>): boolean {\n const [isTop, setIsTop] = useState(false)\n\n useEffect(() => {\n const checkIsTop = () => {\n const topDrawer = getTopDrawer()\n setIsTop(ref.current !== null && ref.current === topDrawer)\n }\n\n // Initial check\n checkIsTop()\n\n // Listen to shared drawer state change event (single observer, not N observers)\n window.addEventListener(DRAWER_STATE_CHANGE, checkIsTop)\n return () => window.removeEventListener(DRAWER_STATE_CHANGE, checkIsTop)\n }, [ref])\n\n return isTop\n}\n\n/* ===== Root ===== */\ninterface RootProps {\n children: ReactNode\n /** Direction the drawer opens from */\n direction?: Direction\n}\n\nfunction Root({ children, direction }: RootProps) {\n return (\n <DrawerContext.Provider value={{ direction }}>\n {children}\n </DrawerContext.Provider>\n )\n}\n\n/* ===== Content ===== */\ninterface ContentProps extends Omit<ComponentPropsWithoutRef<'dialog'>, 'open'> {\n /** Controlled open state */\n open?: boolean\n /** Called when open state changes */\n onOpenChange?: (open: boolean) => void\n /** Close when clicking outside the drawer (default: true) */\n closeOnOutsideClick?: boolean\n}\n\nconst Content = forwardRef<HTMLDialogElement, ContentProps>(\n ({ children, className, open, onOpenChange, closeOnOutsideClick = true, ...props }, ref) => {\n const { direction } = useDrawerContext()\n const internalRef = useRef<HTMLDialogElement>(null)\n const dialogRef = (ref as React.RefObject<HTMLDialogElement>) || internalRef\n\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog) return\n\n if (open && !dialog.open) {\n dialog.showModal()\n onOpenChange?.(true)\n } else if (open === false && dialog.open) {\n dialog.close()\n }\n }, [open])\n\n return (\n <dialog\n ref={dialogRef}\n className={`drawer ${className ?? ''}`.trim()}\n data-direction={direction}\n onClose={(e) => {\n // Only handle close event if it originated from THIS dialog\n // This prevents nested dialogs from triggering parent dialog closes\n if (e.target !== e.currentTarget) return\n\n props.onClose?.(e)\n onOpenChange?.(false)\n }}\n onClick={(e) => {\n props.onClick?.(e)\n // Backdrop click - only if clicking the dialog element itself\n if (closeOnOutsideClick && e.target === e.currentTarget) {\n e.currentTarget.close()\n }\n }}\n {...props}\n >\n {children}\n </dialog>\n )\n }\n)\nContent.displayName = 'Drawer.Content'\n\n/* ===== Handle ===== */\ninterface HandleProps extends ComponentPropsWithoutRef<'div'> {}\n\nconst Handle = forwardRef<HTMLDivElement, HandleProps>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={`drawer-handle ${className ?? ''}`.trim()}\n aria-hidden=\"true\"\n {...props}\n />\n))\nHandle.displayName = 'Drawer.Handle'\n\n/* ===== Title ===== */\ninterface TitleProps extends ComponentPropsWithoutRef<'h2'> {}\n\nconst Title = forwardRef<HTMLHeadingElement, TitleProps>((props, ref) => (\n <h2 ref={ref} {...props} />\n))\nTitle.displayName = 'Drawer.Title'\n\n/* ===== Description ===== */\ninterface DescriptionProps extends ComponentPropsWithoutRef<'p'> {}\n\nconst Description = forwardRef<HTMLParagraphElement, DescriptionProps>((props, ref) => (\n <p ref={ref} {...props} />\n))\nDescription.displayName = 'Drawer.Description'\n\n/* ===== Namespace Export ===== */\nexport const Drawer = {\n Root,\n Content,\n Handle,\n Title,\n Description,\n}\n\nexport type {\n RootProps as DrawerRootProps,\n ContentProps as DrawerContentProps,\n HandleProps as DrawerHandleProps,\n TitleProps as DrawerTitleProps,\n DescriptionProps as DrawerDescriptionProps,\n Direction as DrawerDirection,\n}\n"],"mappings":"mMAsBA,MAAM,EAAgB,EAAkC,CAAE,UAAW,IAAA,GAAW,CAAC,CAEjF,SAAS,GAAmB,CAC1B,OAAO,EAAW,EAAc,CAQlC,SAAS,GAAsC,CAE7C,OADI,OAAO,OAAW,IAAoB,EAAE,CACrC,MAAM,KAAK,SAAS,iBAAoC,sBAAsB,CAAC,CAMxF,SAAgB,GAAyC,CACvD,IAAM,EAAO,GAAgB,CAC7B,OAAO,EAAK,EAAK,OAAS,IAAM,KAOlC,SAAgB,EAAe,EAAmD,CAChF,GAAM,CAAC,EAAO,GAAY,EAAS,GAAM,CAgBzC,OAdA,MAAgB,CACd,IAAM,MAAmB,CACvB,IAAM,EAAY,GAAc,CAChC,EAAS,EAAI,UAAY,MAAQ,EAAI,UAAY,EAAU,EAQ7D,OAJA,GAAY,CAGZ,OAAO,iBAAiB,EAAqB,EAAW,KAC3C,OAAO,oBAAoB,EAAqB,EAAW,EACvE,CAAC,EAAI,CAAC,CAEF,EAUT,SAAS,EAAK,CAAE,WAAU,aAAwB,CAChD,OACE,EAAC,EAAc,
|
|
1
|
+
{"version":3,"file":"react.mjs","names":[],"sources":["../src/react.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n forwardRef,\n useEffect,\n useRef,\n useState,\n type ReactNode,\n type ComponentPropsWithoutRef,\n type RefObject,\n} from 'react'\nimport './drawer.css'\nimport { DRAWER_STATE_CHANGE } from './observer'\n\n/* ===== Types ===== */\ntype Direction = 'bottom' | 'top' | 'left' | 'right' | 'modal'\n\ninterface DrawerContextValue {\n direction?: Direction\n}\n\n/* ===== Context ===== */\nconst DrawerContext = createContext<DrawerContextValue>({ direction: undefined })\n\nfunction useDrawerContext() {\n return useContext(DrawerContext)\n}\n\n/* ===== Utilities ===== */\n\n/**\n * Get all open drawers\n */\nfunction getOpenDrawers(): HTMLDialogElement[] {\n if (typeof window === 'undefined') return []\n return Array.from(document.querySelectorAll<HTMLDialogElement>('dialog.drawer[open]'))\n}\n\n/**\n * Get the topmost open drawer\n */\nexport function getTopDrawer(): HTMLDialogElement | null {\n const open = getOpenDrawers()\n return open[open.length - 1] ?? null\n}\n\n/**\n * Hook to check if a drawer is the topmost open drawer\n * Useful for conditionally rendering content only in the top drawer\n */\nexport function useIsTopDrawer(ref: RefObject<HTMLDialogElement | null>): boolean {\n const [isTop, setIsTop] = useState(false)\n\n useEffect(() => {\n const checkIsTop = () => {\n const topDrawer = getTopDrawer()\n setIsTop(ref.current !== null && ref.current === topDrawer)\n }\n\n // Initial check\n checkIsTop()\n\n // Listen to shared drawer state change event (single observer, not N observers)\n window.addEventListener(DRAWER_STATE_CHANGE, checkIsTop)\n return () => window.removeEventListener(DRAWER_STATE_CHANGE, checkIsTop)\n }, [ref])\n\n return isTop\n}\n\n/* ===== Root ===== */\ninterface RootProps {\n children: ReactNode\n /** Direction the drawer opens from */\n direction?: Direction\n}\n\nfunction Root({ children, direction }: RootProps) {\n return (\n <DrawerContext.Provider value={{ direction }}>\n {children}\n </DrawerContext.Provider>\n )\n}\n\n/* ===== Content ===== */\ninterface ContentProps extends Omit<ComponentPropsWithoutRef<'dialog'>, 'open'> {\n /** Controlled open state */\n open?: boolean\n /** Called when open state changes */\n onOpenChange?: (open: boolean) => void\n /** Close when clicking outside the drawer (default: true) */\n closeOnOutsideClick?: boolean\n /**\n * Lock body scroll while this drawer is open (default: true).\n *\n * Set to `false` for the rare side-panel-allows-scroll case. The CSS-only\n * fallback (`overflow: hidden`) is also disabled for opted-out drawers.\n */\n scrollLock?: boolean\n}\n\nconst Content = forwardRef<HTMLDialogElement, ContentProps>(\n ({ children, className, open, onOpenChange, closeOnOutsideClick = true, scrollLock = true, ...props }, ref) => {\n const { direction } = useDrawerContext()\n const internalRef = useRef<HTMLDialogElement>(null)\n const dialogRef = (ref as React.RefObject<HTMLDialogElement>) || internalRef\n\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog) return\n\n if (open && !dialog.open) {\n dialog.showModal()\n onOpenChange?.(true)\n } else if (open === false && dialog.open) {\n dialog.close()\n }\n }, [open])\n\n return (\n <dialog\n ref={dialogRef}\n className={`drawer ${className ?? ''}`.trim()}\n data-direction={direction}\n data-scroll-lock={scrollLock ? undefined : 'false'}\n onClose={(e) => {\n // Only handle close event if it originated from THIS dialog\n // This prevents nested dialogs from triggering parent dialog closes\n if (e.target !== e.currentTarget) return\n\n props.onClose?.(e)\n onOpenChange?.(false)\n }}\n onClick={(e) => {\n props.onClick?.(e)\n // Backdrop click - only if clicking the dialog element itself\n if (closeOnOutsideClick && e.target === e.currentTarget) {\n e.currentTarget.close()\n }\n }}\n {...props}\n >\n {children}\n </dialog>\n )\n }\n)\nContent.displayName = 'Drawer.Content'\n\n/* ===== Handle ===== */\ninterface HandleProps extends ComponentPropsWithoutRef<'div'> {}\n\nconst Handle = forwardRef<HTMLDivElement, HandleProps>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={`drawer-handle ${className ?? ''}`.trim()}\n aria-hidden=\"true\"\n {...props}\n />\n))\nHandle.displayName = 'Drawer.Handle'\n\n/* ===== Title ===== */\ninterface TitleProps extends ComponentPropsWithoutRef<'h2'> {}\n\nconst Title = forwardRef<HTMLHeadingElement, TitleProps>((props, ref) => (\n <h2 ref={ref} {...props} />\n))\nTitle.displayName = 'Drawer.Title'\n\n/* ===== Description ===== */\ninterface DescriptionProps extends ComponentPropsWithoutRef<'p'> {}\n\nconst Description = forwardRef<HTMLParagraphElement, DescriptionProps>((props, ref) => (\n <p ref={ref} {...props} />\n))\nDescription.displayName = 'Drawer.Description'\n\n/* ===== Namespace Export ===== */\nexport const Drawer = {\n Root,\n Content,\n Handle,\n Title,\n Description,\n}\n\nexport type {\n RootProps as DrawerRootProps,\n ContentProps as DrawerContentProps,\n HandleProps as DrawerHandleProps,\n TitleProps as DrawerTitleProps,\n DescriptionProps as DrawerDescriptionProps,\n Direction as DrawerDirection,\n}\n"],"mappings":"mMAsBA,MAAM,EAAgB,EAAkC,CAAE,UAAW,IAAA,GAAW,CAAC,CAEjF,SAAS,GAAmB,CAC1B,OAAO,EAAW,EAAc,CAQlC,SAAS,GAAsC,CAE7C,OADI,OAAO,OAAW,IAAoB,EAAE,CACrC,MAAM,KAAK,SAAS,iBAAoC,sBAAsB,CAAC,CAMxF,SAAgB,GAAyC,CACvD,IAAM,EAAO,GAAgB,CAC7B,OAAO,EAAK,EAAK,OAAS,IAAM,KAOlC,SAAgB,EAAe,EAAmD,CAChF,GAAM,CAAC,EAAO,GAAY,EAAS,GAAM,CAgBzC,OAdA,MAAgB,CACd,IAAM,MAAmB,CACvB,IAAM,EAAY,GAAc,CAChC,EAAS,EAAI,UAAY,MAAQ,EAAI,UAAY,EAAU,EAQ7D,OAJA,GAAY,CAGZ,OAAO,iBAAiB,EAAqB,EAAW,KAC3C,OAAO,oBAAoB,EAAqB,EAAW,EACvE,CAAC,EAAI,CAAC,CAEF,EAUT,SAAS,EAAK,CAAE,WAAU,aAAwB,CAChD,OACE,EAAC,EAAc,SAAf,CAAwB,MAAO,CAAE,YAAW,CACzC,WACsB,CAAA,CAqB7B,MAAM,EAAU,GACb,CAAE,WAAU,YAAW,OAAM,eAAc,sBAAsB,GAAM,aAAa,GAAM,GAAG,GAAS,IAAQ,CAC7G,GAAM,CAAE,aAAc,GAAkB,CAClC,EAAc,EAA0B,KAAK,CAC7C,EAAa,GAA8C,EAcjE,OAZA,MAAgB,CACd,IAAM,EAAS,EAAU,QACpB,IAED,GAAQ,CAAC,EAAO,MAClB,EAAO,WAAW,CAClB,IAAe,GAAK,EACX,IAAS,IAAS,EAAO,MAClC,EAAO,OAAO,GAEf,CAAC,EAAK,CAAC,CAGR,EAAC,SAAD,CACE,IAAK,EACL,UAAW,UAAU,GAAa,KAAK,MAAM,CAC7C,iBAAgB,EAChB,mBAAkB,EAAa,IAAA,GAAY,QAC3C,QAAU,GAAM,CAGV,EAAE,SAAW,EAAE,gBAEnB,EAAM,UAAU,EAAE,CAClB,IAAe,GAAM,GAEvB,QAAU,GAAM,CACd,EAAM,UAAU,EAAE,CAEd,GAAuB,EAAE,SAAW,EAAE,eACxC,EAAE,cAAc,OAAO,EAG3B,GAAI,EAEH,WACM,CAAA,EAGd,CACD,EAAQ,YAAc,iBAKtB,MAAM,EAAS,GAAyC,CAAE,YAAW,GAAG,GAAS,IAC/E,EAAC,MAAD,CACO,MACL,UAAW,iBAAiB,GAAa,KAAK,MAAM,CACpD,cAAY,OACZ,GAAI,EACJ,CAAA,CACF,CACF,EAAO,YAAc,gBAKrB,MAAM,EAAQ,GAA4C,EAAO,IAC/D,EAAC,KAAD,CAAS,MAAK,GAAI,EAAS,CAAA,CAC3B,CACF,EAAM,YAAc,eAKpB,MAAM,EAAc,GAAoD,EAAO,IAC7E,EAAC,IAAD,CAAQ,MAAK,GAAI,EAAS,CAAA,CAC1B,CACF,EAAY,YAAc,qBAG1B,MAAa,EAAS,CACpB,OACA,UACA,SACA,QACA,cACD"}
|