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 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 a snap point at the bottom of the viewport to allow swiping down
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<{ snapPosition: string; }>`
481
- Notifies that the sheet snap position has changed. Positions: `"0"` indicates
482
- a fully expanded position, `"2"` indicates a fully collapsed (closed) position,
483
- and `"1"` indicates an intermediate position.
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-snap-position`
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-snap-position`\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;;;;"}
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.sheetSnapPosition = event.detail.snapPosition;
22
+ this.dataset.sheetState = event.detail.sheetState;
23
23
  }
24
- if (event.detail?.snapPosition == "2" &&
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.sheetSnapPosition = event.detail.snapPosition;\n }\n if (\n event.detail?.snapPosition == \"2\" &&\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,iBAAiB,GAAG,KAAK,CAAC,MAAM,CAAC,YAAY;YAC5D;AACA,YAAA,IACE,KAAK,CAAC,MAAM,EAAE,YAAY,IAAI,GAAG;gBACjC,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
+ {"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-snap-position="2"]) ::slotted(dialog:not([open])){transition:none}`;
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-snap-position=\"2\"]) ::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,6gBAAA,CAAA;;;;;;;;;;"}
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
- snapPosition: string;
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
- const supportsScrollSnapChange = "onscrollsnapchange" in window;
44
- if (!supportsScrollSnapChange) {
45
- this.#setupIntersectionObserver();
46
- }
41
+ this.#setupIntersectionObserver();
47
42
  window.visualViewport?.addEventListener("resize", this.#handleViewportResize);
48
43
  }
49
44
  #setupIntersectionObserver() {
50
- this.#observer = new IntersectionObserver((entries) => {
51
- let lowestIntersectingSnap = Infinity;
52
- let highestNonIntersectingSnap = -Infinity;
53
- let hasIntersectingElement = false;
54
- for (const entry of entries) {
55
- if (!(entry.target instanceof HTMLElement) ||
56
- entry.target.dataset.snap == null) {
57
- continue;
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
- hasIntersectingElement = true;
62
- lowestIntersectingSnap = Math.min(lowestIntersectingSnap, snap);
55
+ intersectingTargets.add(entry.target);
63
56
  }
64
57
  else {
65
- highestNonIntersectingSnap = Math.max(highestNonIntersectingSnap, snap);
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
- const newSnapPosition = hasIntersectingElement
69
- ? lowestIntersectingSnap
70
- : highestNonIntersectingSnap + 1;
71
- this.#updateSnapPosition(newSnapPosition.toString());
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
- threshold: 0,
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
- this.#observer?.observe(sentinel);
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
- #handleScrollSnapChange(event) {
83
- const snapEvent = event;
84
- if (!(snapEvent.snapTargetBlock instanceof HTMLElement)) {
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
- const newSnapPosition = snapEvent.snapTargetBlock.dataset.snap ?? "1";
88
- this.#updateSnapPosition(newSnapPosition);
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.#observer?.disconnect();
387
+ this.#cleanupIntersectionObserver?.();
245
388
  window.visualViewport?.removeEventListener("resize", this.#handleViewportResize);
246
389
  }
247
390
  }