Skip to content

Commit c767cc6

Browse files
authored
Fix snap-position-change events for content-height mode (#17)
- Fix `snap-position-change` events reporting incorrect `snapIndex` and `sheetState` when `content-height` is set and sheet content is dynamically added/removed - Unify snap detection to use `IntersectionObserver` for all browsers, replacing the `scrollsnapchange` event path which reported inconsistent targets during fast scrolling between different snap points, particularly when using keyboard arrows - Add a content-height sentinel element that tracks where the sheet's content boundary intersects the observation area, enabling accurate snap index calculation relative to actual content height - Add `ResizeObserver` on the sheet to re-evaluate snap state when content height changes (e.g., growing past the current snap point updates `sheetState` from `expanded` to `partially-expanded`) Other changes: - Reduce IntersectionObserver `rootMargin` from `1000%` to `100%` - Add `scrollTop > 1` guard to prevent false "collapsed" events during scroll transitions - Clean up stale entries from `intersectingTargets` when snap points are dynamically removed - Add dynamic-height example page and e2e tests for content-height scenarios
1 parent 11678d7 commit c767cc6

6 files changed

Lines changed: 564 additions & 90 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
import Layout from "../layouts/Layout.astro";
3+
import DummyContent from "../components/DummyContent.astro";
4+
import { bottomSheetTemplate } from "pure-web-bottom-sheet/ssr";
5+
import SheetConfigPanel from "../components/SheetConfigPanel.astro";
6+
---
7+
8+
<Layout>
9+
<section>
10+
<h1>Modal bottom sheet with dynamic height</h1>
11+
<p>
12+
This example demonstrates a bottom sheet with <code>content-height</code>
13+
and dynamically added/removed content blocks. Each content block is
14+
<code>10vh</code> tall, and the header and footer together are <code
15+
>10vh</code
16+
>tall in total, making the total sheet height <code>(N+1) * 10vh</code>
17+
for N content blocks.
18+
</p>
19+
<p><button id="open-modal">Open sheet</button></p>
20+
<pre id="log"></pre>
21+
</section>
22+
<SheetConfigPanel bottomSheetSelector="bottom-sheet.example" />
23+
<DummyContent />
24+
25+
<bottom-sheet-dialog-manager>
26+
<dialog id="bottom-sheet-dialog">
27+
<bottom-sheet
28+
class="example"
29+
content-height
30+
swipe-to-dismiss
31+
tabindex="0"
32+
>
33+
<template shadowrootmode="open">
34+
<Fragment set:html={bottomSheetTemplate} />
35+
</template>
36+
37+
{/* Snap points */}
38+
<div slot="snap" style="--snap: 100%" class="top"></div>
39+
<div slot="snap" style="--snap: 75%"></div>
40+
<div slot="snap" style="--snap: 50%" class="initial"></div>
41+
<div slot="snap" style="--snap: 25%"></div>
42+
43+
<div slot="footer">
44+
<button id="add-block">Add block</button>
45+
<button id="remove-block">Remove block</button>
46+
</div>
47+
48+
<div id="content-container"></div>
49+
</bottom-sheet>
50+
</dialog>
51+
</bottom-sheet-dialog-manager>
52+
</Layout>
53+
54+
<style is:global>
55+
bottom-sheet.example {
56+
--sheet-max-height: 100vh;
57+
}
58+
bottom-sheet::part(header),
59+
bottom-sheet::part(footer) {
60+
box-sizing: border-box;
61+
overflow: hidden;
62+
}
63+
64+
bottom-sheet::part(header) {
65+
height: 2.5vh;
66+
}
67+
bottom-sheet::part(footer) {
68+
height: 7.5vh;
69+
display: flex;
70+
gap: 0.5rem;
71+
justify-content: center;
72+
align-items: center;
73+
}
74+
75+
bottom-sheet h2 {
76+
margin: 0;
77+
text-align: center;
78+
}
79+
80+
#log {
81+
height: 150px;
82+
overflow: scroll;
83+
padding: 0.5rem;
84+
border: 1px solid black;
85+
font-size: 0.8rem;
86+
}
87+
88+
.content-block {
89+
height: 10vh;
90+
display: flex;
91+
align-items: center;
92+
justify-content: center;
93+
}
94+
95+
.content-block:nth-child(odd) {
96+
background-color: #c0c0c0;
97+
}
98+
99+
.content-block:nth-child(even) {
100+
background-color: #e0e0e0;
101+
}
102+
</style>
103+
104+
<script>
105+
import {
106+
registerSheetElements,
107+
type SnapPositionChangeEventDetail,
108+
} from "pure-web-bottom-sheet";
109+
registerSheetElements();
110+
111+
const openModalButton = document.getElementById("open-modal");
112+
const dialog = document.querySelector<HTMLDialogElement>(
113+
"dialog#bottom-sheet-dialog",
114+
);
115+
openModalButton?.addEventListener("click", () => {
116+
dialog?.showModal();
117+
});
118+
119+
const container = document.getElementById("content-container");
120+
const addButton = document.getElementById("add-block");
121+
const removeButton = document.getElementById("remove-block");
122+
123+
let blockCount = 0;
124+
125+
function createBlock() {
126+
blockCount++;
127+
const block = document.createElement("div");
128+
block.className = "content-block";
129+
block.textContent = `Block ${blockCount}`;
130+
container?.appendChild(block);
131+
}
132+
133+
addButton?.addEventListener("click", () => {
134+
createBlock();
135+
});
136+
137+
removeButton?.addEventListener("click", () => {
138+
const lastBlock = container?.lastElementChild;
139+
if (lastBlock) {
140+
lastBlock.remove();
141+
}
142+
});
143+
144+
const logElement = document.getElementById("log");
145+
const log = (message: string) => {
146+
console.log(message);
147+
if (logElement) {
148+
logElement.textContent += message + "\n";
149+
logElement.scrollTop = logElement.scrollHeight;
150+
}
151+
};
152+
153+
dialog?.addEventListener("toggle", (e) => {
154+
log(`Dialog toggled: ${e.newState}`);
155+
});
156+
157+
const sheet = document.querySelector("bottom-sheet.example");
158+
sheet?.addEventListener(
159+
"snap-position-change",
160+
(e: CustomEventInit<SnapPositionChangeEventDetail> & Event) => {
161+
log(
162+
`Snap position changed: index=${e.detail?.snapIndex}, state=${e.detail?.sheetState}`,
163+
);
164+
},
165+
);
166+
</script>

