@allxsmith/bestax-bulma 5.61.0 → 5.62.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/dist/bestax.css +1 -1
- package/dist/bestax.css.map +1 -1
- package/dist/esm/components/TreeView.js +14 -10
- package/dist/esm/components/TreeView.js.map +1 -1
- package/dist/esm/form/Combobox.js +8 -13
- package/dist/esm/form/Combobox.js.map +1 -1
- package/dist/esm/form/Field.js.map +1 -1
- package/dist/esm/form/TreeSelect.js +395 -0
- package/dist/esm/form/TreeSelect.js.map +1 -0
- package/dist/esm/form/_pickerInternals/PickerPopover.js +5 -1
- package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
- package/dist/esm/form/_selectInternals/DialogSelect.js +208 -0
- package/dist/esm/form/_selectInternals/DialogSelect.js.map +1 -0
- package/dist/esm/form/_selectInternals/ValueTags.js +28 -0
- package/dist/esm/form/_selectInternals/ValueTags.js.map +1 -0
- package/dist/esm/helpers/treeData.js +97 -0
- package/dist/esm/helpers/treeData.js.map +1 -0
- package/dist/esm/helpers/useComboboxEngine.js +2 -2
- package/dist/esm/helpers/useComboboxEngine.js.map +1 -1
- package/dist/esm/helpers/useControlled.js +30 -0
- package/dist/esm/helpers/useControlled.js.map +1 -0
- package/dist/esm/helpers/useTreeView.js +15 -13
- package/dist/esm/helpers/useTreeView.js.map +1 -1
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +1099 -392
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +1 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/form/Field.d.ts +1 -1
- package/dist/types/form/TreeSelect.d.ts +308 -0
- package/dist/types/form/_pickerInternals/PickerPopover.d.ts +8 -0
- package/dist/types/form/_selectInternals/DialogSelect.d.ts +86 -0
- package/dist/types/form/_selectInternals/ValueTags.d.ts +40 -0
- package/dist/types/helpers/treeData.d.ts +48 -0
- package/dist/types/helpers/useControlled.d.ts +14 -0
- package/dist/types/helpers/useTreeView.d.ts +30 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types-cjs/form/Field.d.ts +1 -1
- package/dist/types-cjs/form/TreeSelect.d.ts +308 -0
- package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +8 -0
- package/dist/types-cjs/form/_selectInternals/DialogSelect.d.ts +86 -0
- package/dist/types-cjs/form/_selectInternals/ValueTags.d.ts +40 -0
- package/dist/types-cjs/helpers/treeData.d.ts +48 -0
- package/dist/types-cjs/helpers/useControlled.d.ts +14 -0
- package/dist/types-cjs/helpers/useTreeView.d.ts +30 -0
- package/dist/types-cjs/index.d.ts +1 -0
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/form/_index.scss +1 -0
- package/src/scss/form/_picker-popover.scss +6 -0
- package/src/scss/form/_treeselect.scss +146 -0
package/dist/index.cjs
CHANGED
|
@@ -10937,6 +10937,30 @@ function RevealImpl(revealProps) {
|
|
|
10937
10937
|
const Reveal = RevealImpl;
|
|
10938
10938
|
Reveal.displayName = 'Reveal';
|
|
10939
10939
|
|
|
10940
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
10941
|
+
/**
|
|
10942
|
+
* A value the caller holds, or one this hook holds when the caller doesn't:
|
|
10943
|
+
* the `x` / `defaultX` pair of a component's props. The caller holds it
|
|
10944
|
+
* whenever `controlled` isn't `undefined`, so `null` is a held value. The
|
|
10945
|
+
* setter changes only the value held here, so a held one changes when the
|
|
10946
|
+
* caller passes the new value, and it reports nothing: the component calls
|
|
10947
|
+
* its own `onXChange`.
|
|
10948
|
+
*
|
|
10949
|
+
* @function useControlled
|
|
10950
|
+
* @param controlled - The caller's value, or `undefined` to hold one here.
|
|
10951
|
+
* @param initial - The value held here to begin with.
|
|
10952
|
+
* @returns The current value, and the setter for the one held here.
|
|
10953
|
+
*/
|
|
10954
|
+
function useControlled(controlled, initial) {
|
|
10955
|
+
const [own, setOwn] = React.useState(initial);
|
|
10956
|
+
const isControlled = controlled !== undefined;
|
|
10957
|
+
const set = (next) => {
|
|
10958
|
+
if (!isControlled)
|
|
10959
|
+
setOwn(next);
|
|
10960
|
+
};
|
|
10961
|
+
return [isControlled ? controlled : own, set];
|
|
10962
|
+
}
|
|
10963
|
+
|
|
10940
10964
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
10941
10965
|
/** The keys and clicks of both checkbox modes, which differ only in what a check reaches. */
|
|
10942
10966
|
const CHECKBOX_ACTIONS = {
|
|
@@ -11010,6 +11034,14 @@ function pathOf(element, { values }) {
|
|
|
11010
11034
|
}
|
|
11011
11035
|
return path;
|
|
11012
11036
|
}
|
|
11037
|
+
/**
|
|
11038
|
+
* The values of the items a tree hides while they stay in it, as TreeSelect's
|
|
11039
|
+
* search hides the items that don't match. A hidden item renders as one under
|
|
11040
|
+
* a closed parent does: nothing on the page, and in a `checkbox` tree it and
|
|
11041
|
+
* its children still register, so a check still reaches them. Internal, for
|
|
11042
|
+
* the components that put a TreeView inside them.
|
|
11043
|
+
*/
|
|
11044
|
+
const TreeViewHiddenContext = React.createContext(new Set());
|
|
11013
11045
|
/**
|
|
11014
11046
|
* Every known item's check state, by the rules in #1096:
|
|
11015
11047
|
*
|
|
@@ -11085,16 +11117,6 @@ function toggledList(records, list, value) {
|
|
|
11085
11117
|
...list.filter(item => !records.has(item)),
|
|
11086
11118
|
];
|
|
11087
11119
|
}
|
|
11088
|
-
/** A value the caller holds, or one this hook holds when the caller doesn't. */
|
|
11089
|
-
function useControlled(controlled, initial) {
|
|
11090
|
-
const [own, setOwn] = React.useState(initial);
|
|
11091
|
-
const isControlled = controlled !== undefined;
|
|
11092
|
-
const set = (next) => {
|
|
11093
|
-
if (!isControlled)
|
|
11094
|
-
setOwn(next);
|
|
11095
|
-
};
|
|
11096
|
-
return [isControlled ? controlled : own, set];
|
|
11097
|
-
}
|
|
11098
11120
|
/**
|
|
11099
11121
|
* The list with `value` added at the end, or, when it's `present`, with
|
|
11100
11122
|
* every copy of it taken out. Everything else stays as it was, in its order.
|
|
@@ -11362,12 +11384,15 @@ function useTreeView(options) {
|
|
|
11362
11384
|
press(item, actions.space);
|
|
11363
11385
|
break;
|
|
11364
11386
|
case '*': {
|
|
11387
|
+
// The siblings on the page: not the ones a component around the
|
|
11388
|
+
// tree hides, which stay registered.
|
|
11365
11389
|
const closed = [];
|
|
11366
11390
|
registry.records.forEach((record, value) => {
|
|
11367
11391
|
if (record.parent === item.parent &&
|
|
11368
11392
|
record.isParent &&
|
|
11369
11393
|
!record.disabled &&
|
|
11370
|
-
!expanded.includes(value)
|
|
11394
|
+
!expanded.includes(value) &&
|
|
11395
|
+
registry.elements.has(value)) {
|
|
11371
11396
|
closed.push(value);
|
|
11372
11397
|
}
|
|
11373
11398
|
});
|
|
@@ -11844,7 +11869,7 @@ const sayLoaded = (name) => `${name} loaded`;
|
|
|
11844
11869
|
const TOP = { path: [], hidden: false };
|
|
11845
11870
|
const BranchContext = React.createContext(TOP);
|
|
11846
11871
|
/** The box's input lets the row's click toggle it, so a change on its own does nothing. */
|
|
11847
|
-
const ignoreChange = () => { };
|
|
11872
|
+
const ignoreChange$1 = () => { };
|
|
11848
11873
|
/** Whether an `icon` was given something to render. */
|
|
11849
11874
|
const isGiven$1 = (node) => node !== undefined &&
|
|
11850
11875
|
node !== null &&
|
|
@@ -11952,6 +11977,10 @@ const TreeViewRoot = React.forwardRef(function TreeView({ children, selectionMod
|
|
|
11952
11977
|
const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon, children, hasChildren = false, isLoading = false, loadError, onRetry, disabled = false, className, onClick, onKeyDown, onFocus, onBlur, ...props }, ref) {
|
|
11953
11978
|
const tree = React.useContext(TreeViewContext);
|
|
11954
11979
|
const branch = React.useContext(BranchContext);
|
|
11980
|
+
// Hidden by a component around the tree, such as TreeSelect's search,
|
|
11981
|
+
// it renders as an item under a closed parent does.
|
|
11982
|
+
const hiddenItems = React.useContext(TreeViewHiddenContext);
|
|
11983
|
+
const hiddenHere = branch.hidden || hiddenItems.has(value);
|
|
11955
11984
|
// Also the item's key in the live region.
|
|
11956
11985
|
const labelId = React.useId();
|
|
11957
11986
|
const hasItems = React.Children.toArray(children).length > 0;
|
|
@@ -11959,7 +11988,7 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
|
|
|
11959
11988
|
const open = isParent && !!tree?.isOpen(value);
|
|
11960
11989
|
const parent = branch.path[branch.path.length - 1] ?? null;
|
|
11961
11990
|
// The row at the end of its group, while it's open and on the page.
|
|
11962
|
-
const shown = open && !
|
|
11991
|
+
const shown = open && !hiddenHere;
|
|
11963
11992
|
const status = !shown
|
|
11964
11993
|
? null
|
|
11965
11994
|
: isLoading
|
|
@@ -12024,8 +12053,8 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
|
|
|
12024
12053
|
const toggleRef = React.useRef(null);
|
|
12025
12054
|
const childBranch = React.useMemo(() => ({
|
|
12026
12055
|
path: [...branch.path, value],
|
|
12027
|
-
hidden:
|
|
12028
|
-
}), [branch, value, open]);
|
|
12056
|
+
hidden: hiddenHere || !open,
|
|
12057
|
+
}), [branch, value, hiddenHere, open]);
|
|
12029
12058
|
const { selected, checked } = tree?.stateOf(value, branch.path, disabled) ?? {};
|
|
12030
12059
|
const rowClass = usePrefixedClassNames('tree-view-row', {
|
|
12031
12060
|
'is-selected': selected,
|
|
@@ -12034,10 +12063,10 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
|
|
|
12034
12063
|
if (!tree)
|
|
12035
12064
|
return null;
|
|
12036
12065
|
// While it's open the children render in its group. In a `checkbox` tree
|
|
12037
|
-
// they render while it's closed too, drawing nothing, so a
|
|
12038
|
-
// what's under it.
|
|
12039
|
-
const items = (
|
|
12040
|
-
if (
|
|
12066
|
+
// they render while it's closed or hidden too, drawing nothing, so a
|
|
12067
|
+
// check knows what's under it.
|
|
12068
|
+
const items = (shown || tree.rendersClosed) && (jsxRuntime.jsx(BranchContext.Provider, { value: childBranch, children: children }));
|
|
12069
|
+
if (hiddenHere)
|
|
12041
12070
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: items });
|
|
12042
12071
|
const info = { value, parent, disabled, isParent, open };
|
|
12043
12072
|
// A position the caller gives wins, for a set the page holds only part of.
|
|
@@ -12073,7 +12102,7 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
|
|
|
12073
12102
|
if (!(event.target instanceof HTMLInputElement)) {
|
|
12074
12103
|
event.preventDefault();
|
|
12075
12104
|
}
|
|
12076
|
-
}, children: jsxRuntime.jsx(Checkbox, { tabIndex: -1, checked: checked === true, indeterminate: checked === 'mixed', disabled: disabled, onChange: ignoreChange }) })), isGiven$1(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), jsxRuntime.jsx("span", { className: labelClass, id: labelId, ref: labelRef, children: label })] }), open && (hasItems || status) ? (jsxRuntime.jsxs("ul", { className: groupClass, role: "group", children: [items, status && (jsxRuntime.jsx(StatusRow, { kind: status, parent: value, level: branch.path.length + 2, message: loadError, onRetry: onRetry, labelRef: statusLabelRef }))] })) : (items)] }));
|
|
12105
|
+
}, children: jsxRuntime.jsx(Checkbox, { tabIndex: -1, checked: checked === true, indeterminate: checked === 'mixed', disabled: disabled, onChange: ignoreChange$1 }) })), isGiven$1(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), jsxRuntime.jsx("span", { className: labelClass, id: labelId, ref: labelRef, children: label })] }), open && (hasItems || status) ? (jsxRuntime.jsxs("ul", { className: groupClass, role: "group", children: [items, status && (jsxRuntime.jsx(StatusRow, { kind: status, parent: value, level: branch.path.length + 2, message: loadError, onRetry: onRetry, labelRef: statusLabelRef }))] })) : (items)] }));
|
|
12077
12106
|
});
|
|
12078
12107
|
TreeViewItem.displayName = 'TreeView.Item';
|
|
12079
12108
|
/**
|
|
@@ -12342,7 +12371,7 @@ ScrollArea.displayName = 'ScrollArea';
|
|
|
12342
12371
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
12343
12372
|
/** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */
|
|
12344
12373
|
const isComposing$2 = (event) => event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
|
|
12345
|
-
const sameValues = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
|
|
12374
|
+
const sameValues$1 = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
|
|
12346
12375
|
/**
|
|
12347
12376
|
* The keys, the open state, the highlight and the ARIA of an editable
|
|
12348
12377
|
* combobox whose list the app renders and filters (#1083). DOM focus stays
|
|
@@ -12444,7 +12473,7 @@ function useComboboxEngine({ open: controlledOpen, defaultOpen, onOpenChange, on
|
|
|
12444
12473
|
}
|
|
12445
12474
|
const options = shownOptions();
|
|
12446
12475
|
const values = options.map(option => option.value);
|
|
12447
|
-
const changed = !sameValues(values, shownValuesRef.current);
|
|
12476
|
+
const changed = !sameValues$1(values, shownValuesRef.current);
|
|
12448
12477
|
shownValuesRef.current = values;
|
|
12449
12478
|
setListShown(values.length > 0);
|
|
12450
12479
|
const landing = landingRef.current ?? (opened ? 'open' : null);
|
|
@@ -18862,6 +18891,25 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
18862
18891
|
});
|
|
18863
18892
|
Autocomplete.displayName = 'Autocomplete';
|
|
18864
18893
|
|
|
18894
|
+
const everyValue = () => true;
|
|
18895
|
+
/**
|
|
18896
|
+
* The values of a field holding several, as Bulma tags under it, each with a
|
|
18897
|
+
* `Delete` that takes its value off. Nothing while there are none.
|
|
18898
|
+
*/
|
|
18899
|
+
function ValueTags({ values, ids, labelFor, disabled, ariaCloseLabel, onRemove, removable = everyValue, }) {
|
|
18900
|
+
if (values.length === 0)
|
|
18901
|
+
return null;
|
|
18902
|
+
return (jsxRuntime.jsx(Tags, { mt: "2", children: values.map((value, index) => (jsxRuntime.jsxs(Tag, { children: [jsxRuntime.jsx("span", { id: ids[index], children: labelFor(value) }), !disabled && removable(value) && (jsxRuntime.jsx(Delete, { size: "small", ariaLabel: ariaCloseLabel || `Remove ${labelFor(value)}`, "aria-describedby": ariaCloseLabel ? ids[index] : undefined, onClick: () => onRemove(value) }))] }, value))) }));
|
|
18903
|
+
}
|
|
18904
|
+
/** The ids `ValueTags` gives its labels, built from one id of the field's. */
|
|
18905
|
+
const tagIds = (base, values) => values.map((_, index) => `${base}-${index}`);
|
|
18906
|
+
/** A hidden input for each value, so a form posts them under `name`. */
|
|
18907
|
+
function HiddenValues({ name, values, form, disabled, }) {
|
|
18908
|
+
if (!name)
|
|
18909
|
+
return null;
|
|
18910
|
+
return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: values.map(value => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: value, form: form, disabled: disabled }, value))) }));
|
|
18911
|
+
}
|
|
18912
|
+
|
|
18865
18913
|
const validComboboxSizes = ['small', 'medium', 'large'];
|
|
18866
18914
|
// What the status region announces: the `Combobox.Empty` content, as the one
|
|
18867
18915
|
// item it's handed while that content is on screen because of the user's
|
|
@@ -19254,7 +19302,7 @@ ComboboxEmpty.displayName = 'ComboboxEmpty';
|
|
|
19254
19302
|
const toItem = (entry) => typeof entry === 'string' ? { value: entry, label: entry } : entry;
|
|
19255
19303
|
const labelOf = (item) => item.label ?? item.value;
|
|
19256
19304
|
/** One value or several, as a list: one value is a list of none or one. */
|
|
19257
|
-
const asList = (value) => value == null ? [] : Array.isArray(value) ? value : [value];
|
|
19305
|
+
const asList$1 = (value) => value == null ? [] : Array.isArray(value) ? value : [value];
|
|
19258
19306
|
/**
|
|
19259
19307
|
* The Combobox given `data`. It renders the parts itself, filters the items
|
|
19260
19308
|
* by label as the user types, and keeps the value, so a pick goes through
|
|
@@ -19286,9 +19334,9 @@ const ComboboxFromData = React.forwardRef((props, ref) => {
|
|
|
19286
19334
|
return item ? labelOf(item) : picked;
|
|
19287
19335
|
};
|
|
19288
19336
|
const controlled = value !== undefined;
|
|
19289
|
-
const [ownValue, setOwnValue] = React.useState(() => asList(defaultValue));
|
|
19337
|
+
const [ownValue, setOwnValue] = React.useState(() => asList$1(defaultValue));
|
|
19290
19338
|
const matchText = useLocaleMatcher();
|
|
19291
|
-
const picked = controlled ? asList(value) : ownValue;
|
|
19339
|
+
const picked = controlled ? asList$1(value) : ownValue;
|
|
19292
19340
|
const change = (next) => {
|
|
19293
19341
|
if (!controlled)
|
|
19294
19342
|
setOwnValue(next);
|
|
@@ -19340,7 +19388,7 @@ const ComboboxFromData = React.forwardRef((props, ref) => {
|
|
|
19340
19388
|
};
|
|
19341
19389
|
// The picks, for the field's description, since its text is the search.
|
|
19342
19390
|
const tagsId = React.useId();
|
|
19343
|
-
const
|
|
19391
|
+
const ids = tagIds(tagsId, picked);
|
|
19344
19392
|
// Each group where `data` first names it, and the items with none
|
|
19345
19393
|
// together where the first of them is.
|
|
19346
19394
|
const sections = new Map();
|
|
@@ -19354,14 +19402,11 @@ const ComboboxFromData = React.forwardRef((props, ref) => {
|
|
|
19354
19402
|
if (!next)
|
|
19355
19403
|
settle();
|
|
19356
19404
|
onOpenChange?.(next);
|
|
19357
|
-
}, below: multiple
|
|
19358
|
-
? picked.length > 0 && (jsxRuntime.jsx(Tags, { mt: "2", children: picked.map((v, index) => (jsxRuntime.jsxs(Tag, { children: [jsxRuntime.jsx("span", { id: tagIds[index], children: labelFor(v) }), !disabled && (jsxRuntime.jsx(Delete, { size: "small", ariaLabel: ariaCloseLabel || `Remove ${labelFor(v)}`, "aria-describedby": ariaCloseLabel ? tagIds[index] : undefined, onClick: () => remove(v) }))] }, v))) }))
|
|
19359
|
-
: undefined, children: [jsxRuntime.jsx(ComboboxInput, { ...named, ref: inputRef, id: id, disabled: disabled,
|
|
19405
|
+
}, below: multiple ? (jsxRuntime.jsx(ValueTags, { values: picked, ids: ids, labelFor: labelFor, disabled: disabled, ariaCloseLabel: ariaCloseLabel, onRemove: remove })) : undefined, children: [jsxRuntime.jsx(ComboboxInput, { ...named, ref: inputRef, id: id, disabled: disabled,
|
|
19360
19406
|
// The browser's check reads the field's text, and only a pick
|
|
19361
19407
|
// fills it for good, so it's required until there is one.
|
|
19362
|
-
required: required && picked.length === 0, "aria-required": required || undefined, form: form, "aria-describedby": multiple && picked.length > 0 ?
|
|
19363
|
-
(multiple || picked.length === 0 ? '' : labelFor(picked[0])), onChange: event => setQuery(event.target.value), onBlur: settle }), inline ? list : jsxRuntime.jsx(ComboboxMenu, { children: list }), name
|
|
19364
|
-
(multiple ? picked : [picked[0] ?? '']).map(v => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: v, form: form, disabled: disabled }, v)))] }));
|
|
19408
|
+
required: required && picked.length === 0, "aria-required": required || undefined, form: form, "aria-describedby": multiple && picked.length > 0 ? ids.join(' ') : undefined, placeholder: placeholder, value: query ??
|
|
19409
|
+
(multiple || picked.length === 0 ? '' : labelFor(picked[0])), onChange: event => setQuery(event.target.value), onBlur: settle }), inline ? list : jsxRuntime.jsx(ComboboxMenu, { children: list }), jsxRuntime.jsx(HiddenValues, { name: name, values: multiple ? picked : [picked[0] ?? ''], form: form, disabled: disabled })] }));
|
|
19365
19410
|
});
|
|
19366
19411
|
ComboboxFromData.displayName = 'ComboboxFromData';
|
|
19367
19412
|
/**
|
|
@@ -19434,138 +19479,1033 @@ const Combobox = withSubComponents(ComboboxRoot, {
|
|
|
19434
19479
|
Empty: ComboboxEmpty,
|
|
19435
19480
|
}, 'Combobox');
|
|
19436
19481
|
|
|
19437
|
-
//
|
|
19438
|
-
//
|
|
19439
|
-
//
|
|
19440
|
-
|
|
19482
|
+
// INTERNAL: deliberately not exported from src/index.ts. Walking the nodes a
|
|
19483
|
+
// TreeSelect takes as `data`, so a picker can name, check and search a tree
|
|
19484
|
+
// while it isn't on the page.
|
|
19485
|
+
/**
|
|
19486
|
+
* Every node by its value, in the order the tree reads from top to bottom.
|
|
19487
|
+
* Values are meant to be unique, and the first node with a value wins.
|
|
19488
|
+
*/
|
|
19489
|
+
function indexTree(data) {
|
|
19490
|
+
const index = new Map();
|
|
19491
|
+
const walk = (nodes, path) => {
|
|
19492
|
+
for (const node of nodes) {
|
|
19493
|
+
if (!index.has(node.value))
|
|
19494
|
+
index.set(node.value, { node, path });
|
|
19495
|
+
if (node.children)
|
|
19496
|
+
walk(node.children, [...path, node.value]);
|
|
19497
|
+
}
|
|
19498
|
+
};
|
|
19499
|
+
walk(data, []);
|
|
19500
|
+
return index;
|
|
19501
|
+
}
|
|
19502
|
+
/** What a node is called: its `label`, or its `value` when that's empty. */
|
|
19503
|
+
const nodeLabel = (node) => node.label || node.value;
|
|
19504
|
+
/** The nodes under a node, none when it has no `children`. */
|
|
19505
|
+
const childNodes = (node) => node.children ?? [];
|
|
19506
|
+
/** Whether a node is a parent: it has children, or `hasChildren` says it will. */
|
|
19507
|
+
const isParentNode = (node) => childNodes(node).length > 0 || !!node.hasChildren;
|
|
19508
|
+
/**
|
|
19509
|
+
* The records TreeView's check rules read, built from the nodes rather than
|
|
19510
|
+
* from the items on the page, so the checked list can be worked out while
|
|
19511
|
+
* the tree isn't there.
|
|
19512
|
+
*/
|
|
19513
|
+
function checkRecords(index) {
|
|
19514
|
+
const records = new Map();
|
|
19515
|
+
index.forEach(({ node, path }, value) => records.set(value, {
|
|
19516
|
+
parent: path[path.length - 1] ?? null,
|
|
19517
|
+
disabled: !!node.disabled,
|
|
19518
|
+
isParent: isParentNode(node),
|
|
19519
|
+
}));
|
|
19520
|
+
return records;
|
|
19521
|
+
}
|
|
19441
19522
|
/**
|
|
19442
|
-
* The
|
|
19443
|
-
*
|
|
19444
|
-
*
|
|
19445
|
-
*
|
|
19446
|
-
*
|
|
19447
|
-
*
|
|
19448
|
-
|
|
19449
|
-
|
|
19450
|
-
|
|
19451
|
-
|
|
19452
|
-
|
|
19453
|
-
|
|
19454
|
-
|
|
19455
|
-
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
|
|
19462
|
-
* />
|
|
19463
|
-
*/
|
|
19464
|
-
const Taginput = React.forwardRef(({
|
|
19465
|
-
// Field props
|
|
19466
|
-
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = [], data: dataProp, placeholder, field = 'label', allowNew = true, allowDuplicates = false, openOnFocus = false, removeOnKeys = true, confirmKeys = ['Enter', ','], closable = true, attached = false, maxTags, maxlength, disabled: disabledProp, readonly = false, rounded = false, ellipsis = false, hasCounter = true, onPasteSeparators = [','], beforeAdding, createTag, keepFirst = false, keepOpen = true, loading = false, ariaCloseLabel, icon, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, tagColor, size, onChange, onAdd, onRemove, onTyping, tagTemplate, name, form, id, className, ...props }, ref) => {
|
|
19467
|
-
const disabled = useFieldsetDisabled(disabledProp);
|
|
19468
|
-
const insideField = useInsideField();
|
|
19469
|
-
const insideControl = useInsideControl();
|
|
19470
|
-
const ownField = rendersOwnField('Taginput', {
|
|
19471
|
-
insideField,
|
|
19472
|
-
insideControl,
|
|
19473
|
-
label,
|
|
19474
|
-
message,
|
|
19475
|
-
horizontal,
|
|
19476
|
-
fieldClassName,
|
|
19477
|
-
});
|
|
19478
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
19479
|
-
const { classPrefix } = useConfig();
|
|
19480
|
-
const defaultIconLibrary = useIconLibrary();
|
|
19481
|
-
const formatNumber = useScopedNumberFormat();
|
|
19482
|
-
const matchText = useLocaleMatcher();
|
|
19483
|
-
const resolvedLibrary = iconLibraryProp || defaultIconLibrary || 'fa';
|
|
19484
|
-
const pcn = (...args) => prefixedClassNames(classPrefix, ...args);
|
|
19485
|
-
const containerRef = React.useRef(null);
|
|
19486
|
-
const inputRef = React.useRef(null);
|
|
19487
|
-
const dropdownRef = React.useRef(null);
|
|
19488
|
-
const [internalTags, setInternalTags] = React.useState(defaultValue);
|
|
19489
|
-
const [inputValue, setInputValue] = React.useState('');
|
|
19490
|
-
const [isActive, setIsActive] = React.useState(false);
|
|
19491
|
-
const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
|
|
19492
|
-
const baseId = React.useId();
|
|
19493
|
-
const listboxId = `${baseId}-listbox`;
|
|
19494
|
-
const counterId = `${baseId}-counter`;
|
|
19495
|
-
// With `data` the input offers suggestions, so it's a combobox, even
|
|
19496
|
-
// while none match. Without it there's never a list, so it stays a plain
|
|
19497
|
-
// text field.
|
|
19498
|
-
const hasSuggestions = dataProp !== undefined;
|
|
19499
|
-
const data = dataProp ?? [];
|
|
19500
|
-
// Use controlled or internal tags
|
|
19501
|
-
const tags = controlledValue !== undefined ? controlledValue : internalTags;
|
|
19502
|
-
const isMaxReached = maxTags !== undefined && tags.length >= maxTags;
|
|
19503
|
-
// The suggestion list points `aria-labelledby` at the label that names
|
|
19504
|
-
// the input, so with suggestions it needs that label's id.
|
|
19505
|
-
const { controlId, fieldLabelProps, labelled, labelId: listLabelId, } = useAutoLabelId({
|
|
19506
|
-
label,
|
|
19507
|
-
id,
|
|
19508
|
-
labelProps,
|
|
19509
|
-
rendersLabel: ownField,
|
|
19510
|
-
needsLabelId: hasSuggestions,
|
|
19523
|
+
* The checked list a `checkedStrategy` reports for the items `listed` checks
|
|
19524
|
+
* by TreeView's rules, in the tree's order. `all` is every checked item;
|
|
19525
|
+
* `child` the checked items with no enabled item under them; `parent` the
|
|
19526
|
+
* checked items whose parent isn't. A disabled item is checked by its own
|
|
19527
|
+
* value only, so a checked one is in every list. Values no node has are
|
|
19528
|
+
* kept, at the end. Passed back in, each list checks the same items.
|
|
19529
|
+
*/
|
|
19530
|
+
function checkedByStrategy(records, listed, strategy) {
|
|
19531
|
+
const { states, ends } = readChecks(records, new Set(listed));
|
|
19532
|
+
const isEnd = new Set(ends);
|
|
19533
|
+
const on = (value) => value !== null && states.get(value) === true;
|
|
19534
|
+
const kept = [];
|
|
19535
|
+
records.forEach(({ parent, disabled }, value) => {
|
|
19536
|
+
if (!on(value))
|
|
19537
|
+
return;
|
|
19538
|
+
if (strategy === 'all' ||
|
|
19539
|
+
disabled ||
|
|
19540
|
+
(strategy === 'child' ? isEnd.has(value) : !on(parent))) {
|
|
19541
|
+
kept.push(value);
|
|
19542
|
+
}
|
|
19511
19543
|
});
|
|
19512
|
-
|
|
19513
|
-
|
|
19514
|
-
|
|
19515
|
-
|
|
19516
|
-
|
|
19517
|
-
|
|
19518
|
-
|
|
19519
|
-
|
|
19544
|
+
const unknown = new Set(listed.filter(value => !records.has(value)));
|
|
19545
|
+
return [...kept, ...unknown];
|
|
19546
|
+
}
|
|
19547
|
+
/**
|
|
19548
|
+
* What a search shows of a tree: the nodes that match, the nodes above them,
|
|
19549
|
+
* so the results stay a tree, and the nodes under them, so a matching branch
|
|
19550
|
+
* can still be opened.
|
|
19551
|
+
*/
|
|
19552
|
+
function searchTree(data, matches) {
|
|
19553
|
+
const hidden = new Set();
|
|
19554
|
+
const open = [];
|
|
19555
|
+
// Whether the nodes, or any node under them, match. `above` is the path
|
|
19556
|
+
// down to them.
|
|
19557
|
+
const walk = (nodes, underMatch, above) => {
|
|
19558
|
+
let found = false;
|
|
19559
|
+
for (const node of nodes) {
|
|
19560
|
+
const path = [...above, node];
|
|
19561
|
+
const self = matches(node, path);
|
|
19562
|
+
const below = walk(childNodes(node), underMatch || self, path);
|
|
19563
|
+
if (below)
|
|
19564
|
+
open.push(node.value);
|
|
19565
|
+
if (!underMatch && !self && !below)
|
|
19566
|
+
hidden.add(node.value);
|
|
19567
|
+
found = found || self || below;
|
|
19568
|
+
}
|
|
19569
|
+
return found;
|
|
19520
19570
|
};
|
|
19521
|
-
|
|
19522
|
-
|
|
19523
|
-
|
|
19524
|
-
|
|
19525
|
-
|
|
19526
|
-
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
|
|
19530
|
-
|
|
19571
|
+
return { hidden, open, found: walk(data, false, []) };
|
|
19572
|
+
}
|
|
19573
|
+
|
|
19574
|
+
const isBrowser = typeof window !== 'undefined';
|
|
19575
|
+
const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'bottom-left', appendToBody = false, className, trapFocus = true, closeOnClickOutside = true, closeOnEscape = true, ariaLabel, ariaLabelledBy, role = 'dialog', id, restoreFocusRef, initialFocusSelector, localeScope, matchWidth = false, followContent = false, }) => {
|
|
19576
|
+
const panelRef = React.useRef(null);
|
|
19577
|
+
const resolveContainer = usePortalContainer();
|
|
19578
|
+
// Read as the trap turns on, once the panel's content has rendered.
|
|
19579
|
+
const initialFocusRef = React.useMemo(() => initialFocusSelector === undefined
|
|
19580
|
+
? undefined
|
|
19581
|
+
: {
|
|
19582
|
+
get current() {
|
|
19583
|
+
return (panelRef.current?.querySelector(initialFocusSelector) ?? null);
|
|
19584
|
+
},
|
|
19585
|
+
}, [initialFocusSelector]);
|
|
19586
|
+
const resolved = useAnchoredPosition(anchorRef, panelRef, {
|
|
19587
|
+
active: isOpen,
|
|
19588
|
+
position,
|
|
19589
|
+
fixed: appendToBody,
|
|
19590
|
+
// The gap comes from `--bulma-picker-popover-offset` as a margin, in place
|
|
19591
|
+
// or portaled, so the coordinates leave it out and `auto` reads it off
|
|
19592
|
+
// the panel.
|
|
19593
|
+
offset: 0,
|
|
19594
|
+
styledGap: marginGap,
|
|
19595
|
+
matchWidth,
|
|
19596
|
+
followContent,
|
|
19531
19597
|
});
|
|
19532
|
-
//
|
|
19533
|
-
//
|
|
19534
|
-
|
|
19535
|
-
|
|
19598
|
+
// Read when a press lands outside or a key arrives, so both listeners stay
|
|
19599
|
+
// put while `onClose` changes identity, as an inline one does on every
|
|
19600
|
+
// render of the field.
|
|
19601
|
+
const onCloseRef = useLatestRef(onClose);
|
|
19536
19602
|
React.useEffect(() => {
|
|
19537
|
-
if (
|
|
19538
|
-
|
|
19539
|
-
|
|
19540
|
-
|
|
19541
|
-
|
|
19542
|
-
|
|
19543
|
-
|
|
19544
|
-
|
|
19603
|
+
if (!isOpen || !closeOnClickOutside || !isBrowser)
|
|
19604
|
+
return undefined;
|
|
19605
|
+
// Listen on `pointerdown` (rather than `mousedown`) so touch starts
|
|
19606
|
+
// outside the popover dismiss it on mobile — including swipe-from-outside
|
|
19607
|
+
// gestures, where the synthetic mouse event would otherwise lag a few
|
|
19608
|
+
// hundred ms behind the actual touch.
|
|
19609
|
+
const handler = (e) => {
|
|
19610
|
+
const path = e.composedPath();
|
|
19611
|
+
if (panelRef.current && path.includes(panelRef.current))
|
|
19612
|
+
return;
|
|
19613
|
+
if (anchorRef.current && path.includes(anchorRef.current))
|
|
19614
|
+
return;
|
|
19615
|
+
onCloseRef.current();
|
|
19616
|
+
};
|
|
19617
|
+
document.addEventListener('pointerdown', handler);
|
|
19618
|
+
return () => document.removeEventListener('pointerdown', handler);
|
|
19619
|
+
}, [isOpen, closeOnClickOutside, anchorRef, onCloseRef]);
|
|
19620
|
+
// Escape closes it, and so does Alt+ArrowUp, the combobox's key for it,
|
|
19621
|
+
// which the calendar and the time wheels inside leave to it. Escape closes
|
|
19622
|
+
// it from anywhere, but the key is only the picker's when it was pressed in
|
|
19623
|
+
// the panel or the field: then it's marked used, so a Modal or Sidebar
|
|
19624
|
+
// around the picker stays open. From anywhere else, such as a dialog opened
|
|
19625
|
+
// over the picker, the key goes on to whatever has focus.
|
|
19626
|
+
React.useEffect(() => {
|
|
19627
|
+
if (!isOpen || !closeOnEscape || !isBrowser)
|
|
19628
|
+
return undefined;
|
|
19629
|
+
const handler = (e) => {
|
|
19630
|
+
if (e.key === 'Escape') {
|
|
19631
|
+
if (isEventInside(e, panelRef.current) ||
|
|
19632
|
+
isEventInside(e, anchorRef.current)) {
|
|
19633
|
+
e.preventDefault();
|
|
19634
|
+
}
|
|
19635
|
+
onCloseRef.current();
|
|
19636
|
+
}
|
|
19637
|
+
else if (e.altKey && e.key === 'ArrowUp') {
|
|
19638
|
+
e.preventDefault();
|
|
19639
|
+
e.stopPropagation();
|
|
19640
|
+
onCloseRef.current();
|
|
19641
|
+
}
|
|
19642
|
+
};
|
|
19643
|
+
document.addEventListener('keydown', handler);
|
|
19644
|
+
return () => document.removeEventListener('keydown', handler);
|
|
19645
|
+
}, [isOpen, closeOnEscape, anchorRef, onCloseRef]);
|
|
19646
|
+
useFocusTrap(panelRef, {
|
|
19647
|
+
active: isOpen && trapFocus,
|
|
19648
|
+
initialFocusRef,
|
|
19649
|
+
restoreFocus: restoreFocusRef ?? true,
|
|
19650
|
+
});
|
|
19651
|
+
const panelClass = usePrefixedClassNames('picker-popover', {
|
|
19652
|
+
'is-active': isOpen,
|
|
19653
|
+
[`is-${resolved.position}`]: true,
|
|
19654
|
+
'is-portal': appendToBody,
|
|
19655
|
+
'has-anchor-width': matchWidth,
|
|
19656
|
+
});
|
|
19657
|
+
if (!isOpen)
|
|
19658
|
+
return null;
|
|
19659
|
+
const style = appendToBody
|
|
19660
|
+
? {
|
|
19661
|
+
position: 'fixed',
|
|
19662
|
+
top: resolved.top,
|
|
19663
|
+
left: resolved.left,
|
|
19664
|
+
minWidth: resolved.minWidth,
|
|
19545
19665
|
}
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
19551
|
-
|
|
19552
|
-
|
|
19553
|
-
|
|
19554
|
-
|
|
19555
|
-
|
|
19556
|
-
|
|
19557
|
-
|
|
19558
|
-
|
|
19559
|
-
|
|
19560
|
-
|
|
19561
|
-
|
|
19562
|
-
|
|
19563
|
-
|
|
19564
|
-
|
|
19565
|
-
|
|
19566
|
-
|
|
19567
|
-
|
|
19568
|
-
|
|
19666
|
+
: {};
|
|
19667
|
+
const panel = (jsxRuntime.jsx("div", { ref: panelRef, id: id, role: role, "aria-modal": role === 'dialog' ? 'false' : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, tabIndex: -1, className: classNames(panelClass, className), style: style,
|
|
19668
|
+
// An Escape pressed in the panel is the picker's, and the listener
|
|
19669
|
+
// above claims it. A Dropdown menu around the picker hears Escape in a
|
|
19670
|
+
// React handler, which runs before that listener, so the panel claims
|
|
19671
|
+
// it here too, where React gets to it first.
|
|
19672
|
+
onKeyDown: e => {
|
|
19673
|
+
if (closeOnEscape && e.key === 'Escape')
|
|
19674
|
+
e.preventDefault();
|
|
19675
|
+
}, children: children }));
|
|
19676
|
+
if (appendToBody && isBrowser) {
|
|
19677
|
+
const target = resolveContainer();
|
|
19678
|
+
return reactDom.createPortal(jsxRuntime.jsx(LocalePortalScope, { target: target, scope: localeScope, children: panel }), target);
|
|
19679
|
+
}
|
|
19680
|
+
return panel;
|
|
19681
|
+
};
|
|
19682
|
+
PickerPopover.displayName = 'PickerPopover';
|
|
19683
|
+
|
|
19684
|
+
/**
|
|
19685
|
+
* Whether a key is Alt+ArrowDown or Alt+ArrowUp, which open and close a
|
|
19686
|
+
* picker's popover. The calendar and the time wheels leave both to it rather
|
|
19687
|
+
* than reading them as plain arrows.
|
|
19688
|
+
*/
|
|
19689
|
+
const isPopoverToggleKey = (e) => e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp');
|
|
19690
|
+
const DEFAULT_PICKER_LABELS = {
|
|
19691
|
+
prevMonth: 'Previous month',
|
|
19692
|
+
nextMonth: 'Next month',
|
|
19693
|
+
prevYear: 'Previous year',
|
|
19694
|
+
nextYear: 'Next year',
|
|
19695
|
+
chooseDate: 'Choose date',
|
|
19696
|
+
chooseMonth: 'Choose month',
|
|
19697
|
+
chooseYear: 'Choose year',
|
|
19698
|
+
rangeStart: 'Start date',
|
|
19699
|
+
rangeEnd: 'End date',
|
|
19700
|
+
rangePreviewEnd: 'Choose as end date',
|
|
19701
|
+
rangeSeparator: '–',
|
|
19702
|
+
chooseDateRange: 'Choose date range',
|
|
19703
|
+
hours: 'hours',
|
|
19704
|
+
minutes: 'minutes',
|
|
19705
|
+
seconds: 'seconds',
|
|
19706
|
+
ampm: 'AM/PM',
|
|
19707
|
+
increaseHours: 'Increase hours',
|
|
19708
|
+
decreaseHours: 'Decrease hours',
|
|
19709
|
+
increaseMinutes: 'Increase minutes',
|
|
19710
|
+
decreaseMinutes: 'Decrease minutes',
|
|
19711
|
+
increaseSeconds: 'Increase seconds',
|
|
19712
|
+
decreaseSeconds: 'Decrease seconds',
|
|
19713
|
+
toggleAmPm: 'Toggle AM/PM',
|
|
19714
|
+
chooseTime: 'Choose time',
|
|
19715
|
+
chooseDateTime: 'Choose date and time',
|
|
19716
|
+
now: 'Now',
|
|
19717
|
+
today: 'Today',
|
|
19718
|
+
clear: 'Clear',
|
|
19719
|
+
cancel: 'Cancel',
|
|
19720
|
+
ok: 'OK',
|
|
19721
|
+
reset: 'Reset',
|
|
19722
|
+
done: 'Done',
|
|
19723
|
+
time: 'Time',
|
|
19724
|
+
};
|
|
19725
|
+
/**
|
|
19726
|
+
* Merge user-supplied label overrides with the defaults. A key set to
|
|
19727
|
+
* `undefined` keeps its default, so a locale table that is missing a string
|
|
19728
|
+
* still names its control instead of leaving it with no accessible name.
|
|
19729
|
+
*/
|
|
19730
|
+
const mergeLabels = (overrides) => {
|
|
19731
|
+
const merged = { ...DEFAULT_PICKER_LABELS };
|
|
19732
|
+
for (const [key, value] of Object.entries(overrides ?? {})) {
|
|
19733
|
+
if (value !== undefined)
|
|
19734
|
+
merged[key] = value;
|
|
19735
|
+
}
|
|
19736
|
+
return merged;
|
|
19737
|
+
};
|
|
19738
|
+
|
|
19739
|
+
/**
|
|
19740
|
+
* A picker's locale: its own `locale`, else the one a `LocaleProvider` around
|
|
19741
|
+
* it sets, else `undefined`, which leaves `Intl` to use the runtime's.
|
|
19742
|
+
*/
|
|
19743
|
+
function usePickerLocale(own) {
|
|
19744
|
+
const scope = useLocaleScope();
|
|
19745
|
+
return own || scope?.locale;
|
|
19746
|
+
}
|
|
19747
|
+
const NO_ROOT_ATTRS = {};
|
|
19748
|
+
/**
|
|
19749
|
+
* A picker's language and direction inside a `LocaleProvider`, by the rule a
|
|
19750
|
+
* nested provider follows: its `lang` is its own `locale`, else the
|
|
19751
|
+
* provider's, and its `dir` is the `dir` passed to it, else its own locale's
|
|
19752
|
+
* direction when it has one, else the provider's. Outside a provider it
|
|
19753
|
+
* changes nothing. A `locale` `Intl` refuses counts as none here, since the
|
|
19754
|
+
* picker's own formatting reports it.
|
|
19755
|
+
*/
|
|
19756
|
+
function usePickerLocaleScope(ownLocale, ownDir) {
|
|
19757
|
+
const scope = useLocaleScope();
|
|
19758
|
+
return React.useMemo(() => {
|
|
19759
|
+
if (!scope)
|
|
19760
|
+
return { rootAttrs: NO_ROOT_ATTRS };
|
|
19761
|
+
let own;
|
|
19762
|
+
try {
|
|
19763
|
+
own = canonicalLocale(ownLocale);
|
|
19764
|
+
}
|
|
19765
|
+
catch {
|
|
19766
|
+
own = undefined;
|
|
19767
|
+
}
|
|
19768
|
+
const dir = ownDir || (own ? localeDirection(own) : scope.dir);
|
|
19769
|
+
return {
|
|
19770
|
+
rootAttrs: own ? { lang: own, dir } : NO_ROOT_ATTRS,
|
|
19771
|
+
panel: { locale: own || scope.locale, dir },
|
|
19772
|
+
};
|
|
19773
|
+
}, [scope, ownLocale, ownDir]);
|
|
19774
|
+
}
|
|
19775
|
+
/**
|
|
19776
|
+
* Whether a picker's `dayNames` replace the calendar's own, which `Calendar`
|
|
19777
|
+
* does only for a full week of seven.
|
|
19778
|
+
*/
|
|
19779
|
+
function hasOwnDayNames(dayNames) {
|
|
19780
|
+
return dayNames !== undefined && dayNames.length === 7;
|
|
19781
|
+
}
|
|
19782
|
+
/**
|
|
19783
|
+
* The day a picker's week starts on: its own `firstDayOfWeek`, else, inside a
|
|
19784
|
+
* `LocaleProvider`, the first day of the locale the picker uses, else Sunday.
|
|
19785
|
+
* Outside a provider it stays Sunday whatever the locale, as it always has,
|
|
19786
|
+
* and so does a picker inside one with no locale from either, since the
|
|
19787
|
+
* runtime's own differs between the server and the browser.
|
|
19788
|
+
*
|
|
19789
|
+
* It also stays Sunday for a picker that brings its own `dayNames`. The
|
|
19790
|
+
* calendar shows those exactly as given, in calendar order, and before the
|
|
19791
|
+
* provider an unset `firstDayOfWeek` was always Sunday, so names written
|
|
19792
|
+
* Sunday first would sit a day or more off their columns if the week moved
|
|
19793
|
+
* under them.
|
|
19794
|
+
*/
|
|
19795
|
+
function usePickerFirstDayOfWeek(own, locale, hasDayNames) {
|
|
19796
|
+
const inProvider = useLocaleScope() !== null;
|
|
19797
|
+
return React.useMemo(() => {
|
|
19798
|
+
if (own !== undefined)
|
|
19799
|
+
return own;
|
|
19800
|
+
if (!inProvider || !locale || hasDayNames)
|
|
19801
|
+
return 0;
|
|
19802
|
+
try {
|
|
19803
|
+
return localeFirstDay(canonicalLocale(locale));
|
|
19804
|
+
}
|
|
19805
|
+
catch {
|
|
19806
|
+
// A tag Intl refuses: the picker's own formatting says so.
|
|
19807
|
+
return 0;
|
|
19808
|
+
}
|
|
19809
|
+
}, [own, inProvider, locale, hasDayNames]);
|
|
19810
|
+
}
|
|
19811
|
+
|
|
19812
|
+
/**
|
|
19813
|
+
* A field laid over the real one, unseen and letting presses through, so the
|
|
19814
|
+
* browser's `required` check reads it and its message points at the field.
|
|
19815
|
+
*/
|
|
19816
|
+
const requiredProxyStyle = {
|
|
19817
|
+
position: 'absolute',
|
|
19818
|
+
inset: 0,
|
|
19819
|
+
width: '100%',
|
|
19820
|
+
height: '100%',
|
|
19821
|
+
margin: 0,
|
|
19822
|
+
padding: 0,
|
|
19823
|
+
border: 0,
|
|
19824
|
+
opacity: 0,
|
|
19825
|
+
pointerEvents: 'none',
|
|
19826
|
+
// Focused, it opens no on-screen keyboard (`inputMode`), and iOS zooms
|
|
19827
|
+
// in on a field set under 16px. It's unseen, so the size shows nowhere.
|
|
19828
|
+
fontSize: '16px',
|
|
19829
|
+
};
|
|
19830
|
+
/** Keys pressed on their own that don't act, such as Ctrl to stop speech. */
|
|
19831
|
+
const MODIFIER_KEYS = new Set([
|
|
19832
|
+
'Alt',
|
|
19833
|
+
'AltGraph',
|
|
19834
|
+
'CapsLock',
|
|
19835
|
+
'Control',
|
|
19836
|
+
'Fn',
|
|
19837
|
+
'Meta',
|
|
19838
|
+
'NumLock',
|
|
19839
|
+
'ScrollLock',
|
|
19840
|
+
'Shift',
|
|
19841
|
+
]);
|
|
19842
|
+
const ignoreChange = () => { };
|
|
19843
|
+
/**
|
|
19844
|
+
* A read-only `role="combobox"` field, wired as the date pickers' are, that
|
|
19845
|
+
* opens a `PickerPopover` dialog panel: in a `Field` of its own unless one is
|
|
19846
|
+
* around it, with a chevron at its inline end, a clear button, the hidden
|
|
19847
|
+
* inputs for `name` and what the browser's `required` check reads. A click,
|
|
19848
|
+
* Enter, Space, Down or Alt+Down opens the panel, which holds focus, as the
|
|
19849
|
+
* date pickers' does: Tab and Shift+Tab wrap inside it. Escape and Alt+Up
|
|
19850
|
+
* close it and put focus back on the field, and a press outside closes it
|
|
19851
|
+
* where the press put focus.
|
|
19852
|
+
*/
|
|
19853
|
+
function DialogSelect({ component, rootClass, panelClass, text, values, hasValue, open, onOpenChange, onFormReset, onPrintableKey, onClear, clearable, clearLabel, chooseLabel, placeholder, name, form, required = false, disabled, color, size, isRounded, position = 'bottom-left', appendToBody = false, inputRef, fieldRef, describedBy, below, initialFocusSelector, children, label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, className, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, ...props }) {
|
|
19854
|
+
const insideField = useInsideField();
|
|
19855
|
+
const insideControl = useInsideControl();
|
|
19856
|
+
const ownField = rendersOwnField(component, {
|
|
19857
|
+
insideField,
|
|
19858
|
+
insideControl,
|
|
19859
|
+
label,
|
|
19860
|
+
message,
|
|
19861
|
+
horizontal,
|
|
19862
|
+
fieldClassName,
|
|
19863
|
+
});
|
|
19864
|
+
const { controlId, fieldLabelProps, labelId } = useAutoLabelId({
|
|
19865
|
+
label,
|
|
19866
|
+
id,
|
|
19867
|
+
labelProps,
|
|
19868
|
+
rendersLabel: ownField,
|
|
19869
|
+
needsLabelId: true,
|
|
19870
|
+
});
|
|
19871
|
+
const reactId = React.useId();
|
|
19872
|
+
const panelId = `${controlId ?? reactId}-panel`;
|
|
19873
|
+
const localeScope = usePickerLocaleScope(undefined, props.dir);
|
|
19874
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
19875
|
+
const rootRef = React.useRef(null);
|
|
19876
|
+
// The field's name, which the panel takes too unless it's given its own.
|
|
19877
|
+
const fieldName = ariaLabel
|
|
19878
|
+
? { 'aria-label': ariaLabel }
|
|
19879
|
+
: ariaLabelledBy
|
|
19880
|
+
? { 'aria-labelledby': ariaLabelledBy }
|
|
19881
|
+
: labelId
|
|
19882
|
+
? { 'aria-labelledby': labelId }
|
|
19883
|
+
: {};
|
|
19884
|
+
const panelName = chooseLabel
|
|
19885
|
+
? { 'aria-label': chooseLabel }
|
|
19886
|
+
: fieldName;
|
|
19887
|
+
// Whether the field `required` hangs on has focus, which the browser gives
|
|
19888
|
+
// it when it stops a submit. Taking it away or disabling it while it has
|
|
19889
|
+
// focus fires no blur, so that clears it too.
|
|
19890
|
+
const [proxyFocused, setProxyFocused] = React.useState(false);
|
|
19891
|
+
if (proxyFocused && (!required || disabled))
|
|
19892
|
+
setProxyFocused(false);
|
|
19893
|
+
const showClear = !!clearable && hasValue && !disabled;
|
|
19894
|
+
const rootClasses = usePrefixedClassNames(rootClass, {
|
|
19895
|
+
'is-disabled': disabled,
|
|
19896
|
+
'has-clear': showClear,
|
|
19897
|
+
});
|
|
19898
|
+
const inputClass = usePrefixedClassNames('input', {
|
|
19899
|
+
[`is-${color}`]: !!color,
|
|
19900
|
+
[`is-${size}`]: !!size,
|
|
19901
|
+
'is-rounded': !!isRounded,
|
|
19902
|
+
'is-focused': proxyFocused,
|
|
19903
|
+
});
|
|
19904
|
+
const chevronClass = usePrefixedClassNames(`${rootClass}-chevron`);
|
|
19905
|
+
const clearClass = usePrefixedClassNames(`${rootClass}-clear`);
|
|
19906
|
+
const panelClasses = usePrefixedClassNames(panelClass, {
|
|
19907
|
+
[`is-${size}`]: !!size,
|
|
19908
|
+
});
|
|
19909
|
+
const helpClass = usePrefixedClassNames('help', {
|
|
19910
|
+
[`is-${messageColor}`]: !!messageColor,
|
|
19911
|
+
});
|
|
19912
|
+
const isOpen = open && !disabled;
|
|
19913
|
+
// A reset of the field's form puts the value back, as it does a native
|
|
19914
|
+
// field's. The reset event fires before the form resets, and a listener
|
|
19915
|
+
// after this one can still cancel it, so the value goes back a task later,
|
|
19916
|
+
// and only if the reset went ahead, as File's and Dropzone's do. It's
|
|
19917
|
+
// heard on the field's document, or the shadow root it's in, and matched
|
|
19918
|
+
// to the field's form as it is then, so a form its `form` names that
|
|
19919
|
+
// mounts later, or a `form` that changes, is followed, and a reset
|
|
19920
|
+
// already under way still lands.
|
|
19921
|
+
const onFormResetRef = useLatestRef(onFormReset);
|
|
19922
|
+
React.useEffect(() => {
|
|
19923
|
+
const root = inputRef.current.getRootNode();
|
|
19924
|
+
let timer;
|
|
19925
|
+
const onReset = (event) => {
|
|
19926
|
+
if (event.target !== inputRef.current?.form)
|
|
19927
|
+
return;
|
|
19928
|
+
timer = setTimeout(() => {
|
|
19929
|
+
if (!event.defaultPrevented)
|
|
19930
|
+
onFormResetRef.current();
|
|
19931
|
+
});
|
|
19932
|
+
};
|
|
19933
|
+
root.addEventListener('reset', onReset, true);
|
|
19934
|
+
return () => {
|
|
19935
|
+
root.removeEventListener('reset', onReset, true);
|
|
19936
|
+
clearTimeout(timer);
|
|
19937
|
+
};
|
|
19938
|
+
}, [inputRef, onFormResetRef]);
|
|
19939
|
+
const handleKeyDown = (event) => {
|
|
19940
|
+
if (disabled)
|
|
19941
|
+
return;
|
|
19942
|
+
if (isPopoverToggleKey(event)) {
|
|
19943
|
+
event.preventDefault();
|
|
19944
|
+
if (event.key === 'ArrowDown')
|
|
19945
|
+
onOpenChange(true);
|
|
19946
|
+
return;
|
|
19947
|
+
}
|
|
19948
|
+
if (event.altKey || event.ctrlKey || event.metaKey)
|
|
19949
|
+
return;
|
|
19950
|
+
if (event.key === 'Enter' ||
|
|
19951
|
+
event.key === ' ' ||
|
|
19952
|
+
event.key === 'ArrowDown') {
|
|
19953
|
+
event.preventDefault();
|
|
19954
|
+
onOpenChange(true);
|
|
19955
|
+
}
|
|
19956
|
+
else if (event.key.length === 1 &&
|
|
19957
|
+
!event.nativeEvent.isComposing &&
|
|
19958
|
+
onPrintableKey?.(event.key)) {
|
|
19959
|
+
event.preventDefault();
|
|
19960
|
+
}
|
|
19961
|
+
};
|
|
19962
|
+
const fieldDescription = [ariaDescribedBy, describedBy].filter(Boolean).join(' ') || undefined;
|
|
19963
|
+
const field = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("input", { ref: fieldRef, type: "text", className: inputClass, id: controlId, readOnly: true, role: "combobox", "aria-haspopup": "dialog", "aria-expanded": isOpen, "aria-controls": isOpen ? panelId : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": fieldDescription, "aria-required": required || undefined, autoComplete: "off", value: text, placeholder: placeholder, disabled: disabled, form: form,
|
|
19964
|
+
// A disabled field takes no clicks.
|
|
19965
|
+
onClick: () => onOpenChange(!isOpen), onKeyDown: handleKeyDown }), showClear && (jsxRuntime.jsx(Delete, { className: clearClass, size: size, ariaLabel: clearLabel, onClick: () => {
|
|
19966
|
+
onClear();
|
|
19967
|
+
onOpenChange(false);
|
|
19968
|
+
inputRef.current?.focus();
|
|
19969
|
+
} })), jsxRuntime.jsx("span", { className: chevronClass, "aria-hidden": "true", children: jsxRuntime.jsx(Icon, { name: "chevron-down", size: size }) })] }));
|
|
19970
|
+
const root = (jsxRuntime.jsxs("div", { ...localeScope.rootAttrs, ...rest, ref: rootRef, className: classNames(rootClasses, bulmaHelperClasses, className), children: [insideControl ? field : jsxRuntime.jsx(Control, { size: size, children: field }), required && (jsxRuntime.jsx("input", { ...(proxyFocused
|
|
19971
|
+
? {
|
|
19972
|
+
...fieldName,
|
|
19973
|
+
role: 'combobox',
|
|
19974
|
+
'aria-haspopup': 'dialog',
|
|
19975
|
+
'aria-expanded': false,
|
|
19976
|
+
'aria-describedby': fieldDescription,
|
|
19977
|
+
}
|
|
19978
|
+
: { 'aria-hidden': true }), tabIndex: -1, inputMode: "none", required: true, value: hasValue ? 'picked' : '', onChange: ignoreChange, form: form, disabled: disabled,
|
|
19979
|
+
// When the browser stops a submit, it focuses this and shows its
|
|
19980
|
+
// message beside it, and every browser takes the message down as
|
|
19981
|
+
// this loses focus. So focus stays here, over the field, which
|
|
19982
|
+
// looks focused, and a press goes where it lands, as from any
|
|
19983
|
+
// field. The next key hands focus to the field and does there
|
|
19984
|
+
// what it would have. Firefox also stops taking clicks if focus
|
|
19985
|
+
// leaves while it's still focusing this.
|
|
19986
|
+
onFocus: () => setProxyFocused(true), onBlur: () => setProxyFocused(false), onKeyDown: event => {
|
|
19987
|
+
if (MODIFIER_KEYS.has(event.key))
|
|
19988
|
+
return;
|
|
19989
|
+
inputRef.current?.focus();
|
|
19990
|
+
handleKeyDown(event);
|
|
19991
|
+
}, style: requiredProxyStyle })), jsxRuntime.jsx(HiddenValues, { name: name, values: values, form: form, disabled: disabled }), jsxRuntime.jsx(PickerPopover, { localeScope: localeScope.panel, isOpen: isOpen, onClose: () => onOpenChange(false), anchorRef: rootRef, position: position, appendToBody: appendToBody, className: panelClasses, ariaLabel: panelName['aria-label'], ariaLabelledBy: panelName['aria-labelledby'], id: panelId, restoreFocusRef: inputRef, initialFocusSelector: initialFocusSelector, matchWidth: true, followContent: true, children: children(panelName) })] }));
|
|
19992
|
+
const fieldElement = below === undefined ? (root) : (jsxRuntime.jsxs("div", { children: [root, below] }));
|
|
19993
|
+
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
19994
|
+
if (ownField) {
|
|
19995
|
+
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fieldElement, messageEl] }));
|
|
19996
|
+
}
|
|
19997
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [fieldElement, messageEl] }));
|
|
19998
|
+
}
|
|
19999
|
+
|
|
20000
|
+
const NOTHING_HIDDEN = new Set();
|
|
20001
|
+
const noMatches = [{ id: 'empty' }];
|
|
20002
|
+
const nothingToSay = [];
|
|
20003
|
+
/** What takes focus as the panel opens. */
|
|
20004
|
+
const INITIAL_FOCUS = '[data-initial-focus]';
|
|
20005
|
+
/** One value or several, as a list. */
|
|
20006
|
+
const asList = (value) => value == null ? [] : Array.isArray(value) ? value : [value];
|
|
20007
|
+
/** Whether two lists hold the same values, whatever their order and repeats. */
|
|
20008
|
+
const sameValues = (a, b) => {
|
|
20009
|
+
const inA = new Set(a);
|
|
20010
|
+
const inB = new Set(b);
|
|
20011
|
+
return inA.size === inB.size && [...inA].every(value => inB.has(value));
|
|
20012
|
+
};
|
|
20013
|
+
/** The list with every value of `more` it doesn't have added at the end. */
|
|
20014
|
+
const withAll = (list, more) => [
|
|
20015
|
+
...list,
|
|
20016
|
+
...more.filter(value => !list.includes(value)),
|
|
20017
|
+
];
|
|
20018
|
+
/**
|
|
20019
|
+
* The `TreeSelect` component is a form field whose options are a tree, such as categories, places or teams: it opens a panel holding a real `TreeView`, picks one item or checks a branch at a time, and searches labels.
|
|
20020
|
+
*
|
|
20021
|
+
* The field is a read-only `role="combobox"` that opens a dialog, wired as
|
|
20022
|
+
* DateInput's is, so focus moves into the tree for real and the tree keeps
|
|
20023
|
+
* TreeView's keys, type-ahead and spoken output. The tree is given as `data`,
|
|
20024
|
+
* so the field and the tags can name what's picked while the panel is shut,
|
|
20025
|
+
* and on the server.
|
|
20026
|
+
*
|
|
20027
|
+
* `selectionMode` picks the kind. `'single'` picks one item and closes. With
|
|
20028
|
+
* `'checkbox'`, a check reaches every enabled item under it, closed or hidden
|
|
20029
|
+
* by a search, and `checkedStrategy` decides what the value holds: the
|
|
20030
|
+
* leaves, the topmost checked items or every one. With
|
|
20031
|
+
* `'checkbox-independent'`, each box is its own item's, so a parent means
|
|
20032
|
+
* itself alone (Mantine's and Ant Design's `checkStrictly`). The value shows
|
|
20033
|
+
* as tags under the field.
|
|
20034
|
+
*
|
|
20035
|
+
* A value no node has is kept and shown as it is, with a warning in
|
|
20036
|
+
* development, so a value under a branch that hasn't loaded survives until
|
|
20037
|
+
* it does.
|
|
20038
|
+
*
|
|
20039
|
+
* @function
|
|
20040
|
+
* @param {TreeSelectProps} props - Props for the TreeSelect component.
|
|
20041
|
+
* @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the field.
|
|
20042
|
+
* @returns {JSX.Element} The field, its tags and its panel, in a `Field` with the label unless one is around it.
|
|
20043
|
+
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ | APG Combobox Pattern}
|
|
20044
|
+
*
|
|
20045
|
+
* @example
|
|
20046
|
+
* <TreeSelect
|
|
20047
|
+
* label="Save to"
|
|
20048
|
+
* data={[
|
|
20049
|
+
* { value: 'docs', label: 'Documents', children: [{ value: 'invoices', label: 'Invoices' }] },
|
|
20050
|
+
* { value: 'photos', label: 'Photos' },
|
|
20051
|
+
* ]}
|
|
20052
|
+
* searchable
|
|
20053
|
+
* />
|
|
20054
|
+
*
|
|
20055
|
+
* @example
|
|
20056
|
+
* <TreeSelect label="Regions" selectionMode="checkbox" checkedStrategy="parent" data={regions} />
|
|
20057
|
+
*/
|
|
20058
|
+
const TreeSelect = React.forwardRef((props, ref) => {
|
|
20059
|
+
const { data, expanded: expandedProp, defaultExpanded = [], onExpandedChange, open: openProp, defaultOpen = false, onOpenChange, searchable = false, query: queryProp, defaultQuery = '', onQueryChange, filter, empty, itemTemplate, disabled: disabledProp, clearable = false, size, labels, selectionMode = 'single', value: valueProp, defaultValue,
|
|
20060
|
+
// Called below through `props`, which knows which value it takes.
|
|
20061
|
+
onChange: _onChange, parentsSelectable = true, closeOnSelect = true, checkedStrategy = 'child', ariaCloseLabel, ...rest } = props;
|
|
20062
|
+
const disabled = useFieldsetDisabled(disabledProp);
|
|
20063
|
+
const single = selectionMode === 'single';
|
|
20064
|
+
const cascade = selectionMode === 'checkbox';
|
|
20065
|
+
const index = React.useMemo(() => indexTree(data), [data]);
|
|
20066
|
+
const records = React.useMemo(() => checkRecords(index), [index]);
|
|
20067
|
+
const labelFor = (value) => {
|
|
20068
|
+
const entry = index.get(value);
|
|
20069
|
+
return entry ? nodeLabel(entry.node) : value;
|
|
20070
|
+
};
|
|
20071
|
+
// The value, as a list: none or one in `single`.
|
|
20072
|
+
const [held, setHeld] = useControlled(valueProp, defaultValue ?? null);
|
|
20073
|
+
const listed = asList(held);
|
|
20074
|
+
const change = (next) => {
|
|
20075
|
+
const value = single ? (next[0] ?? null) : next;
|
|
20076
|
+
setHeld(value);
|
|
20077
|
+
// The mode decides which of the two `onChange` takes.
|
|
20078
|
+
const report = props.onChange;
|
|
20079
|
+
report?.(value);
|
|
20080
|
+
};
|
|
20081
|
+
// What the field, the tags and the hidden inputs show: the value as it's
|
|
20082
|
+
// held. `checkedStrategy` shapes a new value as a check makes it, so a
|
|
20083
|
+
// branch that loads later changes nothing the form posts.
|
|
20084
|
+
const picked = single ? listed.slice(0, 1) : Array.from(new Set(listed));
|
|
20085
|
+
const unknown = picked.find(value => !index.has(value));
|
|
20086
|
+
React.useEffect(() => {
|
|
20087
|
+
if (unknown !== undefined) {
|
|
20088
|
+
warnOnce('TreeSelect:unknown-value', `[bestax-bulma] <TreeSelect> was given the value "${unknown}", ` +
|
|
20089
|
+
'which no node in its data has. It is kept and shown as it is, ' +
|
|
20090
|
+
'as it should be for a node under a branch that has not loaded ' +
|
|
20091
|
+
'yet. Otherwise, check the value against the data.');
|
|
20092
|
+
}
|
|
20093
|
+
}, [unknown]);
|
|
20094
|
+
const [expanded, setExpanded] = useControlled(expandedProp, defaultExpanded);
|
|
20095
|
+
const changeExpanded = (next) => {
|
|
20096
|
+
setExpanded(next);
|
|
20097
|
+
onExpandedChange?.(next);
|
|
20098
|
+
};
|
|
20099
|
+
// The picked item focus goes to: the first in the tree's order.
|
|
20100
|
+
const pickedSet = new Set(picked);
|
|
20101
|
+
const target = Array.from(index.keys()).find(value => pickedSet.has(value));
|
|
20102
|
+
const [query, setQueryState] = useControlled(queryProp, defaultQuery);
|
|
20103
|
+
const setQuery = (next) => {
|
|
20104
|
+
if (next === query)
|
|
20105
|
+
return;
|
|
20106
|
+
setQueryState(next);
|
|
20107
|
+
onQueryChange?.(next);
|
|
20108
|
+
};
|
|
20109
|
+
const [isOpen, setOpenState] = useControlled(openProp, defaultOpen);
|
|
20110
|
+
const setOpen = (next) => {
|
|
20111
|
+
if (next === isOpen)
|
|
20112
|
+
return;
|
|
20113
|
+
if (next) {
|
|
20114
|
+
// Opening shows the picked item, its branch open.
|
|
20115
|
+
const path = (target !== undefined && index.get(target)?.path) || [];
|
|
20116
|
+
if (path.some(value => !expanded.includes(value))) {
|
|
20117
|
+
changeExpanded(withAll(expanded, path));
|
|
20118
|
+
}
|
|
20119
|
+
}
|
|
20120
|
+
else {
|
|
20121
|
+
setQuery('');
|
|
20122
|
+
}
|
|
20123
|
+
setOpenState(next);
|
|
20124
|
+
onOpenChange?.(next);
|
|
20125
|
+
};
|
|
20126
|
+
const matchText = useLocaleMatcher();
|
|
20127
|
+
const typed = searchable ? query.trim() : '';
|
|
20128
|
+
// Every item reads the hidden set, so it's only built again when the
|
|
20129
|
+
// search text or the data changes.
|
|
20130
|
+
const search = React.useMemo(() => typed
|
|
20131
|
+
? searchTree(data, (node, path) => {
|
|
20132
|
+
if (filter)
|
|
20133
|
+
return filter(typed, node, path);
|
|
20134
|
+
const text = nodeLabel(node);
|
|
20135
|
+
return matchText
|
|
20136
|
+
? matchText(text, typed)
|
|
20137
|
+
: text.toLowerCase().includes(typed.toLowerCase());
|
|
20138
|
+
})
|
|
20139
|
+
: null,
|
|
20140
|
+
// `filter` is read as it is when the text or the data changes: one
|
|
20141
|
+
// written inline is a new function every render, and listing it would
|
|
20142
|
+
// search again on each.
|
|
20143
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
20144
|
+
[data, typed, matchText]);
|
|
20145
|
+
// The branches a search opened that were closed during it, for the
|
|
20146
|
+
// search text they were closed under: any new text, from the box or
|
|
20147
|
+
// from a held `query`, starts over.
|
|
20148
|
+
const [searchClosed, setSearchClosed] = React.useState({
|
|
20149
|
+
query: typed,
|
|
20150
|
+
values: [],
|
|
20151
|
+
});
|
|
20152
|
+
if (searchClosed.query !== typed) {
|
|
20153
|
+
setSearchClosed({ query: typed, values: [] });
|
|
20154
|
+
}
|
|
20155
|
+
const closedHere = searchClosed.query === typed ? searchClosed.values : [];
|
|
20156
|
+
// The branches a search opens show open over the ones you opened, and
|
|
20157
|
+
// close again once it's cleared.
|
|
20158
|
+
const forced = search
|
|
20159
|
+
? search.open.filter(value => !closedHere.includes(value))
|
|
20160
|
+
: [];
|
|
20161
|
+
const shownExpanded = withAll(expanded, forced);
|
|
20162
|
+
// TreeView's click on a row selects it and opens it. A pick by click, as
|
|
20163
|
+
// a pick by Enter, only picks, so the open the tree asks for right after
|
|
20164
|
+
// one on a parent is dropped, and a lazy parent isn't fetched to be
|
|
20165
|
+
// picked.
|
|
20166
|
+
const clickPickedRef = React.useRef(false);
|
|
20167
|
+
const onTreeExpandedChange = (next) => {
|
|
20168
|
+
if (clickPickedRef.current) {
|
|
20169
|
+
clickPickedRef.current = false;
|
|
20170
|
+
return;
|
|
20171
|
+
}
|
|
20172
|
+
const closed = shownExpanded.filter(value => !next.includes(value));
|
|
20173
|
+
const opened = next.filter(value => !shownExpanded.includes(value));
|
|
20174
|
+
const closedForced = closed.filter(value => forced.includes(value));
|
|
20175
|
+
if (closedForced.length > 0) {
|
|
20176
|
+
setSearchClosed({
|
|
20177
|
+
query: typed,
|
|
20178
|
+
values: [...closedHere, ...closedForced],
|
|
20179
|
+
});
|
|
20180
|
+
}
|
|
20181
|
+
if (opened.length > 0 || closed.some(value => expanded.includes(value))) {
|
|
20182
|
+
changeExpanded([
|
|
20183
|
+
...expanded.filter(value => !closed.includes(value)),
|
|
20184
|
+
...opened,
|
|
20185
|
+
]);
|
|
20186
|
+
}
|
|
20187
|
+
};
|
|
20188
|
+
// Whether an item is on the page with the panel open: every branch
|
|
20189
|
+
// above it open, and the search not hiding it.
|
|
20190
|
+
const visible = (value) => !search?.hidden.has(value) &&
|
|
20191
|
+
index.get(value).path.every(above => shownExpanded.includes(above));
|
|
20192
|
+
const focusTarget = target !== undefined && visible(target)
|
|
20193
|
+
? target
|
|
20194
|
+
: data.find(node => visible(node.value))?.value;
|
|
20195
|
+
// Picking in `single`. TreeView shows `selected`, held here, and the
|
|
20196
|
+
// picks are made from the rows' own keys and clicks, so a parent can
|
|
20197
|
+
// open instead and a pick of the picked item still closes.
|
|
20198
|
+
const pickable = (node) => !node.disabled && (parentsSelectable || !isParentNode(node));
|
|
20199
|
+
const pick = (value) => {
|
|
20200
|
+
if (listed.length !== 1 || listed[0] !== value)
|
|
20201
|
+
change([value]);
|
|
20202
|
+
if (closeOnSelect)
|
|
20203
|
+
setOpen(false);
|
|
20204
|
+
};
|
|
20205
|
+
const toggleClass = usePrefixedClassNames('tree-view-toggle');
|
|
20206
|
+
const rowHandlers = (node) => single
|
|
20207
|
+
? {
|
|
20208
|
+
onKeyDown: (event) => {
|
|
20209
|
+
if (event.target !== event.currentTarget ||
|
|
20210
|
+
event.repeat ||
|
|
20211
|
+
event.altKey ||
|
|
20212
|
+
event.ctrlKey ||
|
|
20213
|
+
event.metaKey ||
|
|
20214
|
+
(event.key !== 'Enter' && event.key !== ' ')) {
|
|
20215
|
+
return;
|
|
20216
|
+
}
|
|
20217
|
+
if (pickable(node)) {
|
|
20218
|
+
pick(node.value);
|
|
20219
|
+
}
|
|
20220
|
+
else if (!node.disabled && isParentNode(node)) {
|
|
20221
|
+
// TreeView leaves a key that's been used alone.
|
|
20222
|
+
event.preventDefault();
|
|
20223
|
+
onTreeExpandedChange(shownExpanded.includes(node.value)
|
|
20224
|
+
? shownExpanded.filter(value => value !== node.value)
|
|
20225
|
+
: [...shownExpanded, node.value]);
|
|
20226
|
+
}
|
|
20227
|
+
},
|
|
20228
|
+
onClick: (event) => {
|
|
20229
|
+
const item = event.currentTarget;
|
|
20230
|
+
const target = event.target;
|
|
20231
|
+
// A click in this item's own row, where TreeView reads one,
|
|
20232
|
+
// not one in the group of items inside it, and not on its
|
|
20233
|
+
// chevron, which only opens it.
|
|
20234
|
+
const inRow = target === item || item.firstElementChild.contains(target);
|
|
20235
|
+
const onChevron = isParentNode(node) && !!target.closest(`.${toggleClass}`);
|
|
20236
|
+
if (inRow && !onChevron && pickable(node)) {
|
|
20237
|
+
// TreeView opens a parent it's handed this click on, in
|
|
20238
|
+
// this same click: the flag goes once the click is over.
|
|
20239
|
+
clickPickedRef.current = isParentNode(node);
|
|
20240
|
+
queueMicrotask(() => {
|
|
20241
|
+
clickPickedRef.current = false;
|
|
20242
|
+
});
|
|
20243
|
+
pick(node.value);
|
|
20244
|
+
}
|
|
20245
|
+
},
|
|
20246
|
+
}
|
|
20247
|
+
: undefined;
|
|
20248
|
+
const renderNode = (node) => (jsxRuntime.jsx(TreeView.Item, { value: node.value, label: itemTemplate ? itemTemplate(node) : nodeLabel(node), icon: node.icon, hasChildren: node.hasChildren, isLoading: node.isLoading, loadError: node.loadError, onRetry: node.onRetry, disabled: node.disabled, "data-initial-focus": !searchable && node.value === focusTarget ? '' : undefined, ...rowHandlers(node), children: childNodes(node).map(renderNode) }, node.value));
|
|
20249
|
+
// In `checkbox`, TreeView reports every checked item, read here into
|
|
20250
|
+
// the list `checkedStrategy` gives.
|
|
20251
|
+
const shaped = (next) => cascade ? checkedByStrategy(records, next, checkedStrategy) : next;
|
|
20252
|
+
const onCheckedChange = (next) => change(shaped(next));
|
|
20253
|
+
// Every item's check state, read once a render, which the clear button
|
|
20254
|
+
// and the tags' buttons decide from. Only `checkbox` reads TreeView's
|
|
20255
|
+
// rules: in `checkbox-independent` a listed item is a checked one.
|
|
20256
|
+
const listedKey = JSON.stringify(listed);
|
|
20257
|
+
const checks = React.useMemo(() => (cascade ? readChecks(records, new Set(listed)) : null),
|
|
20258
|
+
// `listed` by what it holds, since a held list is a new array each
|
|
20259
|
+
// render.
|
|
20260
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
20261
|
+
[cascade, records, listedKey]);
|
|
20262
|
+
const isDisabled = (value) => !!index.get(value)?.node.disabled;
|
|
20263
|
+
// Read in `checkbox` only, where there are states to read.
|
|
20264
|
+
const isChecked = (value) => checks.states.get(value) === true;
|
|
20265
|
+
// What a control can uncheck. In `checkbox`, that's each checked end no
|
|
20266
|
+
// checked disabled item above it keeps checked, and every item above it,
|
|
20267
|
+
// which takes it along; elsewhere it's each listed item that isn't
|
|
20268
|
+
// disabled. A disabled item is checked only by its own value, so nothing
|
|
20269
|
+
// unchecks it, or what it keeps checked.
|
|
20270
|
+
const uncheckable = new Set();
|
|
20271
|
+
if (checks) {
|
|
20272
|
+
for (const end of checks.ends) {
|
|
20273
|
+
const { path } = index.get(end);
|
|
20274
|
+
if (isChecked(end) &&
|
|
20275
|
+
!path.some(above => isDisabled(above) && isChecked(above))) {
|
|
20276
|
+
uncheckable.add(end);
|
|
20277
|
+
path.forEach(above => uncheckable.add(above));
|
|
20278
|
+
}
|
|
20279
|
+
}
|
|
20280
|
+
}
|
|
20281
|
+
else if (!single) {
|
|
20282
|
+
picked.forEach(value => !isDisabled(value) && uncheckable.add(value));
|
|
20283
|
+
}
|
|
20284
|
+
// A tag's button takes its value off where that changes what's checked,
|
|
20285
|
+
// the way its box would. A value no node has can always go.
|
|
20286
|
+
const removable = (value) => !index.has(value) || (!isDisabled(value) && uncheckable.has(value));
|
|
20287
|
+
const without = (value) => cascade && records.has(value)
|
|
20288
|
+
? shaped(toggledList(records, listed, value))
|
|
20289
|
+
: listed.filter(item => item !== value);
|
|
20290
|
+
// What the clear button leaves: the checked disabled items, and in
|
|
20291
|
+
// `checkbox` what they keep checked, shaped as a check's value is.
|
|
20292
|
+
const cleared = single
|
|
20293
|
+
? []
|
|
20294
|
+
: shaped(checks
|
|
20295
|
+
? Array.from(records.keys()).filter(value => isDisabled(value) && isChecked(value))
|
|
20296
|
+
: picked.filter(isDisabled));
|
|
20297
|
+
const canClear = single
|
|
20298
|
+
? picked.length > 0
|
|
20299
|
+
: uncheckable.size > 0 || picked.some(value => !index.has(value));
|
|
20300
|
+
const inputRef = React.useRef(null);
|
|
20301
|
+
// A forwarded callback's cleanup, which React 19 lets it return, run on
|
|
20302
|
+
// detach in place of a call with `null`, on React 18 as well.
|
|
20303
|
+
const refCleanupRef = React.useRef(null);
|
|
20304
|
+
const fieldRef = React.useCallback((node) => {
|
|
20305
|
+
inputRef.current = node;
|
|
20306
|
+
if (typeof ref === 'function') {
|
|
20307
|
+
if (node === null) {
|
|
20308
|
+
const cleanup = refCleanupRef.current;
|
|
20309
|
+
refCleanupRef.current = null;
|
|
20310
|
+
if (cleanup)
|
|
20311
|
+
cleanup();
|
|
20312
|
+
else
|
|
20313
|
+
ref(null);
|
|
20314
|
+
return;
|
|
20315
|
+
}
|
|
20316
|
+
const cleanup = ref(node);
|
|
20317
|
+
refCleanupRef.current =
|
|
20318
|
+
typeof cleanup === 'function' ? cleanup : null;
|
|
20319
|
+
}
|
|
20320
|
+
else if (ref) {
|
|
20321
|
+
ref.current = node;
|
|
20322
|
+
}
|
|
20323
|
+
}, [ref]);
|
|
20324
|
+
const tagsId = React.useId();
|
|
20325
|
+
const ids = tagIds(tagsId, picked);
|
|
20326
|
+
const searchClass = usePrefixedClassNames('input', 'tree-select-search', {
|
|
20327
|
+
[`is-${size}`]: !!size,
|
|
20328
|
+
});
|
|
20329
|
+
const emptyClass = usePrefixedClassNames('tree-select-empty');
|
|
20330
|
+
const treeRef = React.useRef(null);
|
|
20331
|
+
const emptyRef = React.useRef(null);
|
|
20332
|
+
const nothingFound = !!search && !search.found;
|
|
20333
|
+
// The region reads the text on screen, whatever `empty` renders.
|
|
20334
|
+
const describeEmpty = React.useCallback(() => {
|
|
20335
|
+
const node = emptyRef.current;
|
|
20336
|
+
/* istanbul ignore if: it's only announced while it's on screen */
|
|
20337
|
+
if (!node)
|
|
20338
|
+
return null;
|
|
20339
|
+
return spokenText(node);
|
|
20340
|
+
}, []);
|
|
20341
|
+
return (jsxRuntime.jsx(DialogSelect, { ...rest, component: "TreeSelect", rootClass: "tree-select", panelClass: "tree-select-panel", text: single && picked.length > 0 ? labelFor(picked[0]) : '', values: single ? [picked[0] ?? ''] : picked, hasValue: picked.length > 0, open: isOpen, onOpenChange: setOpen, onPrintableKey: searchable
|
|
20342
|
+
? key => {
|
|
20343
|
+
setQuery(key);
|
|
20344
|
+
setOpen(true);
|
|
20345
|
+
return true;
|
|
20346
|
+
}
|
|
20347
|
+
: undefined, onClear: () => change(cleared), clearable: clearable && canClear, onFormReset: () => {
|
|
20348
|
+
// Back to `defaultValue`, or to nothing for a held value, which
|
|
20349
|
+
// its owner sets from `onChange`.
|
|
20350
|
+
const back = valueProp === undefined ? asList(defaultValue) : [];
|
|
20351
|
+
if (!sameValues(back, listed))
|
|
20352
|
+
change(back);
|
|
20353
|
+
}, clearLabel: labels?.clear || 'Clear', chooseLabel: labels?.choose, disabled: disabled, size: size, inputRef: inputRef, fieldRef: fieldRef, describedBy: single ? undefined : ids.join(' '), below: single ? undefined : (jsxRuntime.jsx(ValueTags, { values: picked, ids: ids, labelFor: labelFor, disabled: disabled, ariaCloseLabel: ariaCloseLabel, removable: removable, onRemove: value => {
|
|
20354
|
+
change(without(value));
|
|
20355
|
+
inputRef.current?.focus();
|
|
20356
|
+
} })), initialFocusSelector: INITIAL_FOCUS, children: panelName => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [searchable && (jsxRuntime.jsx("input", { className: searchClass, type: "search", "aria-label": labels?.search || 'Search', autoComplete: "off", value: query, "data-initial-focus": "", onChange: event => setQuery(event.target.value), onFocus: event => {
|
|
20357
|
+
// A key that opened the panel is in the box: type after it.
|
|
20358
|
+
const { length } = event.currentTarget.value;
|
|
20359
|
+
event.currentTarget.setSelectionRange(length, length);
|
|
20360
|
+
}, onKeyDown: event => {
|
|
20361
|
+
if (event.key === 'Enter') {
|
|
20362
|
+
// The search box's Enter would submit the form.
|
|
20363
|
+
event.preventDefault();
|
|
20364
|
+
}
|
|
20365
|
+
else if (event.key === 'ArrowDown' && !event.altKey) {
|
|
20366
|
+
event.preventDefault();
|
|
20367
|
+
const tree = treeRef.current;
|
|
20368
|
+
(tree?.querySelector('[role="treeitem"][tabindex="0"]') ?? tree?.querySelector('[role="treeitem"]'))?.focus();
|
|
20369
|
+
}
|
|
20370
|
+
} })), nothingFound ? (empty && (jsxRuntime.jsx("div", { className: emptyClass, ref: emptyRef, children: empty }))) : (jsxRuntime.jsx(TreeViewHiddenContext.Provider, { value: search?.hidden ?? NOTHING_HIDDEN, children: jsxRuntime.jsx(TreeView, { ...panelName, ref: treeRef,
|
|
20371
|
+
// It scrolls, and Firefox makes a scroller a Tab stop of
|
|
20372
|
+
// its own: the roving one on its items is the tree's.
|
|
20373
|
+
tabIndex: -1, selectionMode: selectionMode, size: size, selected: single ? (picked[0] ?? null) : undefined, checked: single ? undefined : listed, onCheckedChange: onCheckedChange, expanded: shownExpanded, onExpandedChange: onTreeExpandedChange, loadingLabel: labels?.loading, retryLabel: labels?.retry, loadingAnnouncement: labels?.loadingAnnouncement, loadedAnnouncement: labels?.loadedAnnouncement, children: data.map(renderNode) }) })), jsxRuntime.jsx(StatusRegion, { items: nothingFound && empty ? noMatches : nothingToSay, describe: describeEmpty })] })) }));
|
|
20374
|
+
});
|
|
20375
|
+
TreeSelect.displayName = 'TreeSelect';
|
|
20376
|
+
|
|
20377
|
+
// An IME that's composing owns every key: Enter confirms what it composed,
|
|
20378
|
+
// the arrows move through its candidates, and Escape cancels it. Safari can
|
|
20379
|
+
// say so with `keyCode` 229 rather than `isComposing` (#1102).
|
|
20380
|
+
const isComposing = (e) => e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
|
|
20381
|
+
/**
|
|
20382
|
+
* The `Taginput` component provides a tag/chip input field for managing multiple tags.
|
|
20383
|
+
*
|
|
20384
|
+
* @function
|
|
20385
|
+
* @param {TaginputProps} props - Props for the Taginput component.
|
|
20386
|
+
* @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the input element.
|
|
20387
|
+
* @returns {JSX.Element} The rendered taginput component.
|
|
20388
|
+
*
|
|
20389
|
+
* @example
|
|
20390
|
+
* // Basic usage
|
|
20391
|
+
* <Taginput
|
|
20392
|
+
* placeholder="Add a tag..."
|
|
20393
|
+
* onChange={(tags) => console.log(tags)}
|
|
20394
|
+
* />
|
|
20395
|
+
*
|
|
20396
|
+
* @example
|
|
20397
|
+
* // With autocomplete
|
|
20398
|
+
* <Taginput
|
|
20399
|
+
* data={['React', 'Vue', 'Angular', 'Svelte']}
|
|
20400
|
+
* allowNew={false}
|
|
20401
|
+
* placeholder="Select frameworks..."
|
|
20402
|
+
* />
|
|
20403
|
+
*/
|
|
20404
|
+
const Taginput = React.forwardRef(({
|
|
20405
|
+
// Field props
|
|
20406
|
+
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = [], data: dataProp, placeholder, field = 'label', allowNew = true, allowDuplicates = false, openOnFocus = false, removeOnKeys = true, confirmKeys = ['Enter', ','], closable = true, attached = false, maxTags, maxlength, disabled: disabledProp, readonly = false, rounded = false, ellipsis = false, hasCounter = true, onPasteSeparators = [','], beforeAdding, createTag, keepFirst = false, keepOpen = true, loading = false, ariaCloseLabel, icon, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, tagColor, size, onChange, onAdd, onRemove, onTyping, tagTemplate, name, form, id, className, ...props }, ref) => {
|
|
20407
|
+
const disabled = useFieldsetDisabled(disabledProp);
|
|
20408
|
+
const insideField = useInsideField();
|
|
20409
|
+
const insideControl = useInsideControl();
|
|
20410
|
+
const ownField = rendersOwnField('Taginput', {
|
|
20411
|
+
insideField,
|
|
20412
|
+
insideControl,
|
|
20413
|
+
label,
|
|
20414
|
+
message,
|
|
20415
|
+
horizontal,
|
|
20416
|
+
fieldClassName,
|
|
20417
|
+
});
|
|
20418
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
20419
|
+
const { classPrefix } = useConfig();
|
|
20420
|
+
const defaultIconLibrary = useIconLibrary();
|
|
20421
|
+
const formatNumber = useScopedNumberFormat();
|
|
20422
|
+
const matchText = useLocaleMatcher();
|
|
20423
|
+
const resolvedLibrary = iconLibraryProp || defaultIconLibrary || 'fa';
|
|
20424
|
+
const pcn = (...args) => prefixedClassNames(classPrefix, ...args);
|
|
20425
|
+
const containerRef = React.useRef(null);
|
|
20426
|
+
const inputRef = React.useRef(null);
|
|
20427
|
+
const dropdownRef = React.useRef(null);
|
|
20428
|
+
const [internalTags, setInternalTags] = React.useState(defaultValue);
|
|
20429
|
+
const [inputValue, setInputValue] = React.useState('');
|
|
20430
|
+
const [isActive, setIsActive] = React.useState(false);
|
|
20431
|
+
const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
|
|
20432
|
+
const baseId = React.useId();
|
|
20433
|
+
const listboxId = `${baseId}-listbox`;
|
|
20434
|
+
const counterId = `${baseId}-counter`;
|
|
20435
|
+
// With `data` the input offers suggestions, so it's a combobox, even
|
|
20436
|
+
// while none match. Without it there's never a list, so it stays a plain
|
|
20437
|
+
// text field.
|
|
20438
|
+
const hasSuggestions = dataProp !== undefined;
|
|
20439
|
+
const data = dataProp ?? [];
|
|
20440
|
+
// Use controlled or internal tags
|
|
20441
|
+
const tags = controlledValue !== undefined ? controlledValue : internalTags;
|
|
20442
|
+
const isMaxReached = maxTags !== undefined && tags.length >= maxTags;
|
|
20443
|
+
// The suggestion list points `aria-labelledby` at the label that names
|
|
20444
|
+
// the input, so with suggestions it needs that label's id.
|
|
20445
|
+
const { controlId, fieldLabelProps, labelled, labelId: listLabelId, } = useAutoLabelId({
|
|
20446
|
+
label,
|
|
20447
|
+
id,
|
|
20448
|
+
labelProps,
|
|
20449
|
+
rendersLabel: ownField,
|
|
20450
|
+
needsLabelId: hasSuggestions,
|
|
20451
|
+
});
|
|
20452
|
+
// Get display value from tag
|
|
20453
|
+
const getDisplayValue = (tag) => {
|
|
20454
|
+
if (typeof tag === 'string')
|
|
20455
|
+
return tag;
|
|
20456
|
+
const fieldValue = tag[field];
|
|
20457
|
+
if (typeof fieldValue === 'string')
|
|
20458
|
+
return fieldValue;
|
|
20459
|
+
return tag.value || '';
|
|
20460
|
+
};
|
|
20461
|
+
// Filter autocomplete data
|
|
20462
|
+
const filteredData = data.filter(item => {
|
|
20463
|
+
const displayValue = item.toLowerCase();
|
|
20464
|
+
const matchesInput = matchText
|
|
20465
|
+
? matchText(item, inputValue)
|
|
20466
|
+
: displayValue.includes(inputValue.toLowerCase());
|
|
20467
|
+
const notAlreadyAdded = allowDuplicates ||
|
|
20468
|
+
!tags.some(tag => getDisplayValue(tag).toLowerCase() === displayValue);
|
|
20469
|
+
const hasInput = inputValue || (openOnFocus && isActive);
|
|
20470
|
+
return matchesInput && notAlreadyAdded && hasInput;
|
|
20471
|
+
});
|
|
20472
|
+
// At the tag limit the list stays closed, since it could only offer tags
|
|
20473
|
+
// past the limit (#1087).
|
|
20474
|
+
const listOpen = isActive && !isMaxReached && filteredData.length > 0;
|
|
20475
|
+
// keepFirst: auto-highlight first item when dropdown opens
|
|
20476
|
+
React.useEffect(() => {
|
|
20477
|
+
if (keepFirst && isActive && filteredData.length > 0) {
|
|
20478
|
+
setHighlightedIndex(0);
|
|
20479
|
+
}
|
|
20480
|
+
}, [keepFirst, isActive, filteredData.length]);
|
|
20481
|
+
// Update tags
|
|
20482
|
+
const updateTags = React.useCallback((newTags) => {
|
|
20483
|
+
if (controlledValue === undefined) {
|
|
20484
|
+
setInternalTags(newTags);
|
|
20485
|
+
}
|
|
20486
|
+
onChange?.(newTags);
|
|
20487
|
+
}, [controlledValue, onChange]);
|
|
20488
|
+
// Add tags from text typed, pasted or picked from the list. Each one is
|
|
20489
|
+
// checked against the tags before it, this batch's included, so a paste
|
|
20490
|
+
// of several adds them all in one change (#1101). Only `createTag` turns
|
|
20491
|
+
// text into an object.
|
|
20492
|
+
const addTags = React.useCallback((values) => {
|
|
20493
|
+
if (disabled || readonly)
|
|
20494
|
+
return;
|
|
20495
|
+
let next = tags;
|
|
20496
|
+
const added = [];
|
|
20497
|
+
for (const value of values) {
|
|
20498
|
+
// A paste can bring more tags than there's room for, and at the
|
|
20499
|
+
// limit the read-only input still takes Enter.
|
|
20500
|
+
if (maxTags !== undefined && next.length >= maxTags)
|
|
20501
|
+
break;
|
|
20502
|
+
const displayValue = value.trim();
|
|
20503
|
+
if (!displayValue)
|
|
20504
|
+
continue;
|
|
20505
|
+
const lower = displayValue.toLowerCase();
|
|
20506
|
+
// Duplicates, data and the dropdown filter all compare
|
|
20507
|
+
// case-insensitively.
|
|
20508
|
+
if (!allowDuplicates &&
|
|
19569
20509
|
next.some(tag => getDisplayValue(tag).toLowerCase() === lower))
|
|
19570
20510
|
continue;
|
|
19571
20511
|
// Without allowNew, only a suggestion can become a tag.
|
|
@@ -21070,61 +22010,6 @@ fieldClassName, controlClassName,
|
|
|
21070
22010
|
});
|
|
21071
22011
|
TextArea.displayName = 'TextArea';
|
|
21072
22012
|
|
|
21073
|
-
/**
|
|
21074
|
-
* Whether a key is Alt+ArrowDown or Alt+ArrowUp, which open and close a
|
|
21075
|
-
* picker's popover. The calendar and the time wheels leave both to it rather
|
|
21076
|
-
* than reading them as plain arrows.
|
|
21077
|
-
*/
|
|
21078
|
-
const isPopoverToggleKey = (e) => e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp');
|
|
21079
|
-
const DEFAULT_PICKER_LABELS = {
|
|
21080
|
-
prevMonth: 'Previous month',
|
|
21081
|
-
nextMonth: 'Next month',
|
|
21082
|
-
prevYear: 'Previous year',
|
|
21083
|
-
nextYear: 'Next year',
|
|
21084
|
-
chooseDate: 'Choose date',
|
|
21085
|
-
chooseMonth: 'Choose month',
|
|
21086
|
-
chooseYear: 'Choose year',
|
|
21087
|
-
rangeStart: 'Start date',
|
|
21088
|
-
rangeEnd: 'End date',
|
|
21089
|
-
rangePreviewEnd: 'Choose as end date',
|
|
21090
|
-
rangeSeparator: '–',
|
|
21091
|
-
chooseDateRange: 'Choose date range',
|
|
21092
|
-
hours: 'hours',
|
|
21093
|
-
minutes: 'minutes',
|
|
21094
|
-
seconds: 'seconds',
|
|
21095
|
-
ampm: 'AM/PM',
|
|
21096
|
-
increaseHours: 'Increase hours',
|
|
21097
|
-
decreaseHours: 'Decrease hours',
|
|
21098
|
-
increaseMinutes: 'Increase minutes',
|
|
21099
|
-
decreaseMinutes: 'Decrease minutes',
|
|
21100
|
-
increaseSeconds: 'Increase seconds',
|
|
21101
|
-
decreaseSeconds: 'Decrease seconds',
|
|
21102
|
-
toggleAmPm: 'Toggle AM/PM',
|
|
21103
|
-
chooseTime: 'Choose time',
|
|
21104
|
-
chooseDateTime: 'Choose date and time',
|
|
21105
|
-
now: 'Now',
|
|
21106
|
-
today: 'Today',
|
|
21107
|
-
clear: 'Clear',
|
|
21108
|
-
cancel: 'Cancel',
|
|
21109
|
-
ok: 'OK',
|
|
21110
|
-
reset: 'Reset',
|
|
21111
|
-
done: 'Done',
|
|
21112
|
-
time: 'Time',
|
|
21113
|
-
};
|
|
21114
|
-
/**
|
|
21115
|
-
* Merge user-supplied label overrides with the defaults. A key set to
|
|
21116
|
-
* `undefined` keeps its default, so a locale table that is missing a string
|
|
21117
|
-
* still names its control instead of leaving it with no accessible name.
|
|
21118
|
-
*/
|
|
21119
|
-
const mergeLabels = (overrides) => {
|
|
21120
|
-
const merged = { ...DEFAULT_PICKER_LABELS };
|
|
21121
|
-
for (const [key, value] of Object.entries(overrides ?? {})) {
|
|
21122
|
-
if (value !== undefined)
|
|
21123
|
-
merged[key] = value;
|
|
21124
|
-
}
|
|
21125
|
-
return merged;
|
|
21126
|
-
};
|
|
21127
|
-
|
|
21128
22013
|
function isSameDay(a, b) {
|
|
21129
22014
|
return (a.getFullYear() === b.getFullYear() &&
|
|
21130
22015
|
a.getMonth() === b.getMonth() &&
|
|
@@ -22453,112 +23338,6 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
|
|
|
22453
23338
|
};
|
|
22454
23339
|
Calendar.displayName = 'Calendar';
|
|
22455
23340
|
|
|
22456
|
-
const isBrowser = typeof window !== 'undefined';
|
|
22457
|
-
const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'bottom-left', appendToBody = false, className, trapFocus = true, closeOnClickOutside = true, closeOnEscape = true, ariaLabel, ariaLabelledBy, role = 'dialog', id, restoreFocusRef, initialFocusSelector, localeScope, }) => {
|
|
22458
|
-
const panelRef = React.useRef(null);
|
|
22459
|
-
const resolveContainer = usePortalContainer();
|
|
22460
|
-
// Read as the trap turns on, once the panel's content has rendered.
|
|
22461
|
-
const initialFocusRef = React.useMemo(() => initialFocusSelector === undefined
|
|
22462
|
-
? undefined
|
|
22463
|
-
: {
|
|
22464
|
-
get current() {
|
|
22465
|
-
return (panelRef.current?.querySelector(initialFocusSelector) ?? null);
|
|
22466
|
-
},
|
|
22467
|
-
}, [initialFocusSelector]);
|
|
22468
|
-
const resolved = useAnchoredPosition(anchorRef, panelRef, {
|
|
22469
|
-
active: isOpen,
|
|
22470
|
-
position,
|
|
22471
|
-
fixed: appendToBody,
|
|
22472
|
-
// The gap comes from `--bulma-picker-popover-offset` as a margin, in place
|
|
22473
|
-
// or portaled, so the coordinates leave it out and `auto` reads it off
|
|
22474
|
-
// the panel.
|
|
22475
|
-
offset: 0,
|
|
22476
|
-
styledGap: marginGap,
|
|
22477
|
-
});
|
|
22478
|
-
// Read when a press lands outside or a key arrives, so both listeners stay
|
|
22479
|
-
// put while `onClose` changes identity, as an inline one does on every
|
|
22480
|
-
// render of the field.
|
|
22481
|
-
const onCloseRef = useLatestRef(onClose);
|
|
22482
|
-
React.useEffect(() => {
|
|
22483
|
-
if (!isOpen || !closeOnClickOutside || !isBrowser)
|
|
22484
|
-
return undefined;
|
|
22485
|
-
// Listen on `pointerdown` (rather than `mousedown`) so touch starts
|
|
22486
|
-
// outside the popover dismiss it on mobile — including swipe-from-outside
|
|
22487
|
-
// gestures, where the synthetic mouse event would otherwise lag a few
|
|
22488
|
-
// hundred ms behind the actual touch.
|
|
22489
|
-
const handler = (e) => {
|
|
22490
|
-
const path = e.composedPath();
|
|
22491
|
-
if (panelRef.current && path.includes(panelRef.current))
|
|
22492
|
-
return;
|
|
22493
|
-
if (anchorRef.current && path.includes(anchorRef.current))
|
|
22494
|
-
return;
|
|
22495
|
-
onCloseRef.current();
|
|
22496
|
-
};
|
|
22497
|
-
document.addEventListener('pointerdown', handler);
|
|
22498
|
-
return () => document.removeEventListener('pointerdown', handler);
|
|
22499
|
-
}, [isOpen, closeOnClickOutside, anchorRef, onCloseRef]);
|
|
22500
|
-
// Escape closes it, and so does Alt+ArrowUp, the combobox's key for it,
|
|
22501
|
-
// which the calendar and the time wheels inside leave to it. Escape closes
|
|
22502
|
-
// it from anywhere, but the key is only the picker's when it was pressed in
|
|
22503
|
-
// the panel or the field: then it's marked used, so a Modal or Sidebar
|
|
22504
|
-
// around the picker stays open. From anywhere else, such as a dialog opened
|
|
22505
|
-
// over the picker, the key goes on to whatever has focus.
|
|
22506
|
-
React.useEffect(() => {
|
|
22507
|
-
if (!isOpen || !closeOnEscape || !isBrowser)
|
|
22508
|
-
return undefined;
|
|
22509
|
-
const handler = (e) => {
|
|
22510
|
-
if (e.key === 'Escape') {
|
|
22511
|
-
if (isEventInside(e, panelRef.current) ||
|
|
22512
|
-
isEventInside(e, anchorRef.current)) {
|
|
22513
|
-
e.preventDefault();
|
|
22514
|
-
}
|
|
22515
|
-
onCloseRef.current();
|
|
22516
|
-
}
|
|
22517
|
-
else if (e.altKey && e.key === 'ArrowUp') {
|
|
22518
|
-
e.preventDefault();
|
|
22519
|
-
e.stopPropagation();
|
|
22520
|
-
onCloseRef.current();
|
|
22521
|
-
}
|
|
22522
|
-
};
|
|
22523
|
-
document.addEventListener('keydown', handler);
|
|
22524
|
-
return () => document.removeEventListener('keydown', handler);
|
|
22525
|
-
}, [isOpen, closeOnEscape, anchorRef, onCloseRef]);
|
|
22526
|
-
useFocusTrap(panelRef, {
|
|
22527
|
-
active: isOpen && trapFocus,
|
|
22528
|
-
initialFocusRef,
|
|
22529
|
-
restoreFocus: restoreFocusRef ?? true,
|
|
22530
|
-
});
|
|
22531
|
-
const panelClass = usePrefixedClassNames('picker-popover', {
|
|
22532
|
-
'is-active': isOpen,
|
|
22533
|
-
[`is-${resolved.position}`]: true,
|
|
22534
|
-
'is-portal': appendToBody,
|
|
22535
|
-
});
|
|
22536
|
-
if (!isOpen)
|
|
22537
|
-
return null;
|
|
22538
|
-
const style = appendToBody
|
|
22539
|
-
? {
|
|
22540
|
-
position: 'fixed',
|
|
22541
|
-
top: resolved.top,
|
|
22542
|
-
left: resolved.left,
|
|
22543
|
-
}
|
|
22544
|
-
: {};
|
|
22545
|
-
const panel = (jsxRuntime.jsx("div", { ref: panelRef, id: id, role: role, "aria-modal": role === 'dialog' ? 'false' : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, tabIndex: -1, className: classNames(panelClass, className), style: style,
|
|
22546
|
-
// An Escape pressed in the panel is the picker's, and the listener
|
|
22547
|
-
// above claims it. A Dropdown menu around the picker hears Escape in a
|
|
22548
|
-
// React handler, which runs before that listener, so the panel claims
|
|
22549
|
-
// it here too, where React gets to it first.
|
|
22550
|
-
onKeyDown: e => {
|
|
22551
|
-
if (closeOnEscape && e.key === 'Escape')
|
|
22552
|
-
e.preventDefault();
|
|
22553
|
-
}, children: children }));
|
|
22554
|
-
if (appendToBody && isBrowser) {
|
|
22555
|
-
const target = resolveContainer();
|
|
22556
|
-
return reactDom.createPortal(jsxRuntime.jsx(LocalePortalScope, { target: target, scope: localeScope, children: panel }), target);
|
|
22557
|
-
}
|
|
22558
|
-
return panel;
|
|
22559
|
-
};
|
|
22560
|
-
PickerPopover.displayName = 'PickerPopover';
|
|
22561
|
-
|
|
22562
23341
|
/**
|
|
22563
23342
|
* Segment-map utility for manual keyboard entry on a date / time picker input.
|
|
22564
23343
|
* Given a token-format string like `'YYYY-MM-DD'`, `'HH:mm:ss'`, `'hh:mm A'`,
|
|
@@ -23311,79 +24090,6 @@ function supportsInputType(type) {
|
|
|
23311
24090
|
return supported;
|
|
23312
24091
|
}
|
|
23313
24092
|
|
|
23314
|
-
/**
|
|
23315
|
-
* A picker's locale: its own `locale`, else the one a `LocaleProvider` around
|
|
23316
|
-
* it sets, else `undefined`, which leaves `Intl` to use the runtime's.
|
|
23317
|
-
*/
|
|
23318
|
-
function usePickerLocale(own) {
|
|
23319
|
-
const scope = useLocaleScope();
|
|
23320
|
-
return own || scope?.locale;
|
|
23321
|
-
}
|
|
23322
|
-
const NO_ROOT_ATTRS = {};
|
|
23323
|
-
/**
|
|
23324
|
-
* A picker's language and direction inside a `LocaleProvider`, by the rule a
|
|
23325
|
-
* nested provider follows: its `lang` is its own `locale`, else the
|
|
23326
|
-
* provider's, and its `dir` is the `dir` passed to it, else its own locale's
|
|
23327
|
-
* direction when it has one, else the provider's. Outside a provider it
|
|
23328
|
-
* changes nothing. A `locale` `Intl` refuses counts as none here, since the
|
|
23329
|
-
* picker's own formatting reports it.
|
|
23330
|
-
*/
|
|
23331
|
-
function usePickerLocaleScope(ownLocale, ownDir) {
|
|
23332
|
-
const scope = useLocaleScope();
|
|
23333
|
-
return React.useMemo(() => {
|
|
23334
|
-
if (!scope)
|
|
23335
|
-
return { rootAttrs: NO_ROOT_ATTRS };
|
|
23336
|
-
let own;
|
|
23337
|
-
try {
|
|
23338
|
-
own = canonicalLocale(ownLocale);
|
|
23339
|
-
}
|
|
23340
|
-
catch {
|
|
23341
|
-
own = undefined;
|
|
23342
|
-
}
|
|
23343
|
-
const dir = ownDir || (own ? localeDirection(own) : scope.dir);
|
|
23344
|
-
return {
|
|
23345
|
-
rootAttrs: own ? { lang: own, dir } : NO_ROOT_ATTRS,
|
|
23346
|
-
panel: { locale: own || scope.locale, dir },
|
|
23347
|
-
};
|
|
23348
|
-
}, [scope, ownLocale, ownDir]);
|
|
23349
|
-
}
|
|
23350
|
-
/**
|
|
23351
|
-
* Whether a picker's `dayNames` replace the calendar's own, which `Calendar`
|
|
23352
|
-
* does only for a full week of seven.
|
|
23353
|
-
*/
|
|
23354
|
-
function hasOwnDayNames(dayNames) {
|
|
23355
|
-
return dayNames !== undefined && dayNames.length === 7;
|
|
23356
|
-
}
|
|
23357
|
-
/**
|
|
23358
|
-
* The day a picker's week starts on: its own `firstDayOfWeek`, else, inside a
|
|
23359
|
-
* `LocaleProvider`, the first day of the locale the picker uses, else Sunday.
|
|
23360
|
-
* Outside a provider it stays Sunday whatever the locale, as it always has,
|
|
23361
|
-
* and so does a picker inside one with no locale from either, since the
|
|
23362
|
-
* runtime's own differs between the server and the browser.
|
|
23363
|
-
*
|
|
23364
|
-
* It also stays Sunday for a picker that brings its own `dayNames`. The
|
|
23365
|
-
* calendar shows those exactly as given, in calendar order, and before the
|
|
23366
|
-
* provider an unset `firstDayOfWeek` was always Sunday, so names written
|
|
23367
|
-
* Sunday first would sit a day or more off their columns if the week moved
|
|
23368
|
-
* under them.
|
|
23369
|
-
*/
|
|
23370
|
-
function usePickerFirstDayOfWeek(own, locale, hasDayNames) {
|
|
23371
|
-
const inProvider = useLocaleScope() !== null;
|
|
23372
|
-
return React.useMemo(() => {
|
|
23373
|
-
if (own !== undefined)
|
|
23374
|
-
return own;
|
|
23375
|
-
if (!inProvider || !locale || hasDayNames)
|
|
23376
|
-
return 0;
|
|
23377
|
-
try {
|
|
23378
|
-
return localeFirstDay(canonicalLocale(locale));
|
|
23379
|
-
}
|
|
23380
|
-
catch {
|
|
23381
|
-
// A tag Intl refuses: the picker's own formatting says so.
|
|
23382
|
-
return 0;
|
|
23383
|
-
}
|
|
23384
|
-
}, [own, inProvider, locale, hasDayNames]);
|
|
23385
|
-
}
|
|
23386
|
-
|
|
23387
24093
|
const DEFAULT_FORMATS = {
|
|
23388
24094
|
day: DEFAULT_DATE_FORMAT,
|
|
23389
24095
|
month: DEFAULT_MONTH_FORMAT,
|
|
@@ -28774,6 +29480,7 @@ exports.ToastContainer = ToastContainer;
|
|
|
28774
29480
|
exports.ToggleButton = ToggleButton;
|
|
28775
29481
|
exports.Tooltip = Tooltip;
|
|
28776
29482
|
exports.Tr = Tr;
|
|
29483
|
+
exports.TreeSelect = TreeSelect;
|
|
28777
29484
|
exports.TreeView = TreeView;
|
|
28778
29485
|
exports.TreeViewItem = TreeViewItem;
|
|
28779
29486
|
exports.UnorderedList = UnorderedList;
|