@navikt/ds-react 5.15.0 → 5.16.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 (70) hide show
  1. package/_docs.json +145 -1
  2. package/cjs/form/combobox/Combobox.js +1 -1
  3. package/cjs/form/combobox/ComboboxProvider.js +2 -1
  4. package/cjs/form/combobox/ComboboxWrapper.js +1 -1
  5. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +59 -41
  6. package/cjs/form/combobox/FilteredOptions/filtered-options-util.js +3 -1
  7. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +15 -3
  8. package/cjs/form/combobox/FilteredOptions/useVirtualFocus.js +52 -32
  9. package/cjs/form/combobox/Input/Input.js +3 -1
  10. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js +3 -1
  11. package/cjs/help-text/HelpText.js +1 -1
  12. package/cjs/util/create-context.js +72 -0
  13. package/cjs/util/hooks/descendants/descendant.js +117 -0
  14. package/cjs/util/hooks/descendants/useDescendant.js +108 -0
  15. package/cjs/util/hooks/descendants/utils.js +53 -0
  16. package/esm/form/combobox/Combobox.js +1 -1
  17. package/esm/form/combobox/Combobox.js.map +1 -1
  18. package/esm/form/combobox/ComboboxProvider.js +2 -1
  19. package/esm/form/combobox/ComboboxProvider.js.map +1 -1
  20. package/esm/form/combobox/ComboboxWrapper.js +1 -1
  21. package/esm/form/combobox/ComboboxWrapper.js.map +1 -1
  22. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +59 -41
  23. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  24. package/esm/form/combobox/FilteredOptions/filtered-options-util.d.ts +2 -1
  25. package/esm/form/combobox/FilteredOptions/filtered-options-util.js +3 -1
  26. package/esm/form/combobox/FilteredOptions/filtered-options-util.js.map +1 -1
  27. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +15 -3
  28. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  29. package/esm/form/combobox/FilteredOptions/useVirtualFocus.d.ts +2 -4
  30. package/esm/form/combobox/FilteredOptions/useVirtualFocus.js +52 -32
  31. package/esm/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
  32. package/esm/form/combobox/Input/Input.js +3 -1
  33. package/esm/form/combobox/Input/Input.js.map +1 -1
  34. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.d.ts +5 -2
  35. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js +3 -1
  36. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -1
  37. package/esm/form/combobox/types.d.ts +14 -0
  38. package/esm/help-text/HelpText.js +1 -1
  39. package/esm/help-text/HelpText.js.map +1 -1
  40. package/esm/util/create-context.d.ts +23 -0
  41. package/esm/util/create-context.js +46 -0
  42. package/esm/util/create-context.js.map +1 -0
  43. package/esm/util/hooks/descendants/descendant.d.ts +47 -0
  44. package/esm/util/hooks/descendants/descendant.js +114 -0
  45. package/esm/util/hooks/descendants/descendant.js.map +1 -0
  46. package/esm/util/hooks/descendants/useDescendant.d.ts +14 -0
  47. package/esm/util/hooks/descendants/useDescendant.js +82 -0
  48. package/esm/util/hooks/descendants/useDescendant.js.map +1 -0
  49. package/esm/util/hooks/descendants/utils.d.ts +12 -0
  50. package/esm/util/hooks/descendants/utils.js +46 -0
  51. package/esm/util/hooks/descendants/utils.js.map +1 -0
  52. package/package.json +3 -3
  53. package/src/form/combobox/Combobox.tsx +1 -1
  54. package/src/form/combobox/ComboboxProvider.tsx +2 -0
  55. package/src/form/combobox/ComboboxWrapper.tsx +0 -1
  56. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +131 -92
  57. package/src/form/combobox/FilteredOptions/filtered-options-util.ts +9 -2
  58. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +22 -3
  59. package/src/form/combobox/FilteredOptions/useVirtualFocus.ts +63 -45
  60. package/src/form/combobox/Input/Input.tsx +3 -1
  61. package/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +11 -1
  62. package/src/form/combobox/combobox.stories.tsx +36 -1
  63. package/src/form/combobox/combobox.test.tsx +1 -3
  64. package/src/form/combobox/types.ts +15 -0
  65. package/src/help-text/HelpText.tsx +1 -1
  66. package/src/util/create-context.tsx +67 -0
  67. package/src/util/hooks/descendants/descendant.stories.tsx +147 -0
  68. package/src/util/hooks/descendants/descendant.ts +161 -0
  69. package/src/util/hooks/descendants/useDescendant.tsx +111 -0
  70. package/src/util/hooks/descendants/utils.ts +56 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * https://github.com/chakra-ui/chakra-ui/tree/5ec0be610b5a69afba01a9c22365155c1b519136/packages/components/descendant
