@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,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.15.0",
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.15.0",
42
- "@navikt/ds-tokens": "^5.15.0",
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 !== null,
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
  }}
@@ -57,7 +57,6 @@ const ComboboxWrapper = ({
57
57
  )}
58
58
  onFocus={onFocusInsideWrapper}
59
59
  onBlur={onBlurWrapper}
60
- tabIndex={-1}
61
60
  >
62
61
  {children}
63
62
  </div>
@@ -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
- <ul
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
- {isLoading && (
45
- <li
46
- className="navds-combobox__list-item--loading"
47
- role="option"
48
- aria-selected={false}
49
- id={filteredOptionsUtil.getIsLoadingId(id)}
50
- data-no-focus="true"
51
- >
52
- <Loader aria-label="Søker..." />
53
- </li>
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
- {isValueNew && allowNewValues && (
56
- <li
57
- tabIndex={-1}
58
- onMouseMove={() => {
59
- if (
60
- activeDecendantId !== filteredOptionsUtil.getAddNewOptionId(id)
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
- &#8220;{value}&#8221;
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
- Ingen søketreff
99
- </li>
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
+ &#8220;{value}&#8221;
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
- {filteredOptions.map((option) => (
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((listItem) => isPartOfText(value, listItem));
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(searchTerm, opts);
83
- }, [customOptions, externalFilteredOptions, options, searchTerm]);
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
- return cl(activeOption, partialAriaDescribedBy) || undefined;
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,