@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,82 @@
|
|
|
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 { DescendantsManager } from "./descendant";
|
|
9
|
+
import { cast } from "./utils";
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
* Initializing DescendantsManager
|
|
13
|
+
*/
|
|
14
|
+
function useDescendants() {
|
|
15
|
+
const descendants = useRef(new DescendantsManager()).current;
|
|
16
|
+
useClientLayoutEffect(() => {
|
|
17
|
+
return () => {
|
|
18
|
+
descendants.destroy();
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
return descendants;
|
|
22
|
+
}
|
|
23
|
+
const [DescendantsContextProvider, useDescendantsContext] = createContext({
|
|
24
|
+
name: "DescendantsProvider",
|
|
25
|
+
errorMessage: "useDescendantsContext must be used within DescendantsProvider",
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* @internal
|
|
29
|
+
* This hook provides information to descendant component:
|
|
30
|
+
* - Index compared to other descendants
|
|
31
|
+
* - ref callback to register the descendant
|
|
32
|
+
* - Its enabled index compared to other enabled descendants
|
|
33
|
+
*/
|
|
34
|
+
function useDescendant(options) {
|
|
35
|
+
const descendants = useDescendantsContext();
|
|
36
|
+
const [index, setIndex] = useState(-1);
|
|
37
|
+
const ref = useRef(null);
|
|
38
|
+
useClientLayoutEffect(() => {
|
|
39
|
+
return () => {
|
|
40
|
+
if (!ref.current)
|
|
41
|
+
return;
|
|
42
|
+
descendants.unregister(ref.current);
|
|
43
|
+
};
|
|
44
|
+
}, []);
|
|
45
|
+
useClientLayoutEffect(() => {
|
|
46
|
+
if (!ref.current)
|
|
47
|
+
return;
|
|
48
|
+
const dataIndex = Number(ref.current.dataset["index"]);
|
|
49
|
+
if (index != dataIndex && !Number.isNaN(dataIndex)) {
|
|
50
|
+
setIndex(dataIndex);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const refCallback = options
|
|
54
|
+
? cast(descendants.register(options))
|
|
55
|
+
: cast(descendants.register);
|
|
56
|
+
return {
|
|
57
|
+
descendants,
|
|
58
|
+
index,
|
|
59
|
+
enabledIndex: descendants.enabledIndexOf(ref.current),
|
|
60
|
+
register: mergeRefs([refCallback, ref]),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Provides strongly typed versions of the context provider and hooks above.
|
|
65
|
+
*/
|
|
66
|
+
export function createDescendantContext() {
|
|
67
|
+
const ContextProvider = cast((props) => (React.createElement(DescendantsContextProvider, Object.assign({}, props.value), props.children)));
|
|
68
|
+
const _useDescendantsContext = () => cast(useDescendantsContext());
|
|
69
|
+
const _useDescendant = (options) => useDescendant(options);
|
|
70
|
+
const _useDescendants = () => useDescendants();
|
|
71
|
+
return [
|
|
72
|
+
// context provider
|
|
73
|
+
ContextProvider,
|
|
74
|
+
// call this when you need to read from context
|
|
75
|
+
_useDescendantsContext,
|
|
76
|
+
// descendants state information, to be called and passed to `ContextProvider`
|
|
77
|
+
_useDescendants,
|
|
78
|
+
// descendant index information
|
|
79
|
+
_useDescendant,
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=useDescendant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDescendant.js","sourceRoot":"","sources":["../../../../src/util/hooks/descendants/useDescendant.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
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 declare function sortNodes(nodes: Node[]): Node[];
|
|
9
|
+
export declare const isElement: (el: any) => el is HTMLElement;
|
|
10
|
+
export declare function getNextIndex(current: number, max: number, loop: boolean): number;
|
|
11
|
+
export declare function getPrevIndex(current: number, max: number, loop: boolean): number;
|
|
12
|
+
export declare const cast: <T>(value: any) => T;
|
|
@@ -0,0 +1,46 @@
|
|
|
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) {
|
|
9
|
+
return nodes.sort((a, b) => {
|
|
10
|
+
const compare = a.compareDocumentPosition(b);
|
|
11
|
+
if (compare & Node.DOCUMENT_POSITION_FOLLOWING ||
|
|
12
|
+
compare & Node.DOCUMENT_POSITION_CONTAINED_BY) {
|
|
13
|
+
// a < b
|
|
14
|
+
return -1;
|
|
15
|
+
}
|
|
16
|
+
if (compare & Node.DOCUMENT_POSITION_PRECEDING ||
|
|
17
|
+
compare & Node.DOCUMENT_POSITION_CONTAINS) {
|
|
18
|
+
// a > b
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
if (compare & Node.DOCUMENT_POSITION_DISCONNECTED ||
|
|
22
|
+
compare & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) {
|
|
23
|
+
throw Error("Cannot sort the given nodes.");
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export const isElement = (el) => typeof el == "object" &&
|
|
31
|
+
"nodeType" in el &&
|
|
32
|
+
el.nodeType === Node.ELEMENT_NODE;
|
|
33
|
+
export function getNextIndex(current, max, loop) {
|
|
34
|
+
let next = current + 1;
|
|
35
|
+
if (loop && next >= max)
|
|
36
|
+
next = 0;
|
|
37
|
+
return next;
|
|
38
|
+
}
|
|
39
|
+
export function getPrevIndex(current, max, loop) {
|
|
40
|
+
let next = current - 1;
|
|
41
|
+
if (loop && next < 0)
|
|
42
|
+
next = max;
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
export const cast = (value) => value;
|
|
46
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../../src/util/hooks/descendants/utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACzB,MAAM,OAAO,GAAG,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC;QAE7C,IACE,OAAO,GAAG,IAAI,CAAC,2BAA2B;YAC1C,OAAO,GAAG,IAAI,CAAC,8BAA8B,EAC7C,CAAC;YACD,QAAQ;YACR,OAAO,CAAC,CAAC,CAAC;QACZ,CAAC;QAED,IACE,OAAO,GAAG,IAAI,CAAC,2BAA2B;YAC1C,OAAO,GAAG,IAAI,CAAC,0BAA0B,EACzC,CAAC;YACD,QAAQ;YACR,OAAO,CAAC,CAAC;QACX,CAAC;QAED,IACE,OAAO,GAAG,IAAI,CAAC,8BAA8B;YAC7C,OAAO,GAAG,IAAI,CAAC,yCAAyC,EACxD,CAAC;YACD,MAAM,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAC9C,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,EAAO,EAAqB,EAAE,CACtD,OAAO,EAAE,IAAI,QAAQ;IACrB,UAAU,IAAI,EAAE;IAChB,EAAE,CAAC,QAAQ,KAAK,IAAI,CAAC,YAAY,CAAC;AAEpC,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,GAAW,EAAE,IAAa;IACtE,IAAI,IAAI,GAAG,OAAO,GAAG,CAAC,CAAC;IACvB,IAAI,IAAI,IAAI,IAAI,IAAI,GAAG;QAAE,IAAI,GAAG,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,GAAW,EAAE,IAAa;IACtE,IAAI,IAAI,GAAG,OAAO,GAAG,CAAC,CAAC;IACvB,IAAI,IAAI,IAAI,IAAI,GAAG,CAAC;QAAE,IAAI,GAAG,GAAG,CAAC;IACjC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,CAAI,KAAU,EAAE,EAAE,CAAC,KAAU,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navikt/ds-react",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.16.0",
|
|
4
4
|
"description": "Aksel react-components for NAV designsystem",
|
|
5
5
|
"author": "Aksel | NAV designsystem team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@floating-ui/react": "0.25.4",
|
|
41
|
-
"@navikt/aksel-icons": "^5.
|
|
42
|
-
"@navikt/ds-tokens": "^5.
|
|
41
|
+
"@navikt/aksel-icons": "^5.16.0",
|
|
42
|
+
"@navikt/ds-tokens": "^5.16.0",
|
|
43
43
|
"@radix-ui/react-tabs": "1.0.0",
|
|
44
44
|
"@radix-ui/react-toggle-group": "1.0.0",
|
|
45
45
|
"clsx": "^1.2.1",
|
|
@@ -89,7 +89,7 @@ export const Combobox = forwardRef<
|
|
|
89
89
|
"navds-combobox__wrapper-inner navds-text-field__input",
|
|
90
90
|
{
|
|
91
91
|
"navds-combobox__wrapper-inner--virtually-unfocused":
|
|
92
|
-
activeDecendantId !==
|
|
92
|
+
activeDecendantId !== undefined,
|
|
93
93
|
},
|
|
94
94
|
)}
|
|
95
95
|
onClick={focusInput}
|
|
@@ -43,6 +43,7 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
|
|
|
43
43
|
isMultiSelect,
|
|
44
44
|
onToggleSelected,
|
|
45
45
|
selectedOptions,
|
|
46
|
+
maxSelected,
|
|
46
47
|
options,
|
|
47
48
|
value,
|
|
48
49
|
onChange,
|
|
@@ -71,6 +72,7 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
|
|
|
71
72
|
allowNewValues,
|
|
72
73
|
isMultiSelect,
|
|
73
74
|
selectedOptions,
|
|
75
|
+
maxSelected,
|
|
74
76
|
onToggleSelected,
|
|
75
77
|
options,
|
|
76
78
|
}}
|
|
@@ -27,111 +27,150 @@ const FilteredOptions = () => {
|
|
|
27
27
|
activeDecendantId,
|
|
28
28
|
virtualFocus,
|
|
29
29
|
} = useFilteredOptionsContext();
|
|
30
|
-
const { isMultiSelect, selectedOptions, toggleOption } =
|
|
30
|
+
const { isMultiSelect, selectedOptions, toggleOption, maxSelected } =
|
|
31
31
|
useSelectedOptionsContext();
|
|
32
32
|
|
|
33
|
+
const isDisabled = (option) =>
|
|
34
|
+
maxSelected?.isLimitReached && !selectedOptions.includes(option);
|
|
35
|
+
|
|
36
|
+
const shouldRenderNonSelectables =
|
|
37
|
+
maxSelected?.isLimitReached || // Render maxSelected message
|
|
38
|
+
isLoading || // Render loading message
|
|
39
|
+
(!isLoading && filteredOptions.length === 0); // Render no hits message
|
|
40
|
+
|
|
41
|
+
const shouldRenderFilteredOptionsList =
|
|
42
|
+
(allowNewValues && isValueNew && !maxSelected?.isLimitReached) || // Render add new option
|
|
43
|
+
filteredOptions.length > 0; // Render filtered options
|
|
44
|
+
|
|
33
45
|
return (
|
|
34
|
-
<
|
|
35
|
-
ref={setFilteredOptionsRef}
|
|
46
|
+
<div
|
|
36
47
|
className={cl("navds-combobox__list", {
|
|
37
48
|
"navds-combobox__list--closed": !isListOpen,
|
|
38
49
|
"navds-combobox__list--with-hover": isMouseLastUsedInputDevice,
|
|
39
50
|
})}
|
|
40
51
|
id={filteredOptionsUtil.getFilteredOptionsId(id)}
|
|
41
|
-
role="listbox"
|
|
42
52
|
tabIndex={-1}
|
|
43
53
|
>
|
|
44
|
-
{
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
{shouldRenderNonSelectables && (
|
|
55
|
+
<div className="navds-combobox__list_non-selectables" role="status">
|
|
56
|
+
{maxSelected?.isLimitReached && (
|
|
57
|
+
<div
|
|
58
|
+
className="navds-combobox__list-item--max-selected"
|
|
59
|
+
id={filteredOptionsUtil.getMaxSelectedOptionsId(id)}
|
|
60
|
+
>
|
|
61
|
+
{maxSelected.message ??
|
|
62
|
+
`${selectedOptions.length} av ${maxSelected.limit} er valgt.`}
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
{isLoading && (
|
|
66
|
+
<div
|
|
67
|
+
className="navds-combobox__list-item--loading"
|
|
68
|
+
id={filteredOptionsUtil.getIsLoadingId(id)}
|
|
69
|
+
>
|
|
70
|
+
<Loader title="Søker..." />
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
{!isLoading && filteredOptions.length === 0 && (
|
|
74
|
+
<div
|
|
75
|
+
className="navds-combobox__list-item--no-options"
|
|
76
|
+
id={filteredOptionsUtil.getNoHitsId(id)}
|
|
77
|
+
>
|
|
78
|
+
Ingen søketreff
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
54
82
|
)}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
) {
|
|
62
|
-
virtualFocus.moveFocusToElement(
|
|
63
|
-
filteredOptionsUtil.getAddNewOptionId(id),
|
|
64
|
-
);
|
|
65
|
-
setIsMouseLastUsedInputDevice(true);
|
|
66
|
-
}
|
|
67
|
-
}}
|
|
68
|
-
onPointerUp={(event) => {
|
|
69
|
-
toggleOption(value, event);
|
|
70
|
-
if (!isMultiSelect && !selectedOptions.includes(value))
|
|
71
|
-
toggleIsListOpen(false);
|
|
72
|
-
}}
|
|
73
|
-
id={filteredOptionsUtil.getAddNewOptionId(id)}
|
|
74
|
-
className={cl("navds-combobox__list-item__new-option", {
|
|
75
|
-
"navds-combobox__list-item__new-option--focus":
|
|
76
|
-
activeDecendantId === filteredOptionsUtil.getAddNewOptionId(id),
|
|
77
|
-
})}
|
|
78
|
-
role="option"
|
|
79
|
-
aria-selected={false}
|
|
80
|
-
>
|
|
81
|
-
<PlusIcon aria-hidden />
|
|
82
|
-
<BodyShort size={size}>
|
|
83
|
-
Legg til{" "}
|
|
84
|
-
<Label as="span" size={size}>
|
|
85
|
-
“{value}”
|
|
86
|
-
</Label>
|
|
87
|
-
</BodyShort>
|
|
88
|
-
</li>
|
|
89
|
-
)}
|
|
90
|
-
{!isLoading && filteredOptions.length === 0 && (
|
|
91
|
-
<li
|
|
92
|
-
className="navds-combobox__list-item__no-options"
|
|
93
|
-
role="option"
|
|
94
|
-
aria-selected={false}
|
|
95
|
-
id={filteredOptionsUtil.getNoHitsId(id)}
|
|
96
|
-
data-no-focus="true"
|
|
83
|
+
|
|
84
|
+
{shouldRenderFilteredOptionsList && (
|
|
85
|
+
<ul
|
|
86
|
+
ref={setFilteredOptionsRef}
|
|
87
|
+
role="listbox"
|
|
88
|
+
className="navds-combobox__list-options"
|
|
97
89
|
>
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
{isValueNew && !maxSelected?.isLimitReached && allowNewValues && (
|
|
91
|
+
<li
|
|
92
|
+
tabIndex={-1}
|
|
93
|
+
onMouseMove={() => {
|
|
94
|
+
if (
|
|
95
|
+
activeDecendantId !==
|
|
96
|
+
filteredOptionsUtil.getAddNewOptionId(id)
|
|
97
|
+
) {
|
|
98
|
+
virtualFocus.moveFocusToElement(
|
|
99
|
+
filteredOptionsUtil.getAddNewOptionId(id),
|
|
100
|
+
);
|
|
101
|
+
setIsMouseLastUsedInputDevice(true);
|
|
102
|
+
}
|
|
103
|
+
}}
|
|
104
|
+
onPointerUp={(event) => {
|
|
105
|
+
toggleOption(value, event);
|
|
106
|
+
if (!isMultiSelect && !selectedOptions.includes(value))
|
|
107
|
+
toggleIsListOpen(false);
|
|
108
|
+
}}
|
|
109
|
+
id={filteredOptionsUtil.getAddNewOptionId(id)}
|
|
110
|
+
className={cl(
|
|
111
|
+
"navds-combobox__list-item navds-combobox__list-item--new-option",
|
|
112
|
+
{
|
|
113
|
+
"navds-combobox__list-item--new-option--focus":
|
|
114
|
+
activeDecendantId ===
|
|
115
|
+
filteredOptionsUtil.getAddNewOptionId(id),
|
|
116
|
+
},
|
|
117
|
+
)}
|
|
118
|
+
role="option"
|
|
119
|
+
aria-selected={false}
|
|
120
|
+
>
|
|
121
|
+
<PlusIcon aria-hidden />
|
|
122
|
+
<BodyShort size={size}>
|
|
123
|
+
Legg til{" "}
|
|
124
|
+
<Label as="span" size={size}>
|
|
125
|
+
“{value}”
|
|
126
|
+
</Label>
|
|
127
|
+
</BodyShort>
|
|
128
|
+
</li>
|
|
129
|
+
)}
|
|
130
|
+
{filteredOptions.map((option) => (
|
|
131
|
+
<li
|
|
132
|
+
className={cl("navds-combobox__list-item", {
|
|
133
|
+
"navds-combobox__list-item--focus":
|
|
134
|
+
activeDecendantId ===
|
|
135
|
+
filteredOptionsUtil.getOptionId(id, option),
|
|
136
|
+
"navds-combobox__list-item--selected":
|
|
137
|
+
selectedOptions.includes(option),
|
|
138
|
+
})}
|
|
139
|
+
data-no-focus={isDisabled(option) || undefined}
|
|
140
|
+
id={filteredOptionsUtil.getOptionId(id, option)}
|
|
141
|
+
key={option}
|
|
142
|
+
tabIndex={-1}
|
|
143
|
+
onMouseMove={() => {
|
|
144
|
+
if (
|
|
145
|
+
activeDecendantId !==
|
|
146
|
+
filteredOptionsUtil.getOptionId(id, option)
|
|
147
|
+
) {
|
|
148
|
+
virtualFocus.moveFocusToElement(
|
|
149
|
+
filteredOptionsUtil.getOptionId(id, option),
|
|
150
|
+
);
|
|
151
|
+
setIsMouseLastUsedInputDevice(true);
|
|
152
|
+
}
|
|
153
|
+
}}
|
|
154
|
+
onPointerUp={(event) => {
|
|
155
|
+
if (isDisabled(option)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
toggleOption(option, event);
|
|
159
|
+
if (!isMultiSelect && !selectedOptions.includes(option)) {
|
|
160
|
+
toggleIsListOpen(false);
|
|
161
|
+
}
|
|
162
|
+
}}
|
|
163
|
+
role="option"
|
|
164
|
+
aria-selected={selectedOptions.includes(option)}
|
|
165
|
+
aria-disabled={isDisabled(option) || undefined}
|
|
166
|
+
>
|
|
167
|
+
<BodyShort size={size}>{option}</BodyShort>
|
|
168
|
+
{selectedOptions.includes(option) && <CheckmarkIcon />}
|
|
169
|
+
</li>
|
|
170
|
+
))}
|
|
171
|
+
</ul>
|
|
100
172
|
)}
|
|
101
|
-
|
|
102
|
-
<li
|
|
103
|
-
className={cl("navds-combobox__list-item", {
|
|
104
|
-
"navds-combobox__list-item--focus":
|
|
105
|
-
activeDecendantId === filteredOptionsUtil.getOptionId(id, option),
|
|
106
|
-
"navds-combobox__list-item--selected":
|
|
107
|
-
selectedOptions.includes(option),
|
|
108
|
-
})}
|
|
109
|
-
id={filteredOptionsUtil.getOptionId(id, option)}
|
|
110
|
-
key={option}
|
|
111
|
-
tabIndex={-1}
|
|
112
|
-
onMouseMove={() => {
|
|
113
|
-
if (
|
|
114
|
-
activeDecendantId !== filteredOptionsUtil.getOptionId(id, option)
|
|
115
|
-
) {
|
|
116
|
-
virtualFocus.moveFocusToElement(
|
|
117
|
-
filteredOptionsUtil.getOptionId(id, option),
|
|
118
|
-
);
|
|
119
|
-
setIsMouseLastUsedInputDevice(true);
|
|
120
|
-
}
|
|
121
|
-
}}
|
|
122
|
-
onPointerUp={(event) => {
|
|
123
|
-
toggleOption(option, event);
|
|
124
|
-
if (!isMultiSelect && !selectedOptions.includes(option))
|
|
125
|
-
toggleIsListOpen(false);
|
|
126
|
-
}}
|
|
127
|
-
role="option"
|
|
128
|
-
aria-selected={selectedOptions.includes(option)}
|
|
129
|
-
>
|
|
130
|
-
<BodyShort size={size}>{option}</BodyShort>
|
|
131
|
-
{selectedOptions.includes(option) && <CheckmarkIcon />}
|
|
132
|
-
</li>
|
|
133
|
-
))}
|
|
134
|
-
</ul>
|
|
173
|
+
</div>
|
|
135
174
|
);
|
|
136
175
|
};
|
|
137
176
|
|
|
@@ -7,8 +7,11 @@ const isPartOfText = (value, text) =>
|
|
|
7
7
|
const isValueInList = (value, list) =>
|
|
8
8
|
list?.find((listItem) => normalizeText(value) === normalizeText(listItem));
|
|
9
9
|
|
|
10
|
-
const getMatchingValuesFromList = (value, list) =>
|
|
11
|
-
list?.filter(
|
|
10
|
+
const getMatchingValuesFromList = (value, list, alwaysIncluded) =>
|
|
11
|
+
list?.filter(
|
|
12
|
+
(listItem) =>
|
|
13
|
+
isPartOfText(value, listItem) || alwaysIncluded.includes(listItem),
|
|
14
|
+
);
|
|
12
15
|
|
|
13
16
|
const getFilteredOptionsId = (comboboxId: string) =>
|
|
14
17
|
`${comboboxId}-filtered-options`;
|
|
@@ -25,6 +28,9 @@ const getIsLoadingId = (comboboxId: string) => `${comboboxId}-is-loading`;
|
|
|
25
28
|
|
|
26
29
|
const getNoHitsId = (comboboxId: string) => `${comboboxId}-no-hits`;
|
|
27
30
|
|
|
31
|
+
const getMaxSelectedOptionsId = (comboboxId: string) =>
|
|
32
|
+
`${comboboxId}-max-selected-options`;
|
|
33
|
+
|
|
28
34
|
export default {
|
|
29
35
|
normalizeText,
|
|
30
36
|
isPartOfText,
|
|
@@ -35,4 +41,5 @@ export default {
|
|
|
35
41
|
getOptionId,
|
|
36
42
|
getIsLoadingId,
|
|
37
43
|
getNoHitsId,
|
|
44
|
+
getMaxSelectedOptionsId,
|
|
38
45
|
};
|
|
@@ -9,6 +9,7 @@ import React, {
|
|
|
9
9
|
} from "react";
|
|
10
10
|
import { useClientLayoutEffect, usePrevious } from "../../../util/hooks";
|
|
11
11
|
import { useInputContext } from "../Input/inputContext";
|
|
12
|
+
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
|
|
12
13
|
import { useCustomOptionsContext } from "../customOptionsContext";
|
|
13
14
|
import { ComboboxProps } from "../types";
|
|
14
15
|
import filteredOptionsUtils from "./filtered-options-util";
|
|
@@ -70,6 +71,7 @@ export const FilteredOptionsProvider = ({
|
|
|
70
71
|
setSearchTerm,
|
|
71
72
|
shouldAutocomplete,
|
|
72
73
|
} = useInputContext();
|
|
74
|
+
const { selectedOptions, maxSelected } = useSelectedOptionsContext();
|
|
73
75
|
|
|
74
76
|
const [isInternalListOpen, setInternalListOpen] = useState(false);
|
|
75
77
|
const { customOptions } = useCustomOptionsContext();
|
|
@@ -79,8 +81,18 @@ export const FilteredOptionsProvider = ({
|
|
|
79
81
|
return externalFilteredOptions;
|
|
80
82
|
}
|
|
81
83
|
const opts = [...customOptions, ...options];
|
|
82
|
-
return filteredOptionsUtils.getMatchingValuesFromList(
|
|
83
|
-
|
|
84
|
+
return filteredOptionsUtils.getMatchingValuesFromList(
|
|
85
|
+
searchTerm,
|
|
86
|
+
opts,
|
|
87
|
+
selectedOptions,
|
|
88
|
+
);
|
|
89
|
+
}, [
|
|
90
|
+
customOptions,
|
|
91
|
+
externalFilteredOptions,
|
|
92
|
+
options,
|
|
93
|
+
searchTerm,
|
|
94
|
+
selectedOptions,
|
|
95
|
+
]);
|
|
84
96
|
|
|
85
97
|
const previousSearchTerm = usePrevious(searchTerm);
|
|
86
98
|
|
|
@@ -154,10 +166,17 @@ export const FilteredOptionsProvider = ({
|
|
|
154
166
|
activeOption = filteredOptionsUtils.getIsLoadingId(id);
|
|
155
167
|
}
|
|
156
168
|
}
|
|
157
|
-
|
|
169
|
+
const maybeMaxSelectedOptionsId =
|
|
170
|
+
maxSelected?.isLimitReached &&
|
|
171
|
+
filteredOptionsUtils.getMaxSelectedOptionsId(id);
|
|
172
|
+
return (
|
|
173
|
+
cl(activeOption, maybeMaxSelectedOptionsId, partialAriaDescribedBy) ||
|
|
174
|
+
undefined
|
|
175
|
+
);
|
|
158
176
|
}, [
|
|
159
177
|
isListOpen,
|
|
160
178
|
isLoading,
|
|
179
|
+
maxSelected?.isLimitReached,
|
|
161
180
|
value,
|
|
162
181
|
partialAriaDescribedBy,
|
|
163
182
|
shouldAutocomplete,
|