@lumx/react 3.6.3 → 3.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +1 -1
- package/index.js +114 -82
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/expansion-panel/ExpansionPanel.test.tsx +5 -0
- package/src/components/input-helper/InputHelper.tsx +3 -3
- package/src/components/popover/Popover.tsx +15 -55
- package/src/components/popover/useRestoreFocusOnClose.tsx +33 -0
- package/src/components/select/Select.test.tsx +4 -0
- package/src/components/text-field/TextField.test.tsx +5 -0
- package/src/components/tooltip/Tooltip.stories.tsx +16 -0
- package/src/components/tooltip/Tooltip.test.tsx +24 -1
- package/src/components/tooltip/useTooltipOpen.tsx +11 -2
- package/src/utils/isFocusVisible.ts +3 -0
- package/src/utils/mergeRefs.ts +12 -1
- package/src/utils/useBeforeUnmountSentinel.tsx +17 -0
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@juggle/resize-observer": "^3.2.0",
|
|
10
|
-
"@lumx/core": "^3.6.
|
|
11
|
-
"@lumx/icons": "^3.6.
|
|
10
|
+
"@lumx/core": "^3.6.4",
|
|
11
|
+
"@lumx/icons": "^3.6.4",
|
|
12
12
|
"@popperjs/core": "^2.5.4",
|
|
13
13
|
"body-scroll-lock": "^3.1.5",
|
|
14
14
|
"classnames": "^2.3.2",
|
|
@@ -112,5 +112,5 @@
|
|
|
112
112
|
"build:storybook": "storybook build"
|
|
113
113
|
},
|
|
114
114
|
"sideEffects": false,
|
|
115
|
-
"version": "3.6.
|
|
115
|
+
"version": "3.6.4"
|
|
116
116
|
}
|
|
@@ -4,11 +4,14 @@ import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
|
|
|
4
4
|
import { queryByRole, render } from '@testing-library/react';
|
|
5
5
|
import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
|
|
6
6
|
import userEvent from '@testing-library/user-event';
|
|
7
|
+
import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
|
|
7
8
|
|
|
8
9
|
import { ExpansionPanel, ExpansionPanelProps } from '.';
|
|
9
10
|
|
|
10
11
|
const CLASSNAME = ExpansionPanel.className as string;
|
|
11
12
|
|
|
13
|
+
jest.mock('@lumx/react/utils/isFocusVisible');
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Mounts the component and returns common DOM elements / data needed in multiple tests further down.
|
|
14
17
|
*/
|
|
@@ -32,6 +35,8 @@ const setup = (propsOverride: Partial<ExpansionPanelProps> = {}) => {
|
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
describe(`<${ExpansionPanel.displayName}>`, () => {
|
|
38
|
+
(isFocusVisible as jest.Mock).mockReturnValue(false);
|
|
39
|
+
|
|
35
40
|
describe('Render', () => {
|
|
36
41
|
it('should render default', () => {
|
|
37
42
|
const { element, query } = setup();
|
|
@@ -41,18 +41,18 @@ const DEFAULT_PROPS: Partial<InputHelperProps> = {
|
|
|
41
41
|
* @param ref Component ref.
|
|
42
42
|
* @return React element.
|
|
43
43
|
*/
|
|
44
|
-
export const InputHelper: Comp<InputHelperProps,
|
|
44
|
+
export const InputHelper: Comp<InputHelperProps, HTMLParagraphElement> = forwardRef((props, ref) => {
|
|
45
45
|
const { children, className, kind, theme, ...forwardedProps } = props;
|
|
46
46
|
const { color } = INPUT_HELPER_CONFIGURATION[kind as any] || {};
|
|
47
47
|
|
|
48
48
|
return (
|
|
49
|
-
<
|
|
49
|
+
<p
|
|
50
50
|
ref={ref}
|
|
51
51
|
{...forwardedProps}
|
|
52
52
|
className={classNames(className, handleBasicClasses({ prefix: CLASSNAME, color, theme }))}
|
|
53
53
|
>
|
|
54
54
|
{children}
|
|
55
|
-
</
|
|
55
|
+
</p>
|
|
56
56
|
);
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, ReactNode, RefObject,
|
|
1
|
+
import React, { forwardRef, ReactNode, RefObject, useRef } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
|
|
4
4
|
import classNames from 'classnames';
|
|
@@ -9,14 +9,13 @@ import { ClickAwayProvider } from '@lumx/react/utils/ClickAwayProvider';
|
|
|
9
9
|
import { DOCUMENT } from '@lumx/react/constants';
|
|
10
10
|
import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
|
|
11
11
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
12
|
-
import {
|
|
13
|
-
import { useFocusWithin } from '@lumx/react/hooks/useFocusWithin';
|
|
14
|
-
import { getFirstAndLastFocusable } from '@lumx/react/utils/focus/getFirstAndLastFocusable';
|
|
12
|
+
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
15
13
|
import { useFocusTrap } from '@lumx/react/hooks/useFocusTrap';
|
|
16
14
|
import { skipRender } from '@lumx/react/utils/skipRender';
|
|
17
15
|
|
|
16
|
+
import { useRestoreFocusOnClose } from './useRestoreFocusOnClose';
|
|
18
17
|
import { usePopoverStyle } from './usePopoverStyle';
|
|
19
|
-
import {
|
|
18
|
+
import { Elevation, FitAnchorWidth, Offset, Placement } from './constants';
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
21
|
* Defines the props of the component.
|
|
@@ -85,6 +84,7 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
|
85
84
|
const DEFAULT_PROPS: Partial<PopoverProps> = {
|
|
86
85
|
elevation: 3,
|
|
87
86
|
placement: Placement.AUTO,
|
|
87
|
+
focusAnchorOnClose: true,
|
|
88
88
|
usePortal: true,
|
|
89
89
|
zIndex: 9999,
|
|
90
90
|
};
|
|
@@ -110,7 +110,7 @@ const _InnerPopover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, ref
|
|
|
110
110
|
onClose,
|
|
111
111
|
parentElement,
|
|
112
112
|
usePortal,
|
|
113
|
-
focusAnchorOnClose
|
|
113
|
+
focusAnchorOnClose,
|
|
114
114
|
withFocusTrap,
|
|
115
115
|
boundaryRef,
|
|
116
116
|
fitToAnchorWidth,
|
|
@@ -122,8 +122,7 @@ const _InnerPopover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, ref
|
|
|
122
122
|
zIndex,
|
|
123
123
|
...forwardedProps
|
|
124
124
|
} = props;
|
|
125
|
-
const
|
|
126
|
-
const contentRef = useRef<HTMLDivElement>(null);
|
|
125
|
+
const popoverRef = useRef<HTMLDivElement>(null);
|
|
127
126
|
|
|
128
127
|
const {
|
|
129
128
|
styles,
|
|
@@ -146,62 +145,22 @@ const _InnerPopover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, ref
|
|
|
146
145
|
zIndex,
|
|
147
146
|
});
|
|
148
147
|
|
|
149
|
-
|
|
150
|
-
* Track whether the focus is currently set in the
|
|
151
|
-
* popover.
|
|
152
|
-
* */
|
|
153
|
-
const isFocusedWithin = useRef(false);
|
|
154
|
-
|
|
155
|
-
useFocusWithin({
|
|
156
|
-
element: popperElement || null,
|
|
157
|
-
onFocusIn: () => {
|
|
158
|
-
isFocusedWithin.current = true;
|
|
159
|
-
},
|
|
160
|
-
onFocusOut: () => {
|
|
161
|
-
isFocusedWithin.current = false;
|
|
162
|
-
},
|
|
163
|
-
});
|
|
148
|
+
const unmountSentinel = useRestoreFocusOnClose({ focusAnchorOnClose, anchorRef, parentElement }, popperElement);
|
|
164
149
|
|
|
165
|
-
|
|
166
|
-
const handleClose = useCallback(() => {
|
|
167
|
-
if (!onClose) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* If the focus is currently within the popover
|
|
173
|
-
* when the popover closes, reset the focus back to the anchor element
|
|
174
|
-
* unless specifically requested not to.
|
|
175
|
-
*/
|
|
176
|
-
if (isFocusedWithin.current && focusAnchorOnClose) {
|
|
177
|
-
let elementToFocus = parentElement?.current;
|
|
178
|
-
if (!elementToFocus && anchorRef?.current) {
|
|
179
|
-
// Focus the first focusable element in anchor.
|
|
180
|
-
elementToFocus = getFirstAndLastFocusable(anchorRef.current).first;
|
|
181
|
-
}
|
|
182
|
-
if (!elementToFocus) {
|
|
183
|
-
// Fallback on the anchor element.
|
|
184
|
-
elementToFocus = anchorRef?.current;
|
|
185
|
-
}
|
|
186
|
-
elementToFocus?.focus({ preventScroll: true });
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
onClose();
|
|
190
|
-
}, [anchorRef, focusAnchorOnClose, onClose, parentElement]);
|
|
191
|
-
|
|
192
|
-
useCallbackOnEscape(handleClose, isOpen && closeOnEscape);
|
|
150
|
+
useCallbackOnEscape(onClose, isOpen && closeOnEscape);
|
|
193
151
|
|
|
194
152
|
/** Only set focus within if the focus trap is disabled as they interfere with one another. */
|
|
195
153
|
useFocus(focusElement?.current, !withFocusTrap && isOpen && isPositioned);
|
|
196
|
-
useFocusTrap(withFocusTrap && isOpen &&
|
|
154
|
+
useFocusTrap(withFocusTrap && isOpen && popoverRef?.current, focusElement?.current);
|
|
197
155
|
|
|
198
|
-
const clickAwayRefs = useRef([
|
|
156
|
+
const clickAwayRefs = useRef([popoverRef, anchorRef]);
|
|
157
|
+
const mergedRefs = useMergeRefs<HTMLDivElement>(setPopperElement, ref, popoverRef);
|
|
199
158
|
|
|
200
159
|
return isOpen
|
|
201
160
|
? renderPopover(
|
|
202
161
|
<Component
|
|
203
162
|
{...forwardedProps}
|
|
204
|
-
ref={
|
|
163
|
+
ref={mergedRefs}
|
|
205
164
|
className={classNames(
|
|
206
165
|
className,
|
|
207
166
|
handleBasicClasses({
|
|
@@ -214,7 +173,8 @@ const _InnerPopover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, ref
|
|
|
214
173
|
style={styles.popover}
|
|
215
174
|
{...attributes.popper}
|
|
216
175
|
>
|
|
217
|
-
|
|
176
|
+
{unmountSentinel}
|
|
177
|
+
<ClickAwayProvider callback={closeOnClickAway && onClose} childrenRefs={clickAwayRefs}>
|
|
218
178
|
{hasArrow && (
|
|
219
179
|
<div ref={setArrowElement} className={`${CLASSNAME}__arrow`} style={styles.arrow}>
|
|
220
180
|
<svg viewBox="0 0 14 14" aria-hidden>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getFirstAndLastFocusable } from '@lumx/react/utils/focus/getFirstAndLastFocusable';
|
|
3
|
+
import { useBeforeUnmountSentinel } from '@lumx/react/utils/useBeforeUnmountSentinel';
|
|
4
|
+
import type { PopoverProps } from './Popover';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provides an unmount sentinel to inject in the popover to detect when it closes and restore the focus to the
|
|
8
|
+
* anchor if needed.
|
|
9
|
+
*/
|
|
10
|
+
export function useRestoreFocusOnClose(
|
|
11
|
+
props: Pick<PopoverProps, 'focusAnchorOnClose' | 'anchorRef' | 'parentElement'>,
|
|
12
|
+
popoverElement?: HTMLElement | null,
|
|
13
|
+
) {
|
|
14
|
+
const onBeforeUnmount = React.useMemo(() => {
|
|
15
|
+
if (!popoverElement || !props.focusAnchorOnClose) return undefined;
|
|
16
|
+
return () => {
|
|
17
|
+
const isFocusWithin = popoverElement?.contains(document.activeElement);
|
|
18
|
+
if (!isFocusWithin) return;
|
|
19
|
+
|
|
20
|
+
const anchor = props.anchorRef.current;
|
|
21
|
+
const elementToFocus =
|
|
22
|
+
// Provided parent element
|
|
23
|
+
props.parentElement?.current ||
|
|
24
|
+
// Or first focusable element in anchor
|
|
25
|
+
(anchor ? getFirstAndLastFocusable(anchor).first : undefined) ||
|
|
26
|
+
// Fallback to anchor
|
|
27
|
+
anchor;
|
|
28
|
+
|
|
29
|
+
elementToFocus?.focus({ preventScroll: true });
|
|
30
|
+
};
|
|
31
|
+
}, [popoverElement, props.anchorRef, props.focusAnchorOnClose, props.parentElement]);
|
|
32
|
+
return useBeforeUnmountSentinel(onBeforeUnmount);
|
|
33
|
+
}
|
|
@@ -7,11 +7,13 @@ import { getByClassName, queryAllByClassName, queryByClassName } from '@lumx/rea
|
|
|
7
7
|
import { render, within } from '@testing-library/react';
|
|
8
8
|
import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
|
|
9
9
|
import userEvent from '@testing-library/user-event';
|
|
10
|
+
import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
|
|
10
11
|
|
|
11
12
|
import { Select, SelectProps, SelectVariant } from './Select';
|
|
12
13
|
|
|
13
14
|
const CLASSNAME = Select.className as string;
|
|
14
15
|
|
|
16
|
+
jest.mock('@lumx/react/utils/isFocusVisible');
|
|
15
17
|
jest.mock('uid', () => ({ uid: () => 'uid' }));
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -33,6 +35,8 @@ const setup = (propsOverride: Partial<SelectProps> = {}) => {
|
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
describe(`<${Select.displayName}>`, () => {
|
|
38
|
+
(isFocusVisible as jest.Mock).mockReturnValue(false);
|
|
39
|
+
|
|
36
40
|
describe('Props', () => {
|
|
37
41
|
it('should have default classNames', () => {
|
|
38
42
|
const { select, getDropdown } = setup();
|
|
@@ -15,10 +15,13 @@ import {
|
|
|
15
15
|
import partition from 'lodash/partition';
|
|
16
16
|
import userEvent from '@testing-library/user-event';
|
|
17
17
|
|
|
18
|
+
import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
|
|
18
19
|
import { TextField, TextFieldProps } from './TextField';
|
|
19
20
|
|
|
20
21
|
const CLASSNAME = TextField.className as string;
|
|
21
22
|
|
|
23
|
+
jest.mock('@lumx/react/utils/isFocusVisible');
|
|
24
|
+
|
|
22
25
|
/**
|
|
23
26
|
* Mounts the component and returns common DOM elements / data needed in multiple tests further down.
|
|
24
27
|
*/
|
|
@@ -46,6 +49,8 @@ const setup = (propsOverride: Partial<TextFieldProps> = {}) => {
|
|
|
46
49
|
};
|
|
47
50
|
|
|
48
51
|
describe(`<${TextField.displayName}>`, () => {
|
|
52
|
+
(isFocusVisible as jest.Mock).mockReturnValue(false);
|
|
53
|
+
|
|
49
54
|
describe('Render', () => {
|
|
50
55
|
it('should render defaults', () => {
|
|
51
56
|
const { element, inputNative } = setup({ id: 'fixedId' });
|
|
@@ -86,3 +86,19 @@ export const HideTooltipOnHiddenAnchor = () => {
|
|
|
86
86
|
</>
|
|
87
87
|
);
|
|
88
88
|
};
|
|
89
|
+
HideTooltipOnHiddenAnchor.parameters = { chromatic: { disableSnapshot: true } };
|
|
90
|
+
|
|
91
|
+
/** Test focusing a tooltip anchor programmatically */
|
|
92
|
+
export const TestProgrammaticFocus = () => {
|
|
93
|
+
const anchorRef = React.useRef<HTMLButtonElement>(null);
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
<p>The tooltip should open on keyboard focus but not on programmatic focus (ex: after a click)</p>
|
|
97
|
+
<Tooltip label="label">
|
|
98
|
+
<Button ref={anchorRef}>button with label</Button>
|
|
99
|
+
</Tooltip>
|
|
100
|
+
<Button onClick={() => anchorRef.current?.focus()}>focus the button</Button>
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
TestProgrammaticFocus.parameters = { chromatic: { disableSnapshot: true } };
|
|
@@ -6,10 +6,12 @@ import { queryAllByTagName, queryByClassName } from '@lumx/react/testing/utils/q
|
|
|
6
6
|
import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
|
|
7
7
|
import userEvent from '@testing-library/user-event';
|
|
8
8
|
|
|
9
|
+
import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
|
|
9
10
|
import { Tooltip, TooltipProps } from './Tooltip';
|
|
10
11
|
|
|
11
12
|
const CLASSNAME = Tooltip.className as string;
|
|
12
13
|
|
|
14
|
+
jest.mock('@lumx/react/utils/isFocusVisible');
|
|
13
15
|
jest.mock('uid', () => ({ uid: () => 'uid' }));
|
|
14
16
|
// Skip delays
|
|
15
17
|
jest.mock('@lumx/react/constants', () => ({
|
|
@@ -148,7 +150,8 @@ describe(`<${Tooltip.displayName}>`, () => {
|
|
|
148
150
|
});
|
|
149
151
|
});
|
|
150
152
|
|
|
151
|
-
it('should activate on anchor focus and close on escape', async () => {
|
|
153
|
+
it('should activate on anchor focus visible and close on escape', async () => {
|
|
154
|
+
(isFocusVisible as jest.Mock).mockReturnValue(true);
|
|
152
155
|
let { tooltip } = await setup({
|
|
153
156
|
label: 'Tooltip label',
|
|
154
157
|
children: <Button>Anchor</Button>,
|
|
@@ -181,6 +184,26 @@ describe(`<${Tooltip.displayName}>`, () => {
|
|
|
181
184
|
await userEvent.keyboard('{Escape}');
|
|
182
185
|
expect(tooltip).not.toBeInTheDocument();
|
|
183
186
|
});
|
|
187
|
+
|
|
188
|
+
it('should not activate on anchor focus if not visible', async () => {
|
|
189
|
+
(isFocusVisible as jest.Mock).mockReturnValue(false);
|
|
190
|
+
let { tooltip } = await setup({
|
|
191
|
+
label: 'Tooltip label',
|
|
192
|
+
children: <Button>Anchor</Button>,
|
|
193
|
+
forceOpen: false,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(tooltip).not.toBeInTheDocument();
|
|
197
|
+
|
|
198
|
+
// Focus anchor button
|
|
199
|
+
await userEvent.tab();
|
|
200
|
+
const button = screen.getByRole('button', { name: 'Anchor' });
|
|
201
|
+
expect(button).toHaveFocus();
|
|
202
|
+
|
|
203
|
+
// Tooltip not opening
|
|
204
|
+
tooltip = screen.queryByRole('tooltip', { name: 'Tooltip label' });
|
|
205
|
+
expect(tooltip).not.toBeInTheDocument();
|
|
206
|
+
});
|
|
184
207
|
});
|
|
185
208
|
|
|
186
209
|
// Common tests suite.
|
|
@@ -2,6 +2,7 @@ import { MutableRefObject, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { browserDoesNotSupportHover } from '@lumx/react/utils/browserDoesNotSupportHover';
|
|
3
3
|
import { TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
|
|
4
4
|
import { useCallbackOnEscape } from '@lumx/react/hooks/useCallbackOnEscape';
|
|
5
|
+
import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Hook controlling tooltip visibility using mouse hover the anchor and delay.
|
|
@@ -106,8 +107,16 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
|
|
|
106
107
|
|
|
107
108
|
// Events always applied no matter the browser:.
|
|
108
109
|
events.push(
|
|
109
|
-
// Open on focus.
|
|
110
|
-
[
|
|
110
|
+
// Open on focus (only if focus is visible).
|
|
111
|
+
[
|
|
112
|
+
anchorElement,
|
|
113
|
+
'focusin',
|
|
114
|
+
(e: Event) => {
|
|
115
|
+
// Skip if focus is not visible
|
|
116
|
+
if (!isFocusVisible(e.target as HTMLElement)) return;
|
|
117
|
+
open();
|
|
118
|
+
},
|
|
119
|
+
],
|
|
111
120
|
// Close on lost focus.
|
|
112
121
|
[anchorElement, 'focusout', closeImmediately],
|
|
113
122
|
);
|
package/src/utils/mergeRefs.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Falsy } from '@lumx/react/utils/type';
|
|
2
|
-
import { MutableRefObject } from 'react';
|
|
2
|
+
import { MutableRefObject, useMemo } from 'react';
|
|
3
3
|
|
|
4
4
|
type FnRef<T> = (value: T) => void;
|
|
5
5
|
|
|
@@ -20,3 +20,14 @@ export function mergeRefs<T>(...refs: Array<MutableRefObject<T | null> | FnRef<T
|
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Same as `mergeRefs` but memoized
|
|
26
|
+
*/
|
|
27
|
+
export const useMergeRefs = <T>(...refs: Array<MutableRefObject<T | null> | FnRef<T> | Falsy>) => {
|
|
28
|
+
return useMemo(
|
|
29
|
+
() => mergeRefs(...refs),
|
|
30
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
|
+
refs,
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { useLayoutEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Small helper component using useLayoutEffect to trigger a callback on before unmount. */
|
|
4
|
+
const OnUnmount = ({ onBeforeUnmount }: { onBeforeUnmount: () => void }) => {
|
|
5
|
+
useLayoutEffect(() => onBeforeUnmount, [onBeforeUnmount]);
|
|
6
|
+
return null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Provides a sentinel to inject the React tree that triggers the callback on before unmount.
|
|
11
|
+
*/
|
|
12
|
+
export function useBeforeUnmountSentinel(onBeforeUnmount?: () => void) {
|
|
13
|
+
return React.useMemo(() => {
|
|
14
|
+
if (!onBeforeUnmount) return undefined;
|
|
15
|
+
return <OnUnmount onBeforeUnmount={onBeforeUnmount} />;
|
|
16
|
+
}, [onBeforeUnmount]);
|
|
17
|
+
}
|