@neo4j-ndl/react 4.18.12 → 4.19.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/lib/LICENSES.txt +54 -0
- package/lib/NOTICE.txt +8 -0
- package/lib/cjs/_common/use-semi-controlled-state.js +6 -1
- package/lib/cjs/_common/use-semi-controlled-state.js.map +1 -1
- package/lib/cjs/combobox/Combobox.js +1031 -0
- package/lib/cjs/combobox/Combobox.js.map +1 -0
- package/lib/cjs/combobox/combobox-types.js +23 -0
- package/lib/cjs/combobox/combobox-types.js.map +1 -0
- package/lib/cjs/combobox/combobox-utils.js +129 -0
- package/lib/cjs/combobox/combobox-utils.js.map +1 -0
- package/lib/cjs/combobox/index.js +26 -0
- package/lib/cjs/combobox/index.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-action-on-select.story.js +82 -0
- package/lib/cjs/combobox/stories/combobox-action-on-select.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-actions.story.js +78 -0
- package/lib/cjs/combobox/stories/combobox-actions.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-async.story.js +75 -0
- package/lib/cjs/combobox/stories/combobox-async.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-clean.story.js +38 -0
- package/lib/cjs/combobox/stories/combobox-clean.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-creatable.story.js +41 -0
- package/lib/cjs/combobox/stories/combobox-creatable.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-custom-multi.story.js +45 -0
- package/lib/cjs/combobox/stories/combobox-custom-multi.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-custom-single.story.js +44 -0
- package/lib/cjs/combobox/stories/combobox-custom-single.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-custom-trigger.story.js +60 -0
- package/lib/cjs/combobox/stories/combobox-custom-trigger.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-default.story.js +42 -0
- package/lib/cjs/combobox/stories/combobox-default.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-descriptions.story.js +45 -0
- package/lib/cjs/combobox/stories/combobox-descriptions.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-disabled.story.js +34 -0
- package/lib/cjs/combobox/stories/combobox-disabled.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-fluid.story.js +34 -0
- package/lib/cjs/combobox/stories/combobox-fluid.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-groups.story.js +49 -0
- package/lib/cjs/combobox/stories/combobox-groups.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-hidden-label.story.js +33 -0
- package/lib/cjs/combobox/stories/combobox-hidden-label.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-icons.story.js +46 -0
- package/lib/cjs/combobox/stories/combobox-icons.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-in-dialog.story.js +40 -0
- package/lib/cjs/combobox/stories/combobox-in-dialog.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-in-popover-dialog.story.js +40 -0
- package/lib/cjs/combobox/stories/combobox-in-popover-dialog.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-in-popover.story.js +36 -0
- package/lib/cjs/combobox/stories/combobox-in-popover.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-large-data-set.story.js +36 -0
- package/lib/cjs/combobox/stories/combobox-large-data-set.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-max-rows.story.js +42 -0
- package/lib/cjs/combobox/stories/combobox-max-rows.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-multi.story.js +45 -0
- package/lib/cjs/combobox/stories/combobox-multi.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-native-form.story.js +49 -0
- package/lib/cjs/combobox/stories/combobox-native-form.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-select-only.story.js +38 -0
- package/lib/cjs/combobox/stories/combobox-select-only.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-sizes.story.js +33 -0
- package/lib/cjs/combobox/stories/combobox-sizes.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-unlimited-rows.story.js +42 -0
- package/lib/cjs/combobox/stories/combobox-unlimited-rows.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-validation.story.js +33 -0
- package/lib/cjs/combobox/stories/combobox-validation.story.js.map +1 -0
- package/lib/cjs/combobox/stories/index.js +133 -0
- package/lib/cjs/combobox/stories/index.js.map +1 -0
- package/lib/cjs/combobox/use-combobox-multi-value-overflow.js +173 -0
- package/lib/cjs/combobox/use-combobox-multi-value-overflow.js.map +1 -0
- package/lib/cjs/combobox/use-combobox-popover.js +80 -0
- package/lib/cjs/combobox/use-combobox-popover.js.map +1 -0
- package/lib/cjs/date-picker/DatePicker.js.map +1 -1
- package/lib/cjs/dismissible-tag/DismissibleTag.js +1 -1
- package/lib/cjs/dismissible-tag/DismissibleTag.js.map +1 -1
- package/lib/cjs/index.js +1 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/next/tree-view/TreeView.js +1 -1
- package/lib/cjs/next/tree-view/TreeView.js.map +1 -1
- package/lib/cjs/select/Select.js +3 -0
- package/lib/cjs/select/Select.js.map +1 -1
- package/lib/cjs/text-area/TextArea.js.map +1 -1
- package/lib/cjs/text-input/TextInput.js.map +1 -1
- package/lib/cjs/tooltip/Tooltip.js.map +1 -1
- package/lib/cjs/tooltip/stories/tooltip-follow-cursor.story.js +1 -1
- package/lib/cjs/tooltip/stories/tooltip-follow-cursor.story.js.map +1 -1
- package/lib/cjs/tooltip/use-tooltip.js +37 -5
- package/lib/cjs/tooltip/use-tooltip.js.map +1 -1
- package/lib/esm/_common/use-semi-controlled-state.js +6 -1
- package/lib/esm/_common/use-semi-controlled-state.js.map +1 -1
- package/lib/esm/combobox/Combobox.js +1024 -0
- package/lib/esm/combobox/Combobox.js.map +1 -0
- package/lib/esm/combobox/combobox-types.js +22 -0
- package/lib/esm/combobox/combobox-types.js.map +1 -0
- package/lib/esm/combobox/combobox-utils.js +112 -0
- package/lib/esm/combobox/combobox-utils.js.map +1 -0
- package/lib/esm/combobox/index.js +22 -0
- package/lib/esm/combobox/index.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-action-on-select.story.js +80 -0
- package/lib/esm/combobox/stories/combobox-action-on-select.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-actions.story.js +76 -0
- package/lib/esm/combobox/stories/combobox-actions.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-async.story.js +73 -0
- package/lib/esm/combobox/stories/combobox-async.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-clean.story.js +36 -0
- package/lib/esm/combobox/stories/combobox-clean.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-creatable.story.js +39 -0
- package/lib/esm/combobox/stories/combobox-creatable.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-custom-multi.story.js +43 -0
- package/lib/esm/combobox/stories/combobox-custom-multi.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-custom-single.story.js +42 -0
- package/lib/esm/combobox/stories/combobox-custom-single.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-custom-trigger.story.js +58 -0
- package/lib/esm/combobox/stories/combobox-custom-trigger.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-default.story.js +40 -0
- package/lib/esm/combobox/stories/combobox-default.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-descriptions.story.js +43 -0
- package/lib/esm/combobox/stories/combobox-descriptions.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-disabled.story.js +32 -0
- package/lib/esm/combobox/stories/combobox-disabled.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-fluid.story.js +32 -0
- package/lib/esm/combobox/stories/combobox-fluid.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-groups.story.js +47 -0
- package/lib/esm/combobox/stories/combobox-groups.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-hidden-label.story.js +31 -0
- package/lib/esm/combobox/stories/combobox-hidden-label.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-icons.story.js +44 -0
- package/lib/esm/combobox/stories/combobox-icons.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-in-dialog.story.js +38 -0
- package/lib/esm/combobox/stories/combobox-in-dialog.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-in-popover-dialog.story.js +38 -0
- package/lib/esm/combobox/stories/combobox-in-popover-dialog.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-in-popover.story.js +34 -0
- package/lib/esm/combobox/stories/combobox-in-popover.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-large-data-set.story.js +34 -0
- package/lib/esm/combobox/stories/combobox-large-data-set.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-max-rows.story.js +40 -0
- package/lib/esm/combobox/stories/combobox-max-rows.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-multi.story.js +43 -0
- package/lib/esm/combobox/stories/combobox-multi.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-native-form.story.js +47 -0
- package/lib/esm/combobox/stories/combobox-native-form.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-select-only.story.js +36 -0
- package/lib/esm/combobox/stories/combobox-select-only.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-sizes.story.js +31 -0
- package/lib/esm/combobox/stories/combobox-sizes.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-unlimited-rows.story.js +40 -0
- package/lib/esm/combobox/stories/combobox-unlimited-rows.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-validation.story.js +31 -0
- package/lib/esm/combobox/stories/combobox-validation.story.js.map +1 -0
- package/lib/esm/combobox/stories/index.js +100 -0
- package/lib/esm/combobox/stories/index.js.map +1 -0
- package/lib/esm/combobox/use-combobox-multi-value-overflow.js +166 -0
- package/lib/esm/combobox/use-combobox-multi-value-overflow.js.map +1 -0
- package/lib/esm/combobox/use-combobox-popover.js +77 -0
- package/lib/esm/combobox/use-combobox-popover.js.map +1 -0
- package/lib/esm/date-picker/DatePicker.js.map +1 -1
- package/lib/esm/dismissible-tag/DismissibleTag.js +1 -1
- package/lib/esm/dismissible-tag/DismissibleTag.js.map +1 -1
- package/lib/esm/index.js +1 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/next/tree-view/TreeView.js +1 -1
- package/lib/esm/next/tree-view/TreeView.js.map +1 -1
- package/lib/esm/select/Select.js +3 -0
- package/lib/esm/select/Select.js.map +1 -1
- package/lib/esm/text-area/TextArea.js.map +1 -1
- package/lib/esm/text-input/TextInput.js.map +1 -1
- package/lib/esm/tooltip/Tooltip.js.map +1 -1
- package/lib/esm/tooltip/stories/tooltip-follow-cursor.story.js +1 -1
- package/lib/esm/tooltip/stories/tooltip-follow-cursor.story.js.map +1 -1
- package/lib/esm/tooltip/use-tooltip.js +38 -6
- package/lib/esm/tooltip/use-tooltip.js.map +1 -1
- package/lib/types/_common/use-semi-controlled-state.d.ts +5 -1
- package/lib/types/_common/use-semi-controlled-state.d.ts.map +1 -1
- package/lib/types/combobox/Combobox.d.ts +24 -0
- package/lib/types/combobox/Combobox.d.ts.map +1 -0
- package/lib/types/combobox/combobox-types.d.ts +419 -0
- package/lib/types/combobox/combobox-types.d.ts.map +1 -0
- package/lib/types/combobox/combobox-utils.d.ts +81 -0
- package/lib/types/combobox/combobox-utils.d.ts.map +1 -0
- package/lib/types/combobox/index.d.ts +23 -0
- package/lib/types/combobox/index.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-action-on-select.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-action-on-select.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-actions.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-actions.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-async.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-async.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-clean.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-clean.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-creatable.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-creatable.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-custom-multi.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-custom-multi.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-custom-single.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-custom-single.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-custom-trigger.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-custom-trigger.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-default.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-default.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-descriptions.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-descriptions.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-disabled.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-disabled.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-fluid.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-fluid.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-groups.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-groups.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-hidden-label.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-hidden-label.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-icons.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-icons.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-in-dialog.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-in-dialog.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-in-popover-dialog.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-in-popover-dialog.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-in-popover.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-in-popover.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-large-data-set.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-large-data-set.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-max-rows.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-max-rows.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-multi.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-multi.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-native-form.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-native-form.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-select-only.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-select-only.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-sizes.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-sizes.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-unlimited-rows.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-unlimited-rows.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-validation.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-validation.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/index.d.ts +73 -0
- package/lib/types/combobox/stories/index.d.ts.map +1 -0
- package/lib/types/combobox/use-combobox-multi-value-overflow.d.ts +76 -0
- package/lib/types/combobox/use-combobox-multi-value-overflow.d.ts.map +1 -0
- package/lib/types/combobox/use-combobox-popover.d.ts +66 -0
- package/lib/types/combobox/use-combobox-popover.d.ts.map +1 -0
- package/lib/types/date-picker/DatePicker.d.ts.map +1 -1
- package/lib/types/dismissible-tag/DismissibleTag.d.ts.map +1 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/select/Select.d.ts +3 -0
- package/lib/types/select/Select.d.ts.map +1 -1
- package/lib/types/text-input/TextInput.d.ts.map +1 -1
- package/lib/types/tooltip/Tooltip.d.ts +6 -2
- package/lib/types/tooltip/Tooltip.d.ts.map +1 -1
- package/lib/types/tooltip/stories/tooltip-follow-cursor.story.d.ts.map +1 -1
- package/lib/types/tooltip/use-tooltip.d.ts +6 -2
- package/lib/types/tooltip/use-tooltip.d.ts.map +1 -1
- package/package.json +10 -4
- package/skills/ndl-react/SKILL.md +20 -0
- package/skills/ndl-react/components/combobox.md +1487 -0
- package/skills/ndl-react/components/tooltip.md +39 -7
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.Combobox = void 0;
|
|
18
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* Copyright (c) "Neo4j"
|
|
22
|
+
* Neo4j Sweden AB [http://neo4j.com]
|
|
23
|
+
*
|
|
24
|
+
* This file is part of Neo4j.
|
|
25
|
+
*
|
|
26
|
+
* Neo4j is free software: you can redistribute it and/or modify
|
|
27
|
+
* it under the terms of the GNU General Public License as published by
|
|
28
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
29
|
+
* (at your option) any later version.
|
|
30
|
+
*
|
|
31
|
+
* This program is distributed in the hope that it will be useful,
|
|
32
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
33
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
34
|
+
* GNU General Public License for more details.
|
|
35
|
+
*
|
|
36
|
+
* You should have received a copy of the GNU General Public License
|
|
37
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
38
|
+
*/
|
|
39
|
+
const react_1 = require("@floating-ui/react");
|
|
40
|
+
const react_virtual_1 = require("@tanstack/react-virtual");
|
|
41
|
+
const classnames_1 = __importDefault(require("classnames"));
|
|
42
|
+
const react_2 = require("react");
|
|
43
|
+
const use_semi_controlled_state_1 = require("../_common/use-semi-controlled-state");
|
|
44
|
+
const utils_1 = require("../_common/utils");
|
|
45
|
+
const checkbox_1 = require("../checkbox");
|
|
46
|
+
const conditional_wrap_1 = require("../conditional-wrap");
|
|
47
|
+
const dismissible_tag_1 = require("../dismissible-tag");
|
|
48
|
+
const divider_1 = require("../divider");
|
|
49
|
+
const heroIcons_1 = require("../icons/generated/heroIcons");
|
|
50
|
+
const loading_spinner_1 = require("../loading-spinner");
|
|
51
|
+
const read_only_tag_1 = require("../read-only-tag");
|
|
52
|
+
const theme_1 = require("../theme");
|
|
53
|
+
const typography_1 = require("../typography");
|
|
54
|
+
const combobox_utils_1 = require("./combobox-utils");
|
|
55
|
+
const use_combobox_multi_value_overflow_1 = require("./use-combobox-multi-value-overflow");
|
|
56
|
+
const use_combobox_popover_1 = require("./use-combobox-popover");
|
|
57
|
+
const DEFAULT_SEARCH_PLACEHOLDER = 'Search';
|
|
58
|
+
const DEFAULT_PLACEHOLDER = 'Select...';
|
|
59
|
+
const DEFAULT_LOADING_MESSAGE = 'Loading...';
|
|
60
|
+
const NO_ACTIVE_INDEX = -1;
|
|
61
|
+
const OPENING_KEYS = ['ArrowDown', 'ArrowUp', 'Enter', ' '];
|
|
62
|
+
// Above this many rendered rows - options and group headers - the listbox
|
|
63
|
+
// switches to windowed rendering. Below it, the DOM, including the nested
|
|
64
|
+
// group semantics, is exactly what it always was.
|
|
65
|
+
const VIRTUALIZATION_ROW_THRESHOLD = 100;
|
|
66
|
+
// Starting guess only: every virtual row is measured once rendered.
|
|
67
|
+
const VIRTUAL_ROW_ESTIMATED_HEIGHT = 38;
|
|
68
|
+
// With a row limit, at most this many candidate tags per permitted row are
|
|
69
|
+
// rendered and measured; anything past them is overflowed arithmetically. Far
|
|
70
|
+
// more than fit a row even with the narrowest plausible tags.
|
|
71
|
+
const TAG_CANDIDATES_PER_ROW = 50;
|
|
72
|
+
// The counter's title and screen reader text name at most this many overflowed
|
|
73
|
+
// options before summarizing the rest as a count.
|
|
74
|
+
const OVERFLOW_LABEL_LIMIT = 20;
|
|
75
|
+
// The hidden form select mirrors the options so browser autofill can match
|
|
76
|
+
// against them, but past this many the DOM cost outweighs an autofill list
|
|
77
|
+
// nobody scrolls. Above it, only what submission, validation, and reset need
|
|
78
|
+
// is mirrored: the selection and the default value. Matches the limit React
|
|
79
|
+
// Aria's HiddenSelect uses for the same trade-off.
|
|
80
|
+
const FORM_OPTION_MIRROR_LIMIT = 300;
|
|
81
|
+
/**
|
|
82
|
+
* Flattens the grouped option items into display rows for the virtualizer,
|
|
83
|
+
* along with a lookup from an option's flat index - the one behind option ids
|
|
84
|
+
* and the active index - to its display row index. Pinned action rows sit at
|
|
85
|
+
* the end of the flat indices but get no display row, so the lookup simply
|
|
86
|
+
* misses for them.
|
|
87
|
+
*/
|
|
88
|
+
const buildComboboxDisplayRows = (optionItems) => {
|
|
89
|
+
const displayRows = [];
|
|
90
|
+
const flatToDisplayIndex = [];
|
|
91
|
+
const pushOption = (row) => {
|
|
92
|
+
flatToDisplayIndex.push(displayRows.length);
|
|
93
|
+
displayRows.push({ kind: 'option', row });
|
|
94
|
+
};
|
|
95
|
+
optionItems.forEach((item) => {
|
|
96
|
+
if ((0, combobox_utils_1.isOptionGroup)(item)) {
|
|
97
|
+
displayRows.push({ kind: 'group-header', label: item.label });
|
|
98
|
+
item.options.forEach(pushOption);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
pushOption(item);
|
|
102
|
+
});
|
|
103
|
+
return { displayRows, flatToDisplayIndex };
|
|
104
|
+
};
|
|
105
|
+
const Combobox = ({ label, name, formId, autoComplete, triggerRef, hasVisuallyHiddenLabel = false, helpText, errorText, isRequired = false, hasRequiredIndicator = false, options, value, defaultValue, onChange, isMulti = false, isSearchable = true, searchValue, defaultSearchValue, onSearchChange, filterOption, searchPlaceholder = DEFAULT_SEARCH_PLACEHOLDER, isLoading = false, loadingMessage = DEFAULT_LOADING_MESSAGE, isCreatable = false, onCreateOption, createOption = combobox_utils_1.buildCreateOption, formatCreateLabel = combobox_utils_1.formatCreateOptionLabel, isValidNewOption = combobox_utils_1.isValidNewComboboxOption, actions, maxRows = 1, isOpen, onOpenChange, isClearable = false, placeholder = DEFAULT_PLACEHOLDER, isDisabled = false, size = 'medium', isFluid = false, isClean = false, isPortaled, portalTarget, floatingStrategy, renderTrigger, components = {}, className, style, htmlAttributes, ref, }) => {
|
|
106
|
+
var _a;
|
|
107
|
+
const baseId = (0, react_2.useId)();
|
|
108
|
+
const listboxId = `${baseId}-listbox`;
|
|
109
|
+
const panelId = `${baseId}-panel`;
|
|
110
|
+
const triggerId = `${baseId}-trigger`;
|
|
111
|
+
const labelId = `${baseId}-label`;
|
|
112
|
+
const helpTextId = `${baseId}-help-text`;
|
|
113
|
+
const errorTextId = `${baseId}-error-text`;
|
|
114
|
+
const initialValue = (0, combobox_utils_1.getInitialValue)(value, defaultValue, isMulti);
|
|
115
|
+
const [selectedValue, setSelectedValue, resetSelectedValue] = (0, use_semi_controlled_state_1.useSemicontrolledState)({
|
|
116
|
+
isControlled: value !== undefined,
|
|
117
|
+
onChange: (nextValue) => {
|
|
118
|
+
if (isMulti) {
|
|
119
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(Array.isArray(nextValue) ? nextValue : []);
|
|
120
|
+
}
|
|
121
|
+
else if (!Array.isArray(nextValue)) {
|
|
122
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(nextValue);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
state: initialValue,
|
|
126
|
+
});
|
|
127
|
+
const [currentSearchValue, setCurrentSearchValue, resetSearchValue] = (0, use_semi_controlled_state_1.useSemicontrolledState)({
|
|
128
|
+
isControlled: searchValue !== undefined,
|
|
129
|
+
onChange: onSearchChange,
|
|
130
|
+
state: (_a = searchValue !== null && searchValue !== void 0 ? searchValue : defaultSearchValue) !== null && _a !== void 0 ? _a : '',
|
|
131
|
+
});
|
|
132
|
+
const [isPanelOpen, setIsPanelOpen, resetPanelOpen] = (0, use_semi_controlled_state_1.useSemicontrolledState)({
|
|
133
|
+
isControlled: isOpen !== undefined,
|
|
134
|
+
onChange: onOpenChange,
|
|
135
|
+
state: isOpen !== null && isOpen !== void 0 ? isOpen : false,
|
|
136
|
+
});
|
|
137
|
+
const { themeClassName } = (0, theme_1.useNeedleTheme)();
|
|
138
|
+
const internalTriggerRef = (0, react_2.useRef)(null);
|
|
139
|
+
const searchInputRef = (0, react_2.useRef)(null);
|
|
140
|
+
const { context, floatingStyles, getFloatingProps, getReferenceProps, isPortaled: isPanelPortaled, refs, } = (0, use_combobox_popover_1.useComboboxPopover)({
|
|
141
|
+
floatingStrategy,
|
|
142
|
+
isDisabled,
|
|
143
|
+
isOpen: isPanelOpen,
|
|
144
|
+
isPortaled,
|
|
145
|
+
onOpenChange: setIsPanelOpen,
|
|
146
|
+
});
|
|
147
|
+
const hasCustomTrigger = renderTrigger !== undefined;
|
|
148
|
+
const builtInTriggerRef = (0, react_1.useMergeRefs)([internalTriggerRef, triggerRef]);
|
|
149
|
+
const customTriggerRef = (0, react_1.useMergeRefs)([
|
|
150
|
+
refs.setReference,
|
|
151
|
+
internalTriggerRef,
|
|
152
|
+
triggerRef,
|
|
153
|
+
]);
|
|
154
|
+
// Creating builds an option out of the search value, so there is nothing to
|
|
155
|
+
// create from without a search input.
|
|
156
|
+
const canCreate = isCreatable && isSearchable;
|
|
157
|
+
(0, react_2.useEffect)(() => {
|
|
158
|
+
if (isCreatable && !isSearchable) {
|
|
159
|
+
(0, utils_1.needleWarningMessage)('A creatable Combobox also needs isSearchable, since a new option is built from the search value.');
|
|
160
|
+
}
|
|
161
|
+
}, [isCreatable, isSearchable]);
|
|
162
|
+
const selectedOptions = getSelectedOptions(selectedValue);
|
|
163
|
+
const resetValue = (0, combobox_utils_1.getInitialValue)(undefined, defaultValue, isMulti);
|
|
164
|
+
const resetSelectedOptions = getSelectedOptions(resetValue);
|
|
165
|
+
const selectedValuesKey = selectedOptions
|
|
166
|
+
.map((option) => option.value)
|
|
167
|
+
.join('\u0000');
|
|
168
|
+
// Selection checks run once per rendered option, so with large lists they
|
|
169
|
+
// must not scan the selection array each time.
|
|
170
|
+
const selectedValuesSet = (0, react_2.useMemo)(() => new Set(getSelectedOptions(selectedValue).map(({ value }) => value)), [selectedValue]);
|
|
171
|
+
const allOptions = (0, react_2.useMemo)(() => (0, combobox_utils_1.flattenComboboxOptions)(options), [options]);
|
|
172
|
+
const [nativeValidationMessage, setNativeValidationMessage] = (0, react_2.useState)(undefined);
|
|
173
|
+
const formOptions = (0, react_2.useMemo)(() => {
|
|
174
|
+
const seenValues = new Set();
|
|
175
|
+
// The default value options are mirrored even above the limit, since the
|
|
176
|
+
// native select can only reset to options it renders.
|
|
177
|
+
const mirroredOptions = allOptions.length > FORM_OPTION_MIRROR_LIMIT
|
|
178
|
+
? [
|
|
179
|
+
...getSelectedOptions(selectedValue),
|
|
180
|
+
...getSelectedOptions((0, combobox_utils_1.getInitialValue)(undefined, defaultValue, isMulti)),
|
|
181
|
+
]
|
|
182
|
+
: [...allOptions, ...getSelectedOptions(selectedValue)];
|
|
183
|
+
return mirroredOptions.filter((option) => {
|
|
184
|
+
if (seenValues.has(option.value)) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
seenValues.add(option.value);
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
}, [allOptions, selectedValue, defaultValue, isMulti]);
|
|
191
|
+
(0, react_2.useEffect)(() => {
|
|
192
|
+
setNativeValidationMessage(undefined);
|
|
193
|
+
}, [selectedValuesKey]);
|
|
194
|
+
const resetFromForm = () => {
|
|
195
|
+
resetSelectedValue(resetValue);
|
|
196
|
+
resetSearchValue(defaultSearchValue !== null && defaultSearchValue !== void 0 ? defaultSearchValue : '');
|
|
197
|
+
resetPanelOpen(false);
|
|
198
|
+
setNativeValidationMessage(undefined);
|
|
199
|
+
};
|
|
200
|
+
const filteredOptions = (0, react_2.useMemo)(() => isSearchable
|
|
201
|
+
? (0, combobox_utils_1.filterComboboxOptions)(options, currentSearchValue, filterOption)
|
|
202
|
+
: options, [isSearchable, options, currentSearchValue, filterOption]);
|
|
203
|
+
// Created options are never added to `options`, so the selection is offered
|
|
204
|
+
// for deduplication as well.
|
|
205
|
+
const createRow = canCreate &&
|
|
206
|
+
isValidNewOption(currentSearchValue, [...allOptions, ...selectedOptions])
|
|
207
|
+
? (0, combobox_utils_1.asCreateOption)(createOption(currentSearchValue))
|
|
208
|
+
: undefined;
|
|
209
|
+
// The create row is appended after any groups rather than inside one, since it
|
|
210
|
+
// belongs to the list as a whole.
|
|
211
|
+
const optionItems = createRow === undefined ? filteredOptions : [...filteredOptions, createRow];
|
|
212
|
+
// Actions are not data, so they survive filtering and close the list off at
|
|
213
|
+
// the end, after anything that selects.
|
|
214
|
+
const actionRows = (actions !== null && actions !== void 0 ? actions : []).map(combobox_utils_1.asActionRow);
|
|
215
|
+
const flatOptions = [
|
|
216
|
+
...(0, combobox_utils_1.flattenComboboxOptions)(optionItems),
|
|
217
|
+
...actionRows,
|
|
218
|
+
];
|
|
219
|
+
const optionsKey = flatOptions.map((option) => option.value).join('\u0000');
|
|
220
|
+
// Ids and hover both need a row's index; one O(n) map build per render
|
|
221
|
+
// replaces an O(n) scan per rendered row.
|
|
222
|
+
const rowIndexes = new Map(flatOptions.map((row, index) => [row, index]));
|
|
223
|
+
const getOptionId = (row) => { var _a; return `${baseId}-option-${(_a = rowIndexes.get(row)) !== null && _a !== void 0 ? _a : NO_ACTIVE_INDEX}`; };
|
|
224
|
+
const groupHeaderCount = optionItems.reduce((count, item) => ((0, combobox_utils_1.isOptionGroup)(item) ? count + 1 : count), 0);
|
|
225
|
+
// Only the options listbox is windowed; the pinned action rows below it
|
|
226
|
+
// are always rendered, so they do not count towards the threshold.
|
|
227
|
+
const displayRowCount = flatOptions.length - actionRows.length + groupHeaderCount;
|
|
228
|
+
const isListVirtualized = displayRowCount > VIRTUALIZATION_ROW_THRESHOLD;
|
|
229
|
+
const virtualRows = isListVirtualized
|
|
230
|
+
? buildComboboxDisplayRows(optionItems)
|
|
231
|
+
: undefined;
|
|
232
|
+
// Registered by the virtualized listbox while it is mounted, since scrolling
|
|
233
|
+
// a windowed list works by index rather than by element.
|
|
234
|
+
const scrollToActiveRowRef = (0, react_2.useRef)(null);
|
|
235
|
+
// No option is active until the user navigates or hovers, so nothing is
|
|
236
|
+
// highlighted and Enter is inert before then.
|
|
237
|
+
const [activeIndex, setActiveIndex] = (0, react_2.useState)(NO_ACTIVE_INDEX);
|
|
238
|
+
const [renderedOptionsKey, setRenderedOptionsKey] = (0, react_2.useState)(optionsKey);
|
|
239
|
+
const wasActivatedByPointerRef = (0, react_2.useRef)(false);
|
|
240
|
+
// Indices change meaning when the list is filtered, so a stale one would
|
|
241
|
+
// point at the wrong option.
|
|
242
|
+
if (renderedOptionsKey !== optionsKey) {
|
|
243
|
+
setRenderedOptionsKey(optionsKey);
|
|
244
|
+
setActiveIndex(NO_ACTIVE_INDEX);
|
|
245
|
+
}
|
|
246
|
+
const activeOption = flatOptions[activeIndex];
|
|
247
|
+
const activeOptionId = activeOption === undefined ? undefined : getOptionId(activeOption);
|
|
248
|
+
// The listbox scrolls, so keep the virtually focused option visible.
|
|
249
|
+
// Pointer activation is skipped, otherwise hovering a partially visible
|
|
250
|
+
// option would nudge the list out from under the cursor. Pinned action rows
|
|
251
|
+
// sit outside the scrolling listbox and are always visible, so scrolling
|
|
252
|
+
// for them is a harmless no-op in both paths.
|
|
253
|
+
(0, react_2.useEffect)(() => {
|
|
254
|
+
var _a;
|
|
255
|
+
if (wasActivatedByPointerRef.current) {
|
|
256
|
+
wasActivatedByPointerRef.current = false;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (activeOptionId === undefined) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// A windowed listbox scrolls by index, since the active option's element
|
|
263
|
+
// may not be mounted yet. Scrolling is also what mounts it, keeping
|
|
264
|
+
// aria-activedescendant resolvable.
|
|
265
|
+
if (scrollToActiveRowRef.current !== null) {
|
|
266
|
+
scrollToActiveRowRef.current(activeIndex);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
(_a = document
|
|
270
|
+
.getElementById(activeOptionId)) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: 'nearest' });
|
|
271
|
+
}, [activeOptionId, activeIndex]);
|
|
272
|
+
// Reopening the panel starts with nothing highlighted, the same as the first
|
|
273
|
+
// open, so a stale active option can never be one Enter away from selection.
|
|
274
|
+
(0, react_2.useEffect)(() => {
|
|
275
|
+
if (!isPanelOpen) {
|
|
276
|
+
setActiveIndex(NO_ACTIVE_INDEX);
|
|
277
|
+
}
|
|
278
|
+
}, [isPanelOpen]);
|
|
279
|
+
const isOptionSelected = (option) => selectedValuesSet.has(option.value);
|
|
280
|
+
const selectOption = (option) => {
|
|
281
|
+
if (isMulti) {
|
|
282
|
+
const currentValue = Array.isArray(selectedValue) ? selectedValue : [];
|
|
283
|
+
const isSelected = selectedValuesSet.has(option.value);
|
|
284
|
+
const nextValue = isSelected
|
|
285
|
+
? currentValue.filter((selected) => selected.value !== option.value)
|
|
286
|
+
: [...currentValue, option];
|
|
287
|
+
setSelectedValue(nextValue);
|
|
288
|
+
// Multi select keeps the panel open so several options can be picked in
|
|
289
|
+
// one pass, matching Select.
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (Array.isArray(selectedValue) || (selectedValue === null || selectedValue === void 0 ? void 0 : selectedValue.value) !== option.value) {
|
|
293
|
+
setSelectedValue(option);
|
|
294
|
+
}
|
|
295
|
+
setIsPanelOpen(false);
|
|
296
|
+
};
|
|
297
|
+
const createFromSearch = () => {
|
|
298
|
+
const created = createOption(currentSearchValue);
|
|
299
|
+
selectOption(created);
|
|
300
|
+
onCreateOption === null || onCreateOption === void 0 ? void 0 : onCreateOption(created);
|
|
301
|
+
// Multi select stays open, so the query is reset to leave the full list
|
|
302
|
+
// usable for the next pick.
|
|
303
|
+
setCurrentSearchValue('');
|
|
304
|
+
};
|
|
305
|
+
const handleOptionChange = (row) => {
|
|
306
|
+
if (isDisabled || row.isDisabled) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// An action leaves the selection alone, so it only runs and closes.
|
|
310
|
+
if ((0, combobox_utils_1.isActionRow)(row)) {
|
|
311
|
+
(0, combobox_utils_1.getAction)(row).onSelect({ searchValue: currentSearchValue });
|
|
312
|
+
setIsPanelOpen(false);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if ((0, combobox_utils_1.isCreateOption)(row)) {
|
|
316
|
+
createFromSearch();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
selectOption(row);
|
|
320
|
+
};
|
|
321
|
+
const clearSelection = () => {
|
|
322
|
+
if (isDisabled) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
setSelectedValue(isMulti ? [] : null);
|
|
326
|
+
};
|
|
327
|
+
const removeOption = (option) => {
|
|
328
|
+
if (isDisabled) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (!isMulti) {
|
|
332
|
+
clearSelection();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
setSelectedValue(selectedOptions.filter((selected) => selected.value !== option.value));
|
|
336
|
+
};
|
|
337
|
+
// Hover and the keyboard share one active option, so exactly one option is
|
|
338
|
+
// ever highlighted. Unlike the keyboard, hover skips disabled options: a
|
|
339
|
+
// pointer user sees the disabled state directly, so there is no
|
|
340
|
+
// discoverability to preserve.
|
|
341
|
+
const handleOptionHover = (row) => {
|
|
342
|
+
var _a;
|
|
343
|
+
if (row.isDisabled === true) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const index = (_a = rowIndexes.get(row)) !== null && _a !== void 0 ? _a : NO_ACTIVE_INDEX;
|
|
347
|
+
if (index === activeIndex) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
wasActivatedByPointerRef.current = true;
|
|
351
|
+
setActiveIndex(index);
|
|
352
|
+
};
|
|
353
|
+
// Disabled options are included so they stay discoverable to screen reader
|
|
354
|
+
// users, per the APG guidance on disabled controls in composite widgets.
|
|
355
|
+
// Activating one is a no-op.
|
|
356
|
+
const moveActiveIndex = (direction) => {
|
|
357
|
+
if (activeIndex === NO_ACTIVE_INDEX) {
|
|
358
|
+
setActiveIndex(direction === 1
|
|
359
|
+
? (0, combobox_utils_1.getFirstOptionIndex)(flatOptions)
|
|
360
|
+
: (0, combobox_utils_1.getLastOptionIndex)(flatOptions));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
setActiveIndex((0, combobox_utils_1.getNextOptionIndex)(flatOptions, activeIndex, direction));
|
|
364
|
+
};
|
|
365
|
+
/**
|
|
366
|
+
* Shared by the search input and the select-only trigger, since both drive
|
|
367
|
+
* the same virtual focus. Returns whether the key was consumed.
|
|
368
|
+
*/
|
|
369
|
+
const handleNavigationKeyDown = (event) => {
|
|
370
|
+
switch (event.key) {
|
|
371
|
+
case 'ArrowDown':
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
moveActiveIndex(1);
|
|
374
|
+
return true;
|
|
375
|
+
case 'ArrowUp':
|
|
376
|
+
event.preventDefault();
|
|
377
|
+
moveActiveIndex(-1);
|
|
378
|
+
return true;
|
|
379
|
+
case 'Home':
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
setActiveIndex((0, combobox_utils_1.getFirstOptionIndex)(flatOptions));
|
|
382
|
+
return true;
|
|
383
|
+
case 'End':
|
|
384
|
+
event.preventDefault();
|
|
385
|
+
setActiveIndex((0, combobox_utils_1.getLastOptionIndex)(flatOptions));
|
|
386
|
+
return true;
|
|
387
|
+
case 'Enter':
|
|
388
|
+
if (activeOption === undefined) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
event.preventDefault();
|
|
392
|
+
handleOptionChange(activeOption);
|
|
393
|
+
return true;
|
|
394
|
+
default:
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const handleSearchKeyDown = (event) => {
|
|
399
|
+
// Home and End belong to the text cursor while typing.
|
|
400
|
+
if (event.key === 'Home' || event.key === 'End') {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (handleNavigationKeyDown(event)) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (event.key === 'Escape') {
|
|
407
|
+
// Escape belongs to this combobox: close immediately and keep it from
|
|
408
|
+
// dismissing an enclosing dialog, popover, or other composite widget.
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
event.stopPropagation();
|
|
411
|
+
if (currentSearchValue !== '') {
|
|
412
|
+
setCurrentSearchValue('');
|
|
413
|
+
}
|
|
414
|
+
setIsPanelOpen(false);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const handleTriggerKeyDown = (event) => {
|
|
418
|
+
if (isDisabled) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (!isPanelOpen) {
|
|
422
|
+
if (!OPENING_KEYS.includes(event.key)) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Preventing the default keeps the button from synthesizing a click,
|
|
426
|
+
// which would let useClick toggle the panel straight back shut.
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
setIsPanelOpen(true);
|
|
429
|
+
if (!isSearchable && event.key === 'ArrowDown') {
|
|
430
|
+
setActiveIndex((0, combobox_utils_1.getFirstOptionIndex)(flatOptions));
|
|
431
|
+
}
|
|
432
|
+
else if (!isSearchable && event.key === 'ArrowUp') {
|
|
433
|
+
setActiveIndex((0, combobox_utils_1.getLastOptionIndex)(flatOptions));
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (event.key === 'Escape') {
|
|
438
|
+
event.preventDefault();
|
|
439
|
+
event.stopPropagation();
|
|
440
|
+
setIsPanelOpen(false);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
// A searchable panel holds DOM focus in its search input, so the trigger
|
|
444
|
+
// only ever sees keys while the panel is closed.
|
|
445
|
+
if (isSearchable) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (event.key === ' ' && activeOption !== undefined) {
|
|
449
|
+
event.preventDefault();
|
|
450
|
+
handleOptionChange(activeOption);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (handleNavigationKeyDown(event)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
457
|
+
event.preventDefault();
|
|
458
|
+
setIsPanelOpen(false);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
// Actions are rows of the listbox but not results, so they keep the list
|
|
462
|
+
// rendered without hiding the message that nothing matched.
|
|
463
|
+
const hasRows = flatOptions.length > 0;
|
|
464
|
+
const hasOptionRows = flatOptions.length > actionRows.length;
|
|
465
|
+
// The pinned action rows live outside the scrolling listbox, so the listbox
|
|
466
|
+
// claims them through aria-owns to keep them its logical children.
|
|
467
|
+
const actionOwnsIds = actionRows.length > 0
|
|
468
|
+
? actionRows.map((row) => getOptionId(row)).join(' ')
|
|
469
|
+
: undefined;
|
|
470
|
+
const isDialogPanel = isSearchable;
|
|
471
|
+
// An error replaces the help text, so only one message is ever rendered and
|
|
472
|
+
// only that one is described.
|
|
473
|
+
const displayedErrorText = errorText !== null && errorText !== void 0 ? errorText : nativeValidationMessage;
|
|
474
|
+
const hasError = Boolean(displayedErrorText);
|
|
475
|
+
const hasHelpText = Boolean(helpText) && !hasError;
|
|
476
|
+
const messageId = hasError ? errorTextId : helpTextId;
|
|
477
|
+
const describedById = hasError || hasHelpText ? messageId : undefined;
|
|
478
|
+
const triggerAria = {
|
|
479
|
+
'aria-activedescendant': isPanelOpen && !isSearchable ? activeOptionId : undefined,
|
|
480
|
+
'aria-controls': getTriggerControlsId({
|
|
481
|
+
hasRows,
|
|
482
|
+
isDialogPanel,
|
|
483
|
+
isPanelOpen,
|
|
484
|
+
listboxId,
|
|
485
|
+
panelId,
|
|
486
|
+
}),
|
|
487
|
+
'aria-describedby': describedById,
|
|
488
|
+
'aria-disabled': isDisabled ? true : undefined,
|
|
489
|
+
'aria-expanded': isPanelOpen,
|
|
490
|
+
'aria-haspopup': isDialogPanel ? 'dialog' : 'listbox',
|
|
491
|
+
'aria-invalid': hasError ? true : undefined,
|
|
492
|
+
'aria-required': isRequired ? true : undefined,
|
|
493
|
+
// The label points at the trigger through `htmlFor`, so clicking it moves
|
|
494
|
+
// focus to the trigger.
|
|
495
|
+
id: triggerId,
|
|
496
|
+
onKeyDown: handleTriggerKeyDown,
|
|
497
|
+
role: 'combobox',
|
|
498
|
+
};
|
|
499
|
+
// A custom trigger becomes the anchor itself, so it gets the reference props
|
|
500
|
+
// and the reference ref. The built-in trigger leaves both to its wrapper,
|
|
501
|
+
// which is what the panel should size to, and takes only the ARIA.
|
|
502
|
+
//
|
|
503
|
+
// It is also named by the label text itself rather than by a reference to it,
|
|
504
|
+
// since a custom trigger is free to leave the label unrendered.
|
|
505
|
+
const triggerProps = Object.assign(Object.assign(Object.assign({}, getReferenceProps()), triggerAria), { 'aria-label': label, ref: customTriggerRef });
|
|
506
|
+
const builtInTriggerProps = Object.assign(Object.assign({}, triggerAria), {
|
|
507
|
+
// The label row also holds the required indicator, so only the label text
|
|
508
|
+
// is referenced, keeping "Optional" out of the accessible name.
|
|
509
|
+
'aria-labelledby': labelId, ref: builtInTriggerRef });
|
|
510
|
+
const field = {
|
|
511
|
+
errorText: displayedErrorText,
|
|
512
|
+
errorTextId,
|
|
513
|
+
hasError,
|
|
514
|
+
hasRequiredIndicator,
|
|
515
|
+
hasVisuallyHiddenLabel,
|
|
516
|
+
helpText,
|
|
517
|
+
helpTextId,
|
|
518
|
+
isRequired,
|
|
519
|
+
label,
|
|
520
|
+
labelId,
|
|
521
|
+
triggerId,
|
|
522
|
+
};
|
|
523
|
+
const triggerState = {
|
|
524
|
+
clear: clearSelection,
|
|
525
|
+
field,
|
|
526
|
+
isDisabled,
|
|
527
|
+
isMulti,
|
|
528
|
+
isOpen: isPanelOpen,
|
|
529
|
+
remove: removeOption,
|
|
530
|
+
selectedOptions,
|
|
531
|
+
};
|
|
532
|
+
// While windowed, the DOM no longer holds every option, so each one carries
|
|
533
|
+
// its position in the full list for assistive technology.
|
|
534
|
+
const getSetPositionProps = (row) => {
|
|
535
|
+
var _a;
|
|
536
|
+
return isListVirtualized
|
|
537
|
+
? {
|
|
538
|
+
posInSet: ((_a = rowIndexes.get(row)) !== null && _a !== void 0 ? _a : 0) + 1,
|
|
539
|
+
setSize: flatOptions.length,
|
|
540
|
+
}
|
|
541
|
+
: {};
|
|
542
|
+
};
|
|
543
|
+
const renderOption = (option) => {
|
|
544
|
+
var _a;
|
|
545
|
+
const sharedProps = Object.assign({ id: getOptionId(option), isFocused: option === activeOption, onClick: () => handleOptionChange(option), onHover: () => handleOptionHover(option) }, getSetPositionProps(option));
|
|
546
|
+
// The create row is never selected and shows the create text rather than
|
|
547
|
+
// the label of the option it would produce.
|
|
548
|
+
if ((0, combobox_utils_1.isCreateOption)(option)) {
|
|
549
|
+
return ((0, jsx_runtime_1.jsx)(Option, Object.assign({}, sharedProps, { isCreate: true, isSelected: false, children: (0, jsx_runtime_1.jsx)(CreateOptionContent, { children: formatCreateLabel(currentSearchValue) }) }), "ndl-combobox-create"));
|
|
550
|
+
}
|
|
551
|
+
const { OptionLabel } = components;
|
|
552
|
+
const isSelected = isOptionSelected(option);
|
|
553
|
+
return ((0, jsx_runtime_1.jsx)(Option, Object.assign({}, sharedProps, { isSelected: isSelected, isDisabled: option.isDisabled, children: (0, jsx_runtime_1.jsx)(OptionContent, { label: option.label, customLabel: OptionLabel === undefined ? undefined : ((0, jsx_runtime_1.jsx)(OptionLabel, { option: option, isSelected: isSelected, isDisabled: (_a = option.isDisabled) !== null && _a !== void 0 ? _a : false, isFocused: option === activeOption, isMulti: isMulti, size: size })), description: option.description, leadingVisual: option.leadingVisual, isDisabled: option.isDisabled, isSelected: isSelected, isMulti: isMulti }) }), option.value));
|
|
554
|
+
};
|
|
555
|
+
// An action row shares the chrome of an option but never the selection, so it
|
|
556
|
+
// is rendered as a single select row: its own leading visual instead of a
|
|
557
|
+
// checkbox, and no selection indicator.
|
|
558
|
+
const renderAction = (row) => {
|
|
559
|
+
const action = (0, combobox_utils_1.getAction)(row);
|
|
560
|
+
return ((0, jsx_runtime_1.jsx)(Option, Object.assign({ id: getOptionId(row), isAction: true, isDisabled: action.isDisabled, isFocused: row === activeOption, onClick: () => handleOptionChange(row), onHover: () => handleOptionHover(row) }, getSetPositionProps(row), { children: (0, jsx_runtime_1.jsx)(OptionContent, { label: action.label, description: action.description, leadingVisual: action.leadingVisual, isDisabled: action.isDisabled }) }), row.value));
|
|
561
|
+
};
|
|
562
|
+
const panel = ((0, jsx_runtime_1.jsxs)(Panel, Object.assign({}, getFloatingProps(), { ref: refs.setFloating, id: panelId, ariaLabel: label, isDialog: isDialogPanel, className: themeClassName, style: floatingStyles, children: [isSearchable && ((0, jsx_runtime_1.jsx)(Input, { ref: searchInputRef, ariaLabel: searchPlaceholder, value: currentSearchValue, onValueChange: setCurrentSearchValue, placeholder: searchPlaceholder, listboxId: hasRows ? listboxId : undefined, activeOptionId: hasRows ? activeOptionId : undefined, onKeyDown: handleSearchKeyDown })), !hasOptionRows &&
|
|
563
|
+
(isLoading ? ((0, jsx_runtime_1.jsx)(LoadingState, { children: loadingMessage })) : ((0, jsx_runtime_1.jsx)(EmptyState, {}))), hasOptionRows &&
|
|
564
|
+
(virtualRows !== undefined ? ((0, jsx_runtime_1.jsx)(VirtualizedListBox, { id: listboxId, ariaLabel: label, isMulti: isMulti, ownsIds: actionOwnsIds, rows: virtualRows.displayRows, rowsKey: optionsKey, flatToDisplayIndex: virtualRows.flatToDisplayIndex, scrollToFlatIndexRef: scrollToActiveRowRef, renderRow: renderOption })) : ((0, jsx_runtime_1.jsx)(ListBox, { id: listboxId, ariaLabel: label, isMulti: isMulti, ownsIds: actionOwnsIds, children: optionItems.map((item) => (0, combobox_utils_1.isOptionGroup)(item) ? ((0, jsx_runtime_1.jsx)(Group, { label: item.label, children: item.options.map(renderOption) }, item.label)) : (renderOption(item))) }))), actionRows.length > 0 && ((0, jsx_runtime_1.jsxs)("div", Object.assign({}, (hasOptionRows
|
|
565
|
+
? { role: 'presentation' }
|
|
566
|
+
: {
|
|
567
|
+
'aria-label': label,
|
|
568
|
+
'aria-multiselectable': isMulti || undefined,
|
|
569
|
+
id: listboxId,
|
|
570
|
+
role: 'listbox',
|
|
571
|
+
}), { className: "ndl-combobox-actions", children: [(0, jsx_runtime_1.jsx)(divider_1.Divider, { className: "ndl-combobox-action-divider", htmlAttributes: { 'aria-hidden': true } }), actionRows.map((row, index) => ((0, jsx_runtime_1.jsxs)(react_2.Fragment, { children: [index > 0 && (0, combobox_utils_1.getAction)(row).hasDivider === true && ((0, jsx_runtime_1.jsx)(divider_1.Divider, { className: "ndl-combobox-action-divider", htmlAttributes: { 'aria-hidden': true } })), renderAction(row)] }, row.value)))] })))] })));
|
|
572
|
+
return ((0, jsx_runtime_1.jsxs)("div", Object.assign({ ref: ref, style: style, className: (0, classnames_1.default)('ndl-combobox', {
|
|
573
|
+
'ndl-clean': isClean,
|
|
574
|
+
'ndl-combobox-custom-trigger': hasCustomTrigger,
|
|
575
|
+
'ndl-combobox-field': !hasCustomTrigger,
|
|
576
|
+
'ndl-disabled': isDisabled,
|
|
577
|
+
'ndl-fluid': isFluid,
|
|
578
|
+
'ndl-has-error': hasError,
|
|
579
|
+
'ndl-large': size === 'large',
|
|
580
|
+
'ndl-medium': size === 'medium',
|
|
581
|
+
'ndl-small': size === 'small',
|
|
582
|
+
}, className) }, htmlAttributes, { children: [name !== undefined && ((0, jsx_runtime_1.jsx)(ComboboxFormControl, { ref: internalTriggerRef, name: name, form: formId, autoComplete: autoComplete, options: formOptions, selectedOptions: selectedOptions, resetSelectedOptions: resetSelectedOptions, isMulti: isMulti, isDisabled: isDisabled, isRequired: isRequired, isSelectionControlled: value !== undefined, onValueChange: setSelectedValue, onInvalid: setNativeValidationMessage, onReset: resetFromForm })), renderTrigger === undefined ? ((0, jsx_runtime_1.jsx)(Control, { triggerProps: builtInTriggerProps, state: triggerState, field: field, placeholder: placeholder, isClearable: isClearable, isClean: isClean, size: size, maxRows: isMulti ? maxRows : undefined, components: components, referenceProps: getReferenceProps(), referenceRef: refs.setReference })) : (renderTrigger(triggerProps, triggerState)), context.open && ((0, jsx_runtime_1.jsx)(conditional_wrap_1.ConditionalWrap, { shouldWrap: isPanelPortaled, wrap: (wrapChildren) => ((0, jsx_runtime_1.jsx)(react_1.FloatingPortal, { root: portalTarget, children: wrapChildren })), children: (0, jsx_runtime_1.jsx)(react_1.FloatingFocusManager, { context: context, modal: false,
|
|
583
|
+
// A select-only combobox keeps DOM focus on the trigger and speaks
|
|
584
|
+
// through aria-activedescendant, so nothing inside is focused.
|
|
585
|
+
initialFocus: isSearchable ? searchInputRef : -1, returnFocus: internalTriggerRef, children: panel }) }))] })));
|
|
586
|
+
};
|
|
587
|
+
exports.Combobox = Combobox;
|
|
588
|
+
const getTriggerControlsId = ({ isPanelOpen, isDialogPanel, hasRows, panelId, listboxId, }) => {
|
|
589
|
+
if (!isPanelOpen) {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
if (isDialogPanel) {
|
|
593
|
+
return panelId;
|
|
594
|
+
}
|
|
595
|
+
// An expanded combobox must always control something, so fall back to the
|
|
596
|
+
// panel when the list is empty and only the status message is rendered.
|
|
597
|
+
return hasRows ? listboxId : panelId;
|
|
598
|
+
};
|
|
599
|
+
const getSelectedOptions = (value) => {
|
|
600
|
+
if (Array.isArray(value)) {
|
|
601
|
+
return value;
|
|
602
|
+
}
|
|
603
|
+
return value === null ? [] : [value];
|
|
604
|
+
};
|
|
605
|
+
/**
|
|
606
|
+
* A native select mirrors the custom control for submission, autofill,
|
|
607
|
+
* constraint validation, and reset. It is hidden from assistive technology
|
|
608
|
+
* because the combobox trigger exposes the same field semantics.
|
|
609
|
+
*
|
|
610
|
+
* Above `FORM_OPTION_MIRROR_LIMIT` it receives only the selected and default
|
|
611
|
+
* options rather than the full list, so autofill has nothing to offer but
|
|
612
|
+
* everything else keeps working without thousands of mirrored options.
|
|
613
|
+
*/
|
|
614
|
+
const ComboboxFormControl = ({ ref: triggerRef, name, form, autoComplete, options, selectedOptions, resetSelectedOptions, isMulti, isDisabled, isRequired, isSelectionControlled, onValueChange, onInvalid, onReset, }) => {
|
|
615
|
+
var _a;
|
|
616
|
+
const selectRef = (0, react_2.useRef)(null);
|
|
617
|
+
const isDispatchingChangeRef = (0, react_2.useRef)(false);
|
|
618
|
+
const isResettingRef = (0, react_2.useRef)(false);
|
|
619
|
+
const skipNextChangeRef = (0, react_2.useRef)(false);
|
|
620
|
+
const selectedValues = selectedOptions.map((option) => option.value);
|
|
621
|
+
const resetSelectedValues = resetSelectedOptions.map((option) => option.value);
|
|
622
|
+
const selectedValuesKey = selectedValues.join('\u0000');
|
|
623
|
+
const previousValuesKeyRef = (0, react_2.useRef)(selectedValuesKey);
|
|
624
|
+
const valuesOnReset = isSelectionControlled
|
|
625
|
+
? selectedValues
|
|
626
|
+
: resetSelectedValues;
|
|
627
|
+
const valuesOnResetKey = valuesOnReset.join('\u0000');
|
|
628
|
+
(0, react_2.useEffect)(() => {
|
|
629
|
+
const select = selectRef.current;
|
|
630
|
+
if (select === null) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const resetValues = new Set(valuesOnReset);
|
|
634
|
+
Array.from(select.options).forEach((option) => {
|
|
635
|
+
option.defaultSelected = resetValues.has(option.value);
|
|
636
|
+
});
|
|
637
|
+
// valuesOnReset is rebuilt every render, so the effect keys on its content.
|
|
638
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
639
|
+
}, [valuesOnResetKey]);
|
|
640
|
+
(0, react_2.useEffect)(() => {
|
|
641
|
+
if (previousValuesKeyRef.current === selectedValuesKey) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
previousValuesKeyRef.current = selectedValuesKey;
|
|
645
|
+
if (isResettingRef.current || skipNextChangeRef.current) {
|
|
646
|
+
skipNextChangeRef.current = false;
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const select = selectRef.current;
|
|
650
|
+
if (select === null) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
isDispatchingChangeRef.current = true;
|
|
654
|
+
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
655
|
+
isDispatchingChangeRef.current = false;
|
|
656
|
+
}, [selectedValuesKey]);
|
|
657
|
+
(0, react_2.useEffect)(() => {
|
|
658
|
+
const select = selectRef.current;
|
|
659
|
+
const formElement = select === null || select === void 0 ? void 0 : select.form;
|
|
660
|
+
if (select === null ||
|
|
661
|
+
select === undefined ||
|
|
662
|
+
formElement === null ||
|
|
663
|
+
formElement === undefined) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const handleReset = () => {
|
|
667
|
+
isResettingRef.current = true;
|
|
668
|
+
onReset();
|
|
669
|
+
window.setTimeout(() => {
|
|
670
|
+
isResettingRef.current = false;
|
|
671
|
+
});
|
|
672
|
+
};
|
|
673
|
+
formElement.addEventListener('reset', handleReset);
|
|
674
|
+
return () => formElement.removeEventListener('reset', handleReset);
|
|
675
|
+
}, [onReset]);
|
|
676
|
+
const handleChange = (event) => {
|
|
677
|
+
var _a;
|
|
678
|
+
if (isDispatchingChangeRef.current) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const optionsByValue = new Map(options.map((option) => [option.value, option]));
|
|
682
|
+
const nextOptions = Array.from(event.currentTarget.selectedOptions)
|
|
683
|
+
.map((selectedOption) => optionsByValue.get(selectedOption.value))
|
|
684
|
+
.filter((option) => option !== undefined);
|
|
685
|
+
skipNextChangeRef.current =
|
|
686
|
+
nextOptions.map((option) => option.value).join('\u0000') !==
|
|
687
|
+
selectedValuesKey;
|
|
688
|
+
onValueChange(isMulti ? nextOptions : ((_a = nextOptions[0]) !== null && _a !== void 0 ? _a : null));
|
|
689
|
+
};
|
|
690
|
+
return ((0, jsx_runtime_1.jsxs)("select", { ref: selectRef, className: "ndl-combobox-form-control", "aria-hidden": "true", tabIndex: -1, name: name, form: form, autoComplete: autoComplete, disabled: isDisabled, required: isRequired, multiple: isMulti, value: isMulti ? selectedValues : ((_a = selectedValues[0]) !== null && _a !== void 0 ? _a : ''), onChange: handleChange, onInvalid: (event) => {
|
|
691
|
+
var _a;
|
|
692
|
+
event.preventDefault();
|
|
693
|
+
onInvalid(event.currentTarget.validationMessage);
|
|
694
|
+
(_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
695
|
+
}, children: [!isMulti && (0, jsx_runtime_1.jsx)("option", { value: "" }), options.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option.value, children: option.label }, option.value)))] }));
|
|
696
|
+
};
|
|
697
|
+
const Control = ({ triggerProps, state, field, placeholder, isClearable, isClean, size, maxRows, components, referenceProps, referenceRef, }) => {
|
|
698
|
+
const { isOpen, isMulti, isDisabled, selectedOptions, clear, remove } = state;
|
|
699
|
+
const { MultiValue, SingleValue } = components;
|
|
700
|
+
const { ref: triggerRef } = triggerProps, triggerAria = __rest(triggerProps, ["ref"]);
|
|
701
|
+
const hasValue = selectedOptions.length > 0;
|
|
702
|
+
const singleValueOption = isMulti ? undefined : selectedOptions[0];
|
|
703
|
+
// Selected values are shown as tags, so the trigger renders nothing and is
|
|
704
|
+
// taken out of the wrapping flow rather than claiming a row of its own.
|
|
705
|
+
const isTriggerCollapsed = isMulti && hasValue;
|
|
706
|
+
const multiValueSize = size === 'large' ? 'small' : 'x-small';
|
|
707
|
+
// `null` opts out of the default row limit, so both it and a missing value
|
|
708
|
+
// resolve to unlimited, which the hook expresses as undefined.
|
|
709
|
+
const rowLimit = isMulti && maxRows !== null && maxRows !== undefined ? maxRows : undefined;
|
|
710
|
+
// Under a row limit only the tags that could possibly be visible are
|
|
711
|
+
// rendered and measured; the rest of a huge selection is overflowed
|
|
712
|
+
// arithmetically without ever touching the DOM. Without a limit every tag
|
|
713
|
+
// renders, so unlimited rows with a huge selection is on the consumer.
|
|
714
|
+
const tagCandidateCap = rowLimit === undefined
|
|
715
|
+
? selectedOptions.length
|
|
716
|
+
: Math.max(1, Math.floor(rowLimit)) * TAG_CANDIDATES_PER_ROW;
|
|
717
|
+
const renderedOptions = selectedOptions.length > tagCandidateCap
|
|
718
|
+
? selectedOptions.slice(0, tagCandidateCap)
|
|
719
|
+
: selectedOptions;
|
|
720
|
+
const { containerRef: valueContainerRef, visibleCount } = (0, use_combobox_multi_value_overflow_1.useComboboxMultiValueOverflow)({
|
|
721
|
+
maxRows: rowLimit,
|
|
722
|
+
// Disabling swaps the dismissible tags for read-only ones, which have no
|
|
723
|
+
// remove button and are narrower. The total count is included since it
|
|
724
|
+
// sets the measure tag texts.
|
|
725
|
+
measureKey: [
|
|
726
|
+
size,
|
|
727
|
+
isDisabled,
|
|
728
|
+
selectedOptions.length,
|
|
729
|
+
...renderedOptions.map((option) => `${option.value}:${option.label}`),
|
|
730
|
+
].join('\u0000'),
|
|
731
|
+
tagCount: renderedOptions.length,
|
|
732
|
+
});
|
|
733
|
+
const visibleTagCount = visibleCount !== null && visibleCount !== void 0 ? visibleCount : renderedOptions.length;
|
|
734
|
+
const overflowCount = selectedOptions.length - visibleTagCount;
|
|
735
|
+
const overflowedOptions = selectedOptions.slice(visibleTagCount, visibleTagCount + OVERFLOW_LABEL_LIMIT);
|
|
736
|
+
const unnamedOverflowCount = overflowCount - overflowedOptions.length;
|
|
737
|
+
const overflowedLabels = overflowedOptions.map((option) => option.label).join(', ') +
|
|
738
|
+
(unnamedOverflowCount > 0 ? `, and ${unnamedOverflowCount} more` : '');
|
|
739
|
+
// "+999" is wider than "+1K", so each text the counter could show at its
|
|
740
|
+
// widest is measured; the hook reserves room for the widest of them.
|
|
741
|
+
const counterMeasureTexts = (0, use_combobox_multi_value_overflow_1.getOverflowCounterMeasureTexts)(selectedOptions.length);
|
|
742
|
+
const internalTriggerRef = (0, react_2.useRef)(null);
|
|
743
|
+
const mergedTriggerRef = (0, react_1.useMergeRefs)([
|
|
744
|
+
triggerRef,
|
|
745
|
+
internalTriggerRef,
|
|
746
|
+
]);
|
|
747
|
+
const handleAuxiliaryActionKeyDown = (event) => {
|
|
748
|
+
var _a, _b;
|
|
749
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
750
|
+
event.preventDefault();
|
|
751
|
+
event.stopPropagation();
|
|
752
|
+
(_a = internalTriggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
753
|
+
(_b = triggerAria.onKeyDown) === null || _b === void 0 ? void 0 : _b.call(triggerAria, event);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
const handleTagRemove = (event, option) => {
|
|
757
|
+
var _a;
|
|
758
|
+
// Capture the following removal targets before React removes the current
|
|
759
|
+
// tag. With `maxRows`, the removal can promote a collapsed tag into view,
|
|
760
|
+
// so which of them is focusable is only known after the update.
|
|
761
|
+
const tag = event.currentTarget.closest('[data-ndl-combobox-multi-value]');
|
|
762
|
+
const followingRemoveButtons = [];
|
|
763
|
+
let sibling = (_a = tag === null || tag === void 0 ? void 0 : tag.nextElementSibling) !== null && _a !== void 0 ? _a : null;
|
|
764
|
+
while (sibling !== null) {
|
|
765
|
+
const removeButton = sibling.querySelector('[data-ndl-dismissible-tag-remove]');
|
|
766
|
+
if (removeButton !== null) {
|
|
767
|
+
followingRemoveButtons.push(removeButton);
|
|
768
|
+
}
|
|
769
|
+
sibling = sibling.nextElementSibling;
|
|
770
|
+
}
|
|
771
|
+
// Otherwise the click bubbles to the control and opens the panel.
|
|
772
|
+
event.stopPropagation();
|
|
773
|
+
remove(option);
|
|
774
|
+
requestAnimationFrame(() => {
|
|
775
|
+
var _a;
|
|
776
|
+
// A tag collapsed into the overflow counter is invisible and cannot take
|
|
777
|
+
// focus, so it is passed over.
|
|
778
|
+
const nextRemoveButton = followingRemoveButtons.find((removeButton) => removeButton.isConnected &&
|
|
779
|
+
removeButton.closest('.ndl-overflowed') === null);
|
|
780
|
+
if (nextRemoveButton !== undefined) {
|
|
781
|
+
nextRemoveButton.focus();
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
(_a = internalTriggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
785
|
+
});
|
|
786
|
+
};
|
|
787
|
+
const handleClear = (event) => {
|
|
788
|
+
// Otherwise the click bubbles to the control and opens the panel.
|
|
789
|
+
event.stopPropagation();
|
|
790
|
+
clear();
|
|
791
|
+
// Clearing removes the focused clear button from the DOM. Restore focus to
|
|
792
|
+
// the combobox after React has committed that update.
|
|
793
|
+
requestAnimationFrame(() => {
|
|
794
|
+
var _a;
|
|
795
|
+
(_a = internalTriggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
796
|
+
});
|
|
797
|
+
};
|
|
798
|
+
// Clicking the label moves focus to the trigger without opening the panel,
|
|
799
|
+
// as with a native select. Preventing the default cancels the click the
|
|
800
|
+
// label would otherwise forward to the trigger button, which would toggle
|
|
801
|
+
// the panel.
|
|
802
|
+
const handleLabelClick = (event) => {
|
|
803
|
+
var _a;
|
|
804
|
+
event.preventDefault();
|
|
805
|
+
(_a = internalTriggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
806
|
+
};
|
|
807
|
+
return (
|
|
808
|
+
// A fragment, so the label, the control, and the message are rows of the
|
|
809
|
+
// combobox itself. A custom trigger replaces all three.
|
|
810
|
+
(0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(FieldLabel, { field: field, size: size, onLabelClick: handleLabelClick }), (0, jsx_runtime_1.jsxs)("div", Object.assign({ ref: referenceRef, className: (0, classnames_1.default)('ndl-combobox-control', {
|
|
811
|
+
'ndl-clean': isClean,
|
|
812
|
+
'ndl-disabled': isDisabled,
|
|
813
|
+
'ndl-has-error': field.hasError,
|
|
814
|
+
'ndl-is-multi': isMulti,
|
|
815
|
+
'ndl-large': size === 'large',
|
|
816
|
+
'ndl-medium': size === 'medium',
|
|
817
|
+
'ndl-small': size === 'small',
|
|
818
|
+
}) }, referenceProps, { children: [(0, jsx_runtime_1.jsxs)("div", { ref: valueContainerRef, className: (0, classnames_1.default)('ndl-combobox-value-container', {
|
|
819
|
+
// While a counter may be needed, every tag reserves room for it,
|
|
820
|
+
// so an extreme-width tag shrinks instead of collapsing entirely.
|
|
821
|
+
'ndl-counter-reserved': rowLimit !== undefined && selectedOptions.length > 1,
|
|
822
|
+
}), children: [(0, jsx_runtime_1.jsxs)("button", Object.assign({ type: "button", className: (0, classnames_1.default)('ndl-combobox-trigger', {
|
|
823
|
+
'ndl-collapsed': isTriggerCollapsed,
|
|
824
|
+
'ndl-disabled': isDisabled,
|
|
825
|
+
}) }, triggerAria, { ref: mergedTriggerRef, children: [singleValueOption !== undefined &&
|
|
826
|
+
(SingleValue === undefined ? ((0, jsx_runtime_1.jsx)("span", { className: "ndl-combobox-single-value", children: singleValueOption.label })) : ((0, jsx_runtime_1.jsx)(SingleValue, { option: singleValueOption, isDisabled: isDisabled, size: size }))), !hasValue && ((0, jsx_runtime_1.jsx)("span", { className: "ndl-combobox-placeholder", children: placeholder }))] })), isMulti &&
|
|
827
|
+
renderedOptions.map((option, index) => {
|
|
828
|
+
// Tags past the overflow cutoff stay rendered so they keep their
|
|
829
|
+
// measurements, but the stylesheet takes them out of the layout,
|
|
830
|
+
// out of the accessibility tree, and out of focus reach.
|
|
831
|
+
const multiValueClasses = (0, classnames_1.default)('ndl-combobox-multi-value', {
|
|
832
|
+
'ndl-overflowed': index >= visibleTagCount,
|
|
833
|
+
});
|
|
834
|
+
// A custom value owns its own presentation, so it is only given
|
|
835
|
+
// the wrapper the value container lays out, and no remove button.
|
|
836
|
+
if (MultiValue !== undefined) {
|
|
837
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: multiValueClasses, children: (0, jsx_runtime_1.jsx)(MultiValue, { option: option, index: index, isDisabled: isDisabled, size: size, remove: () => remove(option) }) }, option.value));
|
|
838
|
+
}
|
|
839
|
+
return isDisabled ? ((0, jsx_runtime_1.jsx)(read_only_tag_1.ReadOnlyTag, { className: multiValueClasses, size: multiValueSize, children: option.label }, option.value)) : ((0, jsx_runtime_1.jsx)(dismissible_tag_1.DismissibleTag, { className: multiValueClasses, size: multiValueSize, htmlAttributes: {
|
|
840
|
+
'data-ndl-combobox-multi-value': '',
|
|
841
|
+
onKeyDown: handleAuxiliaryActionKeyDown,
|
|
842
|
+
}, onRemove: (event) => handleTagRemove(event, option), children: option.label }, option.value));
|
|
843
|
+
}), overflowCount > 0 && ((0, jsx_runtime_1.jsxs)(read_only_tag_1.ReadOnlyTag, { className: "ndl-combobox-multi-value-counter", size: multiValueSize, htmlAttributes: {
|
|
844
|
+
title: overflowedLabels,
|
|
845
|
+
}, children: [(0, jsx_runtime_1.jsxs)("span", { "aria-hidden": "true", children: ["+", (0, use_combobox_multi_value_overflow_1.formatOverflowCount)(overflowCount)] }), (0, jsx_runtime_1.jsxs)("span", { className: "ndl-combobox-visually-hidden", children: [overflowCount, " more selected: ", overflowedLabels] })] })), rowLimit !== undefined &&
|
|
846
|
+
hasValue &&
|
|
847
|
+
// Invisible and out of the layout, purely to measure the widest
|
|
848
|
+
// counter the current selection could need, so the fit computation
|
|
849
|
+
// can reserve room for it in a single deterministic pass.
|
|
850
|
+
counterMeasureTexts.map((text) => ((0, jsx_runtime_1.jsxs)(read_only_tag_1.ReadOnlyTag, { className: "ndl-combobox-multi-value-measure", size: multiValueSize, htmlAttributes: { 'aria-hidden': true }, children: ["+", text] }, text)))] }), (0, jsx_runtime_1.jsxs)("div", { className: "ndl-combobox-indicators-container", children: [isClearable && hasValue && !isDisabled && (
|
|
851
|
+
// The divider only separates the clear button from the chevron, so
|
|
852
|
+
// it is tied to the button rather than always rendered, as in
|
|
853
|
+
// Select.
|
|
854
|
+
(0, jsx_runtime_1.jsxs)("div", { className: "ndl-combobox-clear", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "ndl-combobox-clear-button", "aria-label": "Clear selection", onKeyDown: handleAuxiliaryActionKeyDown, onClick: handleClear, children: (0, jsx_runtime_1.jsx)(heroIcons_1.XMarkIconOutline, {}) }), (0, jsx_runtime_1.jsx)("div", { className: "ndl-combobox-divider" })] })), (0, jsx_runtime_1.jsx)("div", { className: (0, classnames_1.default)('ndl-combobox-dropdown-indicator', {
|
|
855
|
+
'ndl-open': isOpen,
|
|
856
|
+
}), children: (0, jsx_runtime_1.jsx)(heroIcons_1.ChevronDownIconOutline, { "aria-hidden": "true", className: "ndl-combobox-icon" }) })] })] })), (0, jsx_runtime_1.jsx)(FieldMessage, { field: field, size: size })] }));
|
|
857
|
+
};
|
|
858
|
+
/**
|
|
859
|
+
* The trigger is a button with `role="combobox"`, which takes its accessible
|
|
860
|
+
* name from the author rather than from its content, so the label is referenced
|
|
861
|
+
* through `aria-labelledby` instead of wrapping the control in a `<label>`.
|
|
862
|
+
* The label is still a `<label>` associated through `htmlFor`, and clicking it
|
|
863
|
+
* moves focus to the trigger, as with a native select. It deliberately does
|
|
864
|
+
* not open the panel, which is also what a native select label leaves alone.
|
|
865
|
+
*/
|
|
866
|
+
const FieldLabel = ({ field, size, onLabelClick }) => {
|
|
867
|
+
const { label, labelId, triggerId, hasVisuallyHiddenLabel, hasRequiredIndicator, isRequired, } = field;
|
|
868
|
+
const variant = size === 'large' ? 'body-large' : 'body-medium';
|
|
869
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)('ndl-combobox-label-row', {
|
|
870
|
+
'ndl-visually-hidden': hasVisuallyHiddenLabel,
|
|
871
|
+
}), children: [(0, jsx_runtime_1.jsx)(typography_1.Typography, { as: "label", variant: variant, className: "ndl-combobox-label", htmlAttributes: {
|
|
872
|
+
htmlFor: triggerId,
|
|
873
|
+
id: labelId,
|
|
874
|
+
onClick: onLabelClick,
|
|
875
|
+
}, children: label }), hasRequiredIndicator && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: variant, className: "ndl-combobox-label-indicator", children: isRequired ? 'Required' : 'Optional' }))] }));
|
|
876
|
+
};
|
|
877
|
+
/**
|
|
878
|
+
* Rendered whether or not there is a message, so the live region is in the
|
|
879
|
+
* document before the text arrives and a validation error is announced.
|
|
880
|
+
*/
|
|
881
|
+
const FieldMessage = ({ field, size }) => {
|
|
882
|
+
const { errorText, hasError, helpText, helpTextId, errorTextId } = field;
|
|
883
|
+
const variant = size === 'large' ? 'body-medium' : 'body-small';
|
|
884
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "ndl-combobox-message", "aria-live": "polite", children: [hasError && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: "ndl-error-icon", children: (0, jsx_runtime_1.jsx)(heroIcons_1.ExclamationCircleIconSolid, {}) }), (0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: variant, className: "ndl-error-text", htmlAttributes: { id: errorTextId }, children: errorText })] })), !hasError && Boolean(helpText) && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: variant, htmlAttributes: { id: helpTextId }, children: helpText }))] }));
|
|
885
|
+
};
|
|
886
|
+
const Panel = (_a) => {
|
|
887
|
+
var { children, ref, id, ariaLabel, isDialog, className, style } = _a, restProps = __rest(_a, ["children", "ref", "id", "ariaLabel", "isDialog", "className", "style"]);
|
|
888
|
+
return ((0, jsx_runtime_1.jsx)("div", Object.assign({ ref: ref, id: id, role: isDialog === true ? 'dialog' : undefined, "aria-label": isDialog === true ? ariaLabel : undefined, className: (0, classnames_1.default)('ndl-combobox-panel', className), style: style }, restProps, { children: children })));
|
|
889
|
+
};
|
|
890
|
+
const Input = ({ ref, ariaLabel, value, onValueChange, placeholder, listboxId, activeOptionId, onKeyDown, }) => {
|
|
891
|
+
return ((0, jsx_runtime_1.jsxs)("label", { className: "ndl-combobox-input-wrapper", children: [(0, jsx_runtime_1.jsx)("span", { className: "ndl-combobox-input-label", children: ariaLabel }), (0, jsx_runtime_1.jsx)(heroIcons_1.MagnifyingGlassIconOutline, { className: "ndl-combobox-input-leading" }), (0, jsx_runtime_1.jsx)("input", { ref: ref, type: "text", role: "combobox", autoComplete: "off", "aria-autocomplete": "list", "aria-haspopup": "listbox", "aria-expanded": listboxId !== undefined, "aria-controls": listboxId, "aria-activedescendant": activeOptionId, className: "ndl-combobox-input", placeholder: placeholder, value: value, onChange: (event) => onValueChange(event.target.value), onKeyDown: onKeyDown })] }));
|
|
892
|
+
};
|
|
893
|
+
const ListBox = ({ ariaLabel, children, isMulti, id, ownsIds, }) => {
|
|
894
|
+
return ((0, jsx_runtime_1.jsx)("div", { id: id, role: "listbox", "aria-label": ariaLabel, "aria-owns": ownsIds, "aria-multiselectable": isMulti || undefined, className: "ndl-combobox-listbox", children: children }));
|
|
895
|
+
};
|
|
896
|
+
/**
|
|
897
|
+
* The windowed listbox used above the row threshold: only the visible slice of
|
|
898
|
+
* rows is in the DOM, absolutely positioned inside one full-height spacer.
|
|
899
|
+
* Group nesting cannot survive windowing, so headers render as presentational
|
|
900
|
+
* rows of their own and options carry `aria-setsize`/`aria-posinset` instead.
|
|
901
|
+
* Pinned action rows render outside, below the listbox, so they are never
|
|
902
|
+
* windowed.
|
|
903
|
+
*/
|
|
904
|
+
const VirtualizedListBox = ({ id, ariaLabel, isMulti, ownsIds, rows, rowsKey, flatToDisplayIndex, scrollToFlatIndexRef, renderRow, }) => {
|
|
905
|
+
const scrollRef = (0, react_2.useRef)(null);
|
|
906
|
+
const virtualizer = (0, react_virtual_1.useVirtualizer)({
|
|
907
|
+
count: rows.length,
|
|
908
|
+
estimateSize: () => VIRTUAL_ROW_ESTIMATED_HEIGHT,
|
|
909
|
+
getScrollElement: () => scrollRef.current,
|
|
910
|
+
overscan: 10,
|
|
911
|
+
});
|
|
912
|
+
const pendingScrollIndexRef = (0, react_2.useRef)(null);
|
|
913
|
+
(0, react_2.useEffect)(() => {
|
|
914
|
+
scrollToFlatIndexRef.current = (flatIndex) => {
|
|
915
|
+
const displayIndex = flatToDisplayIndex[flatIndex];
|
|
916
|
+
if (displayIndex !== undefined) {
|
|
917
|
+
pendingScrollIndexRef.current = displayIndex;
|
|
918
|
+
virtualizer.scrollToIndex(displayIndex);
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
return () => {
|
|
922
|
+
scrollToFlatIndexRef.current = null;
|
|
923
|
+
};
|
|
924
|
+
});
|
|
925
|
+
// A long jump scrolls to an offset built from estimated sizes, and the
|
|
926
|
+
// estimates only get corrected once the rows near the target render, so one
|
|
927
|
+
// scroll can land short. Each render after a scroll re-issues it until the
|
|
928
|
+
// target row is actually mounted, which is also what lets
|
|
929
|
+
// aria-activedescendant resolve.
|
|
930
|
+
(0, react_2.useEffect)(() => {
|
|
931
|
+
const target = pendingScrollIndexRef.current;
|
|
932
|
+
if (target === null) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const isRendered = virtualizer
|
|
936
|
+
.getVirtualItems()
|
|
937
|
+
.some((item) => item.index === target);
|
|
938
|
+
if (isRendered) {
|
|
939
|
+
pendingScrollIndexRef.current = null;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
virtualizer.scrollToIndex(target);
|
|
943
|
+
});
|
|
944
|
+
// A filtered list is a different list, so it starts at the top.
|
|
945
|
+
(0, react_2.useEffect)(() => {
|
|
946
|
+
virtualizer.scrollToOffset(0);
|
|
947
|
+
// The virtualizer is stable and scrolling must not rerun on its rerenders.
|
|
948
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
949
|
+
}, [rowsKey]);
|
|
950
|
+
return ((0, jsx_runtime_1.jsx)("div", { ref: scrollRef, id: id, role: "listbox", "aria-label": ariaLabel, "aria-owns": ownsIds, "aria-multiselectable": isMulti === true || undefined, className: "ndl-combobox-listbox", children: (0, jsx_runtime_1.jsx)("div", { role: "presentation", className: "ndl-combobox-virtual-space", style: { height: virtualizer.getTotalSize() }, children: virtualizer.getVirtualItems().map((item) => {
|
|
951
|
+
const row = rows[item.index];
|
|
952
|
+
return ((0, jsx_runtime_1.jsx)("div", { ref: virtualizer.measureElement, "data-index": item.index, role: "presentation", className: "ndl-combobox-virtual-row", style: { transform: `translateY(${item.start}px)` }, children: row.kind === 'group-header' ? ((0, jsx_runtime_1.jsx)(GroupHeader, { isPresentational: true, label: row.label })) : (renderRow(row.row)) }, item.key));
|
|
953
|
+
}) }) }));
|
|
954
|
+
};
|
|
955
|
+
const Option = ({ children, id, isSelected, isDisabled, isFocused, isCreate, isAction, onClick, onHover, posInSet, setSize, }) => {
|
|
956
|
+
const classes = (0, classnames_1.default)('ndl-combobox-option', {
|
|
957
|
+
'ndl-combobox-option-action': isAction,
|
|
958
|
+
'ndl-combobox-option-create': isCreate,
|
|
959
|
+
'ndl-disabled': isDisabled,
|
|
960
|
+
'ndl-focused': isFocused,
|
|
961
|
+
});
|
|
962
|
+
return (
|
|
963
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus -- options are never DOM focused; keyboard is handled on the search input or the listbox and exposed through aria-activedescendant
|
|
964
|
+
(0, jsx_runtime_1.jsx)("div", { id: id, role: "option", "aria-selected": isSelected, "aria-disabled": isDisabled, "aria-posinset": posInSet, "aria-setsize": setSize, className: classes, onClick: onClick,
|
|
965
|
+
// Options are not focusable, so a plain mousedown would blur whatever
|
|
966
|
+
// holds DOM focus - the search input or the trigger - and drop the panel.
|
|
967
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
968
|
+
// mouseover fires when the element under a stationary cursor changes
|
|
969
|
+
// because the list scrolled, which would let a rested mouse steal virtual
|
|
970
|
+
// focus back during keyboard navigation. mousemove needs real movement.
|
|
971
|
+
onMouseMove: onHover, children: children }));
|
|
972
|
+
};
|
|
973
|
+
const OptionContent = ({ label, customLabel, description, leadingVisual, isDisabled, isSelected, isMulti, }) => {
|
|
974
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "ndl-combobox-option-content", children: [isMulti ? ((0, jsx_runtime_1.jsx)(MultiSelectIndicator, { isDisabled: isDisabled, isSelected: isSelected })) : (leadingVisual && ((0, jsx_runtime_1.jsx)(LeadingVisual, { isDisabled: isDisabled, children: leadingVisual }))), (0, jsx_runtime_1.jsxs)("div", { className: "ndl-combobox-option-content-main", children: [customLabel !== null && customLabel !== void 0 ? customLabel : (0, jsx_runtime_1.jsx)(Label, { isDisabled: isDisabled, children: label }), description && ((0, jsx_runtime_1.jsx)(Description, { isDisabled: isDisabled, children: description }))] }), !isMulti && isSelected && (0, jsx_runtime_1.jsx)(SelectionIndicator, { isDisabled: isDisabled })] }));
|
|
975
|
+
};
|
|
976
|
+
const CreateOptionContent = ({ children }) => {
|
|
977
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "ndl-combobox-option-content", children: [(0, jsx_runtime_1.jsx)(LeadingVisual, { children: (0, jsx_runtime_1.jsx)(heroIcons_1.PlusIconOutline, {}) }), (0, jsx_runtime_1.jsx)("div", { className: "ndl-combobox-option-content-main", children: (0, jsx_runtime_1.jsx)(Label, { children: children }) })] }));
|
|
978
|
+
};
|
|
979
|
+
const LeadingVisual = ({ children, isDisabled }) => {
|
|
980
|
+
const classes = (0, classnames_1.default)('ndl-combobox-option-content-leading-visual', {
|
|
981
|
+
'ndl-disabled': isDisabled,
|
|
982
|
+
});
|
|
983
|
+
return (0, jsx_runtime_1.jsx)("div", { className: classes, children: children });
|
|
984
|
+
};
|
|
985
|
+
const Label = ({ children, isDisabled }) => {
|
|
986
|
+
const classes = (0, classnames_1.default)('ndl-combobox-option-content-label', {
|
|
987
|
+
'ndl-disabled': isDisabled,
|
|
988
|
+
});
|
|
989
|
+
return ((0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: "body-medium", className: classes, children: children }));
|
|
990
|
+
};
|
|
991
|
+
const Description = ({ children, isDisabled }) => {
|
|
992
|
+
const classes = (0, classnames_1.default)('ndl-combobox-option-content-description', {
|
|
993
|
+
'ndl-disabled': isDisabled,
|
|
994
|
+
});
|
|
995
|
+
return ((0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: "body-small", className: classes, children: children }));
|
|
996
|
+
};
|
|
997
|
+
const SelectionIndicator = ({ isDisabled, }) => {
|
|
998
|
+
const classes = (0, classnames_1.default)('ndl-combobox-option-content-selection-indicator', {
|
|
999
|
+
'ndl-disabled': isDisabled,
|
|
1000
|
+
});
|
|
1001
|
+
return (0, jsx_runtime_1.jsx)(heroIcons_1.CheckIconOutline, { className: classes });
|
|
1002
|
+
};
|
|
1003
|
+
/**
|
|
1004
|
+
* The option itself owns selection through `aria-selected` and its click
|
|
1005
|
+
* handler, so the checkbox is decorative. `inert` keeps it out of the
|
|
1006
|
+
* accessibility tree and out of hit testing - without it the wrapping label
|
|
1007
|
+
* forwards a second click to the input, which bubbles back to the option and
|
|
1008
|
+
* undoes the toggle.
|
|
1009
|
+
*/
|
|
1010
|
+
const MultiSelectIndicator = ({ isDisabled, isSelected, }) => {
|
|
1011
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: "ndl-combobox-option-content-multi-select-indicator", inert: true, children: (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { ariaLabel: "Select", isChecked: isSelected, isDisabled: isDisabled }) }));
|
|
1012
|
+
};
|
|
1013
|
+
/**
|
|
1014
|
+
* The visible group header text, shared by both listbox paths: nested inside a
|
|
1015
|
+
* `role="group"` wrapper below the row threshold, or as a presentational row of
|
|
1016
|
+
* its own while windowed.
|
|
1017
|
+
*/
|
|
1018
|
+
const GroupHeader = ({ label, id, isPresentational, }) => {
|
|
1019
|
+
return ((0, jsx_runtime_1.jsx)("div", { id: id, role: isPresentational === true ? 'presentation' : undefined, className: "ndl-combobox-group-header", children: label }));
|
|
1020
|
+
};
|
|
1021
|
+
const Group = ({ children, label }) => {
|
|
1022
|
+
const headerId = (0, react_2.useId)();
|
|
1023
|
+
return ((0, jsx_runtime_1.jsxs)("div", { role: "group", "aria-labelledby": headerId, className: "ndl-combobox-group", children: [(0, jsx_runtime_1.jsx)(GroupHeader, { id: headerId, label: label }), children] }));
|
|
1024
|
+
};
|
|
1025
|
+
const EmptyState = ({ children }) => {
|
|
1026
|
+
return ((0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: "body-medium", className: "ndl-combobox-empty-state", htmlAttributes: { role: 'status' }, children: children !== null && children !== void 0 ? children : 'No results found' }));
|
|
1027
|
+
};
|
|
1028
|
+
const LoadingState = ({ children }) => {
|
|
1029
|
+
return ((0, jsx_runtime_1.jsxs)("div", { role: "status", className: "ndl-combobox-empty-state ndl-combobox-loading-state", children: [(0, jsx_runtime_1.jsx)(loading_spinner_1.LoadingSpinner, { size: "small", htmlAttributes: { 'aria-hidden': true } }), (0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: "body-medium", children: children !== null && children !== void 0 ? children : DEFAULT_LOADING_MESSAGE })] }));
|
|
1030
|
+
};
|
|
1031
|
+
//# sourceMappingURL=Combobox.js.map
|