Skip to content

Commit

Permalink
optimise scrolling
Browse files Browse the repository at this point in the history
  • Loading branch information
sirineJ committed Dec 18, 2024
1 parent db3f353 commit 60c0c08
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 32 deletions.
33 changes: 3 additions & 30 deletions packages/circuit-ui/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { createUseModal } from './createUseModal.js';
import {
getFirstFocusableElement,
hasNativeDialogSupport,
useScrollLock,
} from './ModalService.js';
import { translations } from './translations/index.js';

Expand Down Expand Up @@ -124,6 +125,8 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>((props, ref) => {

const hasNativeDialog = hasNativeDialogSupport();

useScrollLock(open);

// set initial focus on the modal dialog content
useEffect(() => {
const dialogElement = dialogRef.current;
Expand All @@ -142,19 +145,6 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>((props, ref) => {
};
}, [open, initialFocusRef?.current]);

useEffect(() => {
function setScrollProperty() {
document.documentElement.style.setProperty(
'--scroll-y',
`${window.scrollY}px`,
);
}
window.addEventListener('scroll', setScrollProperty);
return () => {
window.removeEventListener('scroll', setScrollProperty);
};
}, []);

const handleDialogClose = useCallback(() => {
const dialogElement = dialogRef.current;
if (!dialogElement) {
Expand All @@ -167,15 +157,6 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>((props, ref) => {
ANIMATION_DURATION,
);
}
// restore scroll to page
const { body } = document;
const scrollY = body.style.top;
body.style.position = '';
body.style.top = '';
body.style.left = '';
body.style.right = '';
window.scrollTo(0, Number.parseInt(scrollY || '0', 10) * -1);

// trigger closing animation
dialogElement.classList.remove(classes.show);
if (!hasNativeDialog) {
Expand Down Expand Up @@ -270,14 +251,6 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>((props, ref) => {

// trigger show animation
dialogElement.classList.add(classes.show);
// disable scroll on page
const scrollY =
document.documentElement.style.getPropertyValue('--scroll-y');
const { body } = document;
body.style.position = 'fixed';
body.style.left = '0';
body.style.right = '0';
body.style.top = `-${scrollY}`;
}
} else if (dialogElement.open) {
handleDialogClose();
Expand Down
71 changes: 69 additions & 2 deletions packages/circuit-ui/components/Modal/ModalService.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
* limitations under the License.
*/

import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { getKeyboardFocusableElements } from './ModalService.js';
import { renderHook, act } from '../../util/test-utils.js';

import { getKeyboardFocusableElements, useScrollLock } from './ModalService.js';

describe('DialogService', () => {
describe('getKeyboardFocusableElements', () => {
Expand Down Expand Up @@ -85,4 +87,69 @@ describe('DialogService', () => {
);
});
});
describe('useScrollLock', () => {
Object.defineProperty(window, 'scrollTo', {
value: vi.fn(),
writable: true,
});

Object.defineProperty(window, 'scrollY', { value: 1, writable: true });

beforeEach(() => {
document.body.style.position = '';
document.body.style.top = '';
document.documentElement.style.setProperty('--scroll-y', '');
});

it('locks the scroll when `isLocked` is true', () => {
document.documentElement.style.setProperty('--scroll-y', '100px');

const { rerender } = renderHook(
({ isLocked }) => useScrollLock(isLocked),
{
initialProps: { isLocked: false },
},
);

rerender({ isLocked: true });

expect(document.body.style.position).toBe('fixed');
expect(document.body.style.top).toBe('-100px');
});

it('unlocks the scroll when `isLocked` is false', () => {
document.body.style.top = '-100px';

const { rerender } = renderHook(
({ isLocked }) => useScrollLock(isLocked),
{
initialProps: { isLocked: true },
},
);

rerender({ isLocked: false });

expect(document.body.style.position).toBe('');
expect(document.body.style.top).toBe('');
expect(window.scrollTo).toHaveBeenCalledWith(0, 100);
});

it('updates `--scroll-y` on scroll', () => {
global.requestAnimationFrame = vi
.fn()
.mockImplementation((callback: () => void) => callback());

renderHook(() => useScrollLock(false));

act(() => {
window.scrollY = 200;
const scrollEvent = new Event('scroll');
window.dispatchEvent(scrollEvent);
});

expect(
document.documentElement.style.getPropertyValue('--scroll-y'),
).toBe('200px');
});
});
});
61 changes: 61 additions & 0 deletions packages/circuit-ui/components/Modal/ModalService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@
* limitations under the License.
*/

/**
* Copyright 2024, SumUp Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';

export function getKeyboardFocusableElements(
element: HTMLElement,
): HTMLElement[] {
Expand Down Expand Up @@ -40,3 +56,48 @@ export function getFirstFocusableElement(

export const hasNativeDialogSupport = (): boolean =>
'HTMLDialogElement' in window;

export const useScrollLock = (isLocked: boolean): void => {
let busy = false;

useEffect(() => {
function setScrollProperty() {
if (!busy) {
requestAnimationFrame(() => {
document.documentElement.style.setProperty(
'--scroll-y',
`${window.scrollY}px`,
);
busy = false;
});
busy = true;
}
}
window.addEventListener('scroll', setScrollProperty);

return () => {
window.removeEventListener('scroll', setScrollProperty);
};
}, [busy]);

useEffect(() => {
if (isLocked) {
const scrollY =
document.documentElement.style.getPropertyValue('--scroll-y');
const { body } = document;
body.style.position = 'fixed';
body.style.left = '0';
body.style.right = '0';
body.style.top = `-${scrollY}`;
} else {
// restore scroll to page
const { body } = document;
const scrollY = body.style.top;
body.style.position = '';
body.style.top = '';
body.style.left = '';
body.style.right = '';
window.scrollTo(0, Number.parseInt(scrollY || '0', 10) * -1);
}
}, [isLocked]);
};

0 comments on commit 60c0c08

Please sign in to comment.