@navikt/ds-react 5.15.1 → 5.17.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 (61) hide show
  1. package/_docs.json +44 -1
  2. package/cjs/form/combobox/Combobox.js +1 -1
  3. package/cjs/form/combobox/ComboboxProvider.js +2 -1
  4. package/cjs/form/combobox/ComboboxWrapper.js +1 -1
  5. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +59 -41
  6. package/cjs/form/combobox/FilteredOptions/filtered-options-util.js +3 -1
  7. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +15 -3
  8. package/cjs/form/combobox/FilteredOptions/useVirtualFocus.js +52 -32
  9. package/cjs/form/combobox/Input/Input.js +3 -1
  10. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js +3 -1
  11. package/cjs/util/hooks/descendants/descendant.js +117 -0
  12. package/cjs/util/hooks/descendants/useDescendant.js +108 -0
  13. package/cjs/util/hooks/descendants/utils.js +53 -0
  14. package/esm/form/combobox/Combobox.js +1 -1
  15. package/esm/form/combobox/Combobox.js.map +1 -1
  16. package/esm/form/combobox/ComboboxProvider.js +2 -1
  17. package/esm/form/combobox/ComboboxProvider.js.map +1 -1
  18. package/esm/form/combobox/ComboboxWrapper.js +1 -1
  19. package/esm/form/combobox/ComboboxWrapper.js.map +1 -1
  20. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +59 -41
  21. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  22. package/esm/form/combobox/FilteredOptions/filtered-options-util.d.ts +2 -1
  23. package/esm/form/combobox/FilteredOptions/filtered-options-util.js +3 -1
  24. package/esm/form/combobox/FilteredOptions/filtered-options-util.js.map +1 -1
  25. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +15 -3
  26. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  27. package/esm/form/combobox/FilteredOptions/useVirtualFocus.d.ts +2 -4
  28. package/esm/form/combobox/FilteredOptions/useVirtualFocus.js +52 -32
  29. package/esm/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
  30. package/esm/form/combobox/Input/Input.js +3 -1
  31. package/esm/form/combobox/Input/Input.js.map +1 -1
  32. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.d.ts +5 -2
  33. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js +3 -1
  34. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -1
  35. package/esm/form/combobox/types.d.ts +14 -0
  36. package/esm/util/hooks/descendants/descendant.d.ts +47 -0
  37. package/esm/util/hooks/descendants/descendant.js +114 -0
  38. package/esm/util/hooks/descendants/descendant.js.map +1 -0
  39. package/esm/util/hooks/descendants/useDescendant.d.ts +14 -0
  40. package/esm/util/hooks/descendants/useDescendant.js +82 -0
  41. package/esm/util/hooks/descendants/useDescendant.js.map +1 -0
  42. package/esm/util/hooks/descendants/utils.d.ts +12 -0
  43. package/esm/util/hooks/descendants/utils.js +46 -0
  44. package/esm/util/hooks/descendants/utils.js.map +1 -0
  45. package/package.json +3 -3
  46. package/src/form/combobox/Combobox.tsx +1 -1
  47. package/src/form/combobox/ComboboxProvider.tsx +2 -0
  48. package/src/form/combobox/ComboboxWrapper.tsx +0 -1
  49. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +131 -92
  50. package/src/form/combobox/FilteredOptions/filtered-options-util.ts +9 -2
  51. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +22 -3
  52. package/src/form/combobox/FilteredOptions/useVirtualFocus.ts +63 -45
  53. package/src/form/combobox/Input/Input.tsx +3 -1
  54. package/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +11 -1
  55. package/src/form/combobox/combobox.stories.tsx +36 -1
  56. package/src/form/combobox/combobox.test.tsx +1 -3
  57. package/src/form/combobox/types.ts +15 -0
  58. package/src/util/hooks/descendants/descendant.stories.tsx +147 -0
  59. package/src/util/hooks/descendants/descendant.ts +161 -0
  60. package/src/util/hooks/descendants/useDescendant.tsx +111 -0
  61. package/src/util/hooks/descendants/utils.ts +56 -0
package/_docs.json CHANGED
@@ -15085,6 +15085,25 @@
15085
15085
  "name": "string[]"
15086
15086
  }
15087
15087
  },
15088
+ "maxSelected": {
15089
+ "defaultValue": null,
15090
+ "description": "Options for the maximum number of selected options.",
15091
+ "name": "maxSelected",
15092
+ "parent": {
15093
+ "fileName": "react/src/form/combobox/types.ts",
15094
+ "name": "ComboboxProps"
15095
+ },
15096
+ "declarations": [
15097
+ {
15098
+ "fileName": "react/src/form/combobox/types.ts",
15099
+ "name": "ComboboxProps"
15100
+ }
15101
+ ],
15102
+ "required": false,
15103
+ "type": {
15104
+ "name": "MaxSelected"
15105
+ }
15106
+ },
15088
15107
  "shouldAutocomplete": {
15089
15108
  "defaultValue": {
15090
15109
  "value": "false"
@@ -15538,6 +15557,25 @@
15538
15557
  "name": "string[]"
15539
15558
  }
15540
15559
  },
