Skip to content

Commit 29d1b2e

Browse files
committed
Merge branch 'master' into feat/user.messages.deleted
# Conflicts: # yarn.lock
2 parents 5eb464a + a8e0694 commit 29d1b2e

File tree

4 files changed

+89
-22
lines changed

4 files changed

+89
-22
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
"fix-webm-duration": "^1.0.5",
112112
"hast-util-find-and-replace": "^5.0.1",
113113
"i18next": "^25.2.1",
114-
"linkifyjs": "^4.1.0",
114+
"linkifyjs": "^4.3.2",
115115
"lodash.debounce": "^4.0.8",
116116
"lodash.defaultsdeep": "^4.6.1",
117117
"lodash.mergewith": "^4.6.2",
@@ -192,7 +192,7 @@
192192
"@types/hast": "^2.3.4",
193193
"@types/jest": "^29.5.14",
194194
"@types/jsdom": "^21.1.5",
195-
"@types/linkifyjs": "^2.1.3",
195+
"@types/linkifyjs": "^2.1.7",
196196
"@types/lodash.debounce": "^4.0.7",
197197
"@types/lodash.defaultsdeep": "^4.6.9",
198198
"@types/lodash.mergewith": "^4.6.9",

src/components/Modal/Modal.tsx

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,72 @@
11
import clsx from 'clsx';
2-
import type { PropsWithChildren } from 'react';
2+
import { type PropsWithChildren, useCallback } from 'react';
33
import React, { useEffect, useRef } from 'react';
44
import { FocusScope } from '@react-aria/focus';
55

66
import { CloseIconRound } from './icons';
77

88
import { useTranslationContext } from '../../context';
99

10+
type CloseEvent =
11+
| KeyboardEvent
12+
| React.KeyboardEvent
13+
| React.MouseEvent<HTMLButtonElement | HTMLDivElement>;
14+
export type ModalCloseSource = 'overlay' | 'button' | 'escape';
15+
1016
export type ModalProps = {
1117
/** If true, modal is opened or visible. */
1218
open: boolean;
1319
/** Custom class to be applied to the modal root div */
1420
className?: string;
1521
/** Callback handler for closing of modal. */
16-
onClose?: (
17-
event: React.KeyboardEvent | React.MouseEvent<HTMLButtonElement | HTMLDivElement>,
18-
) => void;
22+
onClose?: (event: CloseEvent) => void;
23+
/** Optional handler to intercept closing logic. Return false to prevent onClose. */
24+
onCloseAttempt?: (source: ModalCloseSource, event: CloseEvent) => boolean;
1925
};
2026

2127
export const Modal = ({
2228
children,
2329
className,
2430
onClose,
31+
onCloseAttempt,
2532
open,
2633
}: PropsWithChildren<ModalProps>) => {
2734
const { t } = useTranslationContext('Modal');
2835

2936
const innerRef = useRef<HTMLDivElement | null>(null);
30-
const closeRef = useRef<HTMLButtonElement | null>(null);
37+
const closeButtonRef = useRef<HTMLButtonElement | null>(null);
38+
39+
const maybeClose = useCallback(
40+
(source: ModalCloseSource, event: CloseEvent) => {
41+
const allow = onCloseAttempt?.(source, event);
42+
if (allow !== false) {
43+
onClose?.(event);
44+
}
45+
},
46+
[onClose, onCloseAttempt],
47+
);
3148

3249
const handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => {
3350
const target = event.target as HTMLButtonElement | HTMLDivElement;
34-
if (!innerRef.current || !closeRef.current) return;
51+
if (!innerRef.current || !closeButtonRef.current) return;
3552

36-
if (!innerRef.current.contains(target) || closeRef.current.contains(target))
37-
onClose?.(event);
53+
if (closeButtonRef.current.contains(target)) {
54+
maybeClose('button', event);
55+
} else if (!innerRef.current.contains(target)) {
56+
maybeClose('overlay', event);
57+
}
3858
};
3959

4060
useEffect(() => {
4161
if (!open) return;
4262

4363
const handleKeyDown = (event: KeyboardEvent) => {
44-
if (event.key === 'Escape') onClose?.(event as unknown as React.KeyboardEvent);
64+
if (event.key === 'Escape') maybeClose('escape', event);
4565
};
4666

4767
document.addEventListener('keydown', handleKeyDown);
4868
return () => document.removeEventListener('keydown', handleKeyDown);
49-
}, [onClose, open]);
69+
}, [maybeClose, open]);
5070

5171
if (!open) return null;
5272

