Skip to content

Commit 7ca1e6d

Browse files
committed
useModalClose: update to typescript & add test
1 parent b5eea95 commit 7ca1e6d

File tree

2 files changed

+78
-10
lines changed

2 files changed

+78
-10
lines changed

client/common/useModalClose.test.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React, { useRef, useEffect } from 'react';
2+
import { render, fireEvent } from '@testing-library/react';
3+
import useModalClose from './useModalClose';
4+
import useKeyDownHandlers from './useKeyDownHandlers';
5+
6+
jest.mock('./useKeyDownHandlers');
7+
8+
describe('useModalClose', () => {
9+
let onClose: jest.Mock;
10+
11+
beforeEach(() => {
12+
onClose = jest.fn();
13+
jest.clearAllMocks();
14+
});
15+
16+
function TestModal({ handleClose }: { handleClose: () => void }) {
17+
const ref = useModalClose(handleClose);
18+
return (
19+
<div>
20+
<div data-testid="outside">Outside</div>
21+
<div
22+
data-testid="modal"
23+
ref={ref as React.RefObject<HTMLDivElement>}
24+
tabIndex={-1}
25+
style={{ border: '1px solid black' }}
26+
>
27+
Modal content
28+
</div>
29+
</div>
30+
);
31+
}
32+
33+
function rerender() {
34+
return render(<TestModal handleClose={onClose} />);
35+
}
36+
37+
it('calls onClose when clicking outside the modal', () => {
38+
const { getByTestId } = rerender();
39+
40+
fireEvent.click(getByTestId('outside'));
41+
42+
expect(onClose).toHaveBeenCalled();
43+
});
44+
45+
it('does not call onClose when clicking inside the modal', () => {
46+
const { getByTestId } = rerender();
47+
48+
fireEvent.click(getByTestId('modal'));
49+
50+
expect(onClose).not.toHaveBeenCalled();
51+
});
52+
53+
it('returns a ref that is focused on mount', () => {
54+
const { getByTestId } = rerender();
55+
const modal = getByTestId('modal');
56+
57+
expect(document.activeElement).toBe(modal);
58+
});
59+
60+
it('calls useKeyDownHandlers with escape handler', () => {
61+
rerender();
62+
63+
expect(useKeyDownHandlers).toHaveBeenCalledWith({ escape: onClose });
64+
});
65+
});

client/common/useModalClose.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef } from 'react';
1+
import { useEffect, useRef, MutableRefObject } from 'react';
22
import useKeyDownHandlers from './useKeyDownHandlers';
33

44
/**
@@ -14,21 +14,24 @@ import useKeyDownHandlers from './useKeyDownHandlers';
1414
*
1515
* Returns a ref to attach to the outermost element of the modal.
1616
*
17-
* @param {() => void} onClose
18-
* @param {React.MutableRefObject<HTMLElement | null>} [passedRef]
19-
* @return {React.MutableRefObject<HTMLElement | null>}
17+
* @param onClose - Function called when modal should close
18+
* @param passedRef - Optional ref to the modal element. If not provided, one is created internally.
19+
* @returns A ref to be attached to the modal DOM element
2020
*/
21-
export default function useModalClose(onClose, passedRef) {
22-
const createdRef = useRef(null);
23-
const modalRef = passedRef || createdRef;
21+
export default function useModalClose(
22+
onClose: () => void,
23+
passedRef?: MutableRefObject<HTMLElement | null>
24+
): MutableRefObject<HTMLElement | null> {
25+
const createdRef = useRef<HTMLElement | null>(null);
26+
const modalRef = passedRef ?? createdRef;
2427

2528
useEffect(() => {
2629
modalRef.current?.focus();
2730

28-
function handleClick(e) {
31+
function handleClick(e: MouseEvent) {
2932
// ignore clicks on the component itself
30-
if (modalRef.current && !modalRef.current.contains(e.target)) {
31-
onClose?.();
33+
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
34+
onClose();
3235
}
3336
}
3437

0 commit comments

Comments
 (0)