-
Notifications
You must be signed in to change notification settings - Fork 67
Expand file tree
/
Copy pathSessionFocusTrap.tsx
More file actions
102 lines (94 loc) · 3.11 KB
/
SessionFocusTrap.tsx
File metadata and controls
102 lines (94 loc) · 3.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import { FocusTrap, type FocusTrapProps } from 'focus-trap-react';
import { type ReactNode, useEffect, useState } from 'react';
import type { CSSProperties } from 'styled-components';
import { windowErrorFilters } from '../util/logger/renderer_process_logging';
import { getFeatureFlagMemo } from '../state/ducks/types/releasedFeaturesReduxTypes';
const focusTrapErrorSource = 'focus-trap';
type SessionFocusTrapProps = FocusTrapProps['focusTrapOptions'] & {
/** id used for debugging */
focusTrapId: string;
children: ReactNode;
active?: boolean;
containerDivStyle?: CSSProperties;
/** Suppress errors thrown from inside the focus trap, preventing logging or global error emission */
suppressErrors?: boolean;
/** Allows the focus trap to exist without detectable tabbable elements. This is required if the children
* are within a Shadow DOM. Internally sets suppressErrors to true. */
allowNoTabbableNodes?: boolean;
};
export function SessionFocusTrap({
focusTrapId,
children,
active = true,
allowOutsideClick = true,
containerDivStyle,
suppressErrors,
allowNoTabbableNodes,
onActivate,
onPostActivate,
onDeactivate,
onPostDeactivate,
...rest
}: SessionFocusTrapProps) {
const debugFocusTrap = getFeatureFlagMemo('debugFocusTrap');
const defaultTabIndex = allowNoTabbableNodes ? 0 : -1;
const _suppressErrors = suppressErrors || allowNoTabbableNodes;
/**
* NOTE: the tab index tricks the focus trap into thinking it has
* tabbable children by setting a tab index on the empty div child. When
* the trap activates it will see the div in the tab list and render without
* error, then remove that div from the tab index list. Then when the trap
* deactivates the state is reset.
*/
const [tabIndex, setTabIndex] = useState<0 | 1 | -1>(defaultTabIndex);
const _onActivate = () => {
if (debugFocusTrap) {
window.log.debug(`[SessionFocusTrap] onActivate - ${focusTrapId}`);
}
onActivate?.();
};
const _onPostActivate = () => {
if (allowNoTabbableNodes) {
setTabIndex(-1);
}
onPostActivate?.();
};
const _onDeactivate = () => {
if (debugFocusTrap) {
window.log.debug(`[SessionFocusTrap] onDeactivate - ${focusTrapId}`);
}
if (allowNoTabbableNodes) {
setTabIndex(defaultTabIndex);
}
onDeactivate?.();
};
useEffect(() => {
if (!active || !_suppressErrors) {
return;
}
windowErrorFilters.add(focusTrapErrorSource);
// eslint-disable-next-line consistent-return -- This return is the destructor
return () => {
windowErrorFilters.remove(focusTrapErrorSource);
};
}, [_suppressErrors, active]);
return (
<FocusTrap
active={active}
focusTrapOptions={{
...rest,
allowOutsideClick,
onActivate: _onActivate,
onPostActivate: _onPostActivate,
onDeactivate: _onDeactivate,
onPostDeactivate,
}}
>
{/* Note: without this div, the focus trap doesn't work */}
<div style={containerDivStyle}>
{allowNoTabbableNodes ? <div tabIndex={tabIndex} /> : null}
{children}
</div>
</FocusTrap>
);
}