@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.
Files changed (84) hide show
  1. package/cjs/form/combobox/FilteredOptions/AddNewOption.d.ts +3 -0
  2. package/cjs/form/combobox/FilteredOptions/AddNewOption.js +41 -0
  3. package/cjs/form/combobox/FilteredOptions/AddNewOption.js.map +1 -0
  4. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +13 -57
  5. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  6. package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.d.ts +6 -0
  7. package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.js +43 -0
  8. package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.js.map +1 -0
  9. package/cjs/form/combobox/FilteredOptions/LoadingMessage.d.ts +3 -0
  10. package/cjs/form/combobox/FilteredOptions/LoadingMessage.js +16 -0
  11. package/cjs/form/combobox/FilteredOptions/LoadingMessage.js.map +1 -0
  12. package/cjs/form/combobox/FilteredOptions/MaxSelectedMessage.d.ts +3 -0
  13. package/cjs/form/combobox/FilteredOptions/MaxSelectedMessage.js +20 -0
  14. package/cjs/form/combobox/FilteredOptions/MaxSelectedMessage.js.map +1 -0
  15. package/cjs/form/combobox/FilteredOptions/NoSearchHitsMessage.d.ts +3 -0
  16. package/cjs/form/combobox/FilteredOptions/NoSearchHitsMessage.js +14 -0
  17. package/cjs/form/combobox/FilteredOptions/NoSearchHitsMessage.js.map +1 -0
  18. package/cjs/form/combobox/Input/Input.d.ts +1 -0
  19. package/cjs/form/combobox/Input/Input.js +3 -2
  20. package/cjs/form/combobox/Input/Input.js.map +1 -1
  21. package/cjs/form/combobox/Input/InputController.js +1 -1
  22. package/cjs/form/combobox/Input/InputController.js.map +1 -1
  23. package/cjs/overlays/floating-menu/Menu.d.ts +106 -0
  24. package/cjs/overlays/floating-menu/Menu.js +593 -0
  25. package/cjs/overlays/floating-menu/Menu.js.map +1 -0
  26. package/cjs/overlays/floating-menu/parts/FocusScope.d.ts +22 -0
  27. package/cjs/overlays/floating-menu/parts/FocusScope.js +89 -0
  28. package/cjs/overlays/floating-menu/parts/FocusScope.js.map +1 -0
  29. package/cjs/overlays/floating-menu/parts/RovingFocus.d.ts +9 -0
  30. package/cjs/overlays/floating-menu/parts/RovingFocus.js +112 -0
  31. package/cjs/overlays/floating-menu/parts/RovingFocus.js.map +1 -0
  32. package/cjs/overlays/floating-menu/parts/SlottedDivElement.d.ts +7 -0
  33. package/cjs/overlays/floating-menu/parts/SlottedDivElement.js +46 -0
  34. package/cjs/overlays/floating-menu/parts/SlottedDivElement.js.map +1 -0
  35. package/cjs/util/composeEventHandlers.d.ts +1 -1
  36. package/esm/form/combobox/FilteredOptions/AddNewOption.d.ts +3 -0
  37. package/esm/form/combobox/FilteredOptions/AddNewOption.js +36 -0
  38. package/esm/form/combobox/FilteredOptions/AddNewOption.js.map +1 -0
  39. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +13 -57
  40. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  41. package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.d.ts +6 -0
  42. package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.js +38 -0
  43. package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.js.map +1 -0
  44. package/esm/form/combobox/FilteredOptions/LoadingMessage.d.ts +3 -0
  45. package/esm/form/combobox/FilteredOptions/LoadingMessage.js +11 -0
  46. package/esm/form/combobox/FilteredOptions/LoadingMessage.js.map +1 -0
  47. package/esm/form/combobox/FilteredOptions/MaxSelectedMessage.d.ts +3 -0
  48. package/esm/form/combobox/FilteredOptions/MaxSelectedMessage.js +15 -0
  49. package/esm/form/combobox/FilteredOptions/MaxSelectedMessage.js.map +1 -0
  50. package/esm/form/combobox/FilteredOptions/NoSearchHitsMessage.d.ts +3 -0
  51. package/esm/form/combobox/FilteredOptions/NoSearchHitsMessage.js +9 -0
  52. package/esm/form/combobox/FilteredOptions/NoSearchHitsMessage.js.map +1 -0
  53. package/esm/form/combobox/Input/Input.d.ts +1 -0
  54. package/esm/form/combobox/Input/Input.js +3 -2
  55. package/esm/form/combobox/Input/Input.js.map +1 -1
  56. package/esm/form/combobox/Input/InputController.js +1 -1
  57. package/esm/form/combobox/Input/InputController.js.map +1 -1
  58. package/esm/overlays/floating-menu/Menu.d.ts +106 -0
  59. package/esm/overlays/floating-menu/Menu.js +551 -0
  60. package/esm/overlays/floating-menu/Menu.js.map +1 -0
  61. package/esm/overlays/floating-menu/parts/FocusScope.d.ts +22 -0
  62. package/esm/overlays/floating-menu/parts/FocusScope.js +63 -0
  63. package/esm/overlays/floating-menu/parts/FocusScope.js.map +1 -0
  64. package/esm/overlays/floating-menu/parts/RovingFocus.d.ts +9 -0
  65. package/esm/overlays/floating-menu/parts/RovingFocus.js +86 -0
  66. package/esm/overlays/floating-menu/parts/RovingFocus.js.map +1 -0
  67. package/esm/overlays/floating-menu/parts/SlottedDivElement.d.ts +7 -0
  68. package/esm/overlays/floating-menu/parts/SlottedDivElement.js +20 -0
  69. package/esm/overlays/floating-menu/parts/SlottedDivElement.js.map +1 -0
  70. package/esm/util/composeEventHandlers.d.ts +1 -1
  71. package/package.json +3 -3
  72. package/src/form/combobox/FilteredOptions/AddNewOption.tsx +63 -0
  73. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +11 -121
  74. package/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx +73 -0
  75. package/src/form/combobox/FilteredOptions/LoadingMessage.tsx +20 -0
  76. package/src/form/combobox/FilteredOptions/MaxSelectedMessage.tsx +27 -0
  77. package/src/form/combobox/FilteredOptions/NoSearchHitsMessage.tsx +19 -0
  78. package/src/form/combobox/Input/Input.tsx +4 -2
  79. package/src/form/combobox/Input/InputController.tsx +1 -0
  80. package/src/overlays/floating-menu/Menu.tsx +1177 -0
  81. package/src/overlays/floating-menu/parts/FocusScope.tsx +84 -0
  82. package/src/overlays/floating-menu/parts/RovingFocus.tsx +121 -0
  83. package/src/overlays/floating-menu/parts/SlottedDivElement.tsx +17 -0
  84. 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 } = {},