Skip to content

Commit e61d6b1

Browse files
committed
fix: allow scrolling modal inner elements
1 parent cb639ad commit e61d6b1

4 files changed

Lines changed: 68 additions & 20 deletions

File tree

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,37 @@
11
export const api = {
2-
modal: [
2+
"modal": [
33
{
4-
'modal-close': [],
4+
"modal-close": []
55
},
66
{
7-
'modal-content': [],
7+
"modal-content": []
88
},
99
{
10-
'modal-context': [],
10+
"modal-context": []
1111
},
1212
{
13-
'modal-description': [],
13+
"modal-description": []
1414
},
1515
{
16-
'modal-footer': [],
16+
"modal-footer": []
1717
},
1818
{
19-
'modal-header': [],
19+
"modal-header": []
2020
},
2121
{
22-
'modal-panel': [],
22+
"modal-panel": []
2323
},
2424
{
25-
'modal-root': [],
25+
"modal-root": []
2626
},
2727
{
28-
'modal-title': [],
28+
"modal-title": []
2929
},
3030
{
31-
'modal-trigger': [],
31+
"modal-trigger": []
3232
},
3333
{
34-
'use-modal': [],
35-
},
36-
],
37-
};
34+
"use-modal": []
35+
}
36+
]
37+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { component$, useStyles$ } from '@builder.io/qwik';
2+
import { Modal } from '@qwik-ui/headless';
3+
4+
export default component$(() => {
5+
useStyles$(styles);
6+
7+
return (
8+
<Modal.Root>
9+
<Modal.Trigger class="modal-trigger">Open Modal</Modal.Trigger>
10+
<Modal.Panel class="modal-panel">
11+
<h2>Scrollable body</h2>
12+
<div data-testid="inner-scroll" style={{ height: '120px', overflowY: 'auto' }}>
13+
<div style={{ height: '800px' }}>Tall content that scrolls inside the panel</div>
14+
</div>
15+
</Modal.Panel>
16+
</Modal.Root>
17+
);
18+
});
19+
20+
// internal
21+
import styles from '../snippets/modal.css?inline';

packages/kit-headless/src/components/modal/modal-panel.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,7 @@ export const HModalPanel = component$((props: PropsOf<'dialog'>) => {
5353

5454
if (!panelRef.value) return;
5555

56-
// Initialize the scroll locker once. `@fluejs/noscroll` keeps the panel
57-
// itself scrollable/interactive (incl. touch dragging on iOS Safari) while
58-
// the page behind it is locked — unlike body-scroll-lock-upgrade, which
59-
// blocked movement-based touch inside the modal on iOS (#1113).
6056
if (!isInitialized.value) {
61-
markScrollable(panelRef.value);
6257
isInitialized.value = true;
6358

6459
const noScroll = createNoScroll({
@@ -74,6 +69,20 @@ export const HModalPanel = component$((props: PropsOf<'dialog'>) => {
7469

7570
if (isOpen) {
7671
await showModal(panelRef.value);
72+
73+
// noscroll only lets touch gestures scroll elements it has marked.
74+
// Mark the panel and any inner scroll container (e.g. a scrollable body
75+
// with a sticky header/footer) so they remain touch-scrollable on iOS;
76+
// otherwise the gesture resolves to the non-scrollable dialog and gets
77+
// prevented. Re-scanned on each open to pick up the current content.
78+
markScrollable(panelRef.value);
79+
panelRef.value.querySelectorAll<HTMLElement>('*').forEach((el) => {
80+
const { overflowX, overflowY } = getComputedStyle(el);
81+
if (/(auto|scroll|overlay)/.test(`${overflowX} ${overflowY}`)) {
82+
markScrollable(el);
83+
}
84+
});
85+
7786
disablePageScroll.value?.();
7887
activateFocusTrap(focusTrap);
7988
} else {

packages/kit-headless/src/components/modal/modal.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,24 @@ test.describe('Scroll locking', () => {
138138
).toBe(true);
139139
});
140140

141+
test(`GIVEN a modal with an inner scroll container
142+
WHEN the modal is opened
143+
THEN both the panel and the inner container are marked scrollable`, async ({
144+
page,
145+
}) => {
146+
const { driver: d } = await setup(page, 'inner-scroll');
147+
148+
await d.openModal();
149+
150+
// noscroll only allows touch-scrolling on elements it has marked; the panel
151+
// and any inner scroll container must both carry the marker (#1113).
152+
await expect(d.getModal()).toHaveAttribute('data-noscroll-target-scrollable', '');
153+
await expect(page.getByTestId('inner-scroll')).toHaveAttribute(
154+
'data-noscroll-target-scrollable',
155+
'',
156+
);
157+
});
158+
141159
test(`GIVEN a modal
142160
WHEN navigating to another page in SPA
143161
THEN the body should not have overflow hidden`, async ({ page }) => {

0 commit comments

Comments
 (0)