@navikt/ds-react 4.6.0 → 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 (133) hide show
  1. package/_docs.json +1711 -169
  2. package/cjs/chips/Chips.js +1 -2
  3. package/cjs/date/DateInput.js +1 -0
  4. package/cjs/form/Select.js +1 -0
  5. package/cjs/form/TextField.js +1 -0
  6. package/cjs/form/Textarea.js +1 -0
  7. package/cjs/form/checkbox/Checkbox.js +1 -1
  8. package/cjs/form/combobox/ClearButton.js +27 -0
  9. package/cjs/form/combobox/Combobox.js +78 -0
  10. package/cjs/form/combobox/ComboboxProvider.js +99 -0
  11. package/cjs/form/combobox/ComboboxWrapper.js +51 -0
  12. package/cjs/form/combobox/FilteredOptions/CheckIcon.js +11 -0
  13. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +46 -0
  14. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +208 -0
  15. package/cjs/form/combobox/Input/Input.js +143 -0
  16. package/cjs/form/combobox/Input/inputContext.js +86 -0
  17. package/cjs/form/combobox/SelectedOptions/SelectedOptions.js +27 -0
  18. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js +107 -0
  19. package/cjs/form/combobox/ToggleListButton.js +36 -0
  20. package/cjs/form/combobox/customOptionsContext.js +56 -0
  21. package/cjs/form/combobox/index.js +8 -0
  22. package/cjs/form/combobox/package.json +6 -0
  23. package/cjs/form/combobox/types.js +2 -0
  24. package/cjs/form/index.js +3 -1
  25. package/cjs/timeline/AxisLabels.js +12 -12
  26. package/cjs/timeline/Timeline.js +2 -2
  27. package/cjs/util/usePrevious.js +18 -0
  28. package/esm/chips/Chips.js +1 -2
  29. package/esm/chips/Chips.js.map +1 -1
  30. package/esm/date/DateInput.js +1 -0
  31. package/esm/date/DateInput.js.map +1 -1
  32. package/esm/date/datepicker/TableHead.d.ts +1 -0
  33. package/esm/form/Fieldset/useFieldset.d.ts +1 -1
  34. package/esm/form/Select.js +1 -0
  35. package/esm/form/Select.js.map +1 -1
  36. package/esm/form/TextField.js +1 -0
  37. package/esm/form/TextField.js.map +1 -1
  38. package/esm/form/Textarea.js +1 -0
  39. package/esm/form/Textarea.js.map +1 -1
  40. package/esm/form/checkbox/Checkbox.js +1 -1
  41. package/esm/form/checkbox/Checkbox.js.map +1 -1
  42. package/esm/form/checkbox/useCheckbox.d.ts +4 -4
  43. package/esm/form/combobox/ClearButton.d.ts +7 -0
  44. package/esm/form/combobox/ClearButton.js +21 -0
  45. package/esm/form/combobox/ClearButton.js.map +1 -0
  46. package/esm/form/combobox/Combobox.d.ts +4 -0
  47. package/esm/form/combobox/Combobox.js +50 -0
  48. package/esm/form/combobox/Combobox.js.map +1 -0
  49. package/esm/form/combobox/ComboboxProvider.d.ts +26 -0
  50. package/esm/form/combobox/ComboboxProvider.js +72 -0
  51. package/esm/form/combobox/ComboboxProvider.js.map +1 -0
  52. package/esm/form/combobox/ComboboxWrapper.d.ts +14 -0
  53. package/esm/form/combobox/ComboboxWrapper.js +24 -0
  54. package/esm/form/combobox/ComboboxWrapper.js.map +1 -0
  55. package/esm/form/combobox/FilteredOptions/CheckIcon.d.ts +3 -0
  56. package/esm/form/combobox/FilteredOptions/CheckIcon.js +7 -0
  57. package/esm/form/combobox/FilteredOptions/CheckIcon.js.map +1 -0
  58. package/esm/form/combobox/FilteredOptions/FilteredOptions.d.ts +3 -0
  59. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +42 -0
  60. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -0
  61. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.d.ts +27 -0
  62. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +178 -0
  63. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -0
  64. package/esm/form/combobox/Input/Input.d.ts +10 -0
  65. package/esm/form/combobox/Input/Input.js +116 -0
  66. package/esm/form/combobox/Input/Input.js.map +1 -0
  67. package/esm/form/combobox/Input/inputContext.d.ts +19 -0
  68. package/esm/form/combobox/Input/inputContext.js +59 -0
  69. package/esm/form/combobox/Input/inputContext.js.map +1 -0
  70. package/esm/form/combobox/SelectedOptions/SelectedOptions.d.ts +8 -0
  71. package/esm/form/combobox/SelectedOptions/SelectedOptions.js +23 -0
  72. package/esm/form/combobox/SelectedOptions/SelectedOptions.js.map +1 -0
  73. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.d.ts +17 -0
  74. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js +77 -0
  75. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -0
  76. package/esm/form/combobox/ToggleListButton.d.ts +6 -0
  77. package/esm/form/combobox/ToggleListButton.js +11 -0
  78. package/esm/form/combobox/ToggleListButton.js.map +1 -0
  79. package/esm/form/combobox/customOptionsContext.d.ts +11 -0
  80. package/esm/form/combobox/customOptionsContext.js +29 -0
  81. package/esm/form/combobox/customOptionsContext.js.map +1 -0
  82. package/esm/form/combobox/index.d.ts +2 -0
  83. package/esm/form/combobox/index.js +2 -0
  84. package/esm/form/combobox/index.js.map +1 -0
  85. package/esm/form/combobox/types.d.ts +119 -0
  86. package/esm/form/combobox/types.js +2 -0
  87. package/esm/form/combobox/types.js.map +1 -0
  88. package/esm/form/index.d.ts +1 -0
  89. package/esm/form/index.js +1 -0
  90. package/esm/form/index.js.map +1 -1
  91. package/esm/form/radio/useRadio.d.ts +4 -4
  92. package/esm/form/useFormField.d.ts +11 -10
  93. package/esm/form/useFormField.js.map +1 -1
  94. package/esm/timeline/AxisLabels.d.ts +7 -5
  95. package/esm/timeline/AxisLabels.js +12 -12
  96. package/esm/timeline/AxisLabels.js.map +1 -1
  97. package/esm/timeline/Timeline.d.ts +6 -0
  98. package/esm/timeline/Timeline.js +2 -2
  99. package/esm/timeline/Timeline.js.map +1 -1
  100. package/esm/timeline/utils/types.external.d.ts +5 -0
  101. package/esm/util/usePrevious.d.ts +2 -0
  102. package/esm/util/usePrevious.js +17 -0
  103. package/esm/util/usePrevious.js.map +1 -0
  104. package/package.json +2 -2
  105. package/src/chips/Chips.tsx +1 -1
  106. package/src/date/DateInput.tsx +1 -0
  107. package/src/form/Select.tsx +1 -0
  108. package/src/form/TextField.tsx +2 -0
  109. package/src/form/Textarea.tsx +1 -0
  110. package/src/form/checkbox/Checkbox.tsx +5 -1
  111. package/src/form/combobox/ClearButton.tsx +29 -0
  112. package/src/form/combobox/Combobox.tsx +136 -0
  113. package/src/form/combobox/ComboboxProvider.tsx +99 -0
  114. package/src/form/combobox/ComboboxWrapper.tsx +63 -0
  115. package/src/form/combobox/FilteredOptions/CheckIcon.tsx +23 -0
  116. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +106 -0
  117. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +266 -0
  118. package/src/form/combobox/Input/Input.tsx +170 -0
  119. package/src/form/combobox/Input/inputContext.tsx +127 -0
  120. package/src/form/combobox/SelectedOptions/SelectedOptions.tsx +45 -0
  121. package/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +147 -0
  122. package/src/form/combobox/ToggleListButton.tsx +37 -0
  123. package/src/form/combobox/combobox.stories.tsx +413 -0
  124. package/src/form/combobox/combobox.test.tsx +123 -0
  125. package/src/form/combobox/customOptionsContext.tsx +57 -0
  126. package/src/form/combobox/index.ts +2 -0
  127. package/src/form/combobox/types.ts +122 -0
  128. package/src/form/index.ts +1 -0
  129. package/src/form/useFormField.ts +19 -1
  130. package/src/timeline/AxisLabels.tsx +23 -13
  131. package/src/timeline/Timeline.tsx +18 -2
  132. package/src/timeline/utils/types.external.ts +6 -0
  133. package/src/util/usePrevious.ts +19 -0
