@navikt/ds-react 4.6.1 → 4.7.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 (113) hide show
  1. package/_docs.json +1711 -169
  2. package/cjs/chips/Chips.js +1 -2
  3. package/cjs/form/combobox/ClearButton.js +27 -0
  4. package/cjs/form/combobox/Combobox.js +78 -0
  5. package/cjs/form/combobox/ComboboxProvider.js +99 -0
  6. package/cjs/form/combobox/ComboboxWrapper.js +51 -0
  7. package/cjs/form/combobox/FilteredOptions/CheckIcon.js +11 -0
  8. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +46 -0
  9. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +208 -0
  10. package/cjs/form/combobox/Input/Input.js +143 -0
  11. package/cjs/form/combobox/Input/inputContext.js +86 -0
  12. package/cjs/form/combobox/SelectedOptions/SelectedOptions.js +27 -0
  13. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js +107 -0
  14. package/cjs/form/combobox/ToggleListButton.js +36 -0
  15. package/cjs/form/combobox/customOptionsContext.js +56 -0
  16. package/cjs/form/combobox/index.js +8 -0
  17. package/cjs/form/combobox/package.json +6 -0
  18. package/cjs/form/combobox/types.js +2 -0
  19. package/cjs/form/index.js +3 -1
  20. package/cjs/timeline/AxisLabels.js +12 -12
  21. package/cjs/timeline/Timeline.js +2 -2
  22. package/cjs/util/usePrevious.js +18 -0
  23. package/esm/chips/Chips.js +1 -2
  24. package/esm/chips/Chips.js.map +1 -1
  25. package/esm/date/datepicker/TableHead.d.ts +1 -0
  26. package/esm/form/Fieldset/useFieldset.d.ts +1 -1
  27. package/esm/form/checkbox/useCheckbox.d.ts +4 -4
  28. package/esm/form/combobox/ClearButton.d.ts +7 -0
  29. package/esm/form/combobox/ClearButton.js +21 -0
  30. package/esm/form/combobox/ClearButton.js.map +1 -0
  31. package/esm/form/combobox/Combobox.d.ts +4 -0
  32. package/esm/form/combobox/Combobox.js +50 -0
  33. package/esm/form/combobox/Combobox.js.map +1 -0
  34. package/esm/form/combobox/ComboboxProvider.d.ts +26 -0
  35. package/esm/form/combobox/ComboboxProvider.js +72 -0
  36. package/esm/form/combobox/ComboboxProvider.js.map +1 -0
  37. package/esm/form/combobox/ComboboxWrapper.d.ts +14 -0
  38. package/esm/form/combobox/ComboboxWrapper.js +24 -0
  39. package/esm/form/combobox/ComboboxWrapper.js.map +1 -0
  40. package/esm/form/combobox/FilteredOptions/CheckIcon.d.ts +3 -0
  41. package/esm/form/combobox/FilteredOptions/CheckIcon.js +7 -0
  42. package/esm/form/combobox/FilteredOptions/CheckIcon.js.map +1 -0
  43. package/esm/form/combobox/FilteredOptions/FilteredOptions.d.ts +3 -0
  44. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +42 -0
  45. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -0
  46. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.d.ts +27 -0
  47. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +178 -0
  48. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -0
  49. package/esm/form/combobox/Input/Input.d.ts +10 -0
  50. package/esm/form/combobox/Input/Input.js +116 -0
  51. package/esm/form/combobox/Input/Input.js.map +1 -0
  52. package/esm/form/combobox/Input/inputContext.d.ts +19 -0
  53. package/esm/form/combobox/Input/inputContext.js +59 -0
  54. package/esm/form/combobox/Input/inputContext.js.map +1 -0
  55. package/esm/form/combobox/SelectedOptions/SelectedOptions.d.ts +8 -0
  56. package/esm/form/combobox/SelectedOptions/SelectedOptions.js +23 -0
  57. package/esm/form/combobox/SelectedOptions/SelectedOptions.js.map +1 -0
  58. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.d.ts +17 -0
  59. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js +77 -0
  60. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -0
  61. package/esm/form/combobox/ToggleListButton.d.ts +6 -0
  62. package/esm/form/combobox/ToggleListButton.js +11 -0
  63. package/esm/form/combobox/ToggleListButton.js.map +1 -0
  64. package/esm/form/combobox/customOptionsContext.d.ts +11 -0
  65. package/esm/form/combobox/customOptionsContext.js +29 -0
  66. package/esm/form/combobox/customOptionsContext.js.map +1 -0
  67. package/esm/form/combobox/index.d.ts +2 -0
  68. package/esm/form/combobox/index.js +2 -0
  69. package/esm/form/combobox/index.js.map +1 -0
  70. package/esm/form/combobox/types.d.ts +119 -0
  71. package/esm/form/combobox/types.js +2 -0
  72. package/esm/form/combobox/types.js.map +1 -0
  73. package/esm/form/index.d.ts +1 -0
  74. package/esm/form/index.js +1 -0
  75. package/esm/form/index.js.map +1 -1
  76. package/esm/form/radio/useRadio.d.ts +4 -4
  77. package/esm/form/useFormField.d.ts +11 -10
  78. package/esm/form/useFormField.js.map +1 -1
  79. package/esm/timeline/AxisLabels.d.ts +7 -5
  80. package/esm/timeline/AxisLabels.js +12 -12
  81. package/esm/timeline/AxisLabels.js.map +1 -1
  82. package/esm/timeline/Timeline.d.ts +6 -0
  83. package/esm/timeline/Timeline.js +2 -2
  84. package/esm/timeline/Timeline.js.map +1 -1
  85. package/esm/timeline/utils/types.external.d.ts +5 -0
  86. package/esm/util/usePrevious.d.ts +2 -0
  87. package/esm/util/usePrevious.js +17 -0
  88. package/esm/util/usePrevious.js.map +1 -0
  89. package/package.json +2 -2
  90. package/src/chips/Chips.tsx +1 -1
  91. package/src/form/combobox/ClearButton.tsx +29 -0
  92. package/src/form/combobox/Combobox.tsx +136 -0
  93. package/src/form/combobox/ComboboxProvider.tsx +99 -0
  94. package/src/form/combobox/ComboboxWrapper.tsx +63 -0
  95. package/src/form/combobox/FilteredOptions/CheckIcon.tsx +23 -0
  96. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +106 -0
  97. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +266 -0
  98. package/src/form/combobox/Input/Input.tsx +170 -0
  99. package/src/form/combobox/Input/inputContext.tsx +127 -0
  100. package/src/form/combobox/SelectedOptions/SelectedOptions.tsx +45 -0
  101. package/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +147 -0
  102. package/src/form/combobox/ToggleListButton.tsx +37 -0
  103. package/src/form/combobox/combobox.stories.tsx +413 -0
  104. package/src/form/combobox/combobox.test.tsx +123 -0
  105. package/src/form/combobox/customOptionsContext.tsx +57 -0
  106. package/src/form/combobox/index.ts +2 -0
  107. package/src/form/combobox/types.ts +122 -0
  108. package/src/form/index.ts +1 -0
  109. package/src/form/useFormField.ts +19 -1
  110. package/src/timeline/AxisLabels.tsx +23 -13
  111. package/src/timeline/Timeline.tsx +18 -2
  112. package/src/timeline/utils/types.external.ts +6 -0
  113. package/src/util/usePrevious.ts +19 -0
