@navikt/ds-react 5.8.0 → 5.9.1
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 +1794 -1749
- package/cjs/date/context/useDateInputContext.js +1 -5
- package/cjs/date/datepicker/DatePicker.js +26 -25
- package/cjs/date/hooks/useDatepicker.js +9 -17
- package/cjs/date/hooks/useMonthPicker.js +9 -17
- package/cjs/date/hooks/useRangeDatepicker.js +9 -20
- package/cjs/date/monthpicker/MonthPicker.js +11 -6
- package/cjs/date/{DateInput.js → parts/DateInput.js} +14 -10
- package/cjs/date/parts/DateWrapper.js +55 -0
- package/cjs/date/utils/labels.js +77 -1
- package/cjs/form/combobox/Combobox.js +2 -2
- package/cjs/form/combobox/ComboboxProvider.js +1 -2
- package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +15 -14
- package/cjs/form/combobox/FilteredOptions/filtered-options-util.js +24 -0
- package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +23 -106
- package/cjs/form/combobox/FilteredOptions/useVirtualFocus.js +55 -0
- package/cjs/form/combobox/Input/Input.js +22 -13
- package/cjs/form/combobox/customOptionsContext.js +2 -3
- package/cjs/modal/Modal.js +4 -1
- package/cjs/popover/Popover.js +5 -7
- package/cjs/util/useMedia.js +30 -0
- package/esm/date/context/useDateInputContext.d.ts +6 -2
- package/esm/date/context/useDateInputContext.js +1 -5
- package/esm/date/context/useDateInputContext.js.map +1 -1
- package/esm/date/datepicker/DatePicker.d.ts +1 -1
- package/esm/date/datepicker/DatePicker.js +28 -27
- package/esm/date/datepicker/DatePicker.js.map +1 -1
- package/esm/date/datepicker/types.d.ts +0 -5
- package/esm/date/hooks/useDatepicker.d.ts +8 -5
- package/esm/date/hooks/useDatepicker.js +10 -18
- package/esm/date/hooks/useDatepicker.js.map +1 -1
- package/esm/date/hooks/useMonthPicker.d.ts +7 -4
- package/esm/date/hooks/useMonthPicker.js +10 -18
- package/esm/date/hooks/useMonthPicker.js.map +1 -1
- package/esm/date/hooks/useRangeDatepicker.d.ts +9 -3
- package/esm/date/hooks/useRangeDatepicker.js +10 -21
- package/esm/date/hooks/useRangeDatepicker.js.map +1 -1
- package/esm/date/index.d.ts +1 -1
- package/esm/date/index.js.map +1 -1
- package/esm/date/monthpicker/MonthPicker.d.ts +1 -1
- package/esm/date/monthpicker/MonthPicker.js +13 -8
- package/esm/date/monthpicker/MonthPicker.js.map +1 -1
- package/esm/date/monthpicker/types.d.ts +0 -5
- package/esm/date/{DateInput.d.ts → parts/DateInput.d.ts} +5 -1
- package/esm/date/{DateInput.js → parts/DateInput.js} +15 -11
- package/esm/date/parts/DateInput.js.map +1 -0
- package/esm/date/parts/DateWrapper.d.ts +15 -0
- package/esm/date/parts/DateWrapper.js +26 -0
- package/esm/date/parts/DateWrapper.js.map +1 -0
- package/esm/date/utils/labels.d.ts +2 -0
- package/esm/date/utils/labels.js +74 -0
- package/esm/date/utils/labels.js.map +1 -1
- package/esm/form/combobox/Combobox.js +2 -2
- package/esm/form/combobox/Combobox.js.map +1 -1
- package/esm/form/combobox/ComboboxProvider.js +1 -2
- package/esm/form/combobox/ComboboxProvider.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js +15 -14
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/filtered-options-util.d.ts +12 -0
- package/esm/form/combobox/FilteredOptions/filtered-options-util.js +23 -0
- package/esm/form/combobox/FilteredOptions/filtered-options-util.js.map +1 -0
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.d.ts +10 -13
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +24 -107
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/useVirtualFocus.d.ts +15 -0
- package/esm/form/combobox/FilteredOptions/useVirtualFocus.js +54 -0
- package/esm/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -0
- package/esm/form/combobox/Input/Input.js +22 -13
- package/esm/form/combobox/Input/Input.js.map +1 -1
- package/esm/form/combobox/customOptionsContext.d.ts +4 -1
- package/esm/form/combobox/customOptionsContext.js +2 -3
- package/esm/form/combobox/customOptionsContext.js.map +1 -1
- package/esm/modal/Modal.js +4 -1
- package/esm/modal/Modal.js.map +1 -1
- package/esm/popover/Popover.d.ts +0 -5
- package/esm/popover/Popover.js +5 -7
- package/esm/popover/Popover.js.map +1 -1
- package/esm/util/useMedia.d.ts +8 -0
- package/esm/util/useMedia.js +27 -0
- package/esm/util/useMedia.js.map +1 -0
- package/package.json +3 -3
- package/src/date/context/useDateInputContext.tsx +5 -5
- package/src/date/datepicker/DatePicker.tsx +58 -65
- package/src/date/datepicker/datepicker.stories.tsx +37 -46
- package/src/date/datepicker/types.ts +0 -5
- package/src/date/hooks/useDatepicker.tsx +20 -25
- package/src/date/hooks/useMonthPicker.tsx +18 -24
- package/src/date/hooks/useRangeDatepicker.tsx +27 -30
- package/src/date/index.ts +1 -1
- package/src/date/monthpicker/MonthPicker.tsx +39 -43
- package/src/date/monthpicker/types.ts +0 -5
- package/src/date/{DateInput.tsx → parts/DateInput.tsx} +23 -12
- package/src/date/parts/DateWrapper.tsx +80 -0
- package/src/date/utils/labels.ts +83 -0
- package/src/form/combobox/Combobox.tsx +2 -2
- package/src/form/combobox/ComboboxProvider.tsx +1 -2
- package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +28 -16
- package/src/form/combobox/FilteredOptions/filtered-options-util.ts +38 -0
- package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +70 -140
- package/src/form/combobox/FilteredOptions/useVirtualFocus.ts +87 -0
- package/src/form/combobox/Input/Input.tsx +22 -18
- package/src/form/combobox/customOptionsContext.tsx +10 -5
- package/src/guide-panel/guidepanel.stories.tsx +2 -2
- package/src/modal/Modal.tsx +4 -1
- package/src/modal/modal.stories.tsx +93 -21
- package/src/popover/Popover.tsx +4 -12
- package/src/util/__tests__/useMedia.test.tsx +19 -0
- package/src/util/useMedia.ts +38 -0
- package/cjs/date/hooks/useEscape.js +0 -23
- package/cjs/date/hooks/useOutsideClickHandler.js +0 -26
- package/esm/date/DateInput.js.map +0 -1
- package/esm/date/hooks/useEscape.d.ts +0 -2
- package/esm/date/hooks/useEscape.js +0 -20
- package/esm/date/hooks/useEscape.js.map +0 -1
- package/esm/date/hooks/useOutsideClickHandler.d.ts +0 -1
- package/esm/date/hooks/useOutsideClickHandler.js +0 -23
- package/esm/date/hooks/useOutsideClickHandler.js.map +0 -1
- package/src/date/hooks/useEscape.tsx +0 -30
- package/src/date/hooks/useOutsideClickHandler.tsx +0 -34
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const normalizeText = (text: string): string =>
|
|
2
|
+
typeof text === "string" ? text.toLocaleLowerCase().trim() : "";
|
|
3
|
+
|
|
4
|
+
const isPartOfText = (value, text) =>
|
|
5
|
+
normalizeText(text).startsWith(normalizeText(value ?? ""));
|
|
6
|
+
|
|
7
|
+
const isValueInList = (value, list) =>
|
|
8
|
+
list?.find((listItem) => normalizeText(value) === normalizeText(listItem));
|
|
9
|
+
|
|
10
|
+
const getMatchingValuesFromList = (value, list) =>
|
|
11
|
+
list?.filter((listItem) => isPartOfText(value, listItem));
|
|
12
|
+
|
|
13
|
+
const getFilteredOptionsId = (comboboxId: string) =>
|
|
14
|
+
`${comboboxId}-filtered-options`;
|
|
15
|
+
|
|
16
|
+
const getOptionId = (comboboxId: string, option: string) =>
|
|
17
|
+
`${comboboxId.toLocaleLowerCase()}-option-${option
|
|
18
|
+
.replace(" ", "-")
|
|
19
|
+
.toLocaleLowerCase()}`;
|
|
20
|
+
|
|
21
|
+
const getAddNewOptionId = (comboboxId: string) =>
|
|
22
|
+
`${comboboxId}-combobox-new-option`;
|
|
23
|
+
|
|
24
|
+
const getIsLoadingId = (comboboxId: string) => `${comboboxId}-is-loading`;
|
|
25
|
+
|
|
26
|
+
const getNoHitsId = (comboboxId: string) => `${comboboxId}-no-hits`;
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
normalizeText,
|
|
30
|
+
isPartOfText,
|
|
31
|
+
isValueInList,
|
|
32
|
+
getMatchingValuesFromList,
|
|
33
|
+
getFilteredOptionsId,
|
|
34
|
+
getAddNewOptionId,
|
|
35
|
+
getOptionId,
|
|
36
|
+
getIsLoadingId,
|
|
37
|
+
getNoHitsId,
|
|
38
|
+
};
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
useState,
|
|
3
|
-
useEffect,
|
|
4
3
|
useMemo,
|
|
5
4
|
createContext,
|
|
6
5
|
useContext,
|
|
7
6
|
useCallback,
|
|
8
|
-
useRef,
|
|
9
7
|
SetStateAction,
|
|
10
8
|
} from "react";
|
|
11
9
|
import cl from "clsx";
|
|
@@ -13,26 +11,29 @@ import { useCustomOptionsContext } from "../customOptionsContext";
|
|
|
13
11
|
import { useInputContext } from "../Input/inputContext";
|
|
14
12
|
import usePrevious from "../../../util/usePrevious";
|
|
15
13
|
import { useClientLayoutEffect } from "../../../util";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
import filteredOptionsUtils from "./filtered-options-util";
|
|
15
|
+
import useVirtualFocus, { VirtualFocusType } from "./useVirtualFocus";
|
|
16
|
+
import { ComboboxProps } from "../types";
|
|
17
|
+
|
|
18
|
+
type FilteredOptionsProps = {
|
|
19
|
+
children: any;
|
|
20
|
+
value: Pick<
|
|
21
|
+
ComboboxProps,
|
|
22
|
+
| "allowNewValues"
|
|
23
|
+
| "filteredOptions"
|
|
24
|
+
| "isListOpen"
|
|
25
|
+
| "isLoading"
|
|
26
|
+
| "options"
|
|
27
|
+
>;
|
|
28
|
+
};
|
|
28
29
|
|
|
29
30
|
type FilteredOptionsContextType = {
|
|
30
31
|
activeDecendantId?: string;
|
|
31
32
|
allowNewValues?: boolean;
|
|
32
33
|
ariaDescribedBy?: string;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
setFilteredOptionsRef: React.Dispatch<
|
|
35
|
+
React.SetStateAction<HTMLUListElement | null>
|
|
36
|
+
>;
|
|
36
37
|
isListOpen: boolean;
|
|
37
38
|
isLoading?: boolean;
|
|
38
39
|
filteredOptions: string[];
|
|
@@ -40,19 +41,18 @@ type FilteredOptionsContextType = {
|
|
|
40
41
|
setIsMouseLastUsedInputDevice: React.Dispatch<SetStateAction<boolean>>;
|
|
41
42
|
isValueNew: boolean;
|
|
42
43
|
toggleIsListOpen: (newState?: boolean) => void;
|
|
43
|
-
currentOption
|
|
44
|
-
resetFilteredOptionsIndex: () => void;
|
|
45
|
-
moveFocusUp: () => void;
|
|
46
|
-
moveFocusDown: () => void;
|
|
47
|
-
moveFocusToInput: () => void;
|
|
48
|
-
moveFocusToEnd: () => void;
|
|
44
|
+
currentOption?: string;
|
|
49
45
|
shouldAutocomplete?: boolean;
|
|
46
|
+
virtualFocus: VirtualFocusType;
|
|
50
47
|
};
|
|
51
48
|
const FilteredOptionsContext = createContext<FilteredOptionsContextType>(
|
|
52
49
|
{} as FilteredOptionsContextType
|
|
53
50
|
);
|
|
54
51
|
|
|
55
|
-
export const FilteredOptionsProvider = ({
|
|
52
|
+
export const FilteredOptionsProvider = ({
|
|
53
|
+
children,
|
|
54
|
+
value: props,
|
|
55
|
+
}: FilteredOptionsProps) => {
|
|
56
56
|
const {
|
|
57
57
|
allowNewValues,
|
|
58
58
|
filteredOptions: externalFilteredOptions,
|
|
@@ -60,7 +60,9 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
|
|
60
60
|
isLoading,
|
|
61
61
|
options,
|
|
62
62
|
} = props;
|
|
63
|
-
const filteredOptionsRef =
|
|
63
|
+
const [filteredOptionsRef, setFilteredOptionsRef] =
|
|
64
|
+
useState<HTMLUListElement | null>(null);
|
|
65
|
+
const virtualFocus = useVirtualFocus(filteredOptionsRef);
|
|
64
66
|
const {
|
|
65
67
|
inputProps: { "aria-describedby": partialAriaDescribedBy, id },
|
|
66
68
|
value,
|
|
@@ -70,9 +72,6 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
|
|
70
72
|
shouldAutocomplete,
|
|
71
73
|
} = useInputContext();
|
|
72
74
|
|
|
73
|
-
const [filteredOptionsIndex, setFilteredOptionsIndex] = useState<
|
|
74
|
-
number | null
|
|
75
|
-
>(null);
|
|
76
75
|
const [isInternalListOpen, setInternalListOpen] = useState(false);
|
|
77
76
|
const { customOptions } = useCustomOptionsContext();
|
|
78
77
|
|
|
@@ -81,8 +80,7 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
|
|
81
80
|
return externalFilteredOptions;
|
|
82
81
|
}
|
|
83
82
|
const opts = [...customOptions, ...options];
|
|
84
|
-
|
|
85
|
-
return getMatchingValuesFromList(searchTerm, opts);
|
|
83
|
+
return filteredOptionsUtils.getMatchingValuesFromList(searchTerm, opts);
|
|
86
84
|
}, [customOptions, externalFilteredOptions, options, searchTerm]);
|
|
87
85
|
|
|
88
86
|
const previousSearchTerm = usePrevious(searchTerm);
|
|
@@ -90,10 +88,26 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
|
|
90
88
|
const [isMouseLastUsedInputDevice, setIsMouseLastUsedInputDevice] =
|
|
91
89
|
useState(false);
|
|
92
90
|
|
|
91
|
+
const filteredOptionsMap = useMemo(
|
|
92
|
+
() =>
|
|
93
|
+
options.reduce(
|
|
94
|
+
(map, _option) => ({
|
|
95
|
+
...map,
|
|
96
|
+
[filteredOptionsUtils.getOptionId(id, _option)]: _option,
|
|
97
|
+
}),
|
|
98
|
+
{
|
|
99
|
+
[filteredOptionsUtils.getAddNewOptionId(id)]: allowNewValues
|
|
100
|
+
? value
|
|
101
|
+
: undefined,
|
|
102
|
+
}
|
|
103
|
+
),
|
|
104
|
+
[allowNewValues, id, options, value]
|
|
105
|
+
);
|
|
106
|
+
|
|
93
107
|
useClientLayoutEffect(() => {
|
|
94
108
|
if (
|
|
95
109
|
shouldAutocomplete &&
|
|
96
|
-
normalizeText(searchTerm) !== "" &&
|
|
110
|
+
filteredOptionsUtils.normalizeText(searchTerm) !== "" &&
|
|
97
111
|
(previousSearchTerm?.length || 0) < searchTerm.length &&
|
|
98
112
|
filteredOptions.length > 0
|
|
99
113
|
) {
|
|
@@ -115,29 +129,30 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
|
|
115
129
|
return isExternalListOpen ?? isInternalListOpen;
|
|
116
130
|
}, [isExternalListOpen, isInternalListOpen]);
|
|
117
131
|
|
|
118
|
-
const toggleIsListOpen = useCallback(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
const toggleIsListOpen = useCallback(
|
|
133
|
+
(newState?: boolean) => {
|
|
134
|
+
virtualFocus.moveFocusToTop();
|
|
135
|
+
setInternalListOpen((oldState) => newState ?? !oldState);
|
|
136
|
+
},
|
|
137
|
+
[virtualFocus]
|
|
138
|
+
);
|
|
122
139
|
|
|
123
140
|
const isValueNew = useMemo(
|
|
124
|
-
() =>
|
|
125
|
-
|
|
141
|
+
() =>
|
|
142
|
+
Boolean(value) &&
|
|
143
|
+
!filteredOptionsMap[filteredOptionsUtils.getOptionId(id, value)],
|
|
144
|
+
[filteredOptionsMap, id, value]
|
|
126
145
|
);
|
|
127
146
|
|
|
128
|
-
const getMinimumIndex = useCallback(() => {
|
|
129
|
-
return isValueNew && allowNewValues ? -1 : 0;
|
|
130
|
-
}, [allowNewValues, isValueNew]);
|
|
131
|
-
|
|
132
147
|
const ariaDescribedBy = useMemo(() => {
|
|
133
148
|
let activeOption;
|
|
134
149
|
if (!isLoading && filteredOptions.length === 0) {
|
|
135
|
-
activeOption =
|
|
150
|
+
activeOption = filteredOptionsUtils.getNoHitsId(id);
|
|
136
151
|
} else if ((value && value !== "") || isLoading) {
|
|
137
152
|
if (shouldAutocomplete && filteredOptions[0]) {
|
|
138
|
-
activeOption =
|
|
153
|
+
activeOption = filteredOptionsUtils.getOptionId(id, filteredOptions[0]);
|
|
139
154
|
} else if (isListOpen && isLoading) {
|
|
140
|
-
activeOption =
|
|
155
|
+
activeOption = filteredOptionsUtils.getIsLoadingId(id);
|
|
141
156
|
}
|
|
142
157
|
}
|
|
143
158
|
return cl(activeOption, partialAriaDescribedBy) || undefined;
|
|
@@ -151,102 +166,21 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
|
|
151
166
|
id,
|
|
152
167
|
]);
|
|
153
168
|
|
|
154
|
-
const currentOption = useMemo(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return value;
|
|
160
|
-
}
|
|
161
|
-
return filteredOptions[filteredOptionsIndex];
|
|
162
|
-
}, [filteredOptionsIndex, filteredOptions, value]);
|
|
163
|
-
|
|
164
|
-
const resetFilteredOptionsIndex = () => {
|
|
165
|
-
setFilteredOptionsIndex(getMinimumIndex());
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const scrollToOption = useCallback((newIndex: number) => {
|
|
169
|
-
if (
|
|
170
|
-
filteredOptionsRef.current &&
|
|
171
|
-
filteredOptionsRef.current.children[newIndex]
|
|
172
|
-
) {
|
|
173
|
-
const child = filteredOptionsRef.current.children[newIndex];
|
|
174
|
-
const { top, bottom } = child.getBoundingClientRect();
|
|
175
|
-
const parentRect = filteredOptionsRef.current.getBoundingClientRect();
|
|
176
|
-
if (top < parentRect.top || bottom > parentRect.bottom) {
|
|
177
|
-
child.scrollIntoView({ block: "nearest" });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}, []);
|
|
181
|
-
|
|
182
|
-
useEffect(() => {
|
|
183
|
-
if (filteredOptionsIndex !== null && isListOpen) {
|
|
184
|
-
scrollToOption(filteredOptionsIndex);
|
|
185
|
-
}
|
|
186
|
-
}, [filteredOptionsIndex, isListOpen, scrollToOption]);
|
|
187
|
-
|
|
188
|
-
const moveFocusToInput = useCallback(() => {
|
|
189
|
-
setFilteredOptionsIndex(null);
|
|
190
|
-
toggleIsListOpen(false);
|
|
191
|
-
}, [toggleIsListOpen]);
|
|
192
|
-
|
|
193
|
-
const moveFocusToEnd = useCallback(() => {
|
|
194
|
-
const lastIndex = filteredOptions.length - 1;
|
|
195
|
-
toggleIsListOpen(true);
|
|
196
|
-
setFilteredOptionsIndex(lastIndex);
|
|
197
|
-
}, [filteredOptions.length, toggleIsListOpen]);
|
|
198
|
-
|
|
199
|
-
const moveFocusUp = useCallback(() => {
|
|
200
|
-
if (filteredOptionsIndex === null) {
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (filteredOptionsIndex === getMinimumIndex()) {
|
|
204
|
-
toggleIsListOpen(false);
|
|
205
|
-
setFilteredOptionsIndex(null);
|
|
206
|
-
} else {
|
|
207
|
-
const newIndex = Math.max(getMinimumIndex(), filteredOptionsIndex - 1);
|
|
208
|
-
setFilteredOptionsIndex(newIndex);
|
|
209
|
-
}
|
|
210
|
-
}, [filteredOptionsIndex, getMinimumIndex, toggleIsListOpen]);
|
|
211
|
-
|
|
212
|
-
const moveFocusDown = useCallback(() => {
|
|
213
|
-
if (filteredOptionsIndex === null || !isListOpen) {
|
|
214
|
-
toggleIsListOpen(true);
|
|
215
|
-
if (allowNewValues || filteredOptions.length >= 1) {
|
|
216
|
-
setFilteredOptionsIndex(getMinimumIndex());
|
|
217
|
-
}
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const newIndex = Math.min(
|
|
221
|
-
filteredOptionsIndex + 1,
|
|
222
|
-
Math.max(getMinimumIndex(), filteredOptions.length - 1)
|
|
223
|
-
);
|
|
224
|
-
setFilteredOptionsIndex(newIndex);
|
|
225
|
-
}, [
|
|
226
|
-
allowNewValues,
|
|
227
|
-
filteredOptions.length,
|
|
228
|
-
filteredOptionsIndex,
|
|
229
|
-
getMinimumIndex,
|
|
230
|
-
isListOpen,
|
|
231
|
-
toggleIsListOpen,
|
|
232
|
-
]);
|
|
169
|
+
const currentOption = useMemo(
|
|
170
|
+
() =>
|
|
171
|
+
filteredOptionsMap[virtualFocus.activeElement?.getAttribute("id") || -1],
|
|
172
|
+
[filteredOptionsMap, virtualFocus]
|
|
173
|
+
);
|
|
233
174
|
|
|
234
|
-
const activeDecendantId = useMemo(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return `${id}-combobox-new-option`;
|
|
239
|
-
} else {
|
|
240
|
-
return `${id}-option-${currentOption?.replace(" ", "-")}`;
|
|
241
|
-
}
|
|
242
|
-
}, [filteredOptionsIndex, currentOption, id]);
|
|
175
|
+
const activeDecendantId = useMemo(
|
|
176
|
+
() => virtualFocus.activeElement?.getAttribute("id") || undefined,
|
|
177
|
+
[virtualFocus.activeElement]
|
|
178
|
+
);
|
|
243
179
|
|
|
244
180
|
const filteredOptionsState = {
|
|
245
181
|
activeDecendantId,
|
|
246
182
|
allowNewValues,
|
|
247
|
-
|
|
248
|
-
filteredOptionsIndex,
|
|
249
|
-
setFilteredOptionsIndex,
|
|
183
|
+
setFilteredOptionsRef,
|
|
250
184
|
shouldAutocomplete,
|
|
251
185
|
isListOpen,
|
|
252
186
|
isLoading,
|
|
@@ -256,11 +190,7 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
|
|
256
190
|
isValueNew,
|
|
257
191
|
toggleIsListOpen,
|
|
258
192
|
currentOption,
|
|
259
|
-
|
|
260
|
-
moveFocusUp,
|
|
261
|
-
moveFocusDown,
|
|
262
|
-
moveFocusToInput,
|
|
263
|
-
moveFocusToEnd,
|
|
193
|
+
virtualFocus,
|
|
264
194
|
ariaDescribedBy,
|
|
265
195
|
};
|
|
266
196
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export type VirtualFocusType = {
|
|
4
|
+
activeElement: HTMLElement | undefined;
|
|
5
|
+
getElementById: (id: string) => HTMLElement | undefined;
|
|
6
|
+
isFocusOnTheTop: boolean;
|
|
7
|
+
isFocusOnTheBottom: boolean;
|
|
8
|
+
setIndex: Dispatch<SetStateAction<number>>;
|
|
9
|
+
moveFocusUp: () => void;
|
|
10
|
+
moveFocusDown: () => void;
|
|
11
|
+
moveFocusToElement: (id: string) => void;
|
|
12
|
+
moveFocusToTop: () => void;
|
|
13
|
+
moveFocusToBottom: () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const useVirtualFocus = (
|
|
17
|
+
containerRef: HTMLElement | null
|
|
18
|
+
): VirtualFocusType => {
|
|
19
|
+
const [index, setIndex] = useState(-1);
|
|
20
|
+
|
|
21
|
+
const listOfAllChildren: Array<HTMLElement> = containerRef?.children
|
|
22
|
+
? Array.prototype.slice.call(containerRef?.children)
|
|
23
|
+
: [];
|
|
24
|
+
const elementsAbleToReceiveFocus = listOfAllChildren.filter(
|
|
25
|
+
(child) => child.getAttribute("data-no-focus") !== "true"
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const activeElement = elementsAbleToReceiveFocus[index];
|
|
29
|
+
const getElementById = (id: string) =>
|
|
30
|
+
listOfAllChildren.find((element) => element.id === id);
|
|
31
|
+
const isFocusOnTheTop = index === 0;
|
|
32
|
+
const isFocusOnTheBottom = index === elementsAbleToReceiveFocus.length - 1;
|
|
33
|
+
|
|
34
|
+
const scrollToOption = (newIndex: number) => {
|
|
35
|
+
const indexOfElementToScrollTo = Math.min(
|
|
36
|
+
Math.max(newIndex, 0),
|
|
37
|
+
containerRef?.children.length || 0
|
|
38
|
+
);
|
|
39
|
+
if (containerRef?.children[indexOfElementToScrollTo]) {
|
|
40
|
+
const child = containerRef.children[indexOfElementToScrollTo];
|
|
41
|
+
const { top, bottom } = child.getBoundingClientRect();
|
|
42
|
+
const parentRect = containerRef.getBoundingClientRect();
|
|
43
|
+
if (top < parentRect.top || bottom > parentRect.bottom) {
|
|
44
|
+
child.scrollIntoView({ block: "nearest" });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const _moveFocusAndScrollTo = (_index: number) => {
|
|
50
|
+
setIndex(_index);
|
|
51
|
+
scrollToOption(_index);
|
|
52
|
+
};
|
|
53
|
+
const moveFocusUp = () => _moveFocusAndScrollTo(Math.max(index - 1, -1));
|
|
54
|
+
const moveFocusDown = () =>
|
|
55
|
+
_moveFocusAndScrollTo(
|
|
56
|
+
Math.min(index + 1, elementsAbleToReceiveFocus.length - 1)
|
|
57
|
+
);
|
|
58
|
+
const moveFocusToTop = () => _moveFocusAndScrollTo(-1);
|
|
59
|
+
const moveFocusToBottom = () =>
|
|
60
|
+
_moveFocusAndScrollTo(elementsAbleToReceiveFocus.length - 1);
|
|
61
|
+
const moveFocusToElement = (id: string) => {
|
|
62
|
+
const thisElement = elementsAbleToReceiveFocus.find(
|
|
63
|
+
(_element) => _element.getAttribute("id") === id
|
|
64
|
+
);
|
|
65
|
+
const indexOfElement = thisElement
|
|
66
|
+
? elementsAbleToReceiveFocus.indexOf(thisElement)
|
|
67
|
+
: -1;
|
|
68
|
+
if (indexOfElement >= 0) {
|
|
69
|
+
setIndex(indexOfElement);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
activeElement,
|
|
75
|
+
getElementById,
|
|
76
|
+
isFocusOnTheTop,
|
|
77
|
+
isFocusOnTheBottom,
|
|
78
|
+
setIndex,
|
|
79
|
+
moveFocusUp,
|
|
80
|
+
moveFocusDown,
|
|
81
|
+
moveFocusToElement,
|
|
82
|
+
moveFocusToTop,
|
|
83
|
+
moveFocusToBottom,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default useVirtualFocus;
|
|
@@ -9,6 +9,7 @@ import cl from "clsx";
|
|
|
9
9
|
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
|
|
10
10
|
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
|
|
11
11
|
import { useInputContext } from "./inputContext";
|
|
12
|
+
import filteredOptionsUtil from "../FilteredOptions/filtered-options-util";
|
|
12
13
|
|
|
13
14
|
interface InputProps
|
|
14
15
|
extends Omit<InputHTMLAttributes<HTMLInputElement>, "value"> {
|
|
@@ -34,15 +35,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
34
35
|
isValueNew,
|
|
35
36
|
toggleIsListOpen,
|
|
36
37
|
isListOpen,
|
|
37
|
-
filteredOptionsIndex,
|
|
38
|
-
moveFocusUp,
|
|
39
|
-
moveFocusDown,
|
|
40
38
|
ariaDescribedBy,
|
|
41
|
-
moveFocusToInput,
|
|
42
|
-
moveFocusToEnd,
|
|
43
|
-
setFilteredOptionsIndex,
|
|
44
39
|
setIsMouseLastUsedInputDevice,
|
|
45
40
|
shouldAutocomplete,
|
|
41
|
+
virtualFocus,
|
|
46
42
|
} = useFilteredOptionsContext();
|
|
47
43
|
|
|
48
44
|
const onEnter = useCallback(
|
|
@@ -57,8 +53,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
57
53
|
event.preventDefault();
|
|
58
54
|
// Selecting a value from the dropdown / FilteredOptions
|
|
59
55
|
toggleOption(currentOption, event);
|
|
60
|
-
if (!isMultiSelect && !isTextInSelectedOptions(currentOption))
|
|
56
|
+
if (!isMultiSelect && !isTextInSelectedOptions(currentOption)) {
|
|
61
57
|
toggleIsListOpen(false);
|
|
58
|
+
}
|
|
62
59
|
} else if (shouldAutocomplete && isTextInSelectedOptions(value)) {
|
|
63
60
|
event.preventDefault();
|
|
64
61
|
// Trying to set the same value that is already set, so just clearing the input
|
|
@@ -104,10 +101,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
104
101
|
onEnter(e);
|
|
105
102
|
break;
|
|
106
103
|
case "Home":
|
|
107
|
-
|
|
104
|
+
virtualFocus.moveFocusToTop();
|
|
108
105
|
break;
|
|
109
106
|
case "End":
|
|
110
|
-
|
|
107
|
+
virtualFocus.moveFocusToBottom();
|
|
111
108
|
break;
|
|
112
109
|
default:
|
|
113
110
|
break;
|
|
@@ -128,14 +125,20 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
128
125
|
// so we don't interfere with text editing
|
|
129
126
|
if (e.target.selectionStart === value?.length) {
|
|
130
127
|
e.preventDefault();
|
|
131
|
-
|
|
128
|
+
if (virtualFocus.activeElement === null || !isListOpen) {
|
|
129
|
+
toggleIsListOpen(true);
|
|
130
|
+
}
|
|
131
|
+
virtualFocus.moveFocusDown();
|
|
132
132
|
}
|
|
133
133
|
} else if (e.key === "ArrowUp") {
|
|
134
134
|
// Check that the FilteredOptions list is open and has virtual focus.
|
|
135
135
|
// Otherwise ignore keystrokes, so it doesn't interfere with text editing
|
|
136
|
-
if (isListOpen &&
|
|
136
|
+
if (isListOpen && activeDecendantId) {
|
|
137
137
|
e.preventDefault();
|
|
138
|
-
|
|
138
|
+
if (virtualFocus.isFocusOnTheTop) {
|
|
139
|
+
toggleIsListOpen(false);
|
|
140
|
+
}
|
|
141
|
+
virtualFocus.moveFocusUp();
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
},
|
|
@@ -143,11 +146,11 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
143
146
|
value,
|
|
144
147
|
selectedOptions,
|
|
145
148
|
removeSelectedOption,
|
|
146
|
-
moveFocusDown,
|
|
147
149
|
isListOpen,
|
|
148
|
-
|
|
149
|
-
moveFocusUp,
|
|
150
|
+
activeDecendantId,
|
|
150
151
|
setIsMouseLastUsedInputDevice,
|
|
152
|
+
toggleIsListOpen,
|
|
153
|
+
virtualFocus,
|
|
151
154
|
]
|
|
152
155
|
);
|
|
153
156
|
|
|
@@ -159,13 +162,14 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
159
162
|
} else if (filteredOptions.length === 0) {
|
|
160
163
|
toggleIsListOpen(false);
|
|
161
164
|
}
|
|
165
|
+
virtualFocus.moveFocusToTop();
|
|
162
166
|
onChange(event);
|
|
163
167
|
},
|
|
164
|
-
[filteredOptions.length, onChange, toggleIsListOpen]
|
|
168
|
+
[filteredOptions.length, virtualFocus, onChange, toggleIsListOpen]
|
|
165
169
|
);
|
|
166
170
|
|
|
167
171
|
const onBlur = () => {
|
|
168
|
-
|
|
172
|
+
virtualFocus.moveFocusToTop();
|
|
169
173
|
};
|
|
170
174
|
|
|
171
175
|
return (
|
|
@@ -180,7 +184,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
180
184
|
onBlur={onBlur}
|
|
181
185
|
onKeyUp={handleKeyUp}
|
|
182
186
|
onKeyDown={handleKeyDown}
|
|
183
|
-
aria-controls={
|
|
187
|
+
aria-controls={filteredOptionsUtil.getFilteredOptionsId(inputProps.id)}
|
|
184
188
|
aria-expanded={!!isListOpen}
|
|
185
189
|
autoComplete="off"
|
|
186
190
|
aria-autocomplete={shouldAutocomplete ? "both" : "list"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useState, useCallback, createContext, useContext } from "react";
|
|
2
2
|
import { useInputContext } from "./Input/inputContext";
|
|
3
|
-
import { useSelectedOptionsContext } from "./SelectedOptions/selectedOptionsContext";
|
|
4
3
|
|
|
5
4
|
type CustomOptionsContextType = {
|
|
6
5
|
customOptions: string[];
|
|
@@ -13,13 +12,19 @@ const CustomOptionsContext = createContext<CustomOptionsContextType>(
|
|
|
13
12
|
{} as CustomOptionsContextType
|
|
14
13
|
);
|
|
15
14
|
|
|
16
|
-
export const CustomOptionsProvider = ({
|
|
15
|
+
export const CustomOptionsProvider = ({
|
|
16
|
+
children,
|
|
17
|
+
value,
|
|
18
|
+
}: {
|
|
19
|
+
children: any;
|
|
20
|
+
value: { isMultiSelect?: boolean };
|
|
21
|
+
}) => {
|
|
17
22
|
const [customOptions, setCustomOptions] = useState<string[]>([]);
|
|
18
23
|
const { focusInput } = useInputContext();
|
|
19
|
-
const { isMultiSelect } =
|
|
24
|
+
const { isMultiSelect } = value;
|
|
20
25
|
|
|
21
26
|
const removeCustomOption = useCallback(
|
|
22
|
-
(option) => {
|
|
27
|
+
(option: string) => {
|
|
23
28
|
setCustomOptions((prevCustomOptions) =>
|
|
24
29
|
prevCustomOptions.filter((o) => o !== option)
|
|
25
30
|
);
|
|
@@ -29,7 +34,7 @@ export const CustomOptionsProvider = ({ children }) => {
|
|
|
29
34
|
);
|
|
30
35
|
|
|
31
36
|
const addCustomOption = useCallback(
|
|
32
|
-
(option) => {
|
|
37
|
+
(option: string) => {
|
|
33
38
|
if (isMultiSelect) {
|
|
34
39
|
setCustomOptions((prevOptions) => [...prevOptions, option]);
|
|
35
40
|
} else {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { InformationIcon } from "@navikt/aksel-icons";
|
|
2
|
+
import { Meta } from "@storybook/react";
|
|
1
3
|
import React from "react";
|
|
2
4
|
import { BodyLong, GuidePanel, VStack } from "../index";
|
|
3
|
-
import { Meta } from "@storybook/react";
|
|
4
|
-
import { InformationIcon } from "@navikt/aksel-icons";
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
title: "ds-react/GuidePanel",
|
package/src/modal/Modal.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import React, {
|
|
|
8
8
|
useRef,
|
|
9
9
|
} from "react";
|
|
10
10
|
import { createPortal } from "react-dom";
|
|
11
|
+
import { DateContext } from "../date/context";
|
|
11
12
|
import { useProvider } from "../provider";
|
|
12
13
|
import { Detail, Heading } from "../typography";
|
|
13
14
|
import { mergeRefs, useId } from "../util";
|
|
@@ -99,7 +100,9 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
|
|
|
99
100
|
const rootElement = useProvider()?.rootElement;
|
|
100
101
|
const portalNode = useFloatingPortalNode({ root: rootElement });
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
const dateContext = useContext(DateContext);
|
|
104
|
+
const modalContext = useContext(ModalContext);
|
|
105
|
+
if (modalContext && !dateContext) {
|
|
103
106
|
console.error("Modals should not be nested");
|
|
104
107
|
}
|
|
105
108
|
|