@@ -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
+ });
@@ -0,0 +1,57 @@
1
+ import React, { useState, useCallback, createContext, useContext } from "react";
2
+ import { useInputContext } from "./Input/inputContext";
3
+
4
+ type CustomOptionsContextType = {
5
+ customOptions: string[];
6
+ removeCustomOption: (option: string) => void;
7
+ addCustomOption: (option: string) => void;
8
+ };
9
+
10
+ const CustomOptionsContext = createContext<CustomOptionsContextType>(
11
+ {} as CustomOptionsContextType
12
+ );
13
+
14
+ export const CustomOptionsProvider = ({ children }) => {
15
+ const [customOptions, setCustomOptions] = useState<string[]>([]);
16
+ const { focusInput } = useInputContext();
17
+
18
+ const removeCustomOption = useCallback(
19
+ (option) => {
20
+ setCustomOptions((prevCustomOptions) =>
21
+ prevCustomOptions.filter((o) => o !== option)
22
+ );
23
+ focusInput();
24
+ },
25
+ [focusInput, setCustomOptions]
26
+ );
27
+
28
+ const addCustomOption = useCallback(
29
+ (option) => {
30
+ setCustomOptions((prevOptions) => [...prevOptions, option]);
31
+ focusInput();
32
+ },
33
+ [focusInput, setCustomOptions]
34
+ );
35
+
36
+ const customOptionsState = {
37
+ customOptions,
38
+ removeCustomOption,
39
+ addCustomOption,
40
+ };
41
+
42
+ return (
43
+ <CustomOptionsContext.Provider value={customOptionsState}>
44
+ {children}
45
+ </CustomOptionsContext.Provider>
46
+ );
47
+ };
48
+
49
+ export const useCustomOptionsContext = () => {
50
+ const context = useContext(CustomOptionsContext);
51
+ if (!context) {
52
+ throw new Error(
53
+ "useCustomOptionsContext must be used within a CustomOptionsProvider"
54
+ );
55
+ }
56
+ return context;
57
+ };
@@ -0,0 +1,2 @@
1
+ export { default as Combobox } from "./ComboboxProvider";
2
+ export { type ComboboxProps } from "./types";
@@ -0,0 +1,122 @@
1
+ import React, { ChangeEvent, InputHTMLAttributes } from "react";
2
+ import { FormFieldProps } from "../useFormField";
3
+
4
+ export interface ComboboxProps
5
+ extends FormFieldProps,
6
+ Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "onChange" | "value"> {
7
+ /**
8
+ * Combobox label
9
+ */
10
+ label: React.ReactNode;
11
+ /**
12
+ * List of options to use for autocompletion
13
+ */
14
+ options: string[];
15
+ /**
16
+ * If enabled, adds an option to add the value of the input as an option whenever there are no options matching the value.
17
+ */
18
+ allowNewValues?: boolean;
19
+ /**
20
+ * If "true" adds a button to clear the value in the input field
21
+ */
22
+ clearButton?: boolean;
23
+ /**
24
+ * Custom name for the clear button. Requires "clearButton" to be "true".
25
+ *
26
+ * @default "Tøm"
27
+ */
28
+ clearButtonLabel?: string;
29
+ /**
30
+ * A list of options to display in the dropdown list.
31
+ * If provided, this overrides the internal search logic in the component.
32
+ * Useful for e.g. searching on a server or when overriding the search algorithm to search for synonyms or similar.
33
+ */
34
+ filteredOptions?: string[];
35
+ /**
36
+ * Optionally hide the label visually.
37
+ * Not recommended, but can be considered for e.g. search fields in the top menu.
38
+ */
39
+ hideLabel?: boolean;
40
+ /**
41
+ * Custom class name for the input field.
42
+ *
43
+ * If used for styling, please consider using tokens instead.
44
+ */
45
+ inputClassName?: string | undefined;
46
+ /**
47
+ * Controlled open/closed state for the dropdown list
48
+ */
49
+ isListOpen?: boolean;
50
+ /**
51
+ * Set to "true" when doing an async search and waiting for new filteredOptions.
52
+ *
53
+ * Will show a spinner in the dropdown and announce to screen readers that it is loading.
54
+ */
55
+ isLoading?: boolean;
56
+ /**
57
+ * Set to "true" to allow multiple selections
58
+ *
59
+ * This will display selected values as a list of Chips in front of the input field, instead of a selection replacing the value of the input.
60
+ *
61
+ */
62
+ isMultiSelect?: boolean;
63
+ /**
64
+ * Callback function triggered whenever the value of the input field is triggered.
65
+ *
66
+ * @param event
67
+ * @returns
68
+ */
69
+ onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
70
+ /**
71
+ * Callback function triggered whenever the input field is cleared
72
+ *
73
+ * @param event
74
+ * @returns
75
+ */
76
+ onClear?: (event: React.PointerEvent | React.KeyboardEvent) => void;
77
+ /**
78
+ * Callback function triggered whenever an option is selected or de-selected
79
+ *
80
+ * @param option
81
+ * @param isSelected
82
+ * @returns
83
+ */
84
+ onToggleSelected?: (option: string, isSelected: boolean) => void;
85
+ /**
86
+ * List of selected options.
87
+ *
88
+ * Use this prop when controlling the selected state outside for the component,
89
+ * e.g. for a filter, where options can be toggled elsewhere/programmatically.
90
+ */
91
+ selectedOptions?: string[];
92
+ /**
93
+ * Set to "true" to enable inline autocomplete.
94
+ *
95
+ * @default false
96
+ */
97
+ shouldAutocomplete?: boolean;
98
+ /**
99
+ * When set to "true" displays selected options as Chips before the input field
100
+ *
101
+ * @default true
102
+ */
103
+ shouldShowSelectedOptions?: boolean;
104
+ /**
105
+ * When set to "true" displays the toggle button for opening/closing the dropdown list
106
+ *
107
+ * @default true
108
+ */
109
+ toggleListButton?: boolean;
110
+ /**
111
+ * Custom name for the toggle list-button. Requires "toggleListButton" to be "true".
112
+ *
113
+ * @default "Alternativer"
114
+ */
115
+ toggleListButtonLabel?: string;
116
+ /**
117
+ * Set this to override the value of the input field.
118
+ *
119
+ * This converts the input to a controlled input, so you have to use onChange to update the value.
120
+ */
121
+ value?: string;
122
+ }