@navikt/ds-react 4.11.2 → 4.12.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 (60) hide show
  1. package/_docs.json +47 -5
  2. package/cjs/alert/Alert.js +1 -0
  3. package/cjs/copybutton/CopyButton.js +7 -3
  4. package/cjs/form/checkbox/Checkbox.js +3 -0
  5. package/cjs/form/combobox/Combobox.js +3 -2
  6. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +4 -5
  7. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +17 -9
  8. package/cjs/form/combobox/Input/Input.js +8 -4
  9. package/cjs/form/combobox/Input/inputContext.js +3 -1
  10. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js +18 -12
  11. package/cjs/form/combobox/customOptionsContext.js +10 -2
  12. package/cjs/list/List.js +9 -5
  13. package/cjs/list/ListItem.js +4 -6
  14. package/esm/alert/Alert.js +1 -0
  15. package/esm/alert/Alert.js.map +1 -1
  16. package/esm/copybutton/CopyButton.d.ts +5 -0
  17. package/esm/copybutton/CopyButton.js +7 -3
  18. package/esm/copybutton/CopyButton.js.map +1 -1
  19. package/esm/form/checkbox/Checkbox.js +3 -0
  20. package/esm/form/checkbox/Checkbox.js.map +1 -1
  21. package/esm/form/combobox/Combobox.js +4 -3
  22. package/esm/form/combobox/Combobox.js.map +1 -1
  23. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +4 -5
  24. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  25. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +17 -9
  26. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  27. package/esm/form/combobox/Input/Input.js +8 -4
  28. package/esm/form/combobox/Input/Input.js.map +1 -1
  29. package/esm/form/combobox/Input/inputContext.d.ts +1 -0
  30. package/esm/form/combobox/Input/inputContext.js +3 -1
  31. package/esm/form/combobox/Input/inputContext.js.map +1 -1
  32. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js +18 -12
  33. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -1
  34. package/esm/form/combobox/customOptionsContext.d.ts +1 -0
  35. package/esm/form/combobox/customOptionsContext.js +10 -2
  36. package/esm/form/combobox/customOptionsContext.js.map +1 -1
  37. package/esm/form/combobox/types.d.ts +3 -3
  38. package/esm/list/List.d.ts +7 -1
  39. package/esm/list/List.js +9 -5
  40. package/esm/list/List.js.map +1 -1
  41. package/esm/list/ListItem.js +4 -6
  42. package/esm/list/ListItem.js.map +1 -1
  43. package/package.json +2 -2
  44. package/src/alert/Alert.tsx +1 -0
  45. package/src/alert/alert.stories.tsx +11 -1
  46. package/src/copybutton/CopyButton.tsx +27 -19
  47. package/src/copybutton/copy-button.stories.tsx +10 -0
  48. package/src/form/checkbox/Checkbox.tsx +17 -0
  49. package/src/form/combobox/Combobox.tsx +12 -1
  50. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +3 -5
  51. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +17 -8
  52. package/src/form/combobox/Input/Input.tsx +13 -4
  53. package/src/form/combobox/Input/inputContext.tsx +4 -1
  54. package/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +23 -13
  55. package/src/form/combobox/combobox.stories.tsx +90 -2
  56. package/src/form/combobox/customOptionsContext.tsx +10 -2
  57. package/src/form/combobox/types.ts +6 -3
  58. package/src/list/List.tsx +21 -5
  59. package/src/list/ListItem.tsx +4 -10
  60. package/src/list/list.stories.tsx +132 -1
@@ -185,6 +185,16 @@ export const WithCloseButton: Story = {
185
185
  </BodyLong>
186
186
  <Link href="#">Id elit esse enim reprehenderit</Link>
187
187
  </AlertWithCloseButton>
188
+ <AlertWithCloseButton>
189
+ <DsHeading spacing size="small" level="3">
190
+ Aliquip duis est in commodo pariatur
191
+ </DsHeading>
192
+ <BodyLong>
193
+ Ullamco ullamco laborum et commodo sint culpa cupidatat culpa qui
194
+ laboris ex. Labore ex occaecat proident qui qui fugiat magna. Fugiat
195
+ sint commodo consequat eu aute.
196
+ </BodyLong>
197
+ </AlertWithCloseButton>
188
198
  </div>
189
199
  );
190
200
  },
@@ -197,6 +207,6 @@ export const WithCloseButton: Story = {
197
207
  });
198
208
 
199
209
  const buttonsAfter = canvas.getAllByTitle("Lukk Alert");
200
- expect(buttonsAfter.length).toBe(1);
210
+ expect(buttonsAfter.length).toBe(2);
201
211
  },
202
212
  };
@@ -63,6 +63,11 @@ export interface CopyButtonProps
63
63
  * @default 'Kopiert'
64
64
  */
65
65
  activeTitle?: string;
66
+ /**
67
+ * Icon position in Button
68
+ * @default "left"
69
+ */
70
+ iconPosition?: "left" | "right";
66
71
  }
67
72
 
68
73
  /**
@@ -91,6 +96,7 @@ export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
91
96
  activeDuration = 2000,
92
97
  title = "Kopier",
93
98
  activeTitle = "Kopiert",
99
+ iconPosition = "left",
94
100
  ...rest
95
101
  },
96
102
  ref
@@ -119,6 +125,25 @@ export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
119
125
  }, activeDuration);
120
126
  };
121
127
 
128
+ const CopyIcon = () => {
129
+ return active ? (
130
+ <span className="navds-copybutton__icon">
131
+ {activeIcon ?? (
132
+ <CheckmarkIcon
133
+ aria-hidden={!!text}
134
+ title={text ? undefined : activeTitle}
135
+ />
136
+ )}
137
+ </span>
138
+ ) : (
139
+ <span className="navds-copybutton__icon">
140
+ {icon ?? (
141
+ <FilesIcon aria-hidden={!!text} title={text ? undefined : title} />
142
+ )}
143
+ </span>
144
+ );
145
+ };
146
+
122
147
  return (
123
148
  <button
124
149
  ref={ref}
@@ -138,25 +163,7 @@ export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
138
163
  onClick={handleClick}
139
164
  >
140
165
  <span className="navds-copybutton__content">
141
- {active ? (
142
- <span className="navds-copybutton__icon">
143
- {activeIcon ?? (
144
- <CheckmarkIcon
145
- aria-hidden={!!text}
146
- title={text ? undefined : activeTitle}
147
- />
148
- )}
149
- </span>
150
- ) : (
151
- <span className="navds-copybutton__icon">
152
- {icon ?? (
153
- <FilesIcon
154
- aria-hidden={!!text}
155
- title={text ? undefined : title}
156
- />
157
- )}
158
- </span>
159
- )}
166
+ {iconPosition === "left" && <CopyIcon />}
160
167
 
161
168
  {text &&
162
169
  (active ? (
@@ -176,6 +183,7 @@ export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
176
183
  {text}
177
184
  </Label>
178
185
  ))}
186
+ {iconPosition === "right" && <CopyIcon />}
179
187
  </span>
180
188
  </button>
181
189
  );
@@ -60,6 +60,16 @@ export const Variants = {
60
60
  ),
61
61
  };
62
62
 
63
+ export const IconPosition = {
64
+ render: () => (
65
+ <div className="colgap">
66
+ <CopyButton copyText="3.14" iconPosition="left" text="Kopier" />
67
+
68
+ <CopyButton copyText="3.14" iconPosition="right" text="Kopier" />
69
+ </div>
70
+ ),
71
+ };
72
+
63
73
  export const Sizes = {
64
74
  render: () => (
65
75
  <div className="colgap">
@@ -86,6 +86,23 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
86
86
  }}
87
87
  />
88
88
  <label htmlFor={inputProps.id} className="navds-checkbox__label">
89
+ <span className="navds-checkbox__icon">
90
+ <svg
91
+ xmlns="http://www.w3.org/2000/svg"
92
+ width="13"
93
+ height="10"
94
+ viewBox="0 0 13 10"
95
+ fill="none"
96
+ focusable={false}
97
+ role="img"
98
+ aria-hidden
99
+ >
100
+ <path
101
+ d="M4.03524 6.41478L10.4752 0.404669C11.0792 -0.160351 12.029 -0.130672 12.5955 0.47478C13.162 1.08027 13.1296 2.03007 12.5245 2.59621L5.02111 9.59934C4.74099 9.85904 4.37559 10 4.00025 10C3.60651 10 3.22717 9.84621 2.93914 9.56111L0.439143 7.06111C-0.146381 6.47558 -0.146381 5.52542 0.439143 4.93989C1.02467 4.35437 1.97483 4.35437 2.56036 4.93989L4.03524 6.41478Z"
102
+ fill="currentColor"
103
+ />
104
+ </svg>
105
+ </span>
89
106
  <span
90
107
  className={cl("navds-checkbox__content", {
91
108
  "navds-sr-only": props.hideLabel,
@@ -1,6 +1,6 @@
1
1
  import cl from "clsx";
2
2
  import React, { forwardRef, useMemo, useRef } from "react";
3
- import { BodyShort, Label, mergeRefs } from "../..";
3
+ import { BodyShort, ErrorMessage, Label, mergeRefs } from "../..";
4
4
  import ClearButton from "./ClearButton";
5
5
  import FilteredOptions from "./FilteredOptions/FilteredOptions";
6
6
  import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext";
@@ -39,12 +39,15 @@ export const Combobox = forwardRef<
39
39
 
40
40
  const {
41
41
  clearInput,
42
+ error,
43
+ errorId,
42
44
  focusInput,
43
45
  hasError,
44
46
  inputDescriptionId,
45
47
  inputProps,
46
48
  inputRef,
47
49
  value,
50
+ showErrorMsg,
48
51
  size = "medium",
49
52
  } = useInputContext();
50
53
 
@@ -129,6 +132,14 @@ export const Combobox = forwardRef<
129
132
  </div>
130
133
  <FilteredOptions />
131
134
  </div>
135
+ <div
136
+ className="navds-form-field__error"
137
+ id={errorId}
138
+ aria-relevant="additions removals"
139
+ aria-live="polite"
140
+ >
141
+ {showErrorMsg && <ErrorMessage size={size}>{error}</ErrorMessage>}
142
+ </div>
132
143
  </ComboboxWrapper>
133
144
  );
134
145
  });
@@ -8,7 +8,6 @@ import { useInputContext } from "../Input/inputContext";
8
8
 
9
9
  const FilteredOptions = () => {
10
10
  const {
11
- clearInput,
12
11
  inputProps: { id },
13
12
  size,
14
13
  value,
@@ -51,7 +50,8 @@ const FilteredOptions = () => {
51
50
  tabIndex={-1}
52
51
  onPointerUp={(event) => {
53
52
  toggleOption(value, event);
54
- clearInput(event);
53
+ if (!isMultiSelect && !selectedOptions.includes(value))
54
+ toggleIsListOpen(false);
55
55
  }}
56
56
  id={`${id}-combobox-new-option`}
57
57
  className={cl("navds-combobox__list-item__new-option", {
@@ -92,10 +92,8 @@ const FilteredOptions = () => {
92
92
  tabIndex={-1}
93
93
  onPointerUp={(event) => {
94
94
  toggleOption(option, event);
95
- clearInput(event);
96
- if (!isMultiSelect) {
95
+ if (!isMultiSelect && !selectedOptions.includes(option))
97
96
  toggleIsListOpen(false);
98
- }
99
97
  }}
100
98
  role="option"
101
99
  aria-selected={selectedOptions.includes(option)}
@@ -8,6 +8,7 @@ import React, {
8
8
  useRef,
9
9
  useLayoutEffect,
10
10
  } from "react";
11
+ import cl from "clsx";
11
12
  import { useCustomOptionsContext } from "../customOptionsContext";
12
13
  import { useInputContext } from "../Input/inputContext";
13
14
  import usePrevious from "../../../util/usePrevious";
@@ -58,7 +59,7 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
58
59
  } = props;
59
60
  const filteredOptionsRef = useRef<HTMLUListElement | null>(null);
60
61
  const {
61
- inputProps: { id },
62
+ inputProps: { "aria-describedby": partialAriaDescribedBy, id },
62
63
  value,
63
64
  searchTerm,
64
65
  setValue,
@@ -124,18 +125,26 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
124
125
  }, [allowNewValues, isValueNew]);
125
126
 
126
127
  const ariaDescribedBy = useMemo(() => {
128
+ let activeOption;
127
129
  if (!isLoading && filteredOptions.length === 0) {
128
- return `${id}-no-hits`;
130
+ activeOption = `${id}-no-hits`;
129
131
  } else if ((value && value !== "") || isLoading) {
130
132
  if (shouldAutocomplete && filteredOptions[0]) {
131
- return `${id}-option-${filteredOptions[0].replace(" ", "-")}`;
132
- } else if (isLoading) {
133
- return `${id}-is-loading`;
133
+ activeOption = `${id}-option-${filteredOptions[0].replace(" ", "-")}`;
134
+ } else if (isListOpen && isLoading) {
135
+ activeOption = `${id}-is-loading`;
134
136
  }
135
- } else {
136
- return undefined;
137
137
  }
138
- }, [isLoading, value, shouldAutocomplete, filteredOptions, id]);
138
+ return cl(activeOption, partialAriaDescribedBy) || undefined;
139
+ }, [
140
+ isListOpen,
141
+ isLoading,
142
+ value,
143
+ partialAriaDescribedBy,
144
+ shouldAutocomplete,
145
+ filteredOptions,
146
+ id,
147
+ ]);
139
148
 
140
149
  const currentOption = useMemo(() => {
141
150
  if (filteredOptionsIndex == null) {
@@ -22,8 +22,12 @@ interface InputProps
22
22
  const Input = forwardRef<HTMLInputElement, InputProps>(
23
23
  ({ inputClassName, error, errorId, ...rest }, ref) => {
24
24
  const { clearInput, inputProps, onChange, size, value } = useInputContext();
25
- const { selectedOptions, removeSelectedOption, toggleOption } =
26
- useSelectedOptionsContext();
25
+ const {
26
+ selectedOptions,
27
+ removeSelectedOption,
28
+ toggleOption,
29
+ isMultiSelect,
30
+ } = useSelectedOptionsContext();
27
31
  const {
28
32
  activeDecendantId,
29
33
  allowNewValues,
@@ -47,7 +51,8 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
47
51
  event.preventDefault();
48
52
  // Selecting a value from the dropdown / FilteredOptions
49
53
  toggleOption(currentOption, event);
50
- clearInput(event);
54
+ if (!isMultiSelect && !selectedOptions.includes(currentOption))
55
+ toggleIsListOpen(false);
51
56
  } else if (shouldAutocomplete && selectedOptions.includes(value)) {
52
57
  event.preventDefault();
53
58
  // Trying to set the same value that is already set, so just clearing the input
@@ -56,15 +61,18 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
56
61
  event.preventDefault();
57
62
  // Autocompleting or adding a new value
58
63
  toggleOption(value, event);
59
- clearInput(event);
64
+ if (!isMultiSelect && !selectedOptions.includes(value))
65
+ toggleIsListOpen(false);
60
66
  }
61
67
  },
62
68
  [
63
69
  allowNewValues,
64
70
  clearInput,
65
71
  currentOption,
72
+ isMultiSelect,
66
73
  selectedOptions,
67
74
  shouldAutocomplete,
75
+ toggleIsListOpen,
68
76
  toggleOption,
69
77
  value,
70
78
  ]
@@ -162,6 +170,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
162
170
  aria-autocomplete={shouldAutocomplete ? "both" : "list"}
163
171
  aria-activedescendant={activeDecendantId}
164
172
  aria-describedby={ariaDescribedBy}
173
+ aria-invalid={inputProps["aria-invalid"]}
165
174
  className={cl(
166
175
  inputClassName,
167
176
  "navds-combobox__input",
@@ -13,6 +13,7 @@ import { useFormField, FormFieldType } from "../../useFormField";
13
13
 
14
14
  interface InputContextType extends FormFieldType {
15
15
  clearInput: (event: React.PointerEvent | React.KeyboardEvent) => void;
16
+ error?: string;
16
17
  focusInput: () => void;
17
18
  inputRef: React.RefObject<HTMLInputElement>;
18
19
  value: string;
@@ -80,10 +81,11 @@ export const InputContextProvider = ({ children, value: props }) => {
80
81
  const clearInput = useCallback(
81
82
  (event: React.PointerEvent | React.KeyboardEvent) => {
82
83
  onClear?.(event);
84
+ externalOnChange?.(null, "");
83
85
  setValue("");
84
86
  setSearchTerm("");
85
87
  },
86
- [onClear, setSearchTerm, setValue]
88
+ [externalOnChange, onClear, setValue]
87
89
  );
88
90
 
89
91
  const focusInput = useCallback(() => {
@@ -101,6 +103,7 @@ export const InputContextProvider = ({ children, value: props }) => {
101
103
  value={{
102
104
  ...formFieldProps,
103
105
  clearInput,
106
+ error,
104
107
  focusInput,
105
108
  inputRef,
106
109
  value,
@@ -42,8 +42,12 @@ export const SelectedOptionsProvider = ({
42
42
  >;
43
43
  }) => {
44
44
  const { clearInput, focusInput } = useInputContext();
45
- const { customOptions, removeCustomOption, addCustomOption } =
46
- useCustomOptionsContext();
45
+ const {
46
+ customOptions,
47
+ removeCustomOption,
48
+ addCustomOption,
49
+ setCustomOptions,
50
+ } = useCustomOptionsContext();
47
51
  const {
48
52
  allowNewValues,
49
53
  isMultiSelect,
@@ -60,11 +64,12 @@ export const SelectedOptionsProvider = ({
60
64
 
61
65
  const addSelectedOption = useCallback(
62
66
  (option: string) => {
63
- const isAddedByUser = !options
67
+ const isCustomOption = !options
64
68
  .map((opt) => opt.toLowerCase())
65
69
  .includes(option?.toLowerCase?.());
66
- if (isAddedByUser) {
70
+ if (isCustomOption) {
67
71
  allowNewValues && addCustomOption(option);
72
+ !isMultiSelect && setSelectedOptions([]);
68
73
  } else if (isMultiSelect) {
69
74
  setSelectedOptions((prevSelectedOptions) => [
70
75
  ...prevSelectedOptions,
@@ -72,16 +77,24 @@ export const SelectedOptionsProvider = ({
72
77
  ]);
73
78
  } else {
74
79
  setSelectedOptions([option]);
80
+ setCustomOptions([]);
75
81
  }
76
- onToggleSelected?.(option, true, isAddedByUser);
82
+ onToggleSelected?.(option, true, isCustomOption);
77
83
  },
78
- [addCustomOption, allowNewValues, isMultiSelect, onToggleSelected, options]
84
+ [
85
+ addCustomOption,
86
+ allowNewValues,
87
+ isMultiSelect,
88
+ onToggleSelected,
89
+ options,
90
+ setCustomOptions,
91
+ ]
79
92
  );
80
93
 
81
94
  const removeSelectedOption = useCallback(
82
95
  (option: string) => {
83
- const isAddedByUser = customOptions.includes(option);
84
- if (isAddedByUser) {
96
+ const isCustomOption = customOptions.includes(option);
97
+ if (isCustomOption) {
85
98
  removeCustomOption(option);
86
99
  } else {
87
100
  setSelectedOptions((prevSelectedOptions) =>
@@ -90,7 +103,7 @@ export const SelectedOptionsProvider = ({
90
103
  )
91
104
  );
92
105
  }
93
- onToggleSelected?.(option, false, isAddedByUser);
106
+ onToggleSelected?.(option, false, isCustomOption);
94
107
  },
95
108
  [customOptions, onToggleSelected, removeCustomOption]
96
109
  );
@@ -102,16 +115,13 @@ export const SelectedOptionsProvider = ({
102
115
  } else {
103
116
  addSelectedOption(option);
104
117
  }
105
- if (!isMultiSelect) {
106
- clearInput(event);
107
- }
118
+ clearInput(event);
108
119
  focusInput();
109
120
  },
110
121
  [
111
122
  addSelectedOption,
112
123
  clearInput,
113
124
  focusInput,
114
- isMultiSelect,
115
125
  removeSelectedOption,
116
126
  selectedOptions,
117
127
  ]
@@ -3,7 +3,7 @@ import { Meta } from "@storybook/react";
3
3
  import React, { useState, useId, useMemo } from "react";
4
4
  import { userEvent, within } from "@storybook/testing-library";
5
5
  import { Chips, UNSAFE_Combobox, TextField } from "../../index";
6
- import { expect } from "@storybook/jest";
6
+ import { expect, jest } from "@storybook/jest";
7
7
 
8
8
  export default {
9
9
  title: "ds-react/Combobox",
@@ -94,6 +94,27 @@ MultiSelect.args = {
94
94
  size: "medium",
95
95
  };
96
96
 
97
+ export function WithAddNewOptions(props) {
98
+ const id = useId();
99
+ return (
100
+ <DemoContainer dataTheme={props.darkMode}>
101
+ <UNSAFE_Combobox
102
+ id={id}
103
+ label="Komboboks med mulighet for å legge til nye verdier"
104
+ options={props.options}
105
+ allowNewValues={props.allowNewValues}
106
+ shouldAutocomplete={props.shouldAutoComplete}
107
+ />
108
+ </DemoContainer>
109
+ );
110
+ }
111
+
112
+ WithAddNewOptions.args = {
113
+ options,
114
+ allowNewValues: true,
115
+ shouldAutoComplete: true,
116
+ };
117
+
97
118
  export function MultiSelectWithAddNewOptions(props) {
98
119
  const id = useId();
99
120
  return (
@@ -101,7 +122,7 @@ export function MultiSelectWithAddNewOptions(props) {
101
122
  <UNSAFE_Combobox
102
123
  id={id}
103
124
  isMultiSelect={props.isMultiSelect}
104
- label="Komboboks (med mulighet for å legge til nye verdier)"
125
+ label="Multiselect komboboks med mulighet for å legge til nye verdier"
105
126
  options={props.options}
106
127
  allowNewValues={props.allowNewValues}
107
128
  />
@@ -295,6 +316,33 @@ ComboboxSizes.args = {
295
316
  options,
296
317
  };
297
318
 
319
+ export const WithError = {
320
+ args: {
321
+ error: "Du må velge en favorittfrukt.",
322
+ isLoading: true,
323
+ },
324
+ render: (props) => {
325
+ const [hasSelectedValue, setHasSelectedValue] = useState(false);
326
+ const [isLoading, setIsLoading] = useState(false);
327
+ return (
328
+ <DemoContainer dataTheme={props.darkMode}>
329
+ <UNSAFE_Combobox
330
+ filteredOptions={isLoading ? [] : undefined}
331
+ options={options}
332
+ label="Hva er dine favorittfrukter?"
333
+ error={!hasSelectedValue && props.error}
334
+ isLoading={isLoading}
335
+ onChange={() => {
336
+ setIsLoading(true);
337
+ setTimeout(() => setIsLoading(false), 2000);
338
+ }}
339
+ onToggleSelected={(_, isSelected) => setHasSelectedValue(isSelected)}
340
+ />
341
+ </DemoContainer>
342
+ );
343
+ },
344
+ };
345
+
298
346
  function sleep(ms: number) {
299
347
  return new Promise((resolve) => setTimeout(resolve, ms));
300
348
  }
@@ -324,6 +372,7 @@ export const CancelInputTest = {
324
372
  userEvent.keyboard("{Escape}");
325
373
  await sleep(1000);
326
374
  userEvent.keyboard("{ArrowDown}");
375
+ await sleep(500);
327
376
  const banana = canvas.getByText("banana");
328
377
  userEvent.click(banana);
329
378
  },
@@ -421,3 +470,42 @@ export const AddWhenAddNewDisabledTest = {
421
470
  expect(invalidSelect).not.toBeInTheDocument();
422
471
  },
423
472
  };
473
+
474
+ export const TestThatCallbacksOnlyFireWhenExpected = {
475
+ args: {
476
+ onChange: jest.fn(),
477
+ onClear: jest.fn(),
478
+ onToggleSelected: jest.fn(),
479
+ },
480
+ render: (props) => {
481
+ return (
482
+ <DemoContainer dataTheme={props.darkMode}>
483
+ <UNSAFE_Combobox
484
+ options={options}
485
+ label="Hva er dine favorittfrukter?"
486
+ {...props}
487
+ />
488
+ </DemoContainer>
489
+ );
490
+ },
491
+ play: async ({ canvasElement, args }) => {
492
+ args.onToggleSelected.mockClear();
493
+ args.onClear.mockClear();
494
+ args.onChange.mockClear();
495
+ const canvas = within(canvasElement);
496
+
497
+ const input = canvas.getByLabelText("Hva er dine favorittfrukter?");
498
+ const searchWord = "tangerine";
499
+
500
+ userEvent.click(input);
501
+ await userEvent.type(input, searchWord, { delay: 200 });
502
+ await sleep(250);
503
+ userEvent.keyboard("{ArrowDown}");
504
+ await sleep(250);
505
+ userEvent.keyboard("{Enter}");
506
+ await sleep(250);
507
+ expect(args.onClear.mock.calls).toHaveLength(1);
508
+ expect(args.onToggleSelected.mock.calls).toHaveLength(1);
509
+ expect(args.onChange.mock.calls).toHaveLength(searchWord.length + 1);
510
+ },
511
+ };
@@ -1,10 +1,12 @@
1
1
  import React, { useState, useCallback, createContext, useContext } from "react";
2
2
  import { useInputContext } from "./Input/inputContext";
3
+ import { useSelectedOptionsContext } from "./SelectedOptions/selectedOptionsContext";
3
4
 
4
5
  type CustomOptionsContextType = {
5
6
  customOptions: string[];
6
7
  removeCustomOption: (option: string) => void;
7
8
  addCustomOption: (option: string) => void;
9
+ setCustomOptions: React.Dispatch<React.SetStateAction<string[]>>;
8
10
  };
9
11
 
10
12
  const CustomOptionsContext = createContext<CustomOptionsContextType>(
@@ -14,6 +16,7 @@ const CustomOptionsContext = createContext<CustomOptionsContextType>(
14
16
  export const CustomOptionsProvider = ({ children }) => {
15
17
  const [customOptions, setCustomOptions] = useState<string[]>([]);
16
18
  const { focusInput } = useInputContext();
19
+ const { isMultiSelect } = useSelectedOptionsContext();
17
20
 
18
21
  const removeCustomOption = useCallback(
19
22
  (option) => {
@@ -27,16 +30,21 @@ export const CustomOptionsProvider = ({ children }) => {
27
30
 
28
31
  const addCustomOption = useCallback(
29
32
  (option) => {
30
- setCustomOptions((prevOptions) => [...prevOptions, option]);
33
+ if (isMultiSelect) {
34
+ setCustomOptions((prevOptions) => [...prevOptions, option]);
35
+ } else {
36
+ setCustomOptions([option]);
37
+ }
31
38
  focusInput();
32
39
  },
33
- [focusInput, setCustomOptions]
40
+ [focusInput, isMultiSelect, setCustomOptions]
34
41
  );
35
42
 
36
43
  const customOptionsState = {
37
44
  customOptions,
38
45
  removeCustomOption,
39
46
  addCustomOption,
47
+ setCustomOptions,
40
48
  };
41
49
 
42
50
  return (
@@ -66,7 +66,10 @@ export interface ComboboxProps
66
66
  * @param event
67
67
  * @returns
68
68
  */
69
- onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
69
+ onChange?: (
70
+ event: ChangeEvent<HTMLInputElement> | null,
71
+ value?: string
72
+ ) => void;
70
73
  /**
71
74
  * Callback function triggered whenever the input field is cleared
72
75
  *
@@ -79,13 +82,13 @@ export interface ComboboxProps
79
82
  *
80
83
  * @param option
81
84
  * @param isSelected - Whether the option has been selected or unselected
82
- * @param isAddedByUser - Whether the option comes from user input, instead of from the list
85
+ * @param isCustomOption - Whether the option comes from user input, instead of from the list
83
86
  * @returns
84
87
  */
85
88
  onToggleSelected?: (
86
89
  option: string,
87
90
  isSelected: boolean,
88
- isAddedByUser: boolean
91
+ isCustomOption: boolean
89
92
  ) => void;
90
93
  /**
91
94
  * List of selected options.