@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.
- package/_docs.json +145 -1
- package/cjs/form/combobox/Combobox.js +1 -1
- package/cjs/form/combobox/ComboboxProvider.js +2 -1
- package/cjs/form/combobox/ComboboxWrapper.js +1 -1
- package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +59 -41
- package/cjs/form/combobox/FilteredOptions/filtered-options-util.js +3 -1
- package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +15 -3
- package/cjs/form/combobox/FilteredOptions/useVirtualFocus.js +52 -32
- package/cjs/form/combobox/Input/Input.js +3 -1
- package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js +3 -1
- package/cjs/help-text/HelpText.js +1 -1
- package/cjs/util/create-context.js +72 -0
- package/cjs/util/hooks/descendants/descendant.js +117 -0
- package/cjs/util/hooks/descendants/useDescendant.js +108 -0
- package/cjs/util/hooks/descendants/utils.js +53 -0
- package/esm/form/combobox/Combobox.js +1 -1
- package/esm/form/combobox/Combobox.js.map +1 -1
- package/esm/form/combobox/ComboboxProvider.js +2 -1
- package/esm/form/combobox/ComboboxProvider.js.map +1 -1
- package/esm/form/combobox/ComboboxWrapper.js +1 -1
- package/esm/form/combobox/ComboboxWrapper.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js +59 -41
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/filtered-options-util.d.ts +2 -1
- package/esm/form/combobox/FilteredOptions/filtered-options-util.js +3 -1
- package/esm/form/combobox/FilteredOptions/filtered-options-util.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +15 -3
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/useVirtualFocus.d.ts +2 -4
- package/esm/form/combobox/FilteredOptions/useVirtualFocus.js +52 -32
- package/esm/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
- package/esm/form/combobox/Input/Input.js +3 -1
- package/esm/form/combobox/Input/Input.js.map +1 -1
- package/esm/form/combobox/SelectedOptions/selectedOptionsContext.d.ts +5 -2
- package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js +3 -1
- package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -1
- package/esm/form/combobox/types.d.ts +14 -0
- package/esm/help-text/HelpText.js +1 -1
- package/esm/help-text/HelpText.js.map +1 -1
- package/esm/util/create-context.d.ts +23 -0
- package/esm/util/create-context.js +46 -0
- package/esm/util/create-context.js.map +1 -0
- package/esm/util/hooks/descendants/descendant.d.ts +47 -0
- package/esm/util/hooks/descendants/descendant.js +114 -0
- package/esm/util/hooks/descendants/descendant.js.map +1 -0
- package/esm/util/hooks/descendants/useDescendant.d.ts +14 -0
- package/esm/util/hooks/descendants/useDescendant.js +82 -0
- package/esm/util/hooks/descendants/useDescendant.js.map +1 -0
- package/esm/util/hooks/descendants/utils.d.ts +12 -0
- package/esm/util/hooks/descendants/utils.js +46 -0
- package/esm/util/hooks/descendants/utils.js.map +1 -0
- package/package.json +3 -3
- package/src/form/combobox/Combobox.tsx +1 -1
- package/src/form/combobox/ComboboxProvider.tsx +2 -0
- package/src/form/combobox/ComboboxWrapper.tsx +0 -1
- package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +131 -92
- package/src/form/combobox/FilteredOptions/filtered-options-util.ts +9 -2
- package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +22 -3
- package/src/form/combobox/FilteredOptions/useVirtualFocus.ts +63 -45
- package/src/form/combobox/Input/Input.tsx +3 -1
- package/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +11 -1
- package/src/form/combobox/combobox.stories.tsx +36 -1
- package/src/form/combobox/combobox.test.tsx +1 -3
- package/src/form/combobox/types.ts +15 -0
- package/src/help-text/HelpText.tsx +1 -1
- package/src/util/create-context.tsx +67 -0
- package/src/util/hooks/descendants/descendant.stories.tsx +147 -0
- package/src/util/hooks/descendants/descendant.ts +161 -0
- package/src/util/hooks/descendants/useDescendant.tsx +111 -0
- 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;
|