Skip to content

Commit 426bf48

Browse files
thejacksheltonGregOnNet
authored andcommitted
fix(modal): fixing scrollbar flickers
1 parent a030c17 commit 426bf48

File tree

7 files changed

+33
-74
lines changed

7 files changed

+33
-74
lines changed

apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
}
1212

1313
.my-animation.modal-closing {
14-
animation: modalClose 0.45s forwards cubic-bezier(0.6, 0.6, 0, 1);
14+
animation: modalClose 0.35s forwards cubic-bezier(0.6, 0.6, 0, 1);
1515
}
1616

1717
.my-animation.modal-closing::backdrop {
18-
animation: modalFadeOut 0.45s forwards cubic-bezier(0.6, 0.6, 0, 1);
18+
animation: modalFadeOut 0.35s forwards cubic-bezier(0.6, 0.6, 0, 1);
1919
}
2020

2121
@keyframes modalOpen {

apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ export default component$(() => {
2222
}
2323
2424
.sheet.modal-closing {
25-
animation: sheetClose 0.45s forwards cubic-bezier(0.6, 0.6, 0, 1);
25+
animation: sheetClose 0.35s forwards cubic-bezier(0.6, 0.6, 0, 1);
2626
}
2727
2828
.sheet.modal-closing::backdrop {
29-
animation: sheetFadeOut 0.45s forwards cubic-bezier(0.6, 0.6, 0, 1);
29+
animation: sheetFadeOut 0.35s forwards cubic-bezier(0.6, 0.6, 0, 1);
3030
}
3131
3232
@keyframes sheetOpen {

apps/website/src/routes/docs/headless/(components)/modal/examples/transition.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default component$(() => {
1616
1717
.my-transition, .my-transition::backdrop {
1818
opacity: 0;
19-
transition: opacity 500ms ease;
19+
transition: opacity 300ms ease;
2020
}
2121
2222
.my-transition.modal-showing, .my-transition.modal-showing::backdrop {

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@storybook/test-runner": "^0.13.0",
5656
"@storybook/testing-library": "^0.2.0",
5757
"@testing-library/cypress": "9.0.0",
58+
"@types/body-scroll-lock": "3.1.1",
5859
"@types/eslint": "^8.44.2",
5960
"@types/node": "^20.5.7",
6061
"@typescript-eslint/eslint-plugin": "^5",
@@ -64,6 +65,7 @@
6465
"all-contributors-cli": "^6.26.1",
6566
"autoprefixer": "^10.4.15",
6667
"axe-core": "^4.7.2",
68+
"body-scroll-lock": "4.0.0-beta.0",
6769
"chromatic": "^6.24.1",
6870
"clipboard-copy": "4.0.1",
6971
"commitizen": "^4.3.0",

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

Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export type WidthState = {
55
width: number | null;
66
};
77

8+
import { clearAllBodyScrollLocks } from 'body-scroll-lock';
9+
810
/**
911
* Traps the focus of the given Modal
1012
* @returns FocusTrap
@@ -72,44 +74,6 @@ export function wasModalBackdropClicked(
7274
return wasBackdropClicked;
7375
}
7476

75-
/**
76-
* Locks scrolling of the document.
77-
*/
78-
export function lockScroll(scrollbar: WidthState) {
79-
if (scrollbar.width === null) {
80-
scrollbar.width = window.innerWidth - document.documentElement.clientWidth;
81-
}
82-
83-
window.document.body.style.overflow = 'hidden';
84-
document.body.style.paddingRight = `${scrollbar.width}px`;
85-
}
86-
87-
/**
88-
* Unlocks scrolling of the document.
89-
* Adjusts padding of the given scrollbar.
90-
*/
91-
export function unlockScroll() {
92-
window.document.body.style.overflow = '';
93-
document.body.style.paddingRight = '';
94-
}
95-
96-
/**
97-
* When the Modal is opened we are disabling scrolling.
98-
* This means the scrollbar will vanish.
99-
* The scrollbar has a width and causes the Modal to reposition.
100-
*
101-
* That's why we take the scrollbar-width into account so that the
102-
* Modal does not jump to the right.
103-
*/
104-
export function adjustScrollbar(scrollbar: WidthState, modal: HTMLDialogElement) {
105-
if (scrollbar.width === null) {
106-
scrollbar.width = window.innerWidth - document.documentElement.clientWidth;
107-
}
108-
109-
modal.style.left = 0 + 'px';
110-
document.body.style.paddingRight = `${scrollbar.width}px`;
111-
}
112-
11377
export function overrideNativeDialogEscapeBehaviorWith(continuation: () => void) {
11478
return function handleKeydown(e: KeyboardEvent) {
11579
if (e.key === 'Escape') {
@@ -120,27 +84,6 @@ export function overrideNativeDialogEscapeBehaviorWith(continuation: () => void)
12084
};
12185
}
12286

123-
/**
124-
* When the Modal is closed we are enabling scrolling.
125-
* This means the scrollbar will reappear in the browser.
126-
* The scrollbar has a width and causes the Modal to reposition.
127-
*
128-
* That's why we take the scrollbar-width into account so that the
129-
* Modal remains in the same position as before.
130-
*/
131-
export function keepModalInPlaceWhileScrollbarReappears(
132-
scrollbar: WidthState,
133-
modal?: HTMLDialogElement,
134-
) {
135-
if (!modal) return;
136-
137-
if (scrollbar.width) {
138-
const modalLeft = parseInt(modal.style.left);
139-
140-
modal.style.left = `${scrollbar.width - modalLeft}px`;
141-
}
142-
}
143-
14487
/*
14588
* Adds CSS-Class to support modal-opening-animation
14689
*/
@@ -162,11 +105,13 @@ export function supportClosingAnimation(
162105
const { animationDuration, transitionDuration } = getComputedStyle(modal);
163106

164107
const runAnimationEnd = () => {
108+
clearAllBodyScrollLocks();
165109
modal.classList.remove('modal-closing');
166110
afterAnimate();
167111
};
168112

169113
const runTransitionEnd = () => {
114+
clearAllBodyScrollLocks();
170115
modal.classList.remove('modal-closing');
171116
afterAnimate();
172117
};
@@ -177,6 +122,7 @@ export function supportClosingAnimation(
177122
modal.addEventListener('transitionend', runTransitionEnd, { once: true });
178123
} else {
179124
modal.classList.remove('modal-closing');
125+
clearAllBodyScrollLocks();
180126
afterAnimate();
181127
}
182128
}

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,17 @@ import {
1111
useTask$,
1212
} from '@builder.io/qwik';
1313
import {
14-
WidthState,
1514
activateFocusTrap,
16-
adjustScrollbar,
1715
closeModal,
1816
deactivateFocusTrap,
19-
keepModalInPlaceWhileScrollbarReappears,
20-
lockScroll,
2117
overrideNativeDialogEscapeBehaviorWith,
2218
showModal,
2319
trapFocus,
24-
unlockScroll,
2520
wasModalBackdropClicked,
2621
} from './modal-behavior';
2722

23+
import { disableBodyScroll, type BodyScrollOptions } from 'body-scroll-lock';
24+
2825
import styles from './modal.css?inline';
2926

3027
export type ModalProps = Omit<QwikIntrinsicElements['dialog'], 'open'> & {
@@ -38,7 +35,10 @@ export type ModalProps = Omit<QwikIntrinsicElements['dialog'], 'open'> & {
3835
export const Modal = component$((props: ModalProps) => {
3936
useStyles$(styles);
4037
const modalRefSig = useSignal<HTMLDialogElement>();
41-
const scrollbar: WidthState = { width: null };
38+
39+
const scrollOptions: BodyScrollOptions = {
40+
reserveScrollBarGap: true,
41+
};
4242

4343
const { 'bind:show': showSig } = props;
4444

@@ -58,19 +58,16 @@ export const Modal = component$((props: ModalProps) => {
5858

5959
if (isOpen) {
6060
showModal(modal);
61+
disableBodyScroll(modal, scrollOptions);
6162
props.onShow$?.();
62-
adjustScrollbar(scrollbar, modal);
6363
activateFocusTrap(focusTrap);
64-
lockScroll(scrollbar);
6564
} else {
66-
unlockScroll();
6765
closeModal(modal);
6866
props.onClose$?.();
6967
}
7068

7169
cleanup(() => {
7270
deactivateFocusTrap(focusTrap);
73-
keepModalInPlaceWhileScrollbarReappears(scrollbar, modalRefSig.value);
7471
});
7572
});
7673

pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)