examples/astro/src/pages/index.astro

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ import Layout from "../layouts/Layout.astro";
5959
>Modal bottom sheet with snap position change event listener</a
6060
>
6161
</li>
62+
<li>
63+
<a href=`${import.meta.env.BASE_URL}/dynamic-height/`
64+
>Modal bottom sheet with dynamic height
65+
</a>
66+
</li>
6267
</ul>
6368
</section>
6469
<section>

src/web/bottom-sheet.template.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,11 @@ const styles = css`
5050
top: calc(var(--snap) - 1px);
5151
margin-bottom: -1px; /* Compensate height so it does not affect layout */
5252
/*
53-
The bottom sheet uses an IntersectionObserver as a fallback for browsers
54-
that do not support the native scrollsnapchange event. The snap element
55-
needs a bounding box that extends above the observer boundary (the scroll
56-
container's top edge) so that these browsers reliably detect it as
57-
intersecting when snapped. Without this, e.g., WebKit may report the element
58-
as not-intersecting when it sits exactly at the boundary.
53+
The bottom sheet uses an IntersectionObserver to dispatch custom snap-position-change
54+
events. The snap element needs a bounding box that extends above the observer
55+
boundary (the scroll container's top edge) so that these browsers reliably
56+
detect it as intersecting when snapped. Without this, e.g., WebKit may report
57+
the element as not-intersecting when it sits exactly at the boundary.
5958
*/
6059
height: 1px;
6160
}
@@ -109,6 +108,17 @@ const styles = css`
109108
&[data-snap="bottom"] {
110109
top: 1px;
111110
}
111+
&[data-snap="content-height"] {
112+
position: absolute;
113+
top: calc(
114+
(var(--sheet-max-height) - min(100%, var(--sheet-max-height))) * -1 -
115+
1px
116+
);
117+
118+
:host(:not([content-height])) & {
119+
display: none;
120+
}
121+
}
112122
}
113123
114124
.sheet-wrapper {
@@ -256,6 +266,16 @@ const styles = css`
256266
}
257267
}
258268
269+
:host([content-height]) {
270+
.sheet-wrapper {
271+
/*
272+
Needed to position the "content-height" sentinel at the correct position
273+
relative to the sheet for accurate intersection observation.
274+
*/
275+
position: relative;
276+
}
277+
}
278+
259279
:host([nested-scroll]) {
260280
.sheet-wrapper {
261281
display: flex;
@@ -457,6 +477,7 @@ export const template: string = /* HTML */ `
457477
<div class="snap snap-bottom" data-snap="bottom"></div>
458478
<div class="sentinel" data-snap="top"></div>
459479
<div class="sheet-wrapper">
480+
<div class="sentinel" data-snap="content-height"></div>
460481
<aside class="sheet" part="sheet" data-snap="top">
461482
<header class="sheet-header" part="header">
462483
<div class="handle" part="handle"></div>

0 commit comments

Comments
 (0)