pure-web-bottom-sheet 0.4.0 → 0.6.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 +39 -24
- package/dist/react/react/BottomSheet.js +1 -1
- package/dist/react/react/BottomSheet.js.map +1 -1
- package/dist/react/web/bottom-sheet-dialog-manager.js +2 -2
- package/dist/react/web/bottom-sheet-dialog-manager.js.map +1 -1
- package/dist/react/web/bottom-sheet-dialog-manager.template.js +1 -1
- package/dist/react/web/bottom-sheet-dialog-manager.template.js.map +1 -1
- package/dist/react/web/bottom-sheet.d.ts +17 -1
- package/dist/react/web/bottom-sheet.js +185 -42
- package/dist/react/web/bottom-sheet.js.map +1 -1
- package/dist/react/web/bottom-sheet.template.js +6 -5
- package/dist/react/web/bottom-sheet.template.js.map +1 -1
- package/dist/react/web/index.client.d.ts +1 -1
- package/dist/vue/index-CHtDY-Oo.js +114 -0
- package/dist/vue/index-CHtDY-Oo.js.map +1 -0
- package/dist/vue/{index.client-BnjqgBI2.js → index.client-bXgCLqos.js} +159 -46
- package/dist/vue/index.client-bXgCLqos.js.map +1 -0
- package/dist/vue/index.js +1 -1
- package/dist/web/bottom-sheet.d.ts +17 -1
- package/dist/web/index.client.d.ts +1 -1
- package/dist/web.client.js +1 -1
- package/dist/web.ssr.js +1 -1
- package/package.json +1 -1
- package/dist/vue/index-0rU4KEd4.js +0 -113
- package/dist/vue/index-0rU4KEd4.js.map +0 -1
- package/dist/vue/index.client-BnjqgBI2.js.map +0 -1
package/README.md
CHANGED
|
@@ -61,9 +61,9 @@ npm install pure-web-bottom-sheet
|
|
|
61
61
|
<body>
|
|
62
62
|
<bottom-sheet tabindex="0">
|
|
63
63
|
<!-- Snap points -->
|
|
64
|
-
<div slot="snap" style="--snap: 25%"></div>
|
|
65
|
-
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
66
64
|
<div slot="snap" style="--snap: 75%"></div>
|
|
65
|
+
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
66
|
+
<div slot="snap" style="--snap: 25%"></div>
|
|
67
67
|
|
|
68
68
|
<div slot="header">
|
|
69
69
|
<h2>Custom header</h2>
|
|
@@ -100,9 +100,9 @@ npm install pure-web-bottom-sheet
|
|
|
100
100
|
<dialog id="bottom-sheet-dialog">
|
|
101
101
|
<bottom-sheet swipe-to-dismiss tabindex="0">
|
|
102
102
|
<!-- Snap points -->
|
|
103
|
-
<div slot="snap" style="--snap: 25%"></div>
|
|
104
|
-
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
105
103
|
<div slot="snap" style="--snap: 75%"></div>
|
|
104
|
+
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
105
|
+
<div slot="snap" style="--snap: 25%"></div>
|
|
106
106
|
|
|
107
107
|
<div slot="header">
|
|
108
108
|
<h2>Custom header</h2>
|
|
@@ -149,9 +149,9 @@ import { bottomSheetTemplate } from "pure-web-bottom-sheet/ssr";
|
|
|
149
149
|
</template>
|
|
150
150
|
|
|
151
151
|
{/* Snap points */}
|
|
152
|
-
<div slot="snap" style="--snap: 25%"></div>
|
|
153
|
-
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
154
152
|
<div slot="snap" style="--snap: 75%"></div>
|
|
153
|
+
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
154
|
+
<div slot="snap" style="--snap: 25%"></div>
|
|
155
155
|
|
|
156
156
|
<div slot="header">
|
|
157
157
|
<h2>Custom header</h2>
|
|
@@ -188,9 +188,9 @@ import { bottomSheetTemplate } from "pure-web-bottom-sheet/ssr";
|
|
|
188
188
|
</template>
|
|
189
189
|
|
|
190
190
|
<!-- Snap points -->
|
|
191
|
-
<div slot="snap" style="--snap: 25%"></div>
|
|
192
|
-
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
193
191
|
<div slot="snap" style="--snap: 75%"></div>
|
|
192
|
+
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
193
|
+
<div slot="snap" style="--snap: 25%"></div>
|
|
194
194
|
|
|
195
195
|
<div slot="header">
|
|
196
196
|
<h2>Custom header</h2>
|
|
@@ -232,9 +232,9 @@ import { BottomSheet } from "pure-web-bottom-sheet/react";
|
|
|
232
232
|
function Example() {
|
|
233
233
|
return (
|
|
234
234
|
<BottomSheet tabIndex={0}>
|
|
235
|
-
<div slot="snap" style={{ "--snap": "25%" }} />
|
|
236
|
-
<div slot="snap" style={{ "--snap": "50%" }} className="initial" />
|
|
237
235
|
<div slot="snap" style={{ "--snap": "75%" }} />
|
|
236
|
+
<div slot="snap" style={{ "--snap": "50%" }} className="initial" />
|
|
237
|
+
<div slot="snap" style={{ "--snap": "25%" }} />
|
|
238
238
|
|
|
239
239
|
<div slot="header">
|
|
240
240
|
<h2>Custom header</h2>
|
|
@@ -278,9 +278,9 @@ function Example() {
|
|
|
278
278
|
<BottomSheetDialogManager>
|
|
279
279
|
<dialog ref={dialog}>
|
|
280
280
|
<BottomSheet swipe-to-dismiss tabIndex={0}>
|
|
281
|
-
<div slot="snap" style={{ "--snap": "25%" }} />
|
|
282
|
-
<div slot="snap" style={{ "--snap": "50%" }} className="initial" />
|
|
283
281
|
<div slot="snap" style={{ "--snap": "75%" }} />
|
|
282
|
+
<div slot="snap" style={{ "--snap": "50%" }} className="initial" />
|
|
283
|
+
<div slot="snap" style={{ "--snap": "25%" }} />
|
|
284
284
|
<div slot="header">
|
|
285
285
|
<h2>Custom header</h2>
|
|
286
286
|
</div>
|
|
@@ -309,9 +309,9 @@ use the component and provide SSR support out of the box.
|
|
|
309
309
|
```vue
|
|
310
310
|
<template>
|
|
311
311
|
<VBottomSheet tabindex="0">
|
|
312
|
-
<div slot="snap" style="--snap: 25%"></div>
|
|
313
|
-
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
314
312
|
<div slot="snap" style="--snap: 75%"></div>
|
|
313
|
+
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
314
|
+
<div slot="snap" style="--snap: 25%"></div>
|
|
315
315
|
|
|
316
316
|
<div slot="header">
|
|
317
317
|
<h2>Custom header</h2>
|
|
@@ -349,9 +349,9 @@ import { VBottomSheet } from "pure-web-bottom-sheet/vue";
|
|
|
349
349
|
<div slot="footer">
|
|
350
350
|
<h2>Custom footer</h2>
|
|
351
351
|
</div>
|
|
352
|
-
<div slot="snap" style="--snap: 25%"></div>
|
|
353
|
-
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
354
352
|
<div slot="snap" style="--snap: 75%"></div>
|
|
353
|
+
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
354
|
+
<div slot="snap" style="--snap: 25%"></div>
|
|
355
355
|
<DummyContent />
|
|
356
356
|
</VBottomSheet>
|
|
357
357
|
</dialog>
|
|
@@ -390,9 +390,9 @@ using the bottom sheet as an overlay that should always remain visible.
|
|
|
390
390
|
```html
|
|
391
391
|
<bottom-sheet>
|
|
392
392
|
<!-- Snap points -->
|
|
393
|
-
<div slot="snap" style="--snap: 25%"></div>
|
|
394
|
-
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
395
393
|
<div slot="snap" style="--snap: 75%"></div>
|
|
394
|
+
<div slot="snap" style="--snap: 50%" class="initial"></div>
|
|
395
|
+
<div slot="snap" style="--snap: 25%"></div>
|
|
396
396
|
|
|
397
397
|
<!-- Custom header -->
|
|
398
398
|
<div slot="header">
|
|
@@ -446,8 +446,13 @@ using the bottom sheet as an overlay that should always remain visible.
|
|
|
446
446
|
Defines snap points for positioning the bottom sheet. If not specified, the bottom
|
|
447
447
|
sheet will have a single snap point `--snap: 100%` (maximum
|
|
448
448
|
height). Note that when the `<bottom-sheet>` has the `swipe-to-dismiss` attribute
|
|
449
|
-
set, it also has
|
|
450
|
-
to dismiss it.
|
|
449
|
+
set, it also has an implicit snap point at the bottom of the viewport to allow
|
|
450
|
+
swiping down to dismiss it.
|
|
451
|
+
|
|
452
|
+
Note that the snap points should be placed in the DOM in a top-to-bottom order
|
|
453
|
+
due to the snap index calculation assuming this order. E.g., `<div slot="snap" style="--snap: 75vh"></div>`
|
|
454
|
+
should be placed before `<div slot="snap" style="--snap: 50vh"></div>`.
|
|
455
|
+
|
|
451
456
|
Each snap point element should:
|
|
452
457
|
- Be assigned to this slot
|
|
453
458
|
- Specify the `--snap` custom property to set to the wanted offset from the viewport
|
|
@@ -459,6 +464,12 @@ using the bottom sheet as an overlay that should always remain visible.
|
|
|
459
464
|
- Optionally specify the class `initial` to make the bottom sheet
|
|
460
465
|
initially snap to that point each time it is opened. Note that
|
|
461
466
|
only a single snap point should specify this class.
|
|
467
|
+
- Optionally specify the class `top` if the snap point represents the fully
|
|
468
|
+
expanded sheet position (i.e., `--snap: 100%`). This ensures the
|
|
469
|
+
`snap-position-change` event reports `sheetState: "expanded"` for this snap
|
|
470
|
+
point, and that its `snapIndex` matches the fully expanded sheet position.
|
|
471
|
+
Must be the first snap point in the DOM.
|
|
472
|
+
|
|
462
473
|
- **`header`** (optional)
|
|
463
474
|
Optional header content that is displayed at the top of the bottom sheet.
|
|
464
475
|
- **`footer`** (optional)
|
|
@@ -477,10 +488,14 @@ using the bottom sheet as an overlay that should always remain visible.
|
|
|
477
488
|
|
|
478
489
|
#### Events
|
|
479
490
|
|
|
480
|
-
- **`snap-position-change`** - type: `CustomEvent<{
|
|
481
|
-
Notifies that the sheet snap position has changed.
|
|
482
|
-
|
|
483
|
-
|
|
491
|
+
- **`snap-position-change`** - type: `CustomEvent<{ sheetState: "collapsed" | "partially-expanded" | "expanded"; snapIndex: number; }>`
|
|
492
|
+
Notifies that the sheet snap position has changed. Snap index 0 corresponds to
|
|
493
|
+
the collapsed state. The `sheetState` is one of the following:
|
|
494
|
+
- `"collapsed"` - The bottom sheet is collapsed (i.e., snapped to the bottom).
|
|
495
|
+
- `"partially-expanded"` - The bottom sheet is snapped to one of the intermediate
|
|
496
|
+
snap points defined by the user.
|
|
497
|
+
- `"expanded"` - The bottom sheet is fully expanded (i.e., snapped to the full
|
|
498
|
+
height).
|
|
484
499
|
|
|
485
500
|
### `<bottom-sheet-dialog-manager>`: A utility element for the native `<dialog>` element to use the `<bottom-sheet>` element as a dialog
|
|
486
501
|
|
|
@@ -6,7 +6,7 @@ import Template from './ShadowRootTemplate.js';
|
|
|
6
6
|
function BottomSheet({ children, ...props }) {
|
|
7
7
|
return (jsxs(Fragment, { children: [jsxs("bottom-sheet", { ...props,
|
|
8
8
|
// Need to use `suppressHydrationWarning` to avoid hydration mismatch
|
|
9
|
-
// because the bottom-sheet component updates its `data-sheet-
|
|
9
|
+
// because the bottom-sheet component updates its `data-sheet-state`
|
|
10
10
|
// attribute during the initial render, which is not reflected in the
|
|
11
11
|
// server-rendered HTML.
|
|
12
12
|
suppressHydrationWarning: true, children: [jsx(Template, { html: template }), children] }), jsx(Client, {})] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomSheet.js","sources":["../../../../src/react/BottomSheet.tsx"],"sourcesContent":["import { BottomSheetHTMLAttributes } from \"../web/bottom-sheet\";\nimport { BottomSheetEvents } from \"../web/index.client\";\nimport { bottomSheetTemplate } from \"../web/index.ssr\";\nimport Client from \"./Client\";\nimport { CustomElementProps } from \"./custom-element-props\";\nimport ShadowRootTemplate from \"./ShadowRootTemplate\";\n\ntype BottomSheetProps = CustomElementProps<\n BottomSheetHTMLAttributes,\n BottomSheetEvents\n>;\n\ndeclare module \"react/jsx-runtime\" {\n namespace JSX {\n interface IntrinsicElements {\n \"bottom-sheet\": BottomSheetProps;\n }\n }\n}\n\nexport default function BottomSheet({ children, ...props }: BottomSheetProps) {\n return (\n <>\n <bottom-sheet\n {...props}\n // Need to use `suppressHydrationWarning` to avoid hydration mismatch\n // because the bottom-sheet component updates its `data-sheet-
|
|
1
|
+
{"version":3,"file":"BottomSheet.js","sources":["../../../../src/react/BottomSheet.tsx"],"sourcesContent":["import { BottomSheetHTMLAttributes } from \"../web/bottom-sheet\";\nimport { BottomSheetEvents } from \"../web/index.client\";\nimport { bottomSheetTemplate } from \"../web/index.ssr\";\nimport Client from \"./Client\";\nimport { CustomElementProps } from \"./custom-element-props\";\nimport ShadowRootTemplate from \"./ShadowRootTemplate\";\n\ntype BottomSheetProps = CustomElementProps<\n BottomSheetHTMLAttributes,\n BottomSheetEvents\n>;\n\ndeclare module \"react/jsx-runtime\" {\n namespace JSX {\n interface IntrinsicElements {\n \"bottom-sheet\": BottomSheetProps;\n }\n }\n}\n\nexport default function BottomSheet({ children, ...props }: BottomSheetProps) {\n return (\n <>\n <bottom-sheet\n {...props}\n // Need to use `suppressHydrationWarning` to avoid hydration mismatch\n // because the bottom-sheet component updates its `data-sheet-state`\n // attribute during the initial render, which is not reflected in the\n // server-rendered HTML.\n suppressHydrationWarning\n >\n {<ShadowRootTemplate html={bottomSheetTemplate} />}\n {children}\n </bottom-sheet>\n <Client />\n </>\n );\n}\n"],"names":["_jsxs","_Fragment","_jsx","ShadowRootTemplate","bottomSheetTemplate"],"mappings":";;;;;AAoBc,SAAU,WAAW,CAAC,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAoB,EAAA;IAC1E,QACEA,IAAA,CAAAC,QAAA,EAAA,EAAA,QAAA,EAAA,CACED,IAAA,CAAA,cAAA,EAAA,EAAA,GACM,KAAK;;;;;AAKT,gBAAA,wBAAwB,mBAEvBE,GAAA,CAACC,QAAkB,EAAA,EAAC,IAAI,EAAEC,QAAmB,EAAA,CAAI,EACjD,QAAQ,IACI,EACfF,GAAA,CAAC,MAAM,EAAA,EAAA,CAAG,CAAA,EAAA,CACT;AAEP;;;;"}
|
|
@@ -19,9 +19,9 @@ class BottomSheetDialogManager extends HTMLElement {
|
|
|
19
19
|
});
|
|
20
20
|
this.addEventListener("snap-position-change", (event) => {
|
|
21
21
|
if (event.detail) {
|
|
22
|
-
this.dataset.
|
|
22
|
+
this.dataset.sheetState = event.detail.sheetState;
|
|
23
23
|
}
|
|
24
|
-
if (event.detail?.
|
|
24
|
+
if (event.detail?.sheetState === "collapsed" &&
|
|
25
25
|
event.target instanceof HTMLElement &&
|
|
26
26
|
event.target.hasAttribute("swipe-to-dismiss") &&
|
|
27
27
|
event.target.checkVisibility()) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bottom-sheet-dialog-manager.js","sources":["../../../../src/web/bottom-sheet-dialog-manager.ts"],"sourcesContent":["import { SnapPositionChangeEventDetail } from \"./bottom-sheet\";\nimport { template } from \"./bottom-sheet-dialog-manager.template\";\n\nexport class BottomSheetDialogManager extends HTMLElement {\n constructor() {\n super();\n\n const supportsDeclarative =\n HTMLElement.prototype.hasOwnProperty(\"attachInternals\");\n const internals = supportsDeclarative ? this.attachInternals() : undefined;\n\n // Use existing declarative shadow root if present, otherwise create one\n let shadow = internals?.shadowRoot;\n if (!shadow) {\n shadow = this.attachShadow({ mode: \"open\" });\n shadow.innerHTML = template;\n }\n\n this.addEventListener(\"click\", (event) => {\n if (\n event.target instanceof HTMLDialogElement &&\n event.target.matches(\":modal\")\n ) {\n event.target.close();\n }\n });\n this.addEventListener(\n \"snap-position-change\",\n (event: CustomEventInit<SnapPositionChangeEventDetail> & Event) => {\n if (event.detail) {\n this.dataset.
|
|
1
|
+
{"version":3,"file":"bottom-sheet-dialog-manager.js","sources":["../../../../src/web/bottom-sheet-dialog-manager.ts"],"sourcesContent":["import { SnapPositionChangeEventDetail } from \"./bottom-sheet\";\nimport { template } from \"./bottom-sheet-dialog-manager.template\";\n\nexport class BottomSheetDialogManager extends HTMLElement {\n constructor() {\n super();\n\n const supportsDeclarative =\n HTMLElement.prototype.hasOwnProperty(\"attachInternals\");\n const internals = supportsDeclarative ? this.attachInternals() : undefined;\n\n // Use existing declarative shadow root if present, otherwise create one\n let shadow = internals?.shadowRoot;\n if (!shadow) {\n shadow = this.attachShadow({ mode: \"open\" });\n shadow.innerHTML = template;\n }\n\n this.addEventListener(\"click\", (event) => {\n if (\n event.target instanceof HTMLDialogElement &&\n event.target.matches(\":modal\")\n ) {\n event.target.close();\n }\n });\n this.addEventListener(\n \"snap-position-change\",\n (event: CustomEventInit<SnapPositionChangeEventDetail> & Event) => {\n if (event.detail) {\n this.dataset.sheetState = event.detail.sheetState;\n }\n if (\n event.detail?.sheetState === \"collapsed\" &&\n event.target instanceof HTMLElement &&\n event.target.hasAttribute(\"swipe-to-dismiss\") &&\n event.target.checkVisibility()\n ) {\n const parent = event.target.parentElement;\n if (\n parent instanceof HTMLDialogElement &&\n // Prevent Safari from closing the dialog immediately after opening\n // while the dialog open transition is still running.\n getComputedStyle(parent).getPropertyValue(\"translate\") === \"0px\"\n ) {\n parent.close();\n }\n }\n },\n );\n }\n}\n\n/**\n * Interface for the bottom-sheet-dialog-manager custom element.\n * Provides type definitions for its custom properties.\n *\n * @example\n * // Register in TypeScript for proper type checking:\n * declare global {\n * interface HTMLElementTagNameMap {\n * \"bottom-sheet-dialog-manager\": BottomSheetDialogManager;\n * }\n * }\n */\nexport interface BottomSheetDialogManager extends HTMLElement {}\n"],"names":[],"mappings":";;AAGM,MAAO,wBAAyB,SAAQ,WAAW,CAAA;AACvD,IAAA,WAAA,GAAA;AACE,QAAA,KAAK,EAAE;QAEP,MAAM,mBAAmB,GACvB,WAAW,CAAC,SAAS,CAAC,cAAc,CAAC,iBAAiB,CAAC;AACzD,QAAA,MAAM,SAAS,GAAG,mBAAmB,GAAG,IAAI,CAAC,eAAe,EAAE,GAAG,SAAS;;AAG1E,QAAA,IAAI,MAAM,GAAG,SAAS,EAAE,UAAU;QAClC,IAAI,CAAC,MAAM,EAAE;YACX,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC5C,YAAA,MAAM,CAAC,SAAS,GAAG,QAAQ;QAC7B;QAEA,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAK,KAAI;AACvC,YAAA,IACE,KAAK,CAAC,MAAM,YAAY,iBAAiB;gBACzC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAC9B;AACA,gBAAA,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE;YACtB;AACF,QAAA,CAAC,CAAC;QACF,IAAI,CAAC,gBAAgB,CACnB,sBAAsB,EACtB,CAAC,KAA6D,KAAI;AAChE,YAAA,IAAI,KAAK,CAAC,MAAM,EAAE;gBAChB,IAAI,CAAC,OAAO,CAAC,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,UAAU;YACnD;AACA,YAAA,IACE,KAAK,CAAC,MAAM,EAAE,UAAU,KAAK,WAAW;gBACxC,KAAK,CAAC,MAAM,YAAY,WAAW;AACnC,gBAAA,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,kBAAkB,CAAC;AAC7C,gBAAA,KAAK,CAAC,MAAM,CAAC,eAAe,EAAE,EAC9B;AACA,gBAAA,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,aAAa;gBACzC,IACE,MAAM,YAAY,iBAAiB;;;oBAGnC,gBAAgB,CAAC,MAAM,CAAC,CAAC,gBAAgB,CAAC,WAAW,CAAC,KAAK,KAAK,EAChE;oBACA,MAAM,CAAC,KAAK,EAAE;gBAChB;YACF;AACF,QAAA,CAAC,CACF;IACH;AACD;;;;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Needed until Prettier supports identifying embedded CSS by block comments */
|
|
2
|
-
const styles = /*css*/`::slotted(dialog){background:unset;border:none;height:100%;inset:0;margin:0;max-height:none;max-width:none;padding:0;position:fixed;top:auto;width:100%}::slotted(dialog:not(:modal)){pointer-events:none}::slotted(dialog[open]){translate:0 0}@starting-style{::slotted(dialog[open]){translate:0 100vh}}::slotted(dialog){transition:translate .5s ease-out,overlay allow-discrete .5s ease-out,display allow-discrete .5s ease-out;translate:0 100vh}:host([data-sheet-
|
|
2
|
+
const styles = /*css*/`::slotted(dialog){background:unset;border:none;height:100%;inset:0;margin:0;max-height:none;max-width:none;padding:0;position:fixed;top:auto;width:100%}::slotted(dialog:not(:modal)){pointer-events:none}::slotted(dialog[open]){translate:0 0}@starting-style{::slotted(dialog[open]){translate:0 100vh}}::slotted(dialog){transition:translate .5s ease-out,overlay allow-discrete .5s ease-out,display allow-discrete .5s ease-out;translate:0 100vh}:host([data-sheet-state=collapsed]) ::slotted(dialog:not([open])){transition:none}`;
|
|
3
3
|
const template = /* HTML */ `
|
|
4
4
|
<style>
|
|
5
5
|
${styles}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bottom-sheet-dialog-manager.template.js","sources":["../../../../src/web/bottom-sheet-dialog-manager.template.ts"],"sourcesContent":["/* Needed until Prettier supports identifying embedded CSS by block comments */\nconst css = String.raw;\n\nconst styles = css`\n ::slotted(dialog) {\n position: fixed;\n margin: 0;\n inset: 0;\n top: initial;\n border: none;\n background: unset;\n padding: 0;\n width: 100%;\n max-width: none;\n height: 100%;\n max-height: none;\n }\n\n ::slotted(dialog:not(:modal)) {\n pointer-events: none;\n }\n\n ::slotted(dialog[open]) {\n translate: 0 0;\n }\n\n @starting-style {\n ::slotted(dialog[open]) {\n translate: 0 100vh;\n }\n }\n\n ::slotted(dialog) {\n translate: 0 100vh;\n transition:\n translate 0.5s ease-out,\n overlay 0.5s ease-out allow-discrete,\n display 0.5s ease-out allow-discrete;\n }\n\n :host([data-sheet-
|
|
1
|
+
{"version":3,"file":"bottom-sheet-dialog-manager.template.js","sources":["../../../../src/web/bottom-sheet-dialog-manager.template.ts"],"sourcesContent":["/* Needed until Prettier supports identifying embedded CSS by block comments */\nconst css = String.raw;\n\nconst styles = css`\n ::slotted(dialog) {\n position: fixed;\n margin: 0;\n inset: 0;\n top: initial;\n border: none;\n background: unset;\n padding: 0;\n width: 100%;\n max-width: none;\n height: 100%;\n max-height: none;\n }\n\n ::slotted(dialog:not(:modal)) {\n pointer-events: none;\n }\n\n ::slotted(dialog[open]) {\n translate: 0 0;\n }\n\n @starting-style {\n ::slotted(dialog[open]) {\n translate: 0 100vh;\n }\n }\n\n ::slotted(dialog) {\n translate: 0 100vh;\n transition:\n translate 0.5s ease-out,\n overlay 0.5s ease-out allow-discrete,\n display 0.5s ease-out allow-discrete;\n }\n\n :host([data-sheet-state=\"collapsed\"]) ::slotted(dialog:not([open])) {\n transition: none;\n }\n`;\n\nexport const template: string = /* HTML */ `\n <style>\n ${styles}\n </style>\n <slot></slot>\n`;\n"],"names":[],"mappings":"AAAA;AAGA,MAAM,MAAM,UAAM,CAAA,2gBAAA,CAAA;;;;;;;;;;"}
|
|
@@ -44,8 +44,24 @@ export interface BottomSheetHTMLAttributes {
|
|
|
44
44
|
*/
|
|
45
45
|
["swipe-to-dismiss"]?: boolean;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Represents the current state of the bottom sheet.
|
|
49
|
+
* - `collapsed`: Sheet is fully collapsed (closed/minimized)
|
|
50
|
+
* - `partially-expanded`: Sheet is at an intermediate snap point
|
|
51
|
+
* - `expanded`: Sheet is fully expanded to its maximum height
|
|
52
|
+
*/
|
|
53
|
+
export type SheetState = "collapsed" | "partially-expanded" | "expanded";
|
|
54
|
+
/**
|
|
55
|
+
* Detail object for the `snap-position-change` custom event.
|
|
56
|
+
*/
|
|
47
57
|
export interface SnapPositionChangeEventDetail {
|
|
48
|
-
|
|
58
|
+
/** The semantic state of the sheet */
|
|
59
|
+
sheetState: SheetState;
|
|
60
|
+
/**
|
|
61
|
+
* The index of the current snap point (0 = collapsed,
|
|
62
|
+
* higher values = more expanded, with the highest being fully expanded)
|
|
63
|
+
*/
|
|
64
|
+
snapIndex: number;
|
|
49
65
|
}
|
|
50
66
|
export type BottomSheetEvents = {
|
|
51
67
|
"snap-position-change": CustomEvent<SnapPositionChangeEventDetail>;
|
|
@@ -12,13 +12,15 @@ import { template } from './bottom-sheet.template.js';
|
|
|
12
12
|
* }
|
|
13
13
|
*/
|
|
14
14
|
class BottomSheet extends HTMLElement {
|
|
15
|
-
static observedAttributes = ["nested-scroll-optimization"];
|
|
16
|
-
#observer = null;
|
|
15
|
+
static observedAttributes = ["nested-scroll-optimization", "content-height"];
|
|
17
16
|
#handleViewportResize = () => {
|
|
18
17
|
this.style.setProperty("--sw-keyboard-height", `${window.visualViewport?.offsetTop ?? 0}px`);
|
|
19
18
|
};
|
|
20
19
|
#shadow;
|
|
20
|
+
#cleanupIntersectionObserver = null;
|
|
21
|
+
#cleanupSheetSizeObserver = null;
|
|
21
22
|
#cleanupNestedScrollResizeOptimization = null;
|
|
23
|
+
#currentSnapState = null;
|
|
22
24
|
constructor() {
|
|
23
25
|
super();
|
|
24
26
|
const supportsDeclarative = HTMLElement.prototype.hasOwnProperty("attachInternals");
|
|
@@ -30,73 +32,180 @@ class BottomSheet extends HTMLElement {
|
|
|
30
32
|
shadow.innerHTML = template;
|
|
31
33
|
}
|
|
32
34
|
this.#shadow = shadow;
|
|
33
|
-
const supportsScrollSnapChange = "onscrollsnapchange" in window;
|
|
34
|
-
if (supportsScrollSnapChange) {
|
|
35
|
-
this.addEventListener("scrollsnapchange", this.#handleScrollSnapChange);
|
|
36
|
-
}
|
|
37
35
|
if (!CSS.supports("(animation-timeline: scroll()) and (animation-range: 0% 100%)")) {
|
|
38
36
|
this.addEventListener("scroll", this.#handleScroll);
|
|
39
37
|
this.#handleScroll();
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
40
|
connectedCallback() {
|
|
43
|
-
|
|
44
|
-
if (!supportsScrollSnapChange) {
|
|
45
|
-
this.#setupIntersectionObserver();
|
|
46
|
-
}
|
|
41
|
+
this.#setupIntersectionObserver();
|
|
47
42
|
window.visualViewport?.addEventListener("resize", this.#handleViewportResize);
|
|
48
43
|
}
|
|
49
44
|
#setupIntersectionObserver() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const snap = Number.parseInt(entry.target.dataset.snap);
|
|
45
|
+
const snapSlot = this.#shadow.querySelector('slot[name="snap"]');
|
|
46
|
+
const bottomSnapTarget = this.#shadow.querySelector('.sentinel[data-snap="bottom"]');
|
|
47
|
+
if (!snapSlot || !bottomSnapTarget)
|
|
48
|
+
return;
|
|
49
|
+
const contentHeightTarget = this.#shadow.querySelector('.sentinel[data-snap="content-height"]');
|
|
50
|
+
const intersectingTargets = new Set();
|
|
51
|
+
let previousSnapTarget = null;
|
|
52
|
+
const observer = new IntersectionObserver((entries) => {
|
|
53
|
+
entries.forEach((entry) => {
|
|
60
54
|
if (entry.isIntersecting) {
|
|
61
|
-
|
|
62
|
-
lowestIntersectingSnap = Math.min(lowestIntersectingSnap, snap);
|
|
55
|
+
intersectingTargets.add(entry.target);
|
|
63
56
|
}
|
|
64
57
|
else {
|
|
65
|
-
|
|
58
|
+
intersectingTargets.delete(entry.target);
|
|
66
59
|
}
|
|
60
|
+
});
|
|
61
|
+
// Skip when the root has no dimensions (e.g., inside a closed dialog)
|
|
62
|
+
if (!entries[0]?.rootBounds?.height) {
|
|
63
|
+
return;
|
|
67
64
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
// Pick the intersecting target closest to the snap line (host's top edge).
|
|
66
|
+
// Skip bottom snap target (handled separately for collapsed state detection).
|
|
67
|
+
const currentTarget = Array.from(intersectingTargets)
|
|
68
|
+
.filter((target) => target !== bottomSnapTarget)
|
|
69
|
+
.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)
|
|
70
|
+
.at(-1);
|
|
71
|
+
if (currentTarget === previousSnapTarget &&
|
|
72
|
+
// Never skip the "content-height" target even if it was the previous snap
|
|
73
|
+
// target, because the computed snap index may be different if the content
|
|
74
|
+
// height has changed since the last intersection update.
|
|
75
|
+
currentTarget !== contentHeightTarget) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// No snap target within the bounds -> collapsed if the bottom sentinel has
|
|
79
|
+
// also exited (scrollTop at 0) and swipe-to-dismiss is enabled.
|
|
80
|
+
if (!currentTarget) {
|
|
81
|
+
if (intersectingTargets.has(bottomSnapTarget) ||
|
|
82
|
+
this.scrollTop > 1 ||
|
|
83
|
+
!this.hasAttribute("swipe-to-dismiss")) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
previousSnapTarget = bottomSnapTarget;
|
|
87
|
+
this.#updateSnapPosition(bottomSnapTarget);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
previousSnapTarget = currentTarget;
|
|
91
|
+
this.#updateSnapPosition(currentTarget);
|
|
72
92
|
}, {
|
|
73
93
|
root: this,
|
|
74
|
-
|
|
75
|
-
rootMargin: "1000% 0px -100% 0px",
|
|
94
|
+
rootMargin: "100% 0px -100% 0px",
|
|
76
95
|
});
|
|
77
96
|
const sentinels = this.#shadow.querySelectorAll(".sentinel");
|
|
78
97
|
Array.from(sentinels).forEach((sentinel) => {
|
|
79
|
-
|
|
98
|
+
observer.observe(sentinel);
|
|
80
99
|
});
|
|
100
|
+
let observedSnapPoints = new Set();
|
|
101
|
+
const observeSnapPoints = () => {
|
|
102
|
+
const snapPoints = new Set(snapSlot.assignedElements());
|
|
103
|
+
// Unobserve elements no longer assigned to the slot
|
|
104
|
+
observedSnapPoints.forEach((el) => {
|
|
105
|
+
if (!snapPoints.has(el)) {
|
|
106
|
+
observer.unobserve(el);
|
|
107
|
+
intersectingTargets.delete(el);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
snapPoints.forEach((el) => observer.observe(el));
|
|
111
|
+
observedSnapPoints = snapPoints;
|
|
112
|
+
};
|
|
113
|
+
snapSlot.addEventListener("slotchange", observeSnapPoints);
|
|
114
|
+
observeSnapPoints();
|
|
115
|
+
this.#cleanupIntersectionObserver = () => {
|
|
116
|
+
snapSlot.removeEventListener("slotchange", observeSnapPoints);
|
|
117
|
+
observer.disconnect();
|
|
118
|
+
this.#cleanupIntersectionObserver = null;
|
|
119
|
+
};
|
|
81
120
|
}
|
|
82
|
-
#
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
121
|
+
#updateSnapPosition(newSnapTarget) {
|
|
122
|
+
const snapState = this.#calculateSnapState(newSnapTarget);
|
|
123
|
+
if (!snapState)
|
|
124
|
+
return;
|
|
125
|
+
const { snapIndex, sheetState } = snapState;
|
|
126
|
+
if (this.#currentSnapState?.snapIndex === snapIndex &&
|
|
127
|
+
this.#currentSnapState?.sheetState === sheetState) {
|
|
85
128
|
return;
|
|
86
129
|
}
|
|
87
|
-
|
|
88
|
-
this
|
|
89
|
-
}
|
|
90
|
-
#updateSnapPosition(position) {
|
|
91
|
-
this.dataset.sheetSnapPosition = position;
|
|
130
|
+
this.#currentSnapState = { ...snapState, snapTarget: newSnapTarget };
|
|
131
|
+
this.dataset.sheetState = sheetState;
|
|
92
132
|
this.dispatchEvent(new CustomEvent("snap-position-change", {
|
|
93
|
-
detail:
|
|
94
|
-
snapPosition: position,
|
|
95
|
-
},
|
|
133
|
+
detail: snapState,
|
|
96
134
|
bubbles: true,
|
|
97
135
|
composed: true,
|
|
98
136
|
}));
|
|
99
137
|
}
|
|
138
|
+
#calculateSnapState(snapTarget) {
|
|
139
|
+
if (snapTarget instanceof HTMLElement &&
|
|
140
|
+
snapTarget.dataset.snap === "bottom") {
|
|
141
|
+
return { snapIndex: 0, sheetState: "collapsed" };
|
|
142
|
+
}
|
|
143
|
+
const snapSlot = this.#shadow.querySelector('slot[name="snap"]');
|
|
144
|
+
if (!snapSlot)
|
|
145
|
+
return null;
|
|
146
|
+
// Reverse to bottom-to-top order so array index maps to snap index (i + 1)
|
|
147
|
+
const assignedSnapPoints = snapSlot.assignedElements().reverse();
|
|
148
|
+
const hasTopSnapPoint = assignedSnapPoints.at(-1)?.classList.contains("top") ?? false;
|
|
149
|
+
const maxExpandedIndex = assignedSnapPoints.length + (hasTopSnapPoint ? 0 : 1);
|
|
150
|
+
// When content-height is set, the topmost reachable snap index may be
|
|
151
|
+
// lower than maxExpandedIndex (limited by how tall the content is).
|
|
152
|
+
let topmostReachableIndex = maxExpandedIndex;
|
|
153
|
+
if (this.hasAttribute("content-height")) {
|
|
154
|
+
const maxSheetHeight = Math.min(this.offsetHeight, this.scrollHeight - this.offsetHeight);
|
|
155
|
+
let minHeightGap = Infinity;
|
|
156
|
+
for (let i = 0; i < assignedSnapPoints.length; i++) {
|
|
157
|
+
const el = assignedSnapPoints[i];
|
|
158
|
+
if (!(el instanceof HTMLElement))
|
|
159
|
+
continue;
|
|
160
|
+
// Add 1px to account for the -1px offset in CSS
|
|
161
|
+
const snapOffset = el.offsetTop + 1;
|
|
162
|
+
const heightGap = snapOffset - maxSheetHeight;
|
|
163
|
+
if (heightGap >= 0 && heightGap < minHeightGap) {
|
|
164
|
+
minHeightGap = heightGap;
|
|
165
|
+
topmostReachableIndex = i + 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// When snap target is the content-height sentinel, find the snap index from
|
|
169
|
+
// the current scroll position since the sentinel itself is not a snap point.
|
|
170
|
+
if (snapTarget instanceof HTMLElement &&
|
|
171
|
+
snapTarget.dataset.snap === "content-height") {
|
|
172
|
+
let closestSnapIndex = -1;
|
|
173
|
+
let minScrollGap = Infinity;
|
|
174
|
+
for (let i = 0; i < assignedSnapPoints.length; i++) {
|
|
175
|
+
const el = assignedSnapPoints[i];
|
|
176
|
+
if (!(el instanceof HTMLElement))
|
|
177
|
+
continue;
|
|
178
|
+
// Add 1px to account for the -1px offset in CSS
|
|
179
|
+
const snapOffset = el.offsetTop + 1;
|
|
180
|
+
const scrollGap = snapOffset - this.scrollTop;
|
|
181
|
+
if (scrollGap >= 0 && scrollGap < minScrollGap) {
|
|
182
|
+
minScrollGap = scrollGap;
|
|
183
|
+
closestSnapIndex = i + 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (closestSnapIndex !== -1) {
|
|
187
|
+
return {
|
|
188
|
+
snapIndex: closestSnapIndex,
|
|
189
|
+
sheetState: closestSnapIndex >= topmostReachableIndex
|
|
190
|
+
? "expanded"
|
|
191
|
+
: "partially-expanded",
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Snapped on one of the snap points assigned to the "snap" slot
|
|
197
|
+
if (snapTarget.matches('[slot="snap"]')) {
|
|
198
|
+
const snapIndex = assignedSnapPoints.indexOf(snapTarget) + 1;
|
|
199
|
+
return {
|
|
200
|
+
snapIndex,
|
|
201
|
+
sheetState: snapIndex >= topmostReachableIndex
|
|
202
|
+
? "expanded"
|
|
203
|
+
: "partially-expanded",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Snapped on the .sheet element or the "snap" slot fallback element
|
|
207
|
+
return { snapIndex: maxExpandedIndex, sheetState: "expanded" };
|
|
208
|
+
}
|
|
100
209
|
#handleScroll() {
|
|
101
210
|
this.#shadow
|
|
102
211
|
.querySelector(".sheet-wrapper")
|
|
@@ -221,6 +330,29 @@ class BottomSheet extends HTMLElement {
|
|
|
221
330
|
this.#cleanupNestedScrollResizeOptimization = null;
|
|
222
331
|
};
|
|
223
332
|
}
|
|
333
|
+
#setupSheetSizeObserver() {
|
|
334
|
+
// Observe sheet size changes to re-dispatch the snap-position-change event
|
|
335
|
+
// in case the height change results in a different snap point being the closest
|
|
336
|
+
// one to the sheet top.
|
|
337
|
+
let previousBlockSize = 0;
|
|
338
|
+
const resizeObserver = new ResizeObserver((e) => {
|
|
339
|
+
const blockSize = e.at(0)?.contentBoxSize.at(0)?.blockSize ?? 0;
|
|
340
|
+
if (!blockSize || blockSize === previousBlockSize)
|
|
341
|
+
return;
|
|
342
|
+
previousBlockSize = blockSize;
|
|
343
|
+
if (this.#currentSnapState) {
|
|
344
|
+
this.#updateSnapPosition(this.#currentSnapState.snapTarget);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
const sheet = this.#shadow.querySelector(".sheet");
|
|
348
|
+
if (sheet) {
|
|
349
|
+
resizeObserver.observe(sheet);
|
|
350
|
+
}
|
|
351
|
+
this.#cleanupSheetSizeObserver = () => {
|
|
352
|
+
resizeObserver.disconnect();
|
|
353
|
+
this.#cleanupSheetSizeObserver = null;
|
|
354
|
+
};
|
|
355
|
+
}
|
|
224
356
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
225
357
|
if (oldValue === newValue)
|
|
226
358
|
return;
|
|
@@ -236,12 +368,23 @@ class BottomSheet extends HTMLElement {
|
|
|
236
368
|
this.#cleanupNestedScrollResizeOptimization();
|
|
237
369
|
}
|
|
238
370
|
break;
|
|
371
|
+
case "content-height":
|
|
372
|
+
if (newValue !== null) {
|
|
373
|
+
if (!this.#cleanupSheetSizeObserver) {
|
|
374
|
+
// Only setup if not already setup
|
|
375
|
+
this.#setupSheetSizeObserver();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else if (this.#cleanupSheetSizeObserver) {
|
|
379
|
+
this.#cleanupSheetSizeObserver();
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
239
382
|
default:
|
|
240
383
|
console.warn(`Unhandled attribute: ${name}`);
|
|
241
384
|
}
|
|
242
385
|
}
|
|
243
386
|
disconnectedCallback() {
|
|
244
|
-
this.#
|
|
387
|
+
this.#cleanupIntersectionObserver?.();
|
|
245
388
|
window.visualViewport?.removeEventListener("resize", this.#handleViewportResize);
|
|
246
389
|
}
|
|
247
390
|
}
|