Skip to content

Commit

Permalink
export useScrollLock
Browse files Browse the repository at this point in the history
  • Loading branch information
sirineJ committed Dec 18, 2024
1 parent da8f9b7 commit b9ad6ab
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 117 deletions.
3 changes: 2 additions & 1 deletion packages/circuit-ui/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ import { Body } from '../Body/index.js';
import { Button } from '../Button/index.js';
import { FullViewport } from '../../../../.storybook/components/index.js';

import { Modal, type ModalProps, useModal } from './Modal.js';
import { ModalProvider } from './ModalContext.js';

import { Modal, type ModalProps, useModal } from './index.js';

export default {
title: 'Components/Modal',
component: Modal,
Expand Down
2 changes: 1 addition & 1 deletion packages/circuit-ui/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ import { isEscape } from '../../util/key-codes.js';
import { useI18n } from '../../hooks/useI18n/useI18n.js';
import { deprecate } from '../../util/logger.js';
import type { Locale } from '../../util/i18n.js';
import { useScrollLock } from '../../hooks/useScrollLock/useScrollLock.js';

import classes from './Modal.module.css';
import {
getFirstFocusableElement,
hasNativeDialogSupport,
useScrollLock,
} from './ModalService.js';
import { translations } from './translations/index.js';

Expand Down
71 changes: 2 additions & 69 deletions packages/circuit-ui/components/Modal/ModalService.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@
* limitations under the License.
*/

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

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

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

describe('DialogService', () => {
describe('getKeyboardFocusableElements', () => {
Expand Down Expand Up @@ -87,69 +85,4 @@ 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');
});
});
});
46 changes: 0 additions & 46 deletions packages/circuit-ui/components/Modal/ModalService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useRef } from 'react';

export function getKeyboardFocusableElements(
element: HTMLElement,
Expand Down Expand Up @@ -56,48 +55,3 @@ export function getFirstFocusableElement(

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

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

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

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

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]);
};
80 changes: 80 additions & 0 deletions packages/circuit-ui/hooks/useScrollLock/useScrollLock.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* 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 { beforeEach, describe, expect, it, vi } from 'vitest';

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

import { useScrollLock } from './useScrollLock.js';

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/hooks/useScrollLock/useScrollLock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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, useRef } from 'react';

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

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

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

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]);
};
1 change: 1 addition & 0 deletions packages/circuit-ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,4 @@ export { useFocusList } from './hooks/useFocusList/index.js';
export { useCollapsible } from './hooks/useCollapsible/index.js';
export { useSwipe } from './hooks/useSwipe/index.js';
export { useMedia } from './hooks/useMedia/index.js';
export { useScrollLock } from './hooks/useScrollLock/useScrollLock.js';

0 comments on commit b9ad6ab

Please sign in to comment.