@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.
Files changed (59) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/TreeView.js +14 -10
  4. package/dist/esm/components/TreeView.js.map +1 -1
  5. package/dist/esm/form/Combobox.js +8 -13
  6. package/dist/esm/form/Combobox.js.map +1 -1
  7. package/dist/esm/form/Field.js.map +1 -1
  8. package/dist/esm/form/TreeSelect.js +395 -0
  9. package/dist/esm/form/TreeSelect.js.map +1 -0
  10. package/dist/esm/form/_pickerInternals/PickerPopover.js +5 -1
  11. package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
  12. package/dist/esm/form/_selectInternals/DialogSelect.js +208 -0
  13. package/dist/esm/form/_selectInternals/DialogSelect.js.map +1 -0
  14. package/dist/esm/form/_selectInternals/ValueTags.js +28 -0
  15. package/dist/esm/form/_selectInternals/ValueTags.js.map +1 -0
  16. package/dist/esm/helpers/treeData.js +97 -0
  17. package/dist/esm/helpers/treeData.js.map +1 -0
  18. package/dist/esm/helpers/useComboboxEngine.js +2 -2
  19. package/dist/esm/helpers/useComboboxEngine.js.map +1 -1
  20. package/dist/esm/helpers/useControlled.js +30 -0
  21. package/dist/esm/helpers/useControlled.js.map +1 -0
  22. package/dist/esm/helpers/useTreeView.js +15 -13
  23. package/dist/esm/helpers/useTreeView.js.map +1 -1
  24. package/dist/extras.css +1 -1
  25. package/dist/extras.css.map +1 -1
  26. package/dist/index.cjs +1099 -392
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.esm.js +1 -0
  29. package/dist/index.esm.js.map +1 -1
  30. package/dist/types/form/Field.d.ts +1 -1
  31. package/dist/types/form/TreeSelect.d.ts +308 -0
  32. package/dist/types/form/_pickerInternals/PickerPopover.d.ts +8 -0
  33. package/dist/types/form/_selectInternals/DialogSelect.d.ts +86 -0
  34. package/dist/types/form/_selectInternals/ValueTags.d.ts +40 -0
  35. package/dist/types/helpers/treeData.d.ts +48 -0
  36. package/dist/types/helpers/useControlled.d.ts +14 -0
  37. package/dist/types/helpers/useTreeView.d.ts +30 -0
  38. package/dist/types/index.d.ts +1 -0
  39. package/dist/types-cjs/form/Field.d.ts +1 -1
  40. package/dist/types-cjs/form/TreeSelect.d.ts +308 -0
  41. package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +8 -0
  42. package/dist/types-cjs/form/_selectInternals/DialogSelect.d.ts +86 -0
  43. package/dist/types-cjs/form/_selectInternals/ValueTags.d.ts +40 -0
  44. package/dist/types-cjs/helpers/treeData.d.ts +48 -0
  45. package/dist/types-cjs/helpers/useControlled.d.ts +14 -0
  46. package/dist/types-cjs/helpers/useTreeView.d.ts +30 -0
  47. package/dist/types-cjs/index.d.ts +1 -0
  48. package/dist/versions/bestax-no-dark-mode.css +1 -1
  49. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  50. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  51. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  52. package/dist/versions/bestax-no-helpers.css +1 -1
  53. package/dist/versions/bestax-no-helpers.css.map +1 -1
  54. package/dist/versions/bestax-prefixed.css +1 -1
  55. package/dist/versions/bestax-prefixed.css.map +1 -1
  56. package/package.json +1 -1
  57. package/src/scss/form/_index.scss +1 -0
  58. package/src/scss/form/_picker-popover.scss +6 -0
  59. 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 && !branch.hidden;
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: branch.hidden || !open,
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 check knows
12038
- // what's under it.
12039
- const items = (open || tree.rendersClosed) && (jsxRuntime.jsx(BranchContext.Provider, { value: childBranch, children: children }));
12040
- if (branch.hidden)
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 tagIds = picked.map((_, index) => `${tagsId}-${index}`);
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 ? tagIds.join(' ') : undefined, placeholder: placeholder, value: query ??
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
- // An IME that's composing owns every key: Enter confirms what it composed,
19438
- // the arrows move through its candidates, and Escape cancels it. Safari can
19439
- // say so with `keyCode` 229 rather than `isComposing` (#1102).
19440
- const isComposing = (e) => e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
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 `Taginput` component provides a tag/chip input field for managing multiple tags.
19443
- *
19444
- * @function
19445
- * @param {TaginputProps} props - Props for the Taginput component.
19446
- * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the input element.
19447
- * @returns {JSX.Element} The rendered taginput component.
19448
- *
19449
- * @example
19450
- * // Basic usage
19451
- * <Taginput
19452
- * placeholder="Add a tag..."
19453
- * onChange={(tags) => console.log(tags)}
19454
- * />
19455
- *
19456
- * @example
19457
- * // With autocomplete
19458
- * <Taginput
19459
- * data={['React', 'Vue', 'Angular', 'Svelte']}
19460
- * allowNew={false}
19461
- * placeholder="Select frameworks..."
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
- // Get display value from tag
19513
- const getDisplayValue = (tag) => {
19514
- if (typeof tag === 'string')
19515
- return tag;
19516
- const fieldValue = tag[field];
19517
- if (typeof fieldValue === 'string')
19518
- return fieldValue;
19519
- return tag.value || '';
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
- // Filter autocomplete data
19522
- const filteredData = data.filter(item => {
19523
- const displayValue = item.toLowerCase();
19524
- const matchesInput = matchText
19525
- ? matchText(item, inputValue)
19526
- : displayValue.includes(inputValue.toLowerCase());
19527
- const notAlreadyAdded = allowDuplicates ||
19528
- !tags.some(tag => getDisplayValue(tag).toLowerCase() === displayValue);
19529
- const hasInput = inputValue || (openOnFocus && isActive);
19530
- return matchesInput && notAlreadyAdded && hasInput;
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
- // At the tag limit the list stays closed, since it could only offer tags
19533
- // past the limit (#1087).
19534
- const listOpen = isActive && !isMaxReached && filteredData.length > 0;
19535
- // keepFirst: auto-highlight first item when dropdown opens
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 (keepFirst && isActive && filteredData.length > 0) {
19538
- setHighlightedIndex(0);
19539
- }
19540
- }, [keepFirst, isActive, filteredData.length]);
19541
- // Update tags
19542
- const updateTags = React.useCallback((newTags) => {
19543
- if (controlledValue === undefined) {
19544
- setInternalTags(newTags);
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
- onChange?.(newTags);
19547
- }, [controlledValue, onChange]);
19548
- // Add tags from text typed, pasted or picked from the list. Each one is
19549
- // checked against the tags before it, this batch's included, so a paste
19550
- // of several adds them all in one change (#1101). Only `createTag` turns
19551
- // text into an object.
19552
- const addTags = React.useCallback((values) => {
19553
- if (disabled || readonly)
19554
- return;
19555
- let next = tags;
19556
- const added = [];
19557
- for (const value of values) {
19558
- // A paste can bring more tags than there's room for, and at the
19559
- // limit the read-only input still takes Enter.
19560
- if (maxTags !== undefined && next.length >= maxTags)
19561
- break;
19562
- const displayValue = value.trim();
19563
- if (!displayValue)
19564
- continue;
19565
- const lower = displayValue.toLowerCase();
19566
- // Duplicates, data and the dropdown filter all compare
19567
- // case-insensitively.
19568
- if (!allowDuplicates &&
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;