@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.
- package/_docs.json +1711 -169
- package/cjs/chips/Chips.js +1 -2
- package/cjs/date/DateInput.js +1 -0
- package/cjs/form/Select.js +1 -0
- package/cjs/form/TextField.js +1 -0
- package/cjs/form/Textarea.js +1 -0
- package/cjs/form/checkbox/Checkbox.js +1 -1
- package/cjs/form/combobox/ClearButton.js +27 -0
- package/cjs/form/combobox/Combobox.js +78 -0
- package/cjs/form/combobox/ComboboxProvider.js +99 -0
- package/cjs/form/combobox/ComboboxWrapper.js +51 -0
- package/cjs/form/combobox/FilteredOptions/CheckIcon.js +11 -0
- package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +46 -0
- package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +208 -0
- package/cjs/form/combobox/Input/Input.js +143 -0
- package/cjs/form/combobox/Input/inputContext.js +86 -0
- package/cjs/form/combobox/SelectedOptions/SelectedOptions.js +27 -0
- package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js +107 -0
- package/cjs/form/combobox/ToggleListButton.js +36 -0
- package/cjs/form/combobox/customOptionsContext.js +56 -0
- package/cjs/form/combobox/index.js +8 -0
- package/cjs/form/combobox/package.json +6 -0
- package/cjs/form/combobox/types.js +2 -0
- package/cjs/form/index.js +3 -1
- package/cjs/timeline/AxisLabels.js +12 -12
- package/cjs/timeline/Timeline.js +2 -2
- package/cjs/util/usePrevious.js +18 -0
- package/esm/chips/Chips.js +1 -2
- package/esm/chips/Chips.js.map +1 -1
- package/esm/date/DateInput.js +1 -0
- package/esm/date/DateInput.js.map +1 -1
- package/esm/date/datepicker/TableHead.d.ts +1 -0
- package/esm/form/Fieldset/useFieldset.d.ts +1 -1
- package/esm/form/Select.js +1 -0
- package/esm/form/Select.js.map +1 -1
- package/esm/form/TextField.js +1 -0
- package/esm/form/TextField.js.map +1 -1
- package/esm/form/Textarea.js +1 -0
- package/esm/form/Textarea.js.map +1 -1
- package/esm/form/checkbox/Checkbox.js +1 -1
- package/esm/form/checkbox/Checkbox.js.map +1 -1
- package/esm/form/checkbox/useCheckbox.d.ts +4 -4
- package/esm/form/combobox/ClearButton.d.ts +7 -0
- package/esm/form/combobox/ClearButton.js +21 -0
- package/esm/form/combobox/ClearButton.js.map +1 -0
- package/esm/form/combobox/Combobox.d.ts +4 -0
- package/esm/form/combobox/Combobox.js +50 -0
- package/esm/form/combobox/Combobox.js.map +1 -0
- package/esm/form/combobox/ComboboxProvider.d.ts +26 -0
- package/esm/form/combobox/ComboboxProvider.js +72 -0
- package/esm/form/combobox/ComboboxProvider.js.map +1 -0
- package/esm/form/combobox/ComboboxWrapper.d.ts +14 -0
- package/esm/form/combobox/ComboboxWrapper.js +24 -0
- package/esm/form/combobox/ComboboxWrapper.js.map +1 -0
- package/esm/form/combobox/FilteredOptions/CheckIcon.d.ts +3 -0
- package/esm/form/combobox/FilteredOptions/CheckIcon.js +7 -0
- package/esm/form/combobox/FilteredOptions/CheckIcon.js.map +1 -0
- package/esm/form/combobox/FilteredOptions/FilteredOptions.d.ts +3 -0
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js +42 -0
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -0
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.d.ts +27 -0
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +178 -0
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -0
- package/esm/form/combobox/Input/Input.d.ts +10 -0
- package/esm/form/combobox/Input/Input.js +116 -0
- package/esm/form/combobox/Input/Input.js.map +1 -0
- package/esm/form/combobox/Input/inputContext.d.ts +19 -0
- package/esm/form/combobox/Input/inputContext.js +59 -0
- package/esm/form/combobox/Input/inputContext.js.map +1 -0
- package/esm/form/combobox/SelectedOptions/SelectedOptions.d.ts +8 -0
- package/esm/form/combobox/SelectedOptions/SelectedOptions.js +23 -0
- package/esm/form/combobox/SelectedOptions/SelectedOptions.js.map +1 -0
- package/esm/form/combobox/SelectedOptions/selectedOptionsContext.d.ts +17 -0
- package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js +77 -0
- package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -0
- package/esm/form/combobox/ToggleListButton.d.ts +6 -0
- package/esm/form/combobox/ToggleListButton.js +11 -0
- package/esm/form/combobox/ToggleListButton.js.map +1 -0
- package/esm/form/combobox/customOptionsContext.d.ts +11 -0
- package/esm/form/combobox/customOptionsContext.js +29 -0
- package/esm/form/combobox/customOptionsContext.js.map +1 -0
- package/esm/form/combobox/index.d.ts +2 -0
- package/esm/form/combobox/index.js +2 -0
- package/esm/form/combobox/index.js.map +1 -0
- package/esm/form/combobox/types.d.ts +119 -0
- package/esm/form/combobox/types.js +2 -0
- package/esm/form/combobox/types.js.map +1 -0
- package/esm/form/index.d.ts +1 -0
- package/esm/form/index.js +1 -0
- package/esm/form/index.js.map +1 -1
- package/esm/form/radio/useRadio.d.ts +4 -4
- package/esm/form/useFormField.d.ts +11 -10
- package/esm/form/useFormField.js.map +1 -1
- package/esm/timeline/AxisLabels.d.ts +7 -5
- package/esm/timeline/AxisLabels.js +12 -12
- package/esm/timeline/AxisLabels.js.map +1 -1
- package/esm/timeline/Timeline.d.ts +6 -0
- package/esm/timeline/Timeline.js +2 -2
- package/esm/timeline/Timeline.js.map +1 -1
- package/esm/timeline/utils/types.external.d.ts +5 -0
- package/esm/util/usePrevious.d.ts +2 -0
- package/esm/util/usePrevious.js +17 -0
- package/esm/util/usePrevious.js.map +1 -0
- package/package.json +2 -2
- package/src/chips/Chips.tsx +1 -1
- package/src/date/DateInput.tsx +1 -0
- package/src/form/Select.tsx +1 -0
- package/src/form/TextField.tsx +2 -0
- package/src/form/Textarea.tsx +1 -0
- package/src/form/checkbox/Checkbox.tsx +5 -1
- package/src/form/combobox/ClearButton.tsx +29 -0
- package/src/form/combobox/Combobox.tsx +136 -0
- package/src/form/combobox/ComboboxProvider.tsx +99 -0
- package/src/form/combobox/ComboboxWrapper.tsx +63 -0
- package/src/form/combobox/FilteredOptions/CheckIcon.tsx +23 -0
- package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +106 -0
- package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +266 -0
- package/src/form/combobox/Input/Input.tsx +170 -0
- package/src/form/combobox/Input/inputContext.tsx +127 -0
- package/src/form/combobox/SelectedOptions/SelectedOptions.tsx +45 -0
- package/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +147 -0
- package/src/form/combobox/ToggleListButton.tsx +37 -0
- package/src/form/combobox/combobox.stories.tsx +413 -0
- package/src/form/combobox/combobox.test.tsx +123 -0
- package/src/form/combobox/customOptionsContext.tsx +57 -0
- package/src/form/combobox/index.ts +2 -0
- package/src/form/combobox/types.ts +122 -0
- package/src/form/index.ts +1 -0
- package/src/form/useFormField.ts +19 -1
- package/src/timeline/AxisLabels.tsx +23 -13
- package/src/timeline/Timeline.tsx +18 -2
- package/src/timeline/utils/types.external.ts +6 -0
- 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,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
|
+
}
|