@navikt/ds-react 6.14.0 → 6.15.0
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/AddNewOption.d.ts +3 -0
- package/cjs/form/combobox/FilteredOptions/AddNewOption.js +41 -0
- package/cjs/form/combobox/FilteredOptions/AddNewOption.js.map +1 -0
- package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +13 -57
- package/cjs/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
- package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.d.ts +6 -0
- package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.js +43 -0
- package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.js.map +1 -0
- package/cjs/form/combobox/FilteredOptions/LoadingMessage.d.ts +3 -0
- package/cjs/form/combobox/FilteredOptions/LoadingMessage.js +16 -0
- package/cjs/form/combobox/FilteredOptions/LoadingMessage.js.map +1 -0
- package/cjs/form/combobox/FilteredOptions/MaxSelectedMessage.d.ts +3 -0
- package/cjs/form/combobox/FilteredOptions/MaxSelectedMessage.js +20 -0
- package/cjs/form/combobox/FilteredOptions/MaxSelectedMessage.js.map +1 -0
- package/cjs/form/combobox/FilteredOptions/NoSearchHitsMessage.d.ts +3 -0
- package/cjs/form/combobox/FilteredOptions/NoSearchHitsMessage.js +14 -0
- package/cjs/form/combobox/FilteredOptions/NoSearchHitsMessage.js.map +1 -0
- package/cjs/form/combobox/Input/Input.d.ts +1 -0
- package/cjs/form/combobox/Input/Input.js +3 -2
- package/cjs/form/combobox/Input/Input.js.map +1 -1
- package/cjs/form/combobox/Input/InputController.js +1 -1
- package/cjs/form/combobox/Input/InputController.js.map +1 -1
- package/cjs/overlays/floating-menu/Menu.d.ts +106 -0
- package/cjs/overlays/floating-menu/Menu.js +593 -0
- package/cjs/overlays/floating-menu/Menu.js.map +1 -0
- package/cjs/overlays/floating-menu/parts/FocusScope.d.ts +22 -0
- package/cjs/overlays/floating-menu/parts/FocusScope.js +89 -0
- package/cjs/overlays/floating-menu/parts/FocusScope.js.map +1 -0
- package/cjs/overlays/floating-menu/parts/RovingFocus.d.ts +9 -0
- package/cjs/overlays/floating-menu/parts/RovingFocus.js +112 -0
- package/cjs/overlays/floating-menu/parts/RovingFocus.js.map +1 -0
- package/cjs/overlays/floating-menu/parts/SlottedDivElement.d.ts +7 -0
- package/cjs/overlays/floating-menu/parts/SlottedDivElement.js +46 -0
- package/cjs/overlays/floating-menu/parts/SlottedDivElement.js.map +1 -0
- package/cjs/util/composeEventHandlers.d.ts +1 -1
- package/esm/form/combobox/FilteredOptions/AddNewOption.d.ts +3 -0
- package/esm/form/combobox/FilteredOptions/AddNewOption.js +36 -0
- package/esm/form/combobox/FilteredOptions/AddNewOption.js.map +1 -0
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js +13 -57
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.d.ts +6 -0
- package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.js +38 -0
- package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.js.map +1 -0
- package/esm/form/combobox/FilteredOptions/LoadingMessage.d.ts +3 -0
- package/esm/form/combobox/FilteredOptions/LoadingMessage.js +11 -0
- package/esm/form/combobox/FilteredOptions/LoadingMessage.js.map +1 -0
- package/esm/form/combobox/FilteredOptions/MaxSelectedMessage.d.ts +3 -0
- package/esm/form/combobox/FilteredOptions/MaxSelectedMessage.js +15 -0
- package/esm/form/combobox/FilteredOptions/MaxSelectedMessage.js.map +1 -0
- package/esm/form/combobox/FilteredOptions/NoSearchHitsMessage.d.ts +3 -0
- package/esm/form/combobox/FilteredOptions/NoSearchHitsMessage.js +9 -0
- package/esm/form/combobox/FilteredOptions/NoSearchHitsMessage.js.map +1 -0
- package/esm/form/combobox/Input/Input.d.ts +1 -0
- package/esm/form/combobox/Input/Input.js +3 -2
- package/esm/form/combobox/Input/Input.js.map +1 -1
- package/esm/form/combobox/Input/InputController.js +1 -1
- package/esm/form/combobox/Input/InputController.js.map +1 -1
- package/esm/overlays/floating-menu/Menu.d.ts +106 -0
- package/esm/overlays/floating-menu/Menu.js +551 -0
- package/esm/overlays/floating-menu/Menu.js.map +1 -0
- package/esm/overlays/floating-menu/parts/FocusScope.d.ts +22 -0
- package/esm/overlays/floating-menu/parts/FocusScope.js +63 -0
- package/esm/overlays/floating-menu/parts/FocusScope.js.map +1 -0
- package/esm/overlays/floating-menu/parts/RovingFocus.d.ts +9 -0
- package/esm/overlays/floating-menu/parts/RovingFocus.js +86 -0
- package/esm/overlays/floating-menu/parts/RovingFocus.js.map +1 -0
- package/esm/overlays/floating-menu/parts/SlottedDivElement.d.ts +7 -0
- package/esm/overlays/floating-menu/parts/SlottedDivElement.js +20 -0
- package/esm/overlays/floating-menu/parts/SlottedDivElement.js.map +1 -0
- package/esm/util/composeEventHandlers.d.ts +1 -1
- package/package.json +3 -3
- package/src/form/combobox/FilteredOptions/AddNewOption.tsx +63 -0
- package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +11 -121
- package/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx +73 -0
- package/src/form/combobox/FilteredOptions/LoadingMessage.tsx +20 -0
- package/src/form/combobox/FilteredOptions/MaxSelectedMessage.tsx +27 -0
- package/src/form/combobox/FilteredOptions/NoSearchHitsMessage.tsx +19 -0
- package/src/form/combobox/Input/Input.tsx +4 -2
- package/src/form/combobox/Input/InputController.tsx +1 -0
- package/src/overlays/floating-menu/Menu.tsx +1177 -0
- package/src/overlays/floating-menu/parts/FocusScope.tsx +84 -0
- package/src/overlays/floating-menu/parts/RovingFocus.tsx +121 -0
- package/src/overlays/floating-menu/parts/SlottedDivElement.tsx +17 -0
- package/src/util/composeEventHandlers.ts +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { forwardRef, useEffect, useState } from "react";
|
|
3
|
+
import { Slot } from "../../../slot/Slot";
|
|
4
|
+
import { useCallbackRef, useMergeRefs } from "../../../util/hooks";
|
|
5
|
+
|
|
6
|
+
const AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount";
|
|
7
|
+
const AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount";
|
|
8
|
+
const EVENT_OPTIONS = { bubbles: false, cancelable: true };
|
|
9
|
+
|
|
10
|
+
interface FocusScopeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
/**
|
|
12
|
+
* Event handler called on mount, unless the component already has focus. Used for auto-focusing.
|
|
13
|
+
* Can be prevented.
|
|
14
|
+
*/
|
|
15
|
+
onMountHandler?: (event: Event) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Event handler called on unmount. Used for auto-focusing.
|
|
18
|
+
* Can be prevented.
|
|
19
|
+
*/
|
|
20
|
+
onUnmountHandler?: (event: Event) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* FocusScope manages focus on mount and unmount of container.
|
|
25
|
+
* This is used to better handle autofocus of elements when mounted and unmounted.
|
|
26
|
+
* Example usage:
|
|
27
|
+
* - Focus first item in a list when mounted
|
|
28
|
+
* - Focus a button when unmounted
|
|
29
|
+
*/
|
|
30
|
+
const FocusScope = forwardRef<HTMLDivElement, FocusScopeProps>(
|
|
31
|
+
(
|
|
32
|
+
{
|
|
33
|
+
onMountHandler: onMountHandlerCallback,
|
|
34
|
+
onUnmountHandler: onUnmountHandlerCallback,
|
|
35
|
+
...rest
|
|
36
|
+
},
|
|
37
|
+
ref,
|
|
38
|
+
) => {
|
|
39
|
+
const [container, setContainer] = useState<HTMLElement | null>(null);
|
|
40
|
+
const onMountHandler = useCallbackRef(onMountHandlerCallback);
|
|
41
|
+
const onUnmountHandler = useCallbackRef(onUnmountHandlerCallback);
|
|
42
|
+
|
|
43
|
+
const composedRefs = useMergeRefs(ref, setContainer);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!container) return;
|
|
47
|
+
|
|
48
|
+
const ownerDocument = container.ownerDocument ?? globalThis?.document;
|
|
49
|
+
const hasFocus = container.contains(ownerDocument.activeElement);
|
|
50
|
+
|
|
51
|
+
if (!hasFocus) {
|
|
52
|
+
const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);
|
|
53
|
+
container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountHandler);
|
|
54
|
+
container.dispatchEvent(mountEvent);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountHandler);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* https://github.com/facebook/react/issues/17894
|
|
62
|
+
* As usual when dealing with focus and useEffect,
|
|
63
|
+
* we need to defer the focus to the next event-loop
|
|
64
|
+
* setTimeout makes sure the code is ran after the next render-cycle
|
|
65
|
+
*/
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
const unmountEvent = new CustomEvent(
|
|
68
|
+
AUTOFOCUS_ON_UNMOUNT,
|
|
69
|
+
EVENT_OPTIONS,
|
|
70
|
+
);
|
|
71
|
+
container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountHandler);
|
|
72
|
+
container.dispatchEvent(unmountEvent);
|
|
73
|
+
|
|
74
|
+
// we need to remove the listener after we `dispatchEvent`
|
|
75
|
+
container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountHandler);
|
|
76
|
+
}, 0);
|
|
77
|
+
};
|
|
78
|
+
}, [container, onMountHandler, onUnmountHandler]);
|
|
79
|
+
|
|
80
|
+
return <Slot tabIndex={-1} {...rest} ref={composedRefs} />;
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export { FocusScope, type FocusScopeProps };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { Slot } from "../../../slot/Slot";
|
|
3
|
+
import { composeEventHandlers } from "../../../util/composeEventHandlers";
|
|
4
|
+
import { useCallbackRef, useMergeRefs } from "../../../util/hooks";
|
|
5
|
+
import { DescendantsManager } from "../../../util/hooks/descendants/descendant";
|
|
6
|
+
|
|
7
|
+
interface RovingFocusProps
|
|
8
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "tabIndex"> {
|
|
9
|
+
asChild?: boolean;
|
|
10
|
+
descendants: DescendantsManager<HTMLDivElement, object>;
|
|
11
|
+
onEntryFocus?: (event: Event) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ENTRY_FOCUS = "rovingFocusGroup.onEntryFocus";
|
|
15
|
+
const EVENT_OPTIONS = { bubbles: false, cancelable: true };
|
|
16
|
+
|
|
17
|
+
const RovingFocus = forwardRef<HTMLDivElement, RovingFocusProps>(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
children,
|
|
21
|
+
asChild,
|
|
22
|
+
descendants,
|
|
23
|
+
onKeyDown,
|
|
24
|
+
onEntryFocus,
|
|
25
|
+
onMouseDown,
|
|
26
|
+
onFocus,
|
|
27
|
+
...rest
|
|
28
|
+
}: RovingFocusProps,
|
|
29
|
+
ref,
|
|
30
|
+
) => {
|
|
31
|
+
const _ref = React.useRef<HTMLDivElement>(null);
|
|
32
|
+
const composedRefs = useMergeRefs(ref, _ref);
|
|
33
|
+
|
|
34
|
+
const handleEntryFocus = useCallbackRef(onEntryFocus);
|
|
35
|
+
const isMouseFocusRef = useRef(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const node = _ref.current;
|
|
39
|
+
if (node) {
|
|
40
|
+
node.addEventListener(ENTRY_FOCUS, handleEntryFocus);
|
|
41
|
+
return () => node.removeEventListener(ENTRY_FOCUS, handleEntryFocus);
|
|
42
|
+
}
|
|
43
|
+
}, [handleEntryFocus]);
|
|
44
|
+
|
|
45
|
+
const handleKeyDown = useCallback(
|
|
46
|
+
(event: React.KeyboardEvent) => {
|
|
47
|
+
const loop = false;
|
|
48
|
+
|
|
49
|
+
const ownerDocument =
|
|
50
|
+
_ref?.current?.ownerDocument ?? globalThis?.document;
|
|
51
|
+
|
|
52
|
+
const idx = descendants
|
|
53
|
+
.values()
|
|
54
|
+
.findIndex((x) => x.node.isSameNode(ownerDocument.activeElement));
|
|
55
|
+
|
|
56
|
+
const nextItem = () => {
|
|
57
|
+
const next = descendants.nextEnabled(idx, loop);
|
|
58
|
+
next && next.node?.focus();
|
|
59
|
+
};
|
|
60
|
+
const prevItem = () => {
|
|
61
|
+
const prev = descendants.prevEnabled(idx, loop);
|
|
62
|
+
prev && prev.node?.focus();
|
|
63
|
+
};
|
|
64
|
+
const firstItem = () => {
|
|
65
|
+
const first = descendants.firstEnabled();
|
|
66
|
+
first && first.node?.focus();
|
|
67
|
+
};
|
|
68
|
+
const lastItem = () => {
|
|
69
|
+
const last = descendants.lastEnabled();
|
|
70
|
+
last && last.node?.focus();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const keyMap: Record<string, React.KeyboardEventHandler> = {
|
|
74
|
+
ArrowUp: prevItem,
|
|
75
|
+
ArrowDown: nextItem,
|
|
76
|
+
Home: firstItem,
|
|
77
|
+
End: lastItem,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const action = keyMap[event.key];
|
|
81
|
+
|
|
82
|
+
if (action) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
action(event);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[descendants],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const Comp = asChild ? Slot : "div";
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Comp
|
|
94
|
+
ref={composedRefs}
|
|
95
|
+
{...rest}
|
|
96
|
+
tabIndex={descendants.enabledCount() === 0 ? -1 : 0}
|
|
97
|
+
style={{ outline: "none", ...rest.style }}
|
|
98
|
+
onKeyDown={composeEventHandlers(onKeyDown, handleKeyDown)}
|
|
99
|
+
onMouseDown={composeEventHandlers(onMouseDown, () => {
|
|
100
|
+
isMouseFocusRef.current = true;
|
|
101
|
+
})}
|
|
102
|
+
onFocus={composeEventHandlers(onFocus, (event) => {
|
|
103
|
+
if (event.target === event.currentTarget) {
|
|
104
|
+
const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS);
|
|
105
|
+
event.currentTarget.dispatchEvent(entryFocusEvent);
|
|
106
|
+
|
|
107
|
+
if (!entryFocusEvent.defaultPrevented) {
|
|
108
|
+
descendants.firstEnabled()?.node.focus({ preventScroll: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
isMouseFocusRef.current = false;
|
|
113
|
+
})}
|
|
114
|
+
>
|
|
115
|
+
{children}
|
|
116
|
+
</Comp>
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
export { RovingFocus, type RovingFocusProps };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import { Slot } from "../../../slot/Slot";
|
|
3
|
+
|
|
4
|
+
interface SlottedDivProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
asChild?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const SlottedDivElement = forwardRef<HTMLDivElement, SlottedDivProps>(
|
|
9
|
+
({ asChild, ...rest }, forwardedRef) => {
|
|
10
|
+
const Comp = asChild ? Slot : "div";
|
|
11
|
+
return <Comp {...rest} ref={forwardedRef} />;
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
type SlottedDivElementRef = React.ElementRef<typeof SlottedDivElement>;
|
|
16
|
+
|
|
17
|
+
export { SlottedDivElement, type SlottedDivElementRef, type SlottedDivProps };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Utility to consistently call original eventhandler, often from props and internal eventhandler
|
|
5
5
|
* @internal
|
|
6
6
|
*/
|
|
7
|
-
export function composeEventHandlers<T extends React.SyntheticEvent>(
|
|
7
|
+
export function composeEventHandlers<T extends React.SyntheticEvent | Event>(
|
|
8
8
|
originalEventHandler?: (event: T) => void,
|
|
9
9
|
ourEventHandler?: (event: T) => void,
|
|
10
10
|
{ checkForDefaultPrevented = true } = {},
|