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