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