@navikt/ds-react 7.32.5 → 7.33.1
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/cjs/form/combobox/FilteredOptions/FilteredOptions.js +0 -1
- package/cjs/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
- package/cjs/overlays/action-menu/ActionMenu.js +2 -5
- package/cjs/overlays/action-menu/ActionMenu.js.map +1 -1
- package/cjs/overlays/dismissablelayer/DismissableLayer.d.ts +3 -30
- package/cjs/overlays/dismissablelayer/DismissableLayer.js +141 -134
- package/cjs/overlays/dismissablelayer/DismissableLayer.js.map +1 -1
- package/cjs/overlays/dismissablelayer/util/sort-layers.d.ts +18 -0
- package/cjs/overlays/dismissablelayer/util/sort-layers.js +51 -0
- package/cjs/overlays/dismissablelayer/util/sort-layers.js.map +1 -0
- package/cjs/overlays/floating-menu/Menu.d.ts +5 -7
- package/cjs/overlays/floating-menu/Menu.js +7 -15
- package/cjs/overlays/floating-menu/Menu.js.map +1 -1
- package/cjs/popover/Popover.js +0 -1
- package/cjs/popover/Popover.js.map +1 -1
- package/cjs/portal/Portal.d.ts +1 -3
- package/cjs/portal/Portal.js +49 -17
- package/cjs/portal/Portal.js.map +1 -1
- package/cjs/process/Process.d.ts +5 -0
- package/cjs/process/Process.js +6 -6
- package/cjs/process/Process.js.map +1 -1
- package/cjs/timeline/Pin.js +5 -4
- package/cjs/timeline/Pin.js.map +1 -1
- package/cjs/timeline/period/ClickablePeriod.js +3 -2
- package/cjs/timeline/period/ClickablePeriod.js.map +1 -1
- package/cjs/tooltip/Tooltip.js +23 -22
- package/cjs/tooltip/Tooltip.js.map +1 -1
- package/cjs/util/focus-boundary/FocusBoundary.d.ts +19 -10
- package/cjs/util/focus-boundary/FocusBoundary.js +107 -63
- package/cjs/util/focus-boundary/FocusBoundary.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js +0 -1
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
- package/esm/overlays/action-menu/ActionMenu.js +2 -5
- package/esm/overlays/action-menu/ActionMenu.js.map +1 -1
- package/esm/overlays/dismissablelayer/DismissableLayer.d.ts +3 -30
- package/esm/overlays/dismissablelayer/DismissableLayer.js +140 -132
- package/esm/overlays/dismissablelayer/DismissableLayer.js.map +1 -1
- package/esm/overlays/dismissablelayer/util/sort-layers.d.ts +18 -0
- package/esm/overlays/dismissablelayer/util/sort-layers.js +49 -0
- package/esm/overlays/dismissablelayer/util/sort-layers.js.map +1 -0
- package/esm/overlays/floating-menu/Menu.d.ts +5 -7
- package/esm/overlays/floating-menu/Menu.js +7 -15
- package/esm/overlays/floating-menu/Menu.js.map +1 -1
- package/esm/popover/Popover.js +0 -1
- package/esm/popover/Popover.js.map +1 -1
- package/esm/portal/Portal.d.ts +1 -3
- package/esm/portal/Portal.js +50 -18
- package/esm/portal/Portal.js.map +1 -1
- package/esm/process/Process.d.ts +5 -0
- package/esm/process/Process.js +6 -6
- package/esm/process/Process.js.map +1 -1
- package/esm/timeline/Pin.js +5 -4
- package/esm/timeline/Pin.js.map +1 -1
- package/esm/timeline/period/ClickablePeriod.js +3 -2
- package/esm/timeline/period/ClickablePeriod.js.map +1 -1
- package/esm/tooltip/Tooltip.js +23 -22
- package/esm/tooltip/Tooltip.js.map +1 -1
- package/esm/util/focus-boundary/FocusBoundary.d.ts +19 -10
- package/esm/util/focus-boundary/FocusBoundary.js +108 -64
- package/esm/util/focus-boundary/FocusBoundary.js.map +1 -1
- package/package.json +10 -5
- package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +0 -1
- package/src/overlays/action-menu/ActionMenu.tsx +2 -4
- package/src/overlays/dismissablelayer/DismissableLayer.tsx +219 -194
- package/src/overlays/dismissablelayer/util/sort-layers.test.ts +128 -0
- package/src/overlays/dismissablelayer/util/sort-layers.ts +61 -0
- package/src/overlays/floating-menu/Menu.tsx +11 -21
- package/src/popover/Popover.tsx +0 -1
- package/src/portal/Portal.tsx +89 -31
- package/src/process/Process.tsx +17 -3
- package/src/timeline/Pin.tsx +6 -3
- package/src/timeline/period/ClickablePeriod.tsx +4 -1
- package/src/tooltip/Tooltip.tsx +4 -4
- package/src/util/focus-boundary/FocusBoundary.tsx +164 -93
|
@@ -6,18 +6,10 @@ import React, {
|
|
|
6
6
|
useState,
|
|
7
7
|
} from "react";
|
|
8
8
|
import { Slot } from "../../slot/Slot";
|
|
9
|
-
import { useMergeRefs } from "../../util/hooks";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const AUTOFOCUS_ON_UNMOUNT = "focusBoundary.autoFocusOnUnmount";
|
|
14
|
-
const EVENT_OPTIONS = { bubbles: false, cancelable: true };
|
|
15
|
-
|
|
16
|
-
/* TODO: Regular story */
|
|
17
|
-
/**
|
|
18
|
-
* TODO:
|
|
19
|
-
* - owner(container) for corrent document
|
|
20
|
-
*/
|
|
9
|
+
import { useClientLayoutEffect, useMergeRefs } from "../../util/hooks";
|
|
10
|
+
import { hideNonTargetElements } from "../hideNonTargetElements";
|
|
11
|
+
import { useLatestRef } from "../hooks/useLatestRef";
|
|
12
|
+
import { ownerDocument } from "../owner";
|
|
21
13
|
|
|
22
14
|
/* -------------------------------------------------------------------------- */
|
|
23
15
|
/* FocusBoundary */
|
|
@@ -48,15 +40,31 @@ interface FocusBoundaryProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
48
40
|
*/
|
|
49
41
|
trapped?: boolean;
|
|
50
42
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
43
|
+
* Will try to focus the given element on mount.
|
|
44
|
+
*
|
|
45
|
+
* If not provided, FocusBoundary will try to focus the first
|
|
46
|
+
* tabbable element inside the boundary.
|
|
47
|
+
*
|
|
48
|
+
* Set to `false` to not focus anything.
|
|
53
49
|
*/
|
|
54
|
-
|
|
50
|
+
initialFocus?:
|
|
51
|
+
| boolean
|
|
52
|
+
| React.MutableRefObject<HTMLElement | null>
|
|
53
|
+
| (() => boolean | HTMLElement | null | undefined);
|
|
55
54
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* Will try to focus the given element on unmount.
|
|
56
|
+
*
|
|
57
|
+
* If not provided, FocusBoundary will try to focus the element
|
|
58
|
+
* that was focused before the FocusBoundary mounted.
|
|
59
|
+
*
|
|
60
|
+
* Set to `false` to not focus anything.
|
|
58
61
|
*/
|
|
59
|
-
|
|
62
|
+
returnFocus?: boolean | React.MutableRefObject<HTMLElement | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Hides all outside content from screen readers when true.
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
modal?: boolean;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
const FocusBoundary = forwardRef<HTMLDivElement, FocusBoundaryProps>(
|
|
@@ -64,14 +72,15 @@ const FocusBoundary = forwardRef<HTMLDivElement, FocusBoundaryProps>(
|
|
|
64
72
|
{
|
|
65
73
|
loop = false,
|
|
66
74
|
trapped = false,
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
initialFocus = true,
|
|
76
|
+
returnFocus = true,
|
|
77
|
+
modal = false,
|
|
69
78
|
...restProps
|
|
70
79
|
}: FocusBoundaryProps,
|
|
71
80
|
forwardedRef,
|
|
72
81
|
) => {
|
|
73
|
-
const
|
|
74
|
-
const
|
|
82
|
+
const initialFocusRef = useLatestRef(initialFocus);
|
|
83
|
+
const returnFocusRef = useLatestRef(returnFocus);
|
|
75
84
|
|
|
76
85
|
const lastFocusedElementRef = useRef<HTMLElement | null>(null);
|
|
77
86
|
const [container, setContainer] = useState<HTMLElement | null>(null);
|
|
@@ -166,88 +175,127 @@ const FocusBoundary = forwardRef<HTMLDivElement, FocusBoundaryProps>(
|
|
|
166
175
|
};
|
|
167
176
|
}, [trapped, container, focusBoundary.paused]);
|
|
168
177
|
|
|
169
|
-
/*
|
|
178
|
+
/* Adds element to focus-stack */
|
|
170
179
|
useEffect(() => {
|
|
171
180
|
if (!container) {
|
|
172
181
|
return;
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
focusBoundarysStack.add(focusBoundary);
|
|
176
|
-
const initialFocusedElement =
|
|
177
|
-
document.activeElement as HTMLElement | null;
|
|
178
|
-
const containsActiveElement =
|
|
179
|
-
initialFocusedElement && container.contains(initialFocusedElement);
|
|
180
|
-
|
|
181
|
-
/*
|
|
182
|
-
* We only autofocus on mount if container does not contain active element.
|
|
183
|
-
* If container has an element with `autoFocus` attribute, browser will
|
|
184
|
-
* have already moved focus there before this effect runs.
|
|
185
|
-
*/
|
|
186
|
-
if (!containsActiveElement) {
|
|
187
|
-
const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);
|
|
188
|
-
container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);
|
|
189
|
-
container.dispatchEvent(mountEvent);
|
|
190
185
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const candidates = removeLinks(getTabbableCandidates(container));
|
|
198
|
-
const previouslyFocusedElement = document.activeElement;
|
|
199
|
-
for (const candidate of candidates) {
|
|
200
|
-
focus(candidate, { select: true });
|
|
201
|
-
if (document.activeElement !== previouslyFocusedElement) {
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
186
|
+
return () => {
|
|
187
|
+
setTimeout(() => {
|
|
188
|
+
focusBoundarysStack.remove(focusBoundary);
|
|
189
|
+
}, 0);
|
|
190
|
+
};
|
|
191
|
+
}, [container, focusBoundary]);
|
|
205
192
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!container || !modal) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return hideNonTargetElements([container]);
|
|
199
|
+
}, [container, modal]);
|
|
200
|
+
|
|
201
|
+
/* Handles mount focus */
|
|
202
|
+
useClientLayoutEffect(() => {
|
|
203
|
+
if (!container || initialFocusRef.current === false) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const ownerDoc = ownerDocument(container);
|
|
208
|
+
const previouslyFocusedElement = ownerDoc.activeElement;
|
|
209
|
+
|
|
210
|
+
queueMicrotask(() => {
|
|
211
|
+
const focusableElements = removeLinks(getTabbableCandidates(container));
|
|
212
|
+
const initialFocusValueOrFn = initialFocusRef.current;
|
|
213
|
+
const resolvedInitialFocus =
|
|
214
|
+
typeof initialFocusValueOrFn === "function"
|
|
215
|
+
? initialFocusValueOrFn()
|
|
216
|
+
: initialFocusValueOrFn;
|
|
217
|
+
|
|
218
|
+
if (
|
|
219
|
+
resolvedInitialFocus === undefined ||
|
|
220
|
+
resolvedInitialFocus === false
|
|
221
|
+
) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let elToFocus: HTMLElement | null | undefined;
|
|
226
|
+
const fallbackelements = focusableElements[0] || container;
|
|
227
|
+
|
|
228
|
+
/* `null` should fallback to default behavior in case of an empty ref. */
|
|
229
|
+
if (resolvedInitialFocus === true || resolvedInitialFocus === null) {
|
|
230
|
+
elToFocus = fallbackelements;
|
|
231
|
+
} else {
|
|
232
|
+
elToFocus = resolveRef(resolvedInitialFocus) || fallbackelements;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const focusAlreadyInsideFloatingEl = container.contains(
|
|
236
|
+
previouslyFocusedElement,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (focusAlreadyInsideFloatingEl) {
|
|
240
|
+
return;
|
|
210
241
|
}
|
|
242
|
+
|
|
243
|
+
focus(elToFocus, {
|
|
244
|
+
preventScroll: elToFocus === container,
|
|
245
|
+
sync: false,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}, [container, initialFocusRef]);
|
|
249
|
+
|
|
250
|
+
/* Handles unmount focus */
|
|
251
|
+
useClientLayoutEffect(() => {
|
|
252
|
+
if (!container) {
|
|
253
|
+
return;
|
|
211
254
|
}
|
|
255
|
+
const ownerDoc = ownerDocument(container);
|
|
256
|
+
const previouslyFocusedElement = ownerDoc.activeElement;
|
|
212
257
|
|
|
213
|
-
|
|
214
|
-
|
|
258
|
+
function getReturnElement() {
|
|
259
|
+
let resolvedReturnFocusValue = returnFocusRef.current;
|
|
215
260
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const unmountEvent = new CustomEvent(
|
|
223
|
-
AUTOFOCUS_ON_UNMOUNT,
|
|
224
|
-
EVENT_OPTIONS,
|
|
225
|
-
);
|
|
226
|
-
container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);
|
|
227
|
-
container.dispatchEvent(unmountEvent);
|
|
228
|
-
|
|
229
|
-
/* If consumer does not manually prevent event and handle focus themselves */
|
|
230
|
-
if (!unmountEvent.defaultPrevented) {
|
|
231
|
-
/* To avoid CPU-spikes on Chrome, we make sure element is still connected to the DOM. */
|
|
232
|
-
focus(
|
|
233
|
-
initialFocusedElement?.isConnected
|
|
234
|
-
? initialFocusedElement
|
|
235
|
-
: document.body,
|
|
236
|
-
{
|
|
237
|
-
select: true,
|
|
238
|
-
},
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
/* Since this is inside a cleanup, we need to instantly remove the listener ourselves */
|
|
242
|
-
container.removeEventListener(
|
|
243
|
-
AUTOFOCUS_ON_UNMOUNT,
|
|
244
|
-
onUnmountAutoFocus,
|
|
245
|
-
);
|
|
261
|
+
if (
|
|
262
|
+
resolvedReturnFocusValue === undefined ||
|
|
263
|
+
resolvedReturnFocusValue === false
|
|
264
|
+
) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
246
267
|
|
|
247
|
-
|
|
248
|
-
|
|
268
|
+
/* `null` should fallback to default behavior in case of an empty ref. */
|
|
269
|
+
if (resolvedReturnFocusValue === null) {
|
|
270
|
+
resolvedReturnFocusValue = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (typeof resolvedReturnFocusValue === "boolean") {
|
|
274
|
+
const el = previouslyFocusedElement;
|
|
275
|
+
return el?.isConnected ? el : ownerDoc.body;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const fallback = previouslyFocusedElement || ownerDoc.body;
|
|
279
|
+
|
|
280
|
+
return resolveRef(resolvedReturnFocusValue) || fallback;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return () => {
|
|
284
|
+
const returnElement = getReturnElement() as HTMLElement | null;
|
|
285
|
+
const activeEl = ownerDoc.activeElement;
|
|
286
|
+
|
|
287
|
+
queueMicrotask(() => {
|
|
288
|
+
if (
|
|
289
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
290
|
+
returnFocusRef.current &&
|
|
291
|
+
returnElement &&
|
|
292
|
+
returnElement !== activeEl
|
|
293
|
+
) {
|
|
294
|
+
returnElement.focus({ preventScroll: true });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
249
297
|
};
|
|
250
|
-
}, [container,
|
|
298
|
+
}, [container, returnFocusRef]);
|
|
251
299
|
|
|
252
300
|
/* Takes care of looping focus */
|
|
253
301
|
const handleKeyDown = useCallback(
|
|
@@ -387,14 +435,25 @@ function isHidden(node: HTMLElement, { upTo }: { upTo?: HTMLElement }) {
|
|
|
387
435
|
return false;
|
|
388
436
|
}
|
|
389
437
|
|
|
390
|
-
|
|
438
|
+
let rafId = 0;
|
|
439
|
+
function focus(
|
|
440
|
+
element?: HTMLElement | null,
|
|
441
|
+
{ select = false, preventScroll = true, sync = true } = {},
|
|
442
|
+
) {
|
|
391
443
|
if (!element?.focus) {
|
|
392
444
|
return;
|
|
393
445
|
}
|
|
394
446
|
|
|
395
447
|
const previouslyFocusedElement = document.activeElement;
|
|
396
|
-
|
|
397
|
-
|
|
448
|
+
|
|
449
|
+
cancelAnimationFrame(rafId);
|
|
450
|
+
const exec = () => element.focus({ preventScroll });
|
|
451
|
+
|
|
452
|
+
if (sync) {
|
|
453
|
+
exec();
|
|
454
|
+
} else {
|
|
455
|
+
rafId = requestAnimationFrame(exec);
|
|
456
|
+
}
|
|
398
457
|
|
|
399
458
|
if (!select) {
|
|
400
459
|
return;
|
|
@@ -449,5 +508,17 @@ function removeLinks(items: HTMLElement[]) {
|
|
|
449
508
|
return items.filter((item) => item.tagName !== "A");
|
|
450
509
|
}
|
|
451
510
|
|
|
511
|
+
/**
|
|
512
|
+
* If the provided argument is a ref object, returns its `current` value.
|
|
513
|
+
* Otherwise, returns the argument itself.
|
|
514
|
+
*
|
|
515
|
+
* Non-generic to safely handle refs whose `.current` may be `null`.
|
|
516
|
+
*/
|
|
517
|
+
function resolveRef(
|
|
518
|
+
maybeRef: HTMLElement | React.RefObject<HTMLElement | null | undefined>,
|
|
519
|
+
): HTMLElement | null | undefined {
|
|
520
|
+
return "current" in maybeRef ? maybeRef.current : maybeRef;
|
|
521
|
+
}
|
|
522
|
+
|
|
452
523
|
export { FocusBoundary };
|
|
453
524
|
export type { FocusBoundaryProps };
|