Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions dotcom-rendering/scripts/jest/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,22 @@ if (!isServer) {
global.TextEncoder = TextEncoder as unknown as typeof global.TextEncoder;
global.TextDecoder = TextDecoder as unknown as typeof global.TextDecoder;

if (!isServer) {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string): MediaQueryList =>
({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}) as MediaQueryList,
});
}

// Mocks the version number used by CDK, we don't want our tests to fail every time we update our cdk dependency.
jest.mock('@guardian/cdk/lib/constants/tracking-tag');
8 changes: 4 additions & 4 deletions dotcom-rendering/src/components/ModalOverlay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ describe('ModalOverlay', () => {
expect(onClose).toHaveBeenCalledTimes(1);
});

it('calls onClose when the overlay backdrop receives a mousedown', () => {
it('calls onClose when the overlay backdrop receives a pointerdown', () => {
const onClose = jest.fn();
renderOverlay(onClose);

const overlay = screen.getByRole('dialog').parentElement!;
fireEvent.mouseDown(overlay);
fireEvent.pointerDown(overlay);

act(() => {
jest.runAllTimers();
Expand All @@ -62,11 +62,11 @@ describe('ModalOverlay', () => {
expect(onClose).toHaveBeenCalledTimes(1);
});

it('does not call onClose when the dialog itself receives a mousedown', () => {
it('does not call onClose when the dialog itself receives a pointerdown', () => {
const onClose = jest.fn();
renderOverlay(onClose);

fireEvent.mouseDown(screen.getByRole('dialog'));
fireEvent.pointerDown(screen.getByRole('dialog'));

act(() => {
jest.runAllTimers();
Expand Down
41 changes: 22 additions & 19 deletions dotcom-rendering/src/components/ModalOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { getZIndex } from '../lib/getZIndex';
const OPEN_ANIMATION_DURATION_MS = 300;
const CLOSE_ANIMATION_DURATION_MS = 225;

const prefersReducedMotion = (): boolean =>
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;

const ModalRequestCloseContext = createContext<(() => void) | null>(null);

export const useModalRequestClose = (): (() => void) => {
Expand Down Expand Up @@ -130,19 +134,18 @@ export const ModalOverlay = ({
const overlayRef = useRef<HTMLDivElement>(null);
const dialogRef = useRef<HTMLDivElement>(null);
const closeTimeoutRef = useRef<number | null>(null);
const [isVisible, setIsVisible] = useState(false);

const [isVisible, setIsVisible] = useState(() => prefersReducedMotion());

const requestClose = useCallback(() => {
if (closeTimeoutRef.current !== null) {
return;
}

const prefersReducedMotion =
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ??
false;

if (prefersReducedMotion) {
closeTimeoutRef.current = 0;
if (prefersReducedMotion()) {
// Use -1 as a sentinel to block re-entrant calls. 0 is a valid
// setTimeout ID and must not be used here.
closeTimeoutRef.current = -1;
Comment thread
georgerichmond marked this conversation as resolved.
Outdated
onClose();
return;
}
Expand All @@ -156,12 +159,7 @@ export const ModalOverlay = ({

// Trigger open animation on mount
useEffect(() => {
const prefersReducedMotion =
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ??
false;

if (prefersReducedMotion) {
setIsVisible(true);
if (prefersReducedMotion()) {
return;
}

Expand Down Expand Up @@ -205,7 +203,9 @@ export const ModalOverlay = ({
? document.activeElement
: null;

dialogElement.focus();
// preventScroll stops iOS Safari from jerking the viewport to bring
// the off-screen element into view before the slide-up animation runs.
dialogElement.focus({ preventScroll: true });

return () => {
if (
Expand Down Expand Up @@ -282,18 +282,22 @@ export const ModalOverlay = ({
return;
}

const handleOverlayMouseDown = (event: MouseEvent) => {
const handleOverlayPointerDown = (event: PointerEvent) => {
if (event.target === overlayElement) {
event.preventDefault();
requestClose();
}
};

overlayElement.addEventListener('mousedown', handleOverlayMouseDown);
overlayElement.addEventListener(
'pointerdown',
Comment thread
rBangay marked this conversation as resolved.
handleOverlayPointerDown,
);

return () => {
overlayElement.removeEventListener(
'mousedown',
handleOverlayMouseDown,
'pointerdown',
handleOverlayPointerDown,
);
};
}, [requestClose]);
Expand All @@ -305,7 +309,6 @@ export const ModalOverlay = ({
return createPortal(
<div ref={overlayRef} css={overlayStyles(isVisible)}>
<div
/* PRISTINE JSX: No hacky touch handlers needed anymore */
ref={dialogRef}
role="dialog"
aria-modal="true"
Expand Down
Loading