@@ -0,0 +1,147 @@
1
+ import React, {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ useState,
7
+ } from "react";
8
+ import usePrevious from "../../../util/usePrevious";
9
+ import { useInputContext } from "../Input/inputContext";
10
+ import { ComboboxProps } from "../types";
11
+ import { useCustomOptionsContext } from "../customOptionsContext";
12
+
13
+ type SelectedOptionsContextType = {
14
+ addSelectedOption: (option: string) => void;
15
+ isMultiSelect?: boolean;
16
+ removeSelectedOption: (option: string) => void;
17
+ prevSelectedOptions?: string[];
18
+ selectedOptions: string[];
19
+ setSelectedOptions: (any) => void;
20
+ toggleOption: (
21
+ option: string,
22
+ event: React.KeyboardEvent | React.PointerEvent
23
+ ) => void;
24
+ };
25
+
26
+ const SelectedOptionsContext = createContext<SelectedOptionsContextType>(
27
+ {} as SelectedOptionsContextType
28
+ );
29
+
30
+ export const SelectedOptionsProvider = ({
31
+ children,
32
+ value,
33
+ }: {
34
+ children: any;
35
+ value: Pick<
36
+ ComboboxProps,
37
+ | "allowNewValues"
38
+ | "isMultiSelect"
39
+ | "options"
40
+ | "selectedOptions"
41
+ | "onToggleSelected"
42
+ >;
43
+ }) => {
44
+ const { clearInput, focusInput } = useInputContext();
45
+ const { customOptions, removeCustomOption, addCustomOption } =
46
+ useCustomOptionsContext();
47
+ const {
48
+ allowNewValues,
49
+ isMultiSelect,
50
+ selectedOptions: externalSelectedOptions,
51
+ onToggleSelected,
52
+ options,
53
+ } = value;
54
+ const [internalSelectedOptions, setSelectedOptions] = useState<string[]>([]);
55
+ const selectedOptions = useMemo(
56
+ () =>
57
+ externalSelectedOptions ?? [...customOptions, ...internalSelectedOptions],
58
+ [customOptions, externalSelectedOptions, internalSelectedOptions]
59
+ );
60
+
61
+ const addSelectedOption = useCallback(
62
+ (option: string) => {
63
+ if (
64
+ !options
65
+ .map((opt) => opt.toLowerCase())
66
+ .includes(option?.toLowerCase?.())
67
+ ) {
68
+ allowNewValues && addCustomOption(option);
69
+ } else if (isMultiSelect) {
70
+ setSelectedOptions((prevSelectedOptions) => [
71
+ ...prevSelectedOptions,
72
+ option,
73
+ ]);
74
+ } else {
75
+ setSelectedOptions([option]);
76
+ }
77
+ onToggleSelected?.(option, true);
78
+ },
79
+ [addCustomOption, allowNewValues, isMultiSelect, onToggleSelected, options]
80
+ );
81
+
82
+ const removeSelectedOption = useCallback(
83
+ (option: string) => {
84
+ if (customOptions.includes(option)) {
85
+ removeCustomOption(option);
86
+ } else {
87
+ setSelectedOptions((prevSelectedOptions) =>
88
+ prevSelectedOptions.filter(
89
+ (selectedOption) => selectedOption !== option
90
+ )
91
+ );
92
+ }
93
+ onToggleSelected?.(option, false);
94
+ },
95
+ [customOptions, onToggleSelected, removeCustomOption]
96
+ );
97
+
98
+ const toggleOption = useCallback(
99
+ (option: string, event: React.KeyboardEvent | React.PointerEvent) => {
100
+ if (selectedOptions.includes(option)) {
101
+ removeSelectedOption(option);
102
+ } else {
103
+ addSelectedOption(option);
104
+ }
105
+ if (!isMultiSelect) {
106
+ clearInput(event);
107
+ }
108
+ focusInput();
109
+ },
110
+ [
111
+ addSelectedOption,
112
+ clearInput,
113
+ focusInput,
114
+ isMultiSelect,
115
+ removeSelectedOption,
116
+ selectedOptions,
117
+ ]
118
+ );
119
+
120
+ const prevSelectedOptions = usePrevious<string[]>(selectedOptions);
121
+
122
+ const selectedOptionsState = {
123
+ addSelectedOption,
124
+ isMultiSelect,
125
+ removeSelectedOption,
126
+ prevSelectedOptions,
127
+ selectedOptions,
128
+ setSelectedOptions,
129
+ toggleOption,
130
+ };
131
+
132
+ return (
133
+ <SelectedOptionsContext.Provider value={selectedOptionsState}>
134
+ {children}
135
+ </SelectedOptionsContext.Provider>
136
+ );
137
+ };
138
+
139
+ export const useSelectedOptionsContext = () => {
140
+ const context = useContext(SelectedOptionsContext);
141
+ if (!context) {
142
+ throw new Error(
143
+ "useSelectedOptionsContext must be used within a SelectedOptionsProvider"
144
+ );
145
+ }
146
+ return context;
147
+ };
@@ -0,0 +1,37 @@
1
+ import React, { forwardRef } from "react";
2
+ import { ChevronUpIcon, ChevronDownIcon } from "@navikt/aksel-icons";
3
+
4
+ import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext";
5
+
6
+ interface ToggleListButtonProps {
7
+ toggleListButtonLabel?: string;
8
+ }
9
+
10
+ export const ToggleListButton = forwardRef<
11
+ HTMLButtonElement,
12
+ ToggleListButtonProps
13
+ >(({ toggleListButtonLabel }, ref) => {
14
+ const { isListOpen, toggleIsListOpen } = useFilteredOptionsContext();
15
+ return (
16
+ <button
17
+ type="button"
18
+ onPointerUp={() => toggleIsListOpen()}
19
+ onKeyDown={({ key }) => key === "Enter" && toggleIsListOpen()}
20
+ className="navds-combobox__button-toggle-list"
21
+ aria-expanded={isListOpen}
22
+ tabIndex={-1}
23
+ ref={ref}
24
+ >
25
+ <span className="navds-sr-only">
26
+ {toggleListButtonLabel ?? "Alternativer"}
27
+ </span>
28
+ {isListOpen ? (
29
+ <ChevronUpIcon aria-hidden />
30
+ ) : (
31
+ <ChevronDownIcon aria-hidden />
32
+ )}
33
+ </button>
34
+ );
35
+ });
36
+
37
+ export default ToggleListButton;
@@ -0,0 +1,413 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import { Meta } from "@storybook/react";
3
+ import React, { useState, useId, useMemo } from "react";
4
+ import { userEvent, within } from "@storybook/testing-library";
5
+ import { Chips, UNSAFE_Combobox, TextField } from "../../index";
6
+ import { expect } from "@storybook/jest";
7
+
8
+ export default {
9
+ title: "ds-react/Combobox",
10
+ component: UNSAFE_Combobox,
11
+ argTypes: {
12
+ isListOpen: {
13
+ control: {
14
+ type: "boolean",
15
+ },
16
+ },
17
+ isLoading: {
18
+ control: {
19
+ type: "boolean",
20
+ },
21
+ },
22
+ },
23
+ } as Meta;
24
+
25
+ const options = [
26
+ "banana",
27
+ "apple",
28
+ "apple pie",
29
+ "tangerine",
30
+ "pear",
31
+ "grape",
32
+ "kiwi",
33
+ "mango",
34
+ "passion fruit",
35
+ "pineapple",
36
+ "strawberry",
37
+ "watermelon",
38
+ "grape fruit",
39
+ ];
40
+
41
+ const initialSelectedOptions = ["passion fruit", "grape fruit"];
42
+
43
+ const DemoContainer = ({
44
+ dataTheme,
45
+ children,
46
+ }: {
47
+ children: any;
48
+ dataTheme: "dark" | "light";
49
+ }) => (
50
+ <div data-theme={dataTheme} style={{ width: "300px" }}>
51
+ {children}
52
+ </div>
53
+ );
54
+
55
+ export const Default = (props) => {
56
+ const id = useId();
57
+ return (
58
+ <DemoContainer dataTheme={props.darkMode}>
59
+ <UNSAFE_Combobox
60
+ options={props.options}
61
+ label="Hva er dine favorittfrukter?"
62
+ /* everything under here is optional? */
63
+ shouldAutocomplete={props.shouldAutoComplete}
64
+ size="medium"
65
+ id={id}
66
+ />
67
+ </DemoContainer>
68
+ );
69
+ };
70
+
71
+ Default.args = {
72
+ options,
73
+ shouldAutoComplete: true,
74
+ };
75
+
76
+ export function MultiSelect(props) {
77
+ const id = useId();
78
+ return (
79
+ <DemoContainer dataTheme={props.darkMode}>
80
+ <UNSAFE_Combobox
81
+ id={id}
82
+ label="Komboboks - velg flere"
83
+ options={props.options}
84
+ isMultiSelect={props.isMultiSelect}
85
+ />
86
+ </DemoContainer>
87
+ );
88
+ }
89
+
90
+ MultiSelect.args = {
91
+ options,
92
+ isMultiSelect: true,
93
+ };
94
+
95
+ export function MultiSelectWithAddNewOptions(props) {
96
+ const id = useId();
97
+ return (
98
+ <DemoContainer dataTheme={props.darkMode}>
99
+ <UNSAFE_Combobox
100
+ id={id}
101
+ isMultiSelect={props.isMultiSelect}
102
+ label="Komboboks (med mulighet for å legge til nye verdier)"
103
+ options={props.options}
104
+ allowNewValues={props.allowNewValues}
105
+ />
106
+ </DemoContainer>
107
+ );
108
+ }
109
+
110
+ MultiSelectWithAddNewOptions.args = {
111
+ allowNewValues: true,
112
+ isMultiSelect: true,
113
+ options,
114
+ shouldAutocomplete: false,
115
+ };
116
+
117
+ export const MultiSelectWithExternalChips = (props) => {
118
+ const [selectedOptions, setSelectedOptions] = useState<string[]>(
119
+ props.selectedOptions
120
+ );
121
+ const [value, setValue] = useState("");
122
+ const id = useId();
123
+
124
+ const toggleSelected = (option) =>
125
+ selectedOptions.includes(option)
126
+ ? setSelectedOptions(selectedOptions.filter((opt) => opt !== option))
127
+ : setSelectedOptions([...selectedOptions, option]);
128
+ return (
129
+ <DemoContainer dataTheme={props.darkMode}>
130
+ {selectedOptions && (
131
+ <Chips>
132
+ {selectedOptions.map((option) => (
133
+ <Chips.Removable
134
+ key={option}
135
+ onPointerUp={() => toggleSelected(option)}
136
+ onKeyUp={(e) => e.key === "Enter" && toggleSelected(option)}
137
+ >
138
+ {option}
139
+ </Chips.Removable>
140
+ ))}
141
+ </Chips>
142
+ )}
143
+ <UNSAFE_Combobox
144
+ options={options}
145
+ selectedOptions={selectedOptions}
146
+ onToggleSelected={(option: string) => toggleSelected(option)}
147
+ isListOpen={props.isListOpen}
148
+ isMultiSelect
149
+ value={props.controlled ? value : undefined}
150
+ onChange={(event) =>
151
+ props.controlled ? setValue(event.currentTarget.value) : undefined
152
+ }
153
+ label="Komboboks"
154
+ size="medium"
155
+ error={props.error && "error here"}
156
+ id={id}
157
+ shouldShowSelectedOptions={false}
158
+ />
159
+ </DemoContainer>
160
+ );
161
+ };
162
+
163
+ MultiSelectWithExternalChips.args = {
164
+ controlled: false,
165
+ options,
166
+ selectedOptions: [],
167
+ };
168
+
169
+ export function Loading(props) {
170
+ const id = useId();
171
+ return (
172
+ <DemoContainer dataTheme={props.darkMode}>
173
+ <UNSAFE_Combobox
174
+ id={id}
175
+ label="Komboboks (laster)"
176
+ options={[]}
177
+ selectedOptions={[]}
178
+ isListOpen={props.isListOpen}
179
+ isLoading={props.isLoading}
180
+ />
181
+ </DemoContainer>
182
+ );
183
+ }
184
+
185
+ Loading.args = {
186
+ isLoading: true,
187
+ isListOpen: true,
188
+ };
189
+
190
+ export function ComboboxWithNoHits(props) {
191
+ const id = useId();
192
+ const [value, setValue] = useState(props.value);
193
+ return (
194
+ <DemoContainer dataTheme={props.darkMode}>
195
+ <UNSAFE_Combobox
196
+ id={id}
197
+ label="Komboboks (uten søketreff)"
198
+ options={props.options}
199
+ value={value}
200
+ onChange={(event) => setValue(event.currentTarget.value)}
201
+ isListOpen={true}
202
+ />
203
+ </DemoContainer>
204
+ );
205
+ }
206
+
207
+ ComboboxWithNoHits.args = {
208
+ options,
209
+ value: "Orange",
210
+ };
211
+
212
+ export const Controlled = (props) => {
213
+ const id = useId();
214
+ const [value, setValue] = useState(props.value);
215
+ const [selectedOptions, setSelectedOptions] = useState(props.selectedOptions);
216
+ const filteredOptions = useMemo(
217
+ () => props.options.filter((option) => option.includes(value)),
218
+ [props.options, value]
219
+ );
220
+
221
+ const onToggleSelected = (option, isSelected) => {
222
+ if (isSelected) {
223
+ setSelectedOptions([...selectedOptions, option]);
224
+ } else {
225
+ setSelectedOptions(selectedOptions.filter((o) => o !== option));
226
+ }
227
+ };
228
+
229
+ return (
230
+ <DemoContainer dataTheme={props.darkMode}>
231
+ <TextField
232
+ label="Overstyr value"
233
+ onChange={(event) => setValue(event.target.value)}
234
+ value={value}
235
+ />
236
+ <br />
237
+ <UNSAFE_Combobox
238
+ label="Hva er dine favorittfrukter?"
239
+ id={id}
240
+ filteredOptions={filteredOptions}
241
+ isMultiSelect
242
+ options={props.options}
243
+ onChange={(event) => setValue(event.target.value)}
244
+ onToggleSelected={onToggleSelected}
245
+ selectedOptions={selectedOptions}
246
+ value={value}
247
+ />
248
+ </DemoContainer>
249
+ );
250
+ };
251
+
252
+ Controlled.args = {
253
+ value: "apple",
254
+ options,
255
+ selectedOptions: initialSelectedOptions,
256
+ };
257
+
258
+ export const ComboboxSizes = (props) => (
259
+ <DemoContainer dataTheme={props.darkMode}>
260
+ <UNSAFE_Combobox
261
+ label="Hva er dine favorittfrukter?"
262
+ description="Medium single-select"
263
+ options={options}
264
+ />
265
+ <br />
266
+ <UNSAFE_Combobox
267
+ label="Hva er dine favorittfrukter?"
268
+ description="Small single-select"
269
+ options={options}
270
+ size="small"
271
+ />
272
+ <br />
273
+ <UNSAFE_Combobox
274
+ label="Hva er dine favorittfrukter?"
275
+ description="Medium multiselect"
276
+ options={options}
277
+ isMultiSelect
278
+ allowNewValues
279
+ />
280
+ <br />
281
+ <UNSAFE_Combobox
282
+ label="Hva er dine favorittfrukter?"
283
+ description="Small multiselect"
284
+ options={options}
285
+ isMultiSelect
286
+ size="small"
287
+ allowNewValues
288
+ />
289
+ </DemoContainer>
290
+ );
291
+
292
+ ComboboxSizes.args = {
293
+ options,
294
+ };
295
+
296
+ function sleep(ms: number) {
297
+ return new Promise((resolve) => setTimeout(resolve, ms));
298
+ }
299
+
300
+ export const CancelInputTest = {
301
+ render: (props) => {
302
+ return (
303
+ <DemoContainer dataTheme={props.darkMode}>
304
+ <UNSAFE_Combobox
305
+ options={options}
306
+ label="Hva er dine favorittfrukter?"
307
+ />
308
+ </DemoContainer>
309
+ );
310
+ },
311
+ play: async ({ canvasElement }) => {
312
+ const canvas = within(canvasElement);
313
+
314
+ const input = canvas.getByLabelText("Hva er dine favorittfrukter?");
315
+
316
+ userEvent.click(input);
317
+ await userEvent.type(input, "apple", { delay: 200 });
318
+ await sleep(1000);
319
+
320
+ userEvent.keyboard("{ArrowDown}");
321
+ await sleep(1000);
322
+ userEvent.keyboard("{Escape}");
323
+ await sleep(1000);
324
+ userEvent.keyboard("{ArrowDown}");
325
+ const banana = canvas.getByText("banana");
326
+ userEvent.click(banana);
327
+ },
328
+ };
329
+
330
+ export const RemoveSelectedMultiSelectTest = {
331
+ render: (props) => {
332
+ return (
333
+ <DemoContainer dataTheme={props.darkMode}>
334
+ <UNSAFE_Combobox
335
+ options={options}
336
+ label="Hva er dine favorittfrukter?"
337
+ isMultiSelect
338
+ />
339
+ </DemoContainer>
340
+ );
341
+ },
342
+ play: async ({ canvasElement }) => {
343
+ const canvas = within(canvasElement);
344
+
345
+ const input = canvas.getByLabelText("Hva er dine favorittfrukter?");
346
+
347
+ userEvent.click(input);
348
+ await userEvent.type(input, "apple", { delay: 200 });
349
+ await sleep(250);
350
+
351
+ userEvent.keyboard("{ArrowDown}");
352
+ await sleep(250);
353
+ userEvent.keyboard("{Enter}");
354
+ await sleep(250);
355
+ userEvent.keyboard("{Escape}");
356
+ await sleep(250);
357
+
358
+ userEvent.click(input);
359
+ await userEvent.type(input, "banana", { delay: 200 });
360
+ await sleep(250);
361
+
362
+ userEvent.keyboard("{ArrowDown}");
363
+ await sleep(250);
364
+ userEvent.keyboard("{Enter}");
365
+ await sleep(250);
366
+ userEvent.keyboard("{Escape}");
367
+ await sleep(250);
368
+
369
+ const appleSlett = canvas.getByLabelText("apple slett");
370
+ userEvent.click(appleSlett);
371
+ await sleep(250);
372
+
373
+ const bananaSlett = canvas.getByLabelText("banana slett");
374
+ expect(bananaSlett).toBeInTheDocument();
375
+ const appleSlettAgain = canvas.queryByLabelText("apple slett");
376
+ expect(appleSlettAgain).not.toBeInTheDocument();
377
+ },
378
+ };
379
+
380
+ export const AddWhenAddNewDisabledTest = {
381
+ render: (props) => {
382
+ return (
383
+ <DemoContainer dataTheme={props.darkMode}>
384
+ <UNSAFE_Combobox
385
+ options={options}
386
+ label="Hva er dine favorittfrukter?"
387
+ isMultiSelect
388
+ />
389
+ </DemoContainer>
390
+ );
391
+ },
392
+ play: async ({ canvasElement }) => {
393
+ const canvas = within(canvasElement);
394
+
395
+ const input = canvas.getByLabelText("Hva er dine favorittfrukter?");
396
+
397
+ userEvent.click(input);
398
+ await userEvent.type(input, "aaa", { delay: 200 });
399
+ await sleep(250);
400
+
401
+ userEvent.keyboard("{ArrowDown}");
402
+ await sleep(250);
403
+ userEvent.keyboard("{ArrowDown}");
404
+ await sleep(250);
405
+ userEvent.keyboard("{Enter}");
406
+ await sleep(250);
407
+ userEvent.keyboard("{Escape}");
408
+ await sleep(250);
409
+
410
+ const invalidSelect = canvas.queryByLabelText("aaa slett");
411
+ expect(invalidSelect).not.toBeInTheDocument();
412
+ },
413
+ };
@@ -0,0 +1,123 @@
1
+ /* eslint-disable react/jsx-pascal-case */
2
+ import { render } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import React, { useId } from "react";
5
+ import { UNSAFE_Combobox } from "..";
6
+ import { act } from "react-dom/test-utils";
7
+
8
+ const options = [
9
+ "banana",
10
+ "apple",
11
+ "tangerine",
12
+ "pear",
13
+ "grape",
14
+ "kiwi",
15
+ "mango",
16
+ "passion fruit",
17
+ "pineapple",
18
+ "strawberry",
19
+ "watermelon",
20
+ "grape fruit",
21
+ ];
22
+
23
+ const App = (props) => {
24
+ const id = useId();
25
+ return (
26
+ <div data-theme="light">
27
+ <UNSAFE_Combobox
28
+ label="Hva er dine favorittfrukter?"
29
+ size="medium"
30
+ variant="simple"
31
+ id={id}
32
+ {...props}
33
+ />
34
+ </div>
35
+ );
36
+ };
37
+
38
+ describe("Render combobox", () => {
39
+ describe("with multi select", () => {
40
+ it("Should be able to search, select and remove selections", async () => {
41
+ const utils = render(<App isMultiSelect options={options} />);
42
+
43
+ await act(async () => {
44
+ await userEvent.click(
45
+ utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" })
46
+ );
47
+ });
48
+ await act(async () => {
49
+ await userEvent.type(
50
+ utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" }),
51
+ "apple"
52
+ );
53
+ });
54
+ await act(async () => {
55
+ await userEvent.click(
56
+ await utils.findByRole("option", { name: "apple" })
57
+ );
58
+ });
59
+ expect(
60
+ await utils.findByRole("option", { name: "apple", selected: true })
61
+ ).toBeInTheDocument();
62
+ await act(async () => {
63
+ await userEvent.click(
64
+ await utils.findByRole("button", { name: "apple slett" })
65
+ );
66
+ });
67
+ });
68
+ });
69
+
70
+ it("Should show loading icon when loading (used for async search)", async () => {
71
+ const utils = render(<App options={[]} isListOpen isLoading />);
72
+
73
+ expect(await utils.findByRole("option", { name: "venter..." }));
74
+ });
75
+ });
76
+
77
+ describe("Combobox state-handling", () => {
78
+ it("Should not select previous focused element when closes", async () => {
79
+ const utils = render(<App options={options} />);
80
+
81
+ await act(async () => {
82
+ await userEvent.click(
83
+ utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" })
84
+ );
85
+ });
86
+ await act(async () => {
87
+ await userEvent.type(
88
+ utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" }),
89
+ "ban"
90
+ );
91
+ await userEvent.keyboard("{ArrowDown}");
92
+ await userEvent.keyboard("{ArrowUp}");
93
+ await userEvent.keyboard("{Enter}");
94
+ });
95
+
96
+ expect(
97
+ await utils.queryByRole("button", { name: "banana slett" })
98
+ ).toBeNull();
99
+ });
100
+
101
+ it("Should reset list when resetting input (ESC)", async () => {
102
+ const utils = render(<App options={options} />);
103
+
104
+ await act(async () => {
105
+ await userEvent.click(
106
+ utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" })
107
+ );
108
+ });
109
+ await act(async () => {
110
+ await userEvent.type(
111
+ utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" }),
112
+ "apple"
113
+ );
114
+ await userEvent.keyboard("{ArrowDown}");
115
+ await userEvent.keyboard("{Escape}");
116
+ await userEvent.keyboard("{ArrowDown}");
117
+ });
118
+
119
+ expect(
120
+ await utils.findByRole("option", { name: "banana" })
121
+ ).toBeInTheDocument();
122
+ });
123
+ });