3
+ */
4
+ import { getNextIndex, getPrevIndex, isElement, sortNodes } from "./utils";
5
+
6
+ export type DescendantOptions<T = object> = T & {
7
+ /**
8
+ * If `true`, the item will be registered in all nodes map
9
+ * but omitted from enabled nodes map
10
+ */
11
+ disabled?: boolean;
12
+ };
13
+
14
+ export type Descendant<T, K> = DescendantOptions<K> & {
15
+ /**
16
+ * DOM element of the item
17
+ */
18
+ node: T;
19
+ /**
20
+ * index of item in all nodes map and enabled nodes map
21
+ */
22
+ index: number;
23
+ };
24
+
25
+ /**
26
+ * @internal
27
+ *
28
+ * Class to manage descendants and their relative indices in the DOM.
29
+ * It uses `node.compareDocumentPosition(...)` under the hood
30
+ */
31
+ export class DescendantsManager<
32
+ T extends HTMLElement,
33
+ K extends Record<string, any> = object,
34
+ > {
35
+ private descendants = new Map<T, Descendant<T, K>>();
36
+
37
+ register = (nodeOrOptions: T | null | DescendantOptions<K>) => {
38
+ if (nodeOrOptions == null) return;
39
+
40
+ if (isElement(nodeOrOptions)) {
41
+ return this.registerNode(nodeOrOptions);
42
+ }
43
+
44
+ return (node: T | null) => {
45
+ this.registerNode(node, nodeOrOptions);
46
+ };
47
+ };
48
+
49
+ unregister = (node: T) => {
50
+ this.descendants.delete(node);
51
+ const sorted = sortNodes(Array.from(this.descendants.keys()));
52
+ this.assignIndex(sorted);
53
+ };
54
+
55
+ destroy = () => {
56
+ this.descendants.clear();
57
+ };
58
+
59
+ private assignIndex = (descendants: Node[]) => {
60
+ this.descendants.forEach((descendant) => {
61
+ const index = descendants.indexOf(descendant.node);
62
+ descendant.index = index;
63
+ descendant.node.dataset["index"] = descendant.index.toString();
64
+ });
65
+ };
66
+
67
+ count = () => this.descendants.size;
68
+
69
+ enabledCount = () => this.enabledValues().length;
70
+
71
+ values = () => {
72
+ const values = Array.from(this.descendants.values());
73
+ return values.sort((a, b) => a.index - b.index);
74
+ };
75
+
76
+ enabledValues = () => {
77
+ return this.values().filter((descendant) => !descendant.disabled);
78
+ };
79
+
80
+ item = (index: number) => {
81
+ if (this.count() === 0) return undefined;
82
+ return this.values()[index];
83
+ };
84
+
85
+ enabledItem = (index: number) => {
86
+ if (this.enabledCount() === 0) return undefined;
87
+ return this.enabledValues()[index];
88
+ };
89
+
90
+ first = () => this.item(0);
91
+
92
+ firstEnabled = () => this.enabledItem(0);
93
+
94
+ last = () => this.item(this.descendants.size - 1);
95
+
96
+ lastEnabled = () => {
97
+ const lastIndex = this.enabledValues().length - 1;
98
+ return this.enabledItem(lastIndex);
99
+ };
100
+
101
+ indexOf = (node: T | null) => {
102
+ if (!node) return -1;
103
+ return this.descendants.get(node)?.index ?? -1;
104
+ };
105
+
106
+ enabledIndexOf = (node: T | null) => {
107
+ if (node == null) return -1;
108
+ return this.enabledValues().findIndex((i) => i.node.isSameNode(node));
109
+ };
110
+
111
+ next = (index: number, loop = true) => {
112
+ const next = getNextIndex(index, this.count(), loop);
113
+ return this.item(next);
114
+ };
115
+
116
+ nextEnabled = (index: number, loop = true) => {
117
+ const item = this.item(index);
118
+ if (!item) return;
119
+ const enabledIndex = this.enabledIndexOf(item.node);
120
+ const nextEnabledIndex = getNextIndex(
121
+ enabledIndex,
122
+ this.enabledCount(),
123
+ loop,
124
+ );
125
+ return this.enabledItem(nextEnabledIndex);
126
+ };
127
+
128
+ prev = (index: number, loop = true) => {
129
+ const prev = getPrevIndex(index, this.count() - 1, loop);
130
+ return this.item(prev);
131
+ };
132
+
133
+ prevEnabled = (index: number, loop = true) => {
134
+ const item = this.item(index);
135
+ if (!item) return;
136
+ const enabledIndex = this.enabledIndexOf(item.node);
137
+ const prevEnabledIndex = getPrevIndex(
138
+ enabledIndex,
139
+ this.enabledCount() - 1,
140
+ loop,
141
+ );
142
+ return this.enabledItem(prevEnabledIndex);
143
+ };
144
+
145
+ private registerNode = (node: T | null, options?: DescendantOptions<K>) => {
146
+ if (!node || this.descendants.has(node)) return;
147
+
148
+ const keys = Array.from(this.descendants.keys()).concat(node);
149
+ const sorted = sortNodes(keys);
150
+
151
+ if (options?.disabled) {
152
+ options.disabled = !!options.disabled;
153
+ }
154
+
155
+ const descendant = { node, index: -1, ...options };
156
+
157
+ this.descendants.set(node, descendant as Descendant<T, K>);
158
+
159
+ this.assignIndex(sorted);
160
+ };
161
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * https://github.com/chakra-ui/chakra-ui/tree/5ec0be610b5a69afba01a9c22365155c1b519136/packages/components/descendant
3
+ */
4
+ import React, { useRef, useState } from "react";
5
+ import { createContext } from "../../create-context";
6
+ import { useClientLayoutEffect } from "../useClientLayoutEffect";
7
+ import { mergeRefs } from "../useMergeRefs";
8
+ import { DescendantOptions, DescendantsManager } from "./descendant";
9
+ import { cast } from "./utils";
10
+
11
+ /**
12
+ * @internal
13
+ * Initializing DescendantsManager
14
+ */
15
+ function useDescendants<
16
+ T extends HTMLElement = HTMLElement,
17
+ K extends Record<string, any> = object,
18
+ >() {
19
+ const descendants = useRef(new DescendantsManager<T, K>()).current;
20
+ useClientLayoutEffect(() => {
21
+ return () => {
22
+ descendants.destroy();
23
+ };
24
+ });
25
+
26
+ return descendants;
27
+ }
28
+
29
+ const [DescendantsContextProvider, useDescendantsContext] = createContext<
30
+ ReturnType<typeof useDescendants>
31
+ >({
32
+ name: "DescendantsProvider",
33
+ errorMessage: "useDescendantsContext must be used within DescendantsProvider",
34
+ });
35
+
36
+ /**
37
+ * @internal
38
+ * This hook provides information to descendant component:
39
+ * - Index compared to other descendants
40
+ * - ref callback to register the descendant
41
+ * - Its enabled index compared to other enabled descendants
42
+ */
43
+ function useDescendant<
44
+ T extends HTMLElement = HTMLElement,
45
+ K extends Record<string, any> = object,
46
+ >(options?: DescendantOptions<K>) {
47
+ const descendants = useDescendantsContext();
48
+ const [index, setIndex] = useState(-1);
49
+ const ref = useRef<T>(null);
50
+
51
+ useClientLayoutEffect(() => {
52
+ return () => {
53
+ if (!ref.current) return;
54
+ descendants.unregister(ref.current);
55
+ };
56
+ }, []);
57
+
58
+ useClientLayoutEffect(() => {
59
+ if (!ref.current) return;
60
+ const dataIndex = Number(ref.current.dataset["index"]);
61
+ if (index != dataIndex && !Number.isNaN(dataIndex)) {
62
+ setIndex(dataIndex);
63
+ }
64
+ });
65
+
66
+ const refCallback = options
67
+ ? cast<React.RefCallback<T>>(descendants.register(options))
68
+ : cast<React.RefCallback<T>>(descendants.register);
69
+
70
+ return {
71
+ descendants,
72
+ index,
73
+ enabledIndex: descendants.enabledIndexOf(ref.current),
74
+ register: mergeRefs([refCallback, ref]),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Provides strongly typed versions of the context provider and hooks above.
80
+ */
81
+ export function createDescendantContext<
82
+ T extends HTMLElement = HTMLElement,
83
+ K extends Record<string, any> = object,
84
+ >() {
85
+ const ContextProvider = cast<React.Provider<DescendantsManager<T, K>>>(
86
+ (props) => (
87
+ <DescendantsContextProvider {...props.value}>
88
+ {props.children}
89
+ </DescendantsContextProvider>
90
+ ),
91
+ );
92
+
93
+ const _useDescendantsContext = () =>
94
+ cast<DescendantsManager<T, K>>(useDescendantsContext());
95
+
96
+ const _useDescendant = (options?: DescendantOptions<K>) =>
97
+ useDescendant<T, K>(options);
98
+
99
+ const _useDescendants = () => useDescendants<T, K>();
100
+
101
+ return [
102
+ // context provider
103
+ ContextProvider,
104
+ // call this when you need to read from context
105
+ _useDescendantsContext,
106
+ // descendants state information, to be called and passed to `ContextProvider`
107
+ _useDescendants,
108
+ // descendant index information
109
+ _useDescendant,
110
+ ] as const;
111
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Sort an array of DOM nodes according to the HTML tree order
3
+ * @see http://www.w3.org/TR/html5/infrastructure.html#tree-order
4
+ * Inspired by
5
+ * - https://github.com/floating-ui/floating-ui/blob/8e449abb0bfda143c6a6eb01d3e6943c095b744f/packages/react/src/components/FloatingList.tsx#L8
6
+ * - https://github.com/chakra-ui/chakra-ui/tree/5ec0be610b5a69afba01a9c22365155c1b519136/packages/components/descendant
7
+ */
8
+ export function sortNodes(nodes: Node[]) {
9
+ return nodes.sort((a, b) => {
10
+ const compare = a.compareDocumentPosition(b);
11
+
12
+ if (
13
+ compare & Node.DOCUMENT_POSITION_FOLLOWING ||
14
+ compare & Node.DOCUMENT_POSITION_CONTAINED_BY
15
+ ) {
16
+ // a < b
17
+ return -1;
18
+ }
19
+
20
+ if (
21
+ compare & Node.DOCUMENT_POSITION_PRECEDING ||
22
+ compare & Node.DOCUMENT_POSITION_CONTAINS
23
+ ) {
24
+ // a > b
25
+ return 1;
26
+ }
27
+
28
+ if (
29
+ compare & Node.DOCUMENT_POSITION_DISCONNECTED ||
30
+ compare & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
31
+ ) {
32
+ throw Error("Cannot sort the given nodes.");
33
+ } else {
34
+ return 0;
35
+ }
36
+ });
37
+ }
38
+
39
+ export const isElement = (el: any): el is HTMLElement =>
40
+ typeof el == "object" &&
41
+ "nodeType" in el &&
42
+ el.nodeType === Node.ELEMENT_NODE;
43
+
44
+ export function getNextIndex(current: number, max: number, loop: boolean) {
45
+ let next = current + 1;
46
+ if (loop && next >= max) next = 0;
47
+ return next;
48
+ }
49
+
50
+ export function getPrevIndex(current: number, max: number, loop: boolean) {
51
+ let next = current - 1;
52
+ if (loop && next < 0) next = max;
53
+ return next;
54
+ }
55
+
56
+ export const cast = <T>(value: any) => value as T;