Skip to content

Commit a8c4df3

Browse files
committed
fix: review compliance, move captureModalFocusContext into utils file.
1 parent d5ed4c0 commit a8c4df3

File tree

3 files changed

+134
-36
lines changed

3 files changed

+134
-36
lines changed

src/script/components/Modals/PrimaryModal/PrimaryModal.tsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*
1818
*/
1919

20-
import {FC, FormEvent, MouseEvent, useState, useRef, ChangeEvent, useEffect, useMemo} from 'react';
20+
import {FC, FormEvent, MouseEvent, useState, useRef, ChangeEvent, useEffect, useMemo, useCallback} from 'react';
2121

2222
import {ValidationUtil} from '@wireapp/commons';
2323
import {ErrorMessage} from '@wireapp/react-ui-kit';
@@ -209,6 +209,13 @@ export const PrimaryModalComponent: FC = () => {
209209

210210
const secondaryActions = Array.isArray(secondaryAction) ? secondaryAction : [secondaryAction];
211211

212+
const closeAction = useCallback(() => {
213+
if (hasPasswordWithRules) {
214+
const [closeActionItem] = secondaryActions;
215+
closeActionItem?.action?.();
216+
}
217+
}, [hasPasswordWithRules, secondaryActions]);
218+
212219
// Auto-focus close button when modal opens
213220
useEffect(() => {
214221
if (!isModalVisible) {
@@ -217,17 +224,23 @@ export const PrimaryModalComponent: FC = () => {
217224

218225
// Use setTimeout to ensure the modal is fully rendered before focusing
219226
const timeoutId = setTimeout(() => {
220-
if (closeButtonRef.current) {
221-
closeButtonRef.current.focus();
227+
// Focus primary button if it should come first, otherwise focus close button
228+
const targetElement = primaryBtnFirst ? primaryActionButtonRef.current : closeButtonRef.current;
229+
const fallbackElement = primaryBtnFirst ? closeButtonRef.current : primaryActionButtonRef.current;
230+
231+
if (targetElement) {
232+
targetElement.focus();
233+
} else if (fallbackElement) {
234+
fallbackElement.focus();
222235
}
223236
}, 0);
224237

225238
return () => clearTimeout(timeoutId);
226-
}, [isModalVisible]);
239+
}, [isModalVisible, primaryBtnFirst]);
227240

228-
useEffect(() => {
229-
const onKeyDown = (event: KeyboardEvent) => {
230-
if (isEscapeKey(event) && isModalVisible) {
241+
const onKeyDown = useCallback(
242+
(event: KeyboardEvent) => {
243+
if (isEscapeKey(event)) {
231244
removeCurrentModal();
232245
closeAction();
233246
}
@@ -237,18 +250,18 @@ export const PrimaryModalComponent: FC = () => {
237250
primaryAction?.action?.();
238251
removeCurrentModal();
239252
}
240-
};
253+
},
254+
[closeAction, primaryAction],
255+
);
256+
257+
useEffect(() => {
258+
if (!isModalVisible) {
259+
return undefined;
260+
}
241261

242262
document.addEventListener('keydown', onKeyDown);
243263
return () => document.removeEventListener('keydown', onKeyDown);
244-
}, [primaryAction, isModalVisible]);
245-
246-
const closeAction = () => {
247-
if (hasPasswordWithRules) {
248-
const [closeAction] = secondaryActions;
249-
closeAction?.action?.();
250-
}
251-
};
264+
}, [isModalVisible, primaryAction, closeAction, onKeyDown]);
252265

253266
const secondaryButtons = secondaryActions
254267
.filter((action): action is ButtonAction => action !== null && !!action.text)

src/script/repositories/calling/CallingRepository.ts

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {calculateChildWindowPosition} from 'Util/DOM/caculateChildWindowPosition
8484
import {isDetachedCallingFeatureEnabled} from 'Util/isDetachedCallingFeatureEnabled';
8585
import {t} from 'Util/LocalizerUtil';
8686
import {getLogger, Logger} from 'Util/Logger';
87+
import {captureModalFocusContext} from 'Util/ModalFocusUtil';
8788
import {roundLogarithmic} from 'Util/NumberUtil';
8889
import {matchQualifiedIds} from 'Util/QualifiedId';
8990
import {copyStyles} from 'Util/renderElement';
@@ -2782,31 +2783,21 @@ export class CallingRepository {
27822783

27832784
/**
27842785
* Helper method to get modal container and restore focus callback
2785-
* @returns Object containing container, targetDocument, and a focus restoration callback
2786+
* Call this method immediately before showing a modal to ensure the correct active element is captured.
2787+
* @returns Object containing container and focus restoration callback
27862788
*/
2787-
private getModalContainerAndRestoreFocusCallback(): {
2788-
container?: HTMLElement;
2789-
targetDocument: Document;
2790-
restoreFocusCallback: (additionalCallback?: () => void) => () => void;
2791-
} {
2789+
private getModalContainerAndRestoreFocusCallback() {
27922790
const detachedWindow = this.callState.detachedWindow();
27932791
const isDetachedWindow = this.callState.viewMode() === CallingViewMode.DETACHED_WINDOW;
2794-
const targetDocument = isDetachedWindow && detachedWindow ? detachedWindow.document : document;
2795-
const previouslyFocusedElement = targetDocument.activeElement as HTMLElement;
27962792

2797-
return {
2793+
const context = captureModalFocusContext({
2794+
targetDocument: isDetachedWindow && detachedWindow ? detachedWindow.document : undefined,
27982795
container: isDetachedWindow && detachedWindow ? detachedWindow.document.body : undefined,
2799-
targetDocument,
2800-
restoreFocusCallback: (additionalCallback?: () => void) => () => {
2801-
// Execute additional callback if provided
2802-
if (additionalCallback) {
2803-
additionalCallback();
2804-
}
2805-
// Restore focus to the button that triggered the modal
2806-
if (previouslyFocusedElement && typeof previouslyFocusedElement.focus === 'function') {
2807-
previouslyFocusedElement.focus();
2808-
}
2809-
},
2796+
});
2797+
2798+
return {
2799+
container: context.container,
2800+
restoreFocusCallback: context.createFocusRestorationCallback,
28102801
};
28112802
}
28122803

src/script/util/ModalFocusUtil.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2026 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*
18+
*/
19+
20+
interface ModalFocusContext {
21+
/**
22+
* The document to use for capturing the active element (defaults to global document)
23+
*/
24+
targetDocument?: Document;
25+
26+
/**
27+
* The container element where the modal will be rendered (e.g., for detached windows)
28+
*/
29+
container?: HTMLElement;
30+
}
31+
32+
interface ModalFocusResult {
33+
/**
34+
* The target document to use for modal operations
35+
*/
36+
targetDocument: Document;
37+
38+
/**
39+
* The container element where the modal will be rendered (undefined for main window)
40+
*/
41+
container?: HTMLElement;
42+
43+
/**
44+
* Creates a focus restoration callback that should be called when the modal closes
45+
*
46+
* @param additionalCallback - Optional callback to execute before restoring focus
47+
* @returns Callback function to restore focus to the previously focused element
48+
*/
49+
createFocusRestorationCallback: (additionalCallback?: () => void) => () => void;
50+
}
51+
52+
/**
53+
* Captures the current focus context for a modal before it's shown.
54+
*
55+
* @param context - Optional context to specify target document or container
56+
* @returns Object containing document, container, and focus restoration callback creator
57+
*/
58+
export function captureModalFocusContext(context: ModalFocusContext = {}): ModalFocusResult {
59+
const targetDocument = context.targetDocument || document;
60+
61+
// Capture the currently focused element at the time this function is called
62+
const previouslyFocusedElement = targetDocument.activeElement as HTMLElement | null;
63+
64+
return {
65+
targetDocument,
66+
container: context.container,
67+
createFocusRestorationCallback: (additionalCallback?: () => void) => () => {
68+
// Execute additional callback if provided
69+
if (additionalCallback) {
70+
try {
71+
additionalCallback();
72+
} catch (error) {
73+
console.error('Error in modal close callback:', error);
74+
}
75+
}
76+
77+
// Restore focus to the previously focused element
78+
if (previouslyFocusedElement && typeof previouslyFocusedElement.focus === 'function') {
79+
try {
80+
// Check if the element is still in the document before focusing
81+
if (
82+
document.contains(previouslyFocusedElement) ||
83+
(context.targetDocument && context.targetDocument.contains(previouslyFocusedElement))
84+
) {
85+
previouslyFocusedElement.focus();
86+
}
87+
} catch (error) {
88+
// Silently handle focus errors (e.g., element no longer in DOM)
89+
console.error('Failed to restore focus to element:', error);
90+
}
91+
}
92+
},
93+
};
94+
}

0 commit comments

Comments
 (0)