15560
+ "maxSelected": {
15561
+ "defaultValue": null,
15562
+ "description": "Options for the maximum number of selected options.",
15563
+ "name": "maxSelected",
15564
+ "parent": {
15565
+ "fileName": "react/src/form/combobox/types.ts",
15566
+ "name": "ComboboxProps"
15567
+ },
15568
+ "declarations": [
15569
+ {
15570
+ "fileName": "react/src/form/combobox/types.ts",
15571
+ "name": "ComboboxProps"
15572
+ }
15573
+ ],
15574
+ "required": false,
15575
+ "type": {
15576
+ "name": "MaxSelected"
15577
+ }
15578
+ },
15541
15579
  "shouldAutocomplete": {
15542
15580
  "defaultValue": {
15543
15581
  "value": "false"
@@ -19628,7 +19666,7 @@
19628
19666
  ],
19629
19667
  "required": true,
19630
19668
  "type": {
19631
- "name": "Pick<ComboboxProps, \"allowNewValues\" | \"isMultiSelect\" | \"options\" | \"selectedOptions\" | \"onToggleSelected\">"
19669
+ "name": "Pick<ComboboxProps, \"allowNewValues\" | \"isMultiSelect\" | \"options\" | \"selectedOptions\" | \"onToggleSelected\" | \"maxSelected\">"
19632
19670
  }
19633
19671
  }
19634
19672
  }
@@ -19783,5 +19821,10 @@
19783
19821
  }
19784
19822
  }
19785
19823
  }
19824
+ },
19825
+ {
19826
+ "filePath": "src/util/hooks/descendants/useDescendant.tsx",
19827
+ "displayName": "createDescendantContext",
19828
+ "props": {}
19786
19829
  }
19787
19830
  ]
@@ -67,7 +67,7 @@ exports.Combobox = (0, react_1.forwardRef)((props, ref) => {
67
67
  }), id: inputDescriptionId, size: size }, description)),