@@ -58,7 +78,7 @@ export const Modal = ({
5878
<FocusScope autoFocus contain>
5979
<button
6080
className='str-chat__modal__close-button'
61-
ref={closeRef}
81+
ref={closeButtonRef}
6282
title={t('Close')}
6383
>
6484
<CloseIconRound />

src/components/Modal/__tests__/Modal.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import '@testing-library/jest-dom';
55

66
import { Modal } from '../Modal';
77

8+
const CLOSE_BUTTON_SELECTOR = '.str-chat__modal__close-button';
9+
810
describe('Modal', () => {
911
afterEach(cleanup);
1012

@@ -95,4 +97,54 @@ describe('Modal', () => {
9597
const { container } = render(<Modal onClose={() => {}} open={false} />);
9698
expect(container).toBeEmptyDOMElement();
9799
});
100+
101+
it('should call onClose if onCloseAttempt returns true', () => {
102+
const onClose = jest.fn();
103+
const onCloseAttempt = () => true;
104+
const { container } = render(
105+
<Modal onClose={onClose} onCloseAttempt={onCloseAttempt} open />,
106+
);
107+
108+
fireEvent(
109+
document,
110+
new KeyboardEvent('keydown', {
111+
key: 'Escape',
112+
}),
113+
);
114+
115+
expect(onClose).toHaveBeenCalledTimes(1);
116+
117+
fireEvent.click(container.firstChild);
118+
119+
expect(onClose).toHaveBeenCalledTimes(2);
120+
121+
fireEvent.click(container.querySelector(CLOSE_BUTTON_SELECTOR));
122+
123+
expect(onClose).toHaveBeenCalledTimes(3);
124+
});
125+
126+
it('should not call onClose if onCloseAttempt returns false', () => {
127+
const onClose = jest.fn();
128+
const onCloseAttempt = () => false;
129+
const { container } = render(
130+
<Modal onClose={onClose} onCloseAttempt={onCloseAttempt} open />,
131+
);
132+
133+
fireEvent(
134+
document,
135+
new KeyboardEvent('keydown', {
136+
key: 'Escape',
137+
}),
138+
);
139+
140+
expect(onClose).toHaveBeenCalledTimes(0);
141+
142+
fireEvent.click(container.firstChild);
143+
144+
expect(onClose).toHaveBeenCalledTimes(0);
145+
146+
fireEvent.click(container.querySelector(CLOSE_BUTTON_SELECTOR));
147+
148+
expect(onClose).toHaveBeenCalledTimes(0);
149+
});
98150
});

yarn.lock

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,10 +2800,10 @@
28002800
"@types/ms" "*"
28012801
"@types/node" "*"
28022802

2803-
"@types/linkifyjs@^2.1.3":
2804-
version "2.1.3"
2805-
resolved "https://registry.yarnpkg.com/@types/linkifyjs/-/linkifyjs-2.1.3.tgz#80195c3c88c5e75d9f660e3046ce4a42be2c2fa4"
2806-
integrity sha512-V3Xt9wgaOvDPXcpOy3dC8qXCxy3cs0Lr/Hqgd9Bi6m3sf/vpbpTtfmVR0LJklrqYEjaAmc7e3Xh/INT2rCAKjQ==
2803+
"@types/linkifyjs@^2.1.7":
2804+
version "2.1.7"
2805+
resolved "https://registry.yarnpkg.com/@types/linkifyjs/-/linkifyjs-2.1.7.tgz#000b1630ff7a3776f98c8e53ba98b8ad7d92efc4"
2806+
integrity sha512-+SIYXs1lajyD7t/2+V9GLfdFlc/6Nr2tr65kjA2F5oOzBlPH+NiPqySJDHzREoGcL91Au9Qef8M5JdZiRXsaJw==
28072807
dependencies:
28082808
"@types/react" "*"
28092809

@@ -8599,11 +8599,6 @@ lines-and-columns@^1.1.6:
85998599
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
86008600
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
86018601

8602-
linkifyjs@^4.1.0:
8603-
version "4.1.0"
8604-
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.0.tgz#0460bfcc37d3348fa80e078d92e7bbc82588db15"
8605-
integrity sha512-Ffv8VoY3+ixI1b3aZ3O+jM6x17cOsgwfB1Wq7pkytbo1WlyRp6ZO0YDMqiWT/gQPY/CmtiGuKfzDIVqxh1aCTA==
8606-
86078602
linkifyjs@^4.3.2:
86088603
version "4.3.2"
86098604
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1"

0 commit comments

Comments
 (0)