@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.
- package/_docs.json +47 -5
- package/cjs/alert/Alert.js +1 -0
- package/cjs/copybutton/CopyButton.js +7 -3
- package/cjs/form/checkbox/Checkbox.js +3 -0
- package/cjs/form/combobox/Combobox.js +3 -2
- package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +4 -5
- package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +17 -9
- package/cjs/form/combobox/Input/Input.js +8 -4
- package/cjs/form/combobox/Input/inputContext.js +3 -1
- package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js +18 -12
- package/cjs/form/combobox/customOptionsContext.js +10 -2
- package/cjs/list/List.js +9 -5
- package/cjs/list/ListItem.js +4 -6
- package/esm/alert/Alert.js +1 -0
- package/esm/alert/Alert.js.map +1 -1
- package/esm/copybutton/CopyButton.d.ts +5 -0
- package/esm/copybutton/CopyButton.js +7 -3
- package/esm/copybutton/CopyButton.js.map +1 -1
- package/esm/form/checkbox/Checkbox.js +3 -0
- package/esm/form/checkbox/Checkbox.js.map +1 -1
- package/esm/form/combobox/Combobox.js +4 -3
- package/esm/form/combobox/Combobox.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js +4 -5
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +17 -9
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
- package/esm/form/combobox/Input/Input.js +8 -4
- package/esm/form/combobox/Input/Input.js.map +1 -1
- package/esm/form/combobox/Input/inputContext.d.ts +1 -0
- package/esm/form/combobox/Input/inputContext.js +3 -1
- package/esm/form/combobox/Input/inputContext.js.map +1 -1
- package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js +18 -12
- package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -1
- package/esm/form/combobox/customOptionsContext.d.ts +1 -0
- package/esm/form/combobox/customOptionsContext.js +10 -2
- package/esm/form/combobox/customOptionsContext.js.map +1 -1
- package/esm/form/combobox/types.d.ts +3 -3
- package/esm/list/List.d.ts +7 -1
- package/esm/list/List.js +9 -5
- package/esm/list/List.js.map +1 -1
- package/esm/list/ListItem.js +4 -6
- package/esm/list/ListItem.js.map +1 -1
- package/package.json +2 -2
- package/src/alert/Alert.tsx +1 -0
- package/src/alert/alert.stories.tsx +11 -1
- package/src/copybutton/CopyButton.tsx +27 -19
- package/src/copybutton/copy-button.stories.tsx +10 -0
- package/src/form/checkbox/Checkbox.tsx +17 -0
- package/src/form/combobox/Combobox.tsx +12 -1
- package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +3 -5
- package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +17 -8
- package/src/form/combobox/Input/Input.tsx +13 -4
- package/src/form/combobox/Input/inputContext.tsx +4 -1
- package/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +23 -13
- package/src/form/combobox/combobox.stories.tsx +90 -2
- package/src/form/combobox/customOptionsContext.tsx +10 -2
- package/src/form/combobox/types.ts +6 -3
- package/src/list/List.tsx +21 -5
- package/src/list/ListItem.tsx +4 -10
- 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(
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
+
activeOption = `${id}-no-hits`;
|
|
129
131
|
} else if ((value && value !== "") || isLoading) {
|
|
130
132
|
if (shouldAutocomplete && filteredOptions[0]) {
|
|
131
|
-
|
|
132
|
-
} else if (isLoading) {
|
|
133
|
-
|
|
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
|
-
|
|
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 {
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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 {
|
|
46
|
-
|
|
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
|
|
67
|
+
const isCustomOption = !options
|
|
64
68
|
.map((opt) => opt.toLowerCase())
|
|
65
69
|
.includes(option?.toLowerCase?.());
|
|
66
|
-
if (
|
|
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,
|
|
82
|
+
onToggleSelected?.(option, true, isCustomOption);
|
|
77
83
|
},
|
|
78
|
-
[
|
|
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
|
|
84
|
-
if (
|
|
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,
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
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?: (
|
|
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
|
|
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
|
-
|
|
91
|
+
isCustomOption: boolean
|
|
89
92
|
) => void;
|
|
90
93
|
/**
|
|
91
94
|
* List of selected options.
|