68
68
  react_1.default.createElement("div", { className: "navds-combobox__wrapper" },
69
69
  react_1.default.createElement("div", { className: (0, clsx_1.default)("navds-combobox__wrapper-inner navds-text-field__input", {
70
- "navds-combobox__wrapper-inner--virtually-unfocused": activeDecendantId !== null,
70
+ "navds-combobox__wrapper-inner--virtually-unfocused": activeDecendantId !== undefined,
71
71
  }), onClick: focusInput },
72
72
  !shouldShowSelectedOptions ? (react_1.default.createElement(Input_1.default, Object.assign({ id: inputProps.id, ref: mergedInputRef, inputClassName: inputClassName }, rest))) : (react_1.default.createElement(SelectedOptions_1.default, { selectedOptions: selectedOptions, size: size },
73
73
  react_1.default.createElement(Input_1.default, Object.assign({ id: inputProps.id, ref: mergedInputRef, inputClassName: inputClassName }, rest)))),
@@ -66,7 +66,7 @@ const customOptionsContext_1 = require("./customOptionsContext");
66
66
  * ```
67
67
  */
68
68
  const ComboboxProvider = (0, react_1.forwardRef)((props, ref) => {
69
- const { allowNewValues = false, children, defaultValue, error, errorId, filteredOptions, id, isListOpen, isLoading = false, isMultiSelect, onToggleSelected, selectedOptions, options, value, onChange, onClear, shouldAutocomplete, size } = props, rest = __rest(props, ["allowNewValues", "children", "defaultValue", "error", "errorId", "filteredOptions", "id", "isListOpen", "isLoading", "isMultiSelect", "onToggleSelected", "selectedOptions", "options", "value", "onChange", "onClear", "shouldAutocomplete", "size"]);
69
+ const { allowNewValues = false, children, defaultValue, error, errorId, filteredOptions, id, isListOpen, isLoading = false, isMultiSelect, onToggleSelected, selectedOptions, maxSelected, options, value, onChange, onClear, shouldAutocomplete, size } = props, rest = __rest(props, ["allowNewValues", "children", "defaultValue", "error", "errorId", "filteredOptions", "id", "isListOpen", "isLoading", "isMultiSelect", "onToggleSelected", "selectedOptions", "maxSelected", "options", "value", "onChange", "onClear", "shouldAutocomplete", "size"]);
70
70
  return (react_1.default.createElement(inputContext_1.InputContextProvider, { value: {
71
71
  defaultValue,
72
72
  error,
@@ -83,6 +83,7 @@ const ComboboxProvider = (0, react_1.forwardRef)((props, ref) => {
83
83
  allowNewValues,
84
84
  isMultiSelect,
85
85
  selectedOptions,
86
+ maxSelected,
86
87
  onToggleSelected,
87
88
  options,
88
89
  } },
@@ -50,6 +50,6 @@ const ComboboxWrapper = ({ children, className, hasError, inputProps, inputSize,
50
50
  "navds-combobox--error": hasError,
51
51
  "navds-combobox--disabled": !!inputProps.disabled,
52
52
  "navds-combobox--focused": hasFocusWithin,
53
- }), onFocus: onFocusInsideWrapper, onBlur: onBlurWrapper, tabIndex: -1 }, children));
53
+ }), onFocus: onFocusInsideWrapper, onBlur: onBlurWrapper }, children));
54
54
  };
55
55
  exports.default = ComboboxWrapper;
@@ -13,50 +13,68 @@ const selectedOptionsContext_1 = require("../SelectedOptions/selectedOptionsCont
13
13
  const filtered_options_util_1 = __importDefault(require("./filtered-options-util"));
14
14
  const filteredOptionsContext_1 = require("./filteredOptionsContext");
15
15
  const FilteredOptions = () => {
16
+ var _a;
16
17
  const { inputProps: { id }, size, value, } = (0, inputContext_1.useInputContext)();
17
18
  const { allowNewValues, isLoading, isListOpen, filteredOptions, setFilteredOptionsRef, isMouseLastUsedInputDevice, setIsMouseLastUsedInputDevice, isValueNew, toggleIsListOpen, activeDecendantId, virtualFocus, } = (0, filteredOptionsContext_1.useFilteredOptionsContext)();
18
- const { isMultiSelect, selectedOptions, toggleOption } = (0, selectedOptionsContext_1.useSelectedOptionsContext)();
19
- return (react_1.default.createElement("ul", { ref: setFilteredOptionsRef, className: (0, clsx_1.default)("navds-combobox__list", {
19
+ const { isMultiSelect, selectedOptions, toggleOption, maxSelected } = (0, selectedOptionsContext_1.useSelectedOptionsContext)();
20
+ const isDisabled = (option) => (maxSelected === null || maxSelected === void 0 ? void 0 : maxSelected.isLimitReached) && !selectedOptions.includes(option);
21
+ const shouldRenderNonSelectables = (maxSelected === null || maxSelected === void 0 ? void 0 : maxSelected.isLimitReached) || // Render maxSelected message
22
+ isLoading || // Render loading message
23
+ (!isLoading && filteredOptions.length === 0); // Render no hits message
24
+ const shouldRenderFilteredOptionsList = (allowNewValues && isValueNew && !(maxSelected === null || maxSelected === void 0 ? void 0 : maxSelected.isLimitReached)) || // Render add new option
25
+ filteredOptions.length > 0; // Render filtered options
26
+ return (react_1.default.createElement("div", { className: (0, clsx_1.default)("navds-combobox__list", {
20
27
  "navds-combobox__list--closed": !isListOpen,
21
28
  "navds-combobox__list--with-hover": isMouseLastUsedInputDevice,
22
- }), id: filtered_options_util_1.default.getFilteredOptionsId(id), role: "listbox", tabIndex: -1 },
23
- isLoading && (react_1.default.createElement("li", { className: "navds-combobox__list-item--loading", role: "option", "aria-selected": false, id: filtered_options_util_1.default.getIsLoadingId(id), "data-no-focus": "true" },
24
- react_1.default.createElement(loader_1.Loader, { "aria-label": "S\u00F8ker..." }))),
25
- isValueNew && allowNewValues && (react_1.default.createElement("li", { tabIndex: -1, onMouseMove: () => {
26
- if (activeDecendantId !== filtered_options_util_1.default.getAddNewOptionId(id)) {
27
- virtualFocus.moveFocusToElement(filtered_options_util_1.default.getAddNewOptionId(id));
28
- setIsMouseLastUsedInputDevice(true);
29
- }
30
- }, onPointerUp: (event) => {
31
- toggleOption(value, event);
32
- if (!isMultiSelect && !selectedOptions.includes(value))
33
- toggleIsListOpen(false);
34
- }, id: filtered_options_util_1.default.getAddNewOptionId(id), className: (0, clsx_1.default)("navds-combobox__list-item__new-option", {
35
- "navds-combobox__list-item__new-option--focus": activeDecendantId === filtered_options_util_1.default.getAddNewOptionId(id),
36
- }), role: "option", "aria-selected": false },
37
- react_1.default.createElement(aksel_icons_1.PlusIcon, { "aria-hidden": true }),
38
- react_1.default.createElement(typography_1.BodyShort, { size: size },
39
- "Legg til",
40
- " ",
41
- react_1.default.createElement(typography_1.Label, { as: "span", size: size },
42
- "\u201C",
43
- value,
44
- "\u201D")))),
45
- !isLoading && filteredOptions.length === 0 && (react_1.default.createElement("li", { className: "navds-combobox__list-item__no-options", role: "option", "aria-selected": false, id: filtered_options_util_1.default.getNoHitsId(id), "data-no-focus": "true" }, "Ingen s\u00F8ketreff")),
46
- filteredOptions.map((option) => (react_1.default.createElement("li", { className: (0, clsx_1.default)("navds-combobox__list-item", {
47
- "navds-combobox__list-item--focus": activeDecendantId === filtered_options_util_1.default.getOptionId(id, option),
48
- "navds-combobox__list-item--selected": selectedOptions.includes(option),
49
- }), id: filtered_options_util_1.default.getOptionId(id, option), key: option, tabIndex: -1, onMouseMove: () => {
50
- if (activeDecendantId !== filtered_options_util_1.default.getOptionId(id, option)) {
51
- virtualFocus.moveFocusToElement(filtered_options_util_1.default.getOptionId(id, option));
52
- setIsMouseLastUsedInputDevice(true);
53
- }
54
- }, onPointerUp: (event) => {
55
- toggleOption(option, event);
56
- if (!isMultiSelect && !selectedOptions.includes(option))
57
- toggleIsListOpen(false);
58
- }, role: "option", "aria-selected": selectedOptions.includes(option) },
59
- react_1.default.createElement(typography_1.BodyShort, { size: size }, option),
60
- selectedOptions.includes(option) && react_1.default.createElement(aksel_icons_1.CheckmarkIcon, null))))));
29
+ }), id: filtered_options_util_1.default.getFilteredOptionsId(id), tabIndex: -1 },
30
+ shouldRenderNonSelectables && (react_1.default.createElement("div", { className: "navds-combobox__list_non-selectables", role: "status" },
31
+ (maxSelected === null || maxSelected === void 0 ? void 0 : maxSelected.isLimitReached) && (react_1.default.createElement("div", { className: "navds-combobox__list-item--max-selected", id: filtered_options_util_1.default.getMaxSelectedOptionsId(id) }, (_a = maxSelected.message) !== null && _a !== void 0 ? _a : `${selectedOptions.length} av ${maxSelected.limit} er valgt.`)),
32
+ isLoading && (react_1.default.createElement("div", { className: "navds-combobox__list-item--loading", id: filtered_options_util_1.default.getIsLoadingId(id) },
33
+ react_1.default.createElement(loader_1.Loader, { title: "S\u00F8ker..." }))),
34
+ !isLoading && filteredOptions.length === 0 && (react_1.default.createElement("div", { className: "navds-combobox__list-item--no-options", id: filtered_options_util_1.default.getNoHitsId(id) }, "Ingen s\u00F8ketreff")))),
35
+ shouldRenderFilteredOptionsList && (react_1.default.createElement("ul", { ref: setFilteredOptionsRef, role: "listbox", className: "navds-combobox__list-options" },
36
+ isValueNew && !(maxSelected === null || maxSelected === void 0 ? void 0 : maxSelected.isLimitReached) && allowNewValues && (react_1.default.createElement("li", { tabIndex: -1, onMouseMove: () => {
37
+ if (activeDecendantId !==
38
+ filtered_options_util_1.default.getAddNewOptionId(id)) {
39
+ virtualFocus.moveFocusToElement(filtered_options_util_1.default.getAddNewOptionId(id));
40
+ setIsMouseLastUsedInputDevice(true);
41
+ }
42
+ }, onPointerUp: (event) => {
43
+ toggleOption(value, event);
44
+ if (!isMultiSelect && !selectedOptions.includes(value))
45
+ toggleIsListOpen(false);
46
+ }, id: filtered_options_util_1.default.getAddNewOptionId(id), className: (0, clsx_1.default)("navds-combobox__list-item navds-combobox__list-item--new-option", {
47
+ "navds-combobox__list-item--new-option--focus": activeDecendantId ===
48
+ filtered_options_util_1.default.getAddNewOptionId(id),
49
+ }), role: "option", "aria-selected": false },
50
+ react_1.default.createElement(aksel_icons_1.PlusIcon, { "aria-hidden": true }),
51
+ react_1.default.createElement(typography_1.BodyShort, { size: size },
52
+ "Legg til",
53
+ " ",
54
+ react_1.default.createElement(typography_1.Label, { as: "span", size: size },
55
+ "\u201C",
56
+ value,
57
+ "\u201D")))),
58
+ filteredOptions.map((option) => (react_1.default.createElement("li", { className: (0, clsx_1.default)("navds-combobox__list-item", {
59
+ "navds-combobox__list-item--focus": activeDecendantId ===
60
+ filtered_options_util_1.default.getOptionId(id, option),
61
+ "navds-combobox__list-item--selected": selectedOptions.includes(option),
62
+ }), "data-no-focus": isDisabled(option) || undefined, id: filtered_options_util_1.default.getOptionId(id, option), key: option, tabIndex: -1, onMouseMove: () => {
63
+ if (activeDecendantId !==
64
+ filtered_options_util_1.default.getOptionId(id, option)) {
65
+ virtualFocus.moveFocusToElement(filtered_options_util_1.default.getOptionId(id, option));
66
+ setIsMouseLastUsedInputDevice(true);
67
+ }
68
+ }, onPointerUp: (event) => {
69
+ if (isDisabled(option)) {
70
+ return;
71
+ }
72
+ toggleOption(option, event);
73
+ if (!isMultiSelect && !selectedOptions.includes(option)) {
74
+ toggleIsListOpen(false);
75
+ }
76
+ }, role: "option", "aria-selected": selectedOptions.includes(option), "aria-disabled": isDisabled(option) || undefined },
77
+ react_1.default.createElement(typography_1.BodyShort, { size: size }, option),
78
+ selectedOptions.includes(option) && react_1.default.createElement(aksel_icons_1.CheckmarkIcon, null))))))));
61
79
  };
62
80
  exports.default = FilteredOptions;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const normalizeText = (text) => typeof text === "string" ? text.toLocaleLowerCase().trim() : "";
4
4
  const isPartOfText = (value, text) => normalizeText(text).startsWith(normalizeText(value !== null && value !== void 0 ? value : ""));
5
5
  const isValueInList = (value, list) => list === null || list === void 0 ? void 0 : list.find((listItem) => normalizeText(value) === normalizeText(listItem));
6
- const getMatchingValuesFromList = (value, list) => list === null || list === void 0 ? void 0 : list.filter((listItem) => isPartOfText(value, listItem));
6
+ const getMatchingValuesFromList = (value, list, alwaysIncluded) => list === null || list === void 0 ? void 0 : list.filter((listItem) => isPartOfText(value, listItem) || alwaysIncluded.includes(listItem));
7
7
  const getFilteredOptionsId = (comboboxId) => `${comboboxId}-filtered-options`;
8
8
  const getOptionId = (comboboxId, option) => `${comboboxId.toLocaleLowerCase()}-option-${option
9
9
  .replace(" ", "-")
@@ -11,6 +11,7 @@ const getOptionId = (comboboxId, option) => `${comboboxId.toLocaleLowerCase()}-o
11
11
  const getAddNewOptionId = (comboboxId) => `${comboboxId}-combobox-new-option`;
12
12
  const getIsLoadingId = (comboboxId) => `${comboboxId}-is-loading`;
13
13
  const getNoHitsId = (comboboxId) => `${comboboxId}-no-hits`;
14
+ const getMaxSelectedOptionsId = (comboboxId) => `${comboboxId}-max-selected-options`;
14
15
  exports.default = {
15
16
  normalizeText,
16
17
  isPartOfText,
@@ -21,4 +22,5 @@ exports.default = {
21
22
  getOptionId,
22
23
  getIsLoadingId,
23
24
  getNoHitsId,
25
+ getMaxSelectedOptionsId,
24
26
  };
@@ -31,6 +31,7 @@ const clsx_1 = __importDefault(require("clsx"));
31
31
  const react_1 = __importStar(require("react"));
32
32
  const hooks_1 = require("../../../util/hooks");
33
33
  const inputContext_1 = require("../Input/inputContext");
34
+ const selectedOptionsContext_1 = require("../SelectedOptions/selectedOptionsContext");
34
35
  const customOptionsContext_1 = require("../customOptionsContext");
35
36
  const filtered_options_util_1 = __importDefault(require("./filtered-options-util"));
36
37
  const useVirtualFocus_1 = __importDefault(require("./useVirtualFocus"));
@@ -40,6 +41,7 @@ const FilteredOptionsProvider = ({ children, value: props, }) => {
40
41
  const [filteredOptionsRef, setFilteredOptionsRef] = (0, react_1.useState)(null);
41
42
  const virtualFocus = (0, useVirtualFocus_1.default)(filteredOptionsRef);
42
43
  const { inputProps: { "aria-describedby": partialAriaDescribedBy, id }, value, searchTerm, setValue, setSearchTerm, shouldAutocomplete, } = (0, inputContext_1.useInputContext)();
44
+ const { selectedOptions, maxSelected } = (0, selectedOptionsContext_1.useSelectedOptionsContext)();
43
45
  const [isInternalListOpen, setInternalListOpen] = (0, react_1.useState)(false);
44
46
  const { customOptions } = (0, customOptionsContext_1.useCustomOptionsContext)();
45
47
  const filteredOptions = (0, react_1.useMemo)(() => {
@@ -47,8 +49,14 @@ const FilteredOptionsProvider = ({ children, value: props, }) => {
47
49
  return externalFilteredOptions;
48
50
  }
49
51
  const opts = [...customOptions, ...options];
50
- return filtered_options_util_1.default.getMatchingValuesFromList(searchTerm, opts);
51
- }, [customOptions, externalFilteredOptions, options, searchTerm]);
52
+ return filtered_options_util_1.default.getMatchingValuesFromList(searchTerm, opts, selectedOptions);
53
+ }, [
54
+ customOptions,
55
+ externalFilteredOptions,
56
+ options,
57
+ searchTerm,
58
+ selectedOptions,
59
+ ]);
52
60
  const previousSearchTerm = (0, hooks_1.usePrevious)(searchTerm);
53
61
  const [isMouseLastUsedInputDevice, setIsMouseLastUsedInputDevice] = (0, react_1.useState)(false);
54
62
  const filteredOptionsMap = (0, react_1.useMemo)(() => options.reduce((map, _option) => (Object.assign(Object.assign({}, map), { [filtered_options_util_1.default.getOptionId(id, _option)]: _option })), {
@@ -94,10 +102,14 @@ const FilteredOptionsProvider = ({ children, value: props, }) => {
94
102
  activeOption = filtered_options_util_1.default.getIsLoadingId(id);
95
103
  }
96
104
  }
97
- return (0, clsx_1.default)(activeOption, partialAriaDescribedBy) || undefined;
105
+ const maybeMaxSelectedOptionsId = (maxSelected === null || maxSelected === void 0 ? void 0 : maxSelected.isLimitReached) &&
106
+ filtered_options_util_1.default.getMaxSelectedOptionsId(id);
107
+ return ((0, clsx_1.default)(activeOption, maybeMaxSelectedOptionsId, partialAriaDescribedBy) ||
108
+ undefined);
98
109
  }, [
99
110
  isListOpen,
100
111
  isLoading,
112
+ maxSelected === null || maxSelected === void 0 ? void 0 : maxSelected.isLimitReached,
101
113
  value,
102
114
  partialAriaDescribedBy,
103
115
  shouldAutocomplete,
@@ -2,41 +2,62 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const react_1 = require("react");
4
4
  const useVirtualFocus = (containerRef) => {
5
- const [index, setIndex] = (0, react_1.useState)(-1);
6
- const listOfAllChildren = (containerRef === null || containerRef === void 0 ? void 0 : containerRef.children)
7
- ? Array.prototype.slice.call(containerRef === null || containerRef === void 0 ? void 0 : containerRef.children)
8
- : [];
9
- const elementsAbleToReceiveFocus = listOfAllChildren.filter((child) => child.getAttribute("data-no-focus") !== "true");
10
- const activeElement = elementsAbleToReceiveFocus[index];
11
- const getElementById = (id) => listOfAllChildren.find((element) => element.id === id);
12
- const isFocusOnTheTop = index === 0;
13
- const isFocusOnTheBottom = index === elementsAbleToReceiveFocus.length - 1;
14
- const scrollToOption = (newIndex) => {
15
- const indexOfElementToScrollTo = Math.min(Math.max(newIndex, 0), (containerRef === null || containerRef === void 0 ? void 0 : containerRef.children.length) || 0);
16
- if (containerRef === null || containerRef === void 0 ? void 0 : containerRef.children[indexOfElementToScrollTo]) {
17
- const child = containerRef.children[indexOfElementToScrollTo];
18
- const { top, bottom } = child.getBoundingClientRect();
19
- const parentRect = containerRef.getBoundingClientRect();
20
- if (top < parentRect.top || bottom > parentRect.bottom) {
21
- child.scrollIntoView({ block: "nearest" });
22
- }
5
+ const [activeElement, setActiveElement] = (0, react_1.useState)(undefined);
6
+ const getListOfAllChildren = () => { var _a; return Array.from((_a = containerRef === null || containerRef === void 0 ? void 0 : containerRef.children) !== null && _a !== void 0 ? _a : []); };
7
+ const getElementsAbleToReceiveFocus = () => getListOfAllChildren().filter((child) => child.getAttribute("data-no-focus") !== "true");
8
+ const getElementById = (id) => getListOfAllChildren().find((element) => element.id === id);
9
+ const isFocusOnTheTop = () => activeElement
10
+ ? getElementsAbleToReceiveFocus().indexOf(activeElement) === 0
11
+ : false;
12
+ const isFocusOnTheBottom = () => {
13
+ const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
14
+ return activeElement
15
+ ? elementsAbleToReceiveFocus.indexOf(activeElement) ===
16
+ elementsAbleToReceiveFocus.length - 1
17
+ : false;
18
+ };
19
+ const _moveFocusAndScrollTo = (_element) => {
20
+ var _a;
21
+ setActiveElement(_element);
22
+ (_a = _element === null || _element === void 0 ? void 0 : _element.scrollIntoView) === null || _a === void 0 ? void 0 : _a.call(_element, { block: "nearest" });
23
+ };
24
+ const moveFocusUp = () => {
25
+ if (!activeElement) {
26
+ return;
27
+ }
28
+ const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
29
+ const _currentIndex = elementsAbleToReceiveFocus.indexOf(activeElement);
30
+ const elementAbove = elementsAbleToReceiveFocus[_currentIndex - 1];
31
+ if (_currentIndex === 0) {
32
+ setActiveElement(undefined);
33
+ }
34
+ else {
35
+ _moveFocusAndScrollTo(elementAbove);
36
+ }
37
+ };
38
+ const moveFocusDown = () => {
39
+ const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
40
+ if (!activeElement) {
41
+ _moveFocusAndScrollTo(elementsAbleToReceiveFocus[0]);
42
+ return;
43
+ }
44
+ const _currentIndex = elementsAbleToReceiveFocus.indexOf(activeElement);
45
+ if (_currentIndex === elementsAbleToReceiveFocus.length - 1) {
46
+ return;
47
+ }
48
+ else {
49
+ _moveFocusAndScrollTo(elementsAbleToReceiveFocus[_currentIndex + 1]);
23
50
  }
24
51
  };
25
- const _moveFocusAndScrollTo = (_index) => {
26
- setIndex(_index);
27
- scrollToOption(_index);
52
+ const moveFocusToTop = () => _moveFocusAndScrollTo(undefined);
53
+ const moveFocusToBottom = () => {
54
+ const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
55
+ return _moveFocusAndScrollTo(elementsAbleToReceiveFocus[elementsAbleToReceiveFocus.length - 1]);
28
56
  };
29
- const moveFocusUp = () => _moveFocusAndScrollTo(Math.max(index - 1, -1));
30
- const moveFocusDown = () => _moveFocusAndScrollTo(Math.min(index + 1, elementsAbleToReceiveFocus.length - 1));
31
- const moveFocusToTop = () => _moveFocusAndScrollTo(-1);
32
- const moveFocusToBottom = () => _moveFocusAndScrollTo(elementsAbleToReceiveFocus.length - 1);
33
57
  const moveFocusToElement = (id) => {
34
- const thisElement = elementsAbleToReceiveFocus.find((_element) => _element.getAttribute("id") === id);
35
- const indexOfElement = thisElement
36
- ? elementsAbleToReceiveFocus.indexOf(thisElement)
37
- : -1;
38
- if (indexOfElement >= 0) {
39
- setIndex(indexOfElement);
58
+ const _element = getElementsAbleToReceiveFocus().find((_focusableElement) => _focusableElement.getAttribute("id") === id);
59
+ if (_element) {
60
+ setActiveElement(_element);
40
61
  }
41
62
  };
42
63
  return {
@@ -44,7 +65,6 @@ const useVirtualFocus = (containerRef) => {
44
65
  getElementById,
45
66
  isFocusOnTheTop,
46
67
  isFocusOnTheBottom,
47
- setIndex,
48
68
  moveFocusUp,
49
69
  moveFocusDown,
50
70
  moveFocusToElement,
@@ -101,9 +101,11 @@ const Input = (0, react_1.forwardRef)((_a, ref) => {
101
101
  onEnter(e);
102
102
  break;
103
103
  case "Home":
104
+ toggleIsListOpen(false);
104
105
  virtualFocus.moveFocusToTop();
105
106
  break;
106
107
  case "End":
108
+ toggleIsListOpen(true);
107
109
  virtualFocus.moveFocusToBottom();
108
110
  break;
109
111
  default:
@@ -134,7 +136,7 @@ const Input = (0, react_1.forwardRef)((_a, ref) => {
134
136
  // Otherwise ignore keystrokes, so it doesn't interfere with text editing
135
137
  if (isListOpen && activeDecendantId) {
136
138
  e.preventDefault();
137
- if (virtualFocus.isFocusOnTheTop) {
139
+ if (virtualFocus.isFocusOnTheTop()) {
138
140
  toggleIsListOpen(false);
139
141
  }
140
142
  virtualFocus.moveFocusUp();
@@ -32,7 +32,7 @@ const SelectedOptionsContext = (0, react_1.createContext)({});
32
32
  const SelectedOptionsProvider = ({ children, value, }) => {
33
33
  const { clearInput, focusInput } = (0, inputContext_1.useInputContext)();
34
34
  const { customOptions, removeCustomOption, addCustomOption, setCustomOptions, } = (0, customOptionsContext_1.useCustomOptionsContext)();
35
- const { allowNewValues, isMultiSelect, selectedOptions: externalSelectedOptions, onToggleSelected, options, } = value;
35
+ const { allowNewValues, isMultiSelect, selectedOptions: externalSelectedOptions, onToggleSelected, options, maxSelected, } = value;
36
36
  const [internalSelectedOptions, setSelectedOptions] = (0, react_1.useState)([]);
37
37
  const selectedOptions = (0, react_1.useMemo)(() => externalSelectedOptions !== null && externalSelectedOptions !== void 0 ? externalSelectedOptions : [...customOptions, ...internalSelectedOptions], [customOptions, externalSelectedOptions, internalSelectedOptions]);
38
38
  const addSelectedOption = (0, react_1.useCallback)((option) => {
@@ -90,6 +90,7 @@ const SelectedOptionsProvider = ({ children, value, }) => {
90
90
  selectedOptions,
91
91
  ]);
92
92
  const prevSelectedOptions = (0, hooks_1.usePrevious)(selectedOptions);
93
+ const isLimitReached = !!(maxSelected === null || maxSelected === void 0 ? void 0 : maxSelected.limit) && selectedOptions.length >= maxSelected.limit;
93
94
  const selectedOptionsState = {
94
95
  addSelectedOption,
95
96
  isMultiSelect,
@@ -98,6 +99,7 @@ const SelectedOptionsProvider = ({ children, value, }) => {
98
99
  selectedOptions,
99
100
  setSelectedOptions,
100
101
  toggleOption,
102
+ maxSelected: maxSelected && Object.assign(Object.assign({}, maxSelected), { isLimitReached }),
101
103
  };
102
104
  return (react_1.default.createElement(SelectedOptionsContext.Provider, { value: selectedOptionsState }, children));
103
105
  };
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DescendantsManager = void 0;
4
+ /**
5
+ * https://github.com/chakra-ui/chakra-ui/tree/5ec0be610b5a69afba01a9c22365155c1b519136/packages/components/descendant
6
+ */
7
+ const utils_1 = require("./utils");
8
+ /**
9
+ * @internal
10
+ *
11
+ * Class to manage descendants and their relative indices in the DOM.
12
+ * It uses `node.compareDocumentPosition(...)` under the hood
13
+ */
14
+ class DescendantsManager {
15
+ constructor() {
16
+ this.descendants = new Map();
17
+ this.register = (nodeOrOptions) => {
18
+ if (nodeOrOptions == null)
19
+ return;
20
+ if ((0, utils_1.isElement)(nodeOrOptions)) {
21
+ return this.registerNode(nodeOrOptions);
22
+ }
23
+ return (node) => {
24
+ this.registerNode(node, nodeOrOptions);
25
+ };
26
+ };
27
+ this.unregister = (node) => {
28
+ this.descendants.delete(node);
29
+ const sorted = (0, utils_1.sortNodes)(Array.from(this.descendants.keys()));
30
+ this.assignIndex(sorted);
31
+ };
32
+ this.destroy = () => {
33
+ this.descendants.clear();
34
+ };
35
+ this.assignIndex = (descendants) => {
36
+ this.descendants.forEach((descendant) => {
37
+ const index = descendants.indexOf(descendant.node);
38
+ descendant.index = index;
39
+ descendant.node.dataset["index"] = descendant.index.toString();
40
+ });
41
+ };
42
+ this.count = () => this.descendants.size;
43
+ this.enabledCount = () => this.enabledValues().length;
44
+ this.values = () => {
45
+ const values = Array.from(this.descendants.values());
46
+ return values.sort((a, b) => a.index - b.index);
47
+ };
48
+ this.enabledValues = () => {
49
+ return this.values().filter((descendant) => !descendant.disabled);
50
+ };
51
+ this.item = (index) => {
52
+ if (this.count() === 0)
53
+ return undefined;
54
+ return this.values()[index];
55
+ };
56
+ this.enabledItem = (index) => {
57
+ if (this.enabledCount() === 0)
58
+ return undefined;
59
+ return this.enabledValues()[index];
60
+ };
61
+ this.first = () => this.item(0);
62
+ this.firstEnabled = () => this.enabledItem(0);
63
+ this.last = () => this.item(this.descendants.size - 1);
64
+ this.lastEnabled = () => {
65
+ const lastIndex = this.enabledValues().length - 1;
66
+ return this.enabledItem(lastIndex);
67
+ };
68
+ this.indexOf = (node) => {
69
+ var _a, _b;
70
+ if (!node)
71
+ return -1;
72
+ return (_b = (_a = this.descendants.get(node)) === null || _a === void 0 ? void 0 : _a.index) !== null && _b !== void 0 ? _b : -1;
73
+ };
74
+ this.enabledIndexOf = (node) => {
75
+ if (node == null)
76
+ return -1;
77
+ return this.enabledValues().findIndex((i) => i.node.isSameNode(node));
78
+ };
79
+ this.next = (index, loop = true) => {
80
+ const next = (0, utils_1.getNextIndex)(index, this.count(), loop);
81
+ return this.item(next);
82
+ };
83
+ this.nextEnabled = (index, loop = true) => {
84
+ const item = this.item(index);
85
+ if (!item)
86
+ return;
87
+ const enabledIndex = this.enabledIndexOf(item.node);
88
+ const nextEnabledIndex = (0, utils_1.getNextIndex)(enabledIndex, this.enabledCount(), loop);
89
+ return this.enabledItem(nextEnabledIndex);
90
+ };
91
+ this.prev = (index, loop = true) => {
92
+ const prev = (0, utils_1.getPrevIndex)(index, this.count() - 1, loop);
93
+ return this.item(prev);
94
+ };
95
+ this.prevEnabled = (index, loop = true) => {
96
+ const item = this.item(index);
97
+ if (!item)
98
+ return;
99
+ const enabledIndex = this.enabledIndexOf(item.node);
100
+ const prevEnabledIndex = (0, utils_1.getPrevIndex)(enabledIndex, this.enabledCount() - 1, loop);
101
+ return this.enabledItem(prevEnabledIndex);
102
+ };
103
+ this.registerNode = (node, options) => {
104
+ if (!node || this.descendants.has(node))
105
+ return;
106
+ const keys = Array.from(this.descendants.keys()).concat(node);
107
+ const sorted = (0, utils_1.sortNodes)(keys);
108
+ if (options === null || options === void 0 ? void 0 : options.disabled) {
109
+ options.disabled = !!options.disabled;
110
+ }
111
+ const descendant = Object.assign({ node, index: -1 }, options);
112
+ this.descendants.set(node, descendant);
113
+ this.assignIndex(sorted);
114
+ };
115
+ }
116
+ }
117
+ exports.DescendantsManager = DescendantsManager;