@allxsmith/bestax-bulma 5.60.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 (98) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/Carousel.js +3 -43
  4. package/dist/esm/components/Carousel.js.map +1 -1
  5. package/dist/esm/components/Command.js +3 -3
  6. package/dist/esm/components/Navbar.js +3 -1
  7. package/dist/esm/components/Navbar.js.map +1 -1
  8. package/dist/esm/components/Popover.js +7 -27
  9. package/dist/esm/components/Popover.js.map +1 -1
  10. package/dist/esm/components/Sidebar.js +17 -58
  11. package/dist/esm/components/Sidebar.js.map +1 -1
  12. package/dist/esm/components/TreeView.js +14 -10
  13. package/dist/esm/components/TreeView.js.map +1 -1
  14. package/dist/esm/form/ColorPicker.js +4 -4
  15. package/dist/esm/form/ColorPicker.js.map +1 -1
  16. package/dist/esm/form/Combobox.js +11 -16
  17. package/dist/esm/form/Combobox.js.map +1 -1
  18. package/dist/esm/form/Field.js.map +1 -1
  19. package/dist/esm/form/TreeSelect.js +395 -0
  20. package/dist/esm/form/TreeSelect.js.map +1 -0
  21. package/dist/esm/form/_pickerInternals/PickerPopover.js +5 -1
  22. package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
  23. package/dist/esm/form/_selectInternals/DialogSelect.js +208 -0
  24. package/dist/esm/form/_selectInternals/DialogSelect.js.map +1 -0
  25. package/dist/esm/form/_selectInternals/ValueTags.js +28 -0
  26. package/dist/esm/form/_selectInternals/ValueTags.js.map +1 -0
  27. package/dist/esm/helpers/overlayStack.js +33 -1
  28. package/dist/esm/helpers/overlayStack.js.map +1 -1
  29. package/dist/esm/helpers/treeData.js +97 -0
  30. package/dist/esm/helpers/treeData.js.map +1 -0
  31. package/dist/esm/helpers/useBulmaViewport.js +82 -0
  32. package/dist/esm/helpers/useBulmaViewport.js.map +1 -0
  33. package/dist/esm/helpers/useComboboxEngine.js +2 -2
  34. package/dist/esm/helpers/useComboboxEngine.js.map +1 -1
  35. package/dist/esm/helpers/useControlled.js +30 -0
  36. package/dist/esm/helpers/useControlled.js.map +1 -0
  37. package/dist/esm/helpers/useInertSiblings.js +7 -3
  38. package/dist/esm/helpers/useInertSiblings.js.map +1 -1
  39. package/dist/esm/helpers/useOutsidePress.js +53 -0
  40. package/dist/esm/helpers/useOutsidePress.js.map +1 -0
  41. package/dist/esm/helpers/useOverlayFocus.js +74 -0
  42. package/dist/esm/helpers/useOverlayFocus.js.map +1 -0
  43. package/dist/esm/helpers/useTreeView.js +15 -13
  44. package/dist/esm/helpers/useTreeView.js.map +1 -1
  45. package/dist/esm/layout/AppFrame.js +438 -0
  46. package/dist/esm/layout/AppFrame.js.map +1 -0
  47. package/dist/extras.css +1 -1
  48. package/dist/extras.css.map +1 -1
  49. package/dist/index.cjs +1767 -518
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.esm.js +2 -0
  52. package/dist/index.esm.js.map +1 -1
  53. package/dist/types/components/Navbar.d.ts +4 -3
  54. package/dist/types/form/Field.d.ts +1 -1
  55. package/dist/types/form/TreeSelect.d.ts +308 -0
  56. package/dist/types/form/_pickerInternals/PickerPopover.d.ts +8 -0
  57. package/dist/types/form/_selectInternals/DialogSelect.d.ts +86 -0
  58. package/dist/types/form/_selectInternals/ValueTags.d.ts +40 -0
  59. package/dist/types/helpers/overlayStack.d.ts +17 -0
  60. package/dist/types/helpers/treeData.d.ts +48 -0
  61. package/dist/types/helpers/useBulmaViewport.d.ts +34 -0
  62. package/dist/types/helpers/useControlled.d.ts +14 -0
  63. package/dist/types/helpers/useInertSiblings.d.ts +4 -1
  64. package/dist/types/helpers/useOutsidePress.d.ts +24 -0
  65. package/dist/types/helpers/useOverlayFocus.d.ts +44 -0
  66. package/dist/types/helpers/useTreeView.d.ts +30 -0
  67. package/dist/types/index.d.ts +2 -0
  68. package/dist/types/layout/AppFrame.d.ts +306 -0
  69. package/dist/types-cjs/components/Navbar.d.ts +4 -3
  70. package/dist/types-cjs/form/Field.d.ts +1 -1
  71. package/dist/types-cjs/form/TreeSelect.d.ts +308 -0
  72. package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +8 -0
  73. package/dist/types-cjs/form/_selectInternals/DialogSelect.d.ts +86 -0
  74. package/dist/types-cjs/form/_selectInternals/ValueTags.d.ts +40 -0
  75. package/dist/types-cjs/helpers/overlayStack.d.ts +17 -0
  76. package/dist/types-cjs/helpers/treeData.d.ts +48 -0
  77. package/dist/types-cjs/helpers/useBulmaViewport.d.ts +34 -0
  78. package/dist/types-cjs/helpers/useControlled.d.ts +14 -0
  79. package/dist/types-cjs/helpers/useInertSiblings.d.ts +4 -1
  80. package/dist/types-cjs/helpers/useOutsidePress.d.ts +24 -0
  81. package/dist/types-cjs/helpers/useOverlayFocus.d.ts +44 -0
  82. package/dist/types-cjs/helpers/useTreeView.d.ts +30 -0
  83. package/dist/types-cjs/index.d.ts +2 -0
  84. package/dist/types-cjs/layout/AppFrame.d.ts +306 -0
  85. package/dist/versions/bestax-no-dark-mode.css +1 -1
  86. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  87. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  88. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  89. package/dist/versions/bestax-no-helpers.css +1 -1
  90. package/dist/versions/bestax-no-helpers.css.map +1 -1
  91. package/dist/versions/bestax-prefixed.css +1 -1
  92. package/dist/versions/bestax-prefixed.css.map +1 -1
  93. package/package.json +1 -1
  94. package/src/scss/form/_index.scss +1 -0
  95. package/src/scss/form/_picker-popover.scss +6 -0
  96. package/src/scss/form/_treeselect.scss +146 -0
  97. package/src/scss/layout/_appframe.scss +306 -0
  98. package/src/scss/layout/_index.scss +1 -0
package/dist/index.cjs CHANGED
@@ -4755,6 +4755,37 @@ function useOverlayStack(inStack, modal = true) {
4755
4755
  return !above.some(overlay => among !== 'modal' || overlay.modal);
4756
4756
  }, []);
4757
4757
  }
4758
+ /**
4759
+ * Calls `onEscape` for an Escape while `active` and the overlay is the
4760
+ * topmost one, as `isTopmost` (from `useOverlayStack`) says, and marks the key
4761
+ * used with `preventDefault()`, so nothing under it acts on it too.
4762
+ *
4763
+ * It listens on `window`, so everything inside the overlay has had the key
4764
+ * first, and an Escape something there used, such as a popup closing, is
4765
+ * left to it. The listener stays put while `onEscape` changes identity, as an
4766
+ * inline one does on every render of the parent: it reads the latest one as
4767
+ * the key arrives.
4768
+ *
4769
+ * @function useOverlayEscape
4770
+ * @param active - Whether the overlay is open and closes on Escape.
4771
+ * @param isTopmost - The overlay's check from `useOverlayStack`.
4772
+ * @param onEscape - What closing it does.
4773
+ */
4774
+ function useOverlayEscape(active, isTopmost, onEscape) {
4775
+ const onEscapeRef = useLatestRef(onEscape);
4776
+ React.useEffect(() => {
4777
+ if (!active)
4778
+ return undefined;
4779
+ const handleKeyDown = (e) => {
4780
+ if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
4781
+ return;
4782
+ e.preventDefault();
4783
+ onEscapeRef.current();
4784
+ };
4785
+ window.addEventListener('keydown', handleKeyDown);
4786
+ return () => window.removeEventListener('keydown', handleKeyDown);
4787
+ }, [active, isTopmost, onEscapeRef]);
4788
+ }
4758
4789
 
4759
4790
  /**
4760
4791
  * Whether `el` is editable while its parent is not, which makes it an editing
@@ -5680,7 +5711,9 @@ const NavbarBurger = React.forwardRef(function NavbarBurger({ className, active,
5680
5711
  });
5681
5712
  return (jsxRuntime.jsxs("button", { ref: ref, className: classNames(usePrefixedClassNames('navbar-burger', {
5682
5713
  'is-active': active,
5683
- }), bulmaHelperClasses, className), ...rest, "aria-label": rest['aria-label'] || 'menu', "aria-expanded": rest['aria-expanded'] ?? !!active, type: buttonType(rest.type), children: [jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), children] }));
5714
+ }), bulmaHelperClasses, className), ...rest, "aria-label": rest['aria-label'] || 'menu', "aria-expanded": rest['aria-expanded'] === null
5715
+ ? undefined
5716
+ : (rest['aria-expanded'] ?? !!active), type: buttonType(rest.type), children: [jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), children] }));
5684
5717
  });
5685
5718
  NavbarBurger.displayName = 'NavbarBurger';
5686
5719
  /**
@@ -6851,16 +6884,20 @@ const above = (el) => el.parentElement ?? el.getRootNode().host;
6851
6884
  * @param active - Whether what's beside it is inert.
6852
6885
  * @param wholePage - Whether that goes for the whole page rather than only
6853
6886
  * the element's siblings.
6887
+ * @param except - Ref to an element beside it to leave reachable too, such
6888
+ * as the overlay's own backdrop, which has to take the presses that close
6889
+ * it rather than let them through to what's under it.
6854
6890
  */
6855
- function useInertSiblings(ref, active, wholePage) {
6891
+ function useInertSiblings(ref, active, wholePage, except) {
6856
6892
  React.useEffect(() => {
6857
6893
  const el = ref.current;
6858
6894
  if (!active || !el)
6859
6895
  return undefined;
6860
6896
  const doc = el.ownerDocument;
6897
+ const keep = except?.current;
6861
6898
  const held = [];
6862
6899
  for (let node = el; node && node !== doc.body; node = wholePage ? above(node) : undefined) {
6863
- held.push(...siblingsOf(node).filter(hold));
6900
+ held.push(...siblingsOf(node).filter(sibling => sibling !== keep && hold(sibling)));
6864
6901
  }
6865
6902
  // Focus this takes away, to hand back later. It's looked up as `el`'s
6866
6903
  // tree sees it, where focus inside a nested shadow root reads as its
@@ -6875,7 +6912,7 @@ function useInertSiblings(ref, active, wholePage) {
6875
6912
  if (doc.activeElement === doc.body)
6876
6913
  taken?.focus();
6877
6914
  };
6878
- }, [ref, active, wholePage]);
6915
+ }, [ref, active, wholePage, except]);
6879
6916
  }
6880
6917
 
6881
6918
  /**
@@ -7971,6 +8008,51 @@ function useAnchoredPosition(anchorRef, panelRef, { active, position, fixed, off
7971
8008
  return resolved;
7972
8009
  }
7973
8010
 
8011
+ // INTERNAL: deliberately not exported from src/index.ts.
8012
+ /**
8013
+ * Calls `onOutsidePress` for a `pointerdown` outside `ref`'s element while
8014
+ * `active`, as a Popover closes on one.
8015
+ *
8016
+ * A press counts as inside in two ways: on the element's own path in the DOM,
8017
+ * and anywhere in a React tree the returned handler is put on, as
8018
+ * `onPointerDownCapture`. React's tree includes content portaled out of it,
8019
+ * which the DOM path doesn't, and React has seen a press there by the time
8020
+ * the document hears it.
8021
+ *
8022
+ * The listener is on the element's own document, an iframe's when the tree
8023
+ * renders into one, and reads the latest `onOutsidePress` as a press
8024
+ * arrives, so it stays put while the callback changes identity.
8025
+ *
8026
+ * @function useOutsidePress
8027
+ * @param ref - Ref to the element a press inside of doesn't count.
8028
+ * @param active - Whether to listen.
8029
+ * @param onOutsidePress - Called with each press outside.
8030
+ * @returns The `onPointerDownCapture` handler that marks a press in its tree
8031
+ * as inside.
8032
+ */
8033
+ function useOutsidePress(ref, active, onOutsidePress) {
8034
+ // The last pointerdown React saw inside.
8035
+ const insidePressRef = React.useRef(null);
8036
+ const onOutsidePressRef = useLatestRef(onOutsidePress);
8037
+ React.useEffect(() => {
8038
+ const element = ref.current;
8039
+ if (!active || !element)
8040
+ return undefined;
8041
+ const handlePointerDown = (e) => {
8042
+ if (e === insidePressRef.current || isEventInside(e, ref.current)) {
8043
+ return;
8044
+ }
8045
+ onOutsidePressRef.current(e);
8046
+ };
8047
+ const doc = element.ownerDocument;
8048
+ doc.addEventListener('pointerdown', handlePointerDown);
8049
+ return () => doc.removeEventListener('pointerdown', handlePointerDown);
8050
+ }, [ref, active, onOutsidePressRef]);
8051
+ return React.useCallback((event) => {
8052
+ insidePressRef.current = event.nativeEvent;
8053
+ }, []);
8054
+ }
8055
+
7974
8056
  const PopoverContext = React.createContext(null);
7975
8057
  /** Intrinsic elements that are controls already, and keep their own role. */
7976
8058
  const NATIVE_CONTROLS = new Set([
@@ -8021,9 +8103,6 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
8021
8103
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8022
8104
  const rootRef = React.useRef(null);
8023
8105
  const panelRef = React.useRef(null);
8024
- // The last pointerdown React saw inside the panel. React's tree includes
8025
- // content the panel portals elsewhere, which the DOM path doesn't.
8026
- const insidePointerRef = React.useRef(null);
8027
8106
  const baseId = React.useId();
8028
8107
  const panelId = `${baseId}-panel`;
8029
8108
  const titleId = `${baseId}-title`;
@@ -8079,25 +8158,10 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
8079
8158
  }
8080
8159
  };
8081
8160
  }, [panelMounted, trapFocus, triggerNode]);
8082
- // Read when a press lands outside, so the listener stays put while `close`
8083
- // changes identity, which it does with `onOpenChange`, and an inline one
8084
- // changes on every render of the parent.
8085
- const closeRef = useLatestRef(close);
8086
- React.useEffect(() => {
8087
- if (!panelMounted || !closeOnClickOutside)
8088
- return undefined;
8089
- const handlePointerDown = (e) => {
8090
- if (e === insidePointerRef.current || isEventInside(e, rootRef.current)) {
8091
- return;
8092
- }
8093
- closeRef.current();
8094
- };
8095
- // The wrapper is on the page whenever the panel is. Its document, an
8096
- // iframe's when the tree renders into one, is where presses land.
8097
- const doc = rootRef.current.ownerDocument;
8098
- doc.addEventListener('pointerdown', handlePointerDown);
8099
- return () => doc.removeEventListener('pointerdown', handlePointerDown);
8100
- }, [panelMounted, closeOnClickOutside, closeRef]);
8161
+ // A press outside the wrapper, which holds the trigger and the panel,
8162
+ // closes it. One inside the panel's React tree counts as inside, so content
8163
+ // the panel portals elsewhere does too.
8164
+ const markInsidePress = useOutsidePress(rootRef, panelMounted && closeOnClickOutside, close);
8101
8165
  const handlePanelKeyDown = (e) => {
8102
8166
  if (e.key !== 'Escape' || !closeOnEscape || e.defaultPrevented)
8103
8167
  return;
@@ -8163,9 +8227,7 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
8163
8227
  top: resolved.top,
8164
8228
  left: resolved.left,
8165
8229
  }
8166
- : undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture: e => {
8167
- insidePointerRef.current = e.nativeEvent;
8168
- }, children: children }) }))] }));
8230
+ : undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture: markInsidePress, children: children }) }))] }));
8169
8231
  };
8170
8232
  /**
8171
8233
  * The open panel. It mounts with each opening, so it counts the
@@ -9293,6 +9355,69 @@ const TimelineRoot = React.forwardRef(({ className, children, active, pending, a
9293
9355
  });
9294
9356
  const Timeline = withSubComponents(TimelineRoot, { Item: TimelineItem }, 'Timeline');
9295
9357
 
9358
+ // INTERNAL: deliberately not exported from src/index.ts.
9359
+ /**
9360
+ * Moves focus into an overlay as it opens, to the first stop Tab wraps to
9361
+ * inside `ref`'s element or the element itself, and hands it back to
9362
+ * whatever had it as it closes, as `Modal` does. Focus goes back only while
9363
+ * the overlay still holds it, or it's been dropped to `<body>`: closing one
9364
+ * under another overlay mustn't pull focus out of the one on top, and focus
9365
+ * moved out to the page stays there. The opener is found wherever it sits,
9366
+ * inside a shadow root too, in the overlay's own document, an iframe's when
9367
+ * it renders into one. It's read in a layout effect as the overlay opens, so
9368
+ * before the commit's passive effects, such as one that makes the page
9369
+ * inert. Content inside the overlay with a layout effect of its own, or an
9370
+ * `autoFocus`, can still move focus first, and then there's no opener to go
9371
+ * back to: focus is never handed back into the overlay that's closing.
9372
+ *
9373
+ * @function useOverlayFocus
9374
+ * @param ref - Ref to the overlay's element.
9375
+ * @param open - Whether the overlay is open.
9376
+ * @param options - Whether it's modal, what remounts it, and whether a
9377
+ * close hands focus back.
9378
+ */
9379
+ function useOverlayFocus(ref, open, { modal, remountKey, restore }) {
9380
+ const openerRef = React.useRef(null);
9381
+ const restoreRef = useLatestRef(restore);
9382
+ // The element that had focus, wherever it sits: a portaled overlay renders
9383
+ // under `document.body` while its opener can be inside a shadow root.
9384
+ useClientLayoutEffect(() => {
9385
+ if (!open)
9386
+ return;
9387
+ const element = ref.current;
9388
+ /* istanbul ignore if: as below, the ref is set while the overlay is open */
9389
+ if (!element)
9390
+ return;
9391
+ // Focus already inside, where an `autoFocus` in the overlay puts it, has
9392
+ // no opener to go back to.
9393
+ openerRef.current = element.contains(getActiveElementInTree(element))
9394
+ ? null
9395
+ : getDeepestActiveElement(element.ownerDocument);
9396
+ }, [open, ref]);
9397
+ React.useEffect(() => {
9398
+ if (!open)
9399
+ return undefined;
9400
+ const element = ref.current;
9401
+ /* istanbul ignore if: the overlay renders whenever it's open, so its ref is set before this runs */
9402
+ if (!element)
9403
+ return undefined;
9404
+ if (modal || !isModalOverlayOpen()) {
9405
+ focusTabStop(element);
9406
+ }
9407
+ return () => {
9408
+ // A removed subtree leaves focus on <body>, which still counts as held.
9409
+ const active = getActiveElementInTree(element);
9410
+ const held = active === element.ownerDocument.body || element.contains(active);
9411
+ // Asked now rather than as it opened: it's about this close.
9412
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9413
+ if (held && (restoreRef.current?.() ?? true)) {
9414
+ openerRef.current?.focus();
9415
+ }
9416
+ };
9417
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `modal` is read as the overlay opens, on purpose
9418
+ }, [open, remountKey, ref, restoreRef]);
9419
+ }
9420
+
9296
9421
  /**
9297
9422
  * The `Sidebar` component provides a slide-out navigation panel that appears from the left or right side of the screen.
9298
9423
  *
@@ -9342,31 +9467,15 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
9342
9467
  handleClose();
9343
9468
  }
9344
9469
  }, [overlayClose, handleClose]);
9345
- // Read when Escape arrives, so the key listener stays put while
9346
- // `onClose` changes identity, as an inline one does on every render of
9347
- // the parent.
9348
- const handleCloseRef = useLatestRef(handleClose);
9349
9470
  // Joins the stack of open overlays, so the sidebar answers Escape only
9350
9471
  // while it's the topmost overlay, and keeps it from those under it. It's
9351
9472
  // modal with an overlay: without one it leaves Tab to a modal overlay
9352
9473
  // under it.
9353
9474
  const isTopmost = useOverlayStack(isOpen, overlay);
9354
9475
  const closesOnEscape = escapeClose && canCancel && Boolean(onClose);
9355
- // Handle escape key. On `window`, as in `Modal`, so everything inside the
9356
- // sidebar has had the key first, and an Escape it used is left to it. One
9357
- // the sidebar closes on is marked used, so nothing under it acts on it.
9358
- React.useEffect(() => {
9359
- if (!isOpen || !closesOnEscape)
9360
- return;
9361
- const handleKeyDown = (e) => {
9362
- if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
9363
- return;
9364
- e.preventDefault();
9365
- handleCloseRef.current();
9366
- };
9367
- window.addEventListener('keydown', handleKeyDown);
9368
- return () => window.removeEventListener('keydown', handleKeyDown);
9369
- }, [isOpen, closesOnEscape, handleCloseRef, isTopmost]);
9476
+ // Escape closes it, on `window`, as in `Modal`, so everything inside the
9477
+ // sidebar has had the key first, and an Escape it used is left to it.
9478
+ useOverlayEscape(isOpen && closesOnEscape, isTopmost, handleClose);
9370
9479
  // Keep Tab inside while there's an overlay, as `Modal` does:
9371
9480
  // `aria-modal` hides the rest of the page from assistive technology, so
9372
9481
  // the keyboard order has to agree. Only while it's the topmost modal
@@ -9389,41 +9498,17 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
9389
9498
  useScrollLock(isOpen && overlay);
9390
9499
  // Move focus into the sidebar on open, to the first stop Tab wraps to or
9391
9500
  // the sidebar itself, and hand it back on close, as `Modal` does. A
9392
- // different `defaultTarget` remounts the portaled sidebar: the cleanup
9393
- // hands focus back from the old nodes, which left it on <body>, and the
9394
- // re-run records the same opener and focuses the new nodes. Whether it
9395
- // takes focus is decided as it opens, so `overlay` changing while it's
9396
- // open leaves focus where the user has it.
9397
- React.useEffect(() => {
9398
- if (!isOpen)
9399
- return undefined;
9400
- const element = sidebarRef.current;
9401
- /* istanbul ignore if: the aside renders whenever the sidebar is open, so its ref is set before this runs */
9402
- if (!element)
9403
- return undefined;
9404
- // The element that had focus, wherever it sits: a portaled sidebar
9405
- // renders under `document.body` while its opener can be inside a
9406
- // shadow root.
9407
- const opener = getDeepestActiveElement();
9408
- // Without an overlay the sidebar holds no Tab, so with a modal overlay
9409
- // open under it, the first Tab would take focus back there and leave
9410
- // the sidebar's other controls out of reach. It leaves focus alone
9411
- // then, and the cleanup below finds focus outside and leaves it too.
9412
- if (overlay || !isModalOverlayOpen()) {
9413
- focusTabStop(element);
9414
- }
9415
- return () => {
9416
- // Only while the sidebar still holds focus: closing one under another
9417
- // overlay mustn't pull focus out of the one on top, and focus moved
9418
- // out to the page stays there. A removed subtree leaves focus on
9419
- // <body>, which still counts as held.
9420
- const active = getActiveElementInTree(element);
9421
- if (active === document.body || element.contains(active)) {
9422
- opener?.focus();
9423
- }
9424
- };
9425
- // eslint-disable-next-line react-hooks/exhaustive-deps -- `overlay` is read as the sidebar opens, on purpose
9426
- }, [isOpen, defaultTarget]);
9501
+ // different `defaultTarget` remounts the portaled sidebar: focus is handed
9502
+ // back from the old nodes and moved into the new ones. Whether it takes
9503
+ // focus is decided as it opens, so `overlay` changing while it's open
9504
+ // leaves focus where the user has it. Without an overlay the sidebar holds
9505
+ // no Tab, so with a modal overlay open under it, it leaves focus alone:
9506
+ // the first Tab would take focus back there and leave the sidebar's other
9507
+ // controls out of reach.
9508
+ useOverlayFocus(sidebarRef, isOpen, {
9509
+ modal: overlay,
9510
+ remountKey: defaultTarget,
9511
+ });
9427
9512
  // Use combined ref
9428
9513
  const combinedRef = React.useCallback((node) => {
9429
9514
  sidebarRef.current =
@@ -10147,6 +10232,81 @@ const DialogContainer = () => {
10147
10232
  return (jsxRuntime.jsx(Dialog, { isOpen: true, title: options.title, message: options.message, type: options.type, confirmText: options.confirmText, cancelText: options.cancelText, icon: options.icon, showCancel: type === 'confirm', focusCancel: options.focusCancel, onConfirm: () => dialog.close(type === 'confirm' ? true : undefined), onCancel: () => dialog.close(type === 'confirm' ? false : undefined) }));
10148
10233
  };
10149
10234
 
10235
+ // INTERNAL: deliberately not exported from src/index.ts.
10236
+ /**
10237
+ * The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
10238
+ * mixins open with, widest first, at Bulma's default breakpoints. A
10239
+ * stylesheet lays a component out with those mixins; these tell its script
10240
+ * which layout that is. A theme that moves Bulma's breakpoints moves the
10241
+ * stylesheet's switch but not these. Narrower than the last one is `mobile`.
10242
+ */
10243
+ const VIEWPORT_QUERIES = [
10244
+ 'screen and (min-width: 1408px)',
10245
+ 'screen and (min-width: 1216px)',
10246
+ 'screen and (min-width: 1024px)',
10247
+ 'screen and (min-width: 769px), print',
10248
+ ];
10249
+ /** `useBulmaViewport`'s answer below the tablet breakpoint. */
10250
+ const MOBILE = VIEWPORT_QUERIES.length;
10251
+ /** Where Bulma's `widescreen`, `desktop` and `tablet` sit in `VIEWPORT_QUERIES`. */
10252
+ const VIEWPORT_INDEX = {
10253
+ widescreen: 1,
10254
+ desktop: 2,
10255
+ tablet: 3,
10256
+ };
10257
+ /**
10258
+ * Which of Bulma's viewports the window is in: the index of the widest
10259
+ * `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
10260
+ * when that isn't known (on the server, before the first layout effect, and
10261
+ * whenever `enabled` is off, so a caller that needs no answer subscribes to
10262
+ * nothing). It's read in a layout effect, so the first paint after hydration
10263
+ * already has it.
10264
+ *
10265
+ * @function useBulmaViewport
10266
+ * @param enabled - Whether the caller needs the viewport at all.
10267
+ * @returns The viewport, as described above.
10268
+ */
10269
+ function useBulmaViewport(enabled) {
10270
+ const [viewport, setViewport] = React.useState(-1);
10271
+ useClientLayoutEffect(() => {
10272
+ if (!enabled || typeof window.matchMedia !== 'function')
10273
+ return undefined;
10274
+ const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
10275
+ const update = () => {
10276
+ const index = lists.findIndex(list => list.matches);
10277
+ setViewport(index < 0 ? MOBILE : index);
10278
+ };
10279
+ update();
10280
+ lists.forEach(list => list.addEventListener('change', update));
10281
+ return () => lists.forEach(list => list.removeEventListener('change', update));
10282
+ }, [enabled]);
10283
+ return enabled ? viewport : -1;
10284
+ }
10285
+ /**
10286
+ * Whether the window is at or past one of Bulma's breakpoints, at its default
10287
+ * width, through the one media query Bulma's mixin for it opens with, so a
10288
+ * caller re-renders only when the window crosses that breakpoint. `null`
10289
+ * until it's known: on the server, and before the first layout effect. Where
10290
+ * `matchMedia` isn't there, it's `true`, the wide layout a server renders.
10291
+ *
10292
+ * @function useBulmaBreakpoint
10293
+ * @param breakpoint - The breakpoint: `tablet`, `desktop` or `widescreen`.
10294
+ * @returns Whether the window is at or past it, or `null`.
10295
+ */
10296
+ function useBulmaBreakpoint(breakpoint) {
10297
+ const [atOrPast, setAtOrPast] = React.useState(null);
10298
+ useClientLayoutEffect(() => {
10299
+ const list = typeof window.matchMedia === 'function'
10300
+ ? window.matchMedia(VIEWPORT_QUERIES[VIEWPORT_INDEX[breakpoint]])
10301
+ : null;
10302
+ const update = () => setAtOrPast(list ? list.matches : true);
10303
+ update();
10304
+ list?.addEventListener('change', update);
10305
+ return () => list?.removeEventListener('change', update);
10306
+ }, [breakpoint]);
10307
+ return atOrPast;
10308
+ }
10309
+
10150
10310
  /**
10151
10311
  * Individual carousel item/slide.
10152
10312
  *
@@ -10174,24 +10334,10 @@ const DefaultPrevIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
10174
10334
  * @returns {JSX.Element} The rendered SVG chevron-right icon.
10175
10335
  */
10176
10336
  const DefaultNextIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }));
10177
- /**
10178
- * The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
10179
- * mixins open with, widest first, at Bulma's default breakpoints. The
10180
- * stylesheet picks the count in view with those mixins; these tell the
10181
- * script which count that is. Narrower than the last one is `mobile`.
10182
- */
10183
- const VIEWPORT_QUERIES = [
10184
- 'screen and (min-width: 1408px)',
10185
- 'screen and (min-width: 1216px)',
10186
- 'screen and (min-width: 1024px)',
10187
- 'screen and (min-width: 769px), print',
10188
- ];
10189
- /** `useBulmaViewport`'s answer below the tablet breakpoint. */
10190
- const MOBILE = VIEWPORT_QUERIES.length;
10191
10337
  /**
10192
10338
  * The `--bulma-carousel-items-to-show*` variable each count prop writes, in
10193
- * the order the component lists them: the viewports as above, mobile, and
10194
- * last `itemsToShow` itself.
10339
+ * the order the component lists them: `useBulmaViewport`'s viewports, widest
10340
+ * first, then mobile, and last `itemsToShow` itself.
10195
10341
  */
10196
10342
  const COUNT_SUFFIXES = [
10197
10343
  '-fullhd',
@@ -10201,33 +10347,6 @@ const COUNT_SUFFIXES = [
10201
10347
  '-mobile',
10202
10348
  '',
10203
10349
  ];
10204
- /**
10205
- * Which of Bulma's viewports the window is in: the index of the widest
10206
- * `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
10207
- * when that isn't known (on the server, before the first layout effect, and
10208
- * whenever `enabled` is off, so a carousel with one count everywhere
10209
- * subscribes to nothing).
10210
- *
10211
- * @function
10212
- * @param {boolean} enabled - Whether any viewport-specific count is set.
10213
- * @returns {number} The viewport, as described above.
10214
- */
10215
- function useBulmaViewport(enabled) {
10216
- const [viewport, setViewport] = React.useState(-1);
10217
- useClientLayoutEffect(() => {
10218
- if (!enabled || typeof window.matchMedia !== 'function')
10219
- return undefined;
10220
- const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
10221
- const update = () => {
10222
- const index = lists.findIndex(list => list.matches);
10223
- setViewport(index < 0 ? MOBILE : index);
10224
- };
10225
- update();
10226
- lists.forEach(list => list.addEventListener('change', update));
10227
- return () => lists.forEach(list => list.removeEventListener('change', update));
10228
- }, [enabled]);
10229
- return enabled ? viewport : -1;
10230
- }
10231
10350
  /**
10232
10351
  * A count prop as the whole number the carousel works with: rounded down,
10233
10352
  * and never below 1, so a zero or a stray `NaN` can't divide by nothing.
@@ -10818,6 +10937,30 @@ function RevealImpl(revealProps) {
10818
10937
  const Reveal = RevealImpl;
10819
10938
  Reveal.displayName = 'Reveal';
10820
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
+
10821
10964
  // INTERNAL: deliberately not exported from src/index.ts.
10822
10965
  /** The keys and clicks of both checkbox modes, which differ only in what a check reaches. */
10823
10966
  const CHECKBOX_ACTIONS = {
@@ -10891,6 +11034,14 @@ function pathOf(element, { values }) {
10891
11034
  }
10892
11035
  return path;
10893
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());
10894
11045
  /**
10895
11046
  * Every known item's check state, by the rules in #1096:
10896
11047
  *
@@ -10966,16 +11117,6 @@ function toggledList(records, list, value) {
10966
11117
  ...list.filter(item => !records.has(item)),
10967
11118
  ];
10968
11119
  }
10969
- /** A value the caller holds, or one this hook holds when the caller doesn't. */
10970
- function useControlled(controlled, initial) {
10971
- const [own, setOwn] = React.useState(initial);
10972
- const isControlled = controlled !== undefined;
10973
- const set = (next) => {
10974
- if (!isControlled)
10975
- setOwn(next);
10976
- };
10977
- return [isControlled ? controlled : own, set];
10978
- }
10979
11120
  /**
10980
11121
  * The list with `value` added at the end, or, when it's `present`, with
10981
11122
  * every copy of it taken out. Everything else stays as it was, in its order.
@@ -11243,12 +11384,15 @@ function useTreeView(options) {
11243
11384
  press(item, actions.space);
11244
11385
  break;
11245
11386
  case '*': {
11387
+ // The siblings on the page: not the ones a component around the
11388
+ // tree hides, which stay registered.
11246
11389
  const closed = [];
11247
11390
  registry.records.forEach((record, value) => {
11248
11391
  if (record.parent === item.parent &&
11249
11392
  record.isParent &&
11250
11393
  !record.disabled &&
11251
- !expanded.includes(value)) {
11394
+ !expanded.includes(value) &&
11395
+ registry.elements.has(value)) {
11252
11396
  closed.push(value);
11253
11397
  }
11254
11398
  });
@@ -11725,7 +11869,7 @@ const sayLoaded = (name) => `${name} loaded`;
11725
11869
  const TOP = { path: [], hidden: false };
11726
11870
  const BranchContext = React.createContext(TOP);
11727
11871
  /** The box's input lets the row's click toggle it, so a change on its own does nothing. */
11728
- const ignoreChange = () => { };
11872
+ const ignoreChange$1 = () => { };
11729
11873
  /** Whether an `icon` was given something to render. */
11730
11874
  const isGiven$1 = (node) => node !== undefined &&
11731
11875
  node !== null &&
@@ -11833,6 +11977,10 @@ const TreeViewRoot = React.forwardRef(function TreeView({ children, selectionMod
11833
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) {
11834
11978
  const tree = React.useContext(TreeViewContext);
11835
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);
11836
11984
  // Also the item's key in the live region.
11837
11985
  const labelId = React.useId();
11838
11986
  const hasItems = React.Children.toArray(children).length > 0;
@@ -11840,7 +11988,7 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
11840
11988
  const open = isParent && !!tree?.isOpen(value);
11841
11989
  const parent = branch.path[branch.path.length - 1] ?? null;
11842
11990
  // The row at the end of its group, while it's open and on the page.
11843
- const shown = open && !branch.hidden;
11991
+ const shown = open && !hiddenHere;
11844
11992
  const status = !shown
11845
11993
  ? null
11846
11994
  : isLoading
@@ -11905,8 +12053,8 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
11905
12053
  const toggleRef = React.useRef(null);
11906
12054
  const childBranch = React.useMemo(() => ({
11907
12055
  path: [...branch.path, value],
11908
- hidden: branch.hidden || !open,
11909
- }), [branch, value, open]);
12056
+ hidden: hiddenHere || !open,
12057
+ }), [branch, value, hiddenHere, open]);
11910
12058
  const { selected, checked } = tree?.stateOf(value, branch.path, disabled) ?? {};
11911
12059
  const rowClass = usePrefixedClassNames('tree-view-row', {
11912
12060
  'is-selected': selected,
@@ -11915,10 +12063,10 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
11915
12063
  if (!tree)
11916
12064
  return null;
11917
12065
  // While it's open the children render in its group. In a `checkbox` tree
11918
- // they render while it's closed too, drawing nothing, so a check knows
11919
- // what's under it.
11920
- const items = (open || tree.rendersClosed) && (jsxRuntime.jsx(BranchContext.Provider, { value: childBranch, children: children }));
11921
- 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)
11922
12070
  return jsxRuntime.jsx(jsxRuntime.Fragment, { children: items });
11923
12071
  const info = { value, parent, disabled, isParent, open };
11924
12072
  // A position the caller gives wins, for a set the page holds only part of.
@@ -11954,7 +12102,7 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
11954
12102
  if (!(event.target instanceof HTMLInputElement)) {
11955
12103
  event.preventDefault();
11956
12104
  }
11957
- }, 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)] }));
11958
12106
  });
11959
12107
  TreeViewItem.displayName = 'TreeView.Item';
11960
12108
  /**
@@ -12223,7 +12371,7 @@ ScrollArea.displayName = 'ScrollArea';
12223
12371
  // INTERNAL: deliberately not exported from src/index.ts.
12224
12372
  /** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */
12225
12373
  const isComposing$2 = (event) => event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
12226
- 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]);
12227
12375
  /**
12228
12376
  * The keys, the open state, the highlight and the ARIA of an editable
12229
12377
  * combobox whose list the app renders and filters (#1083). DOM focus stays
@@ -12325,7 +12473,7 @@ function useComboboxEngine({ open: controlledOpen, defaultOpen, onOpenChange, on
12325
12473
  }
12326
12474
  const options = shownOptions();
12327
12475
  const values = options.map(option => option.value);
12328
- const changed = !sameValues(values, shownValuesRef.current);
12476
+ const changed = !sameValues$1(values, shownValuesRef.current);
12329
12477
  shownValuesRef.current = values;
12330
12478
  setListShown(values.length > 0);
12331
12479
  const landing = landingRef.current ?? (opened ? 'open' : null);
@@ -13125,7 +13273,7 @@ const standalone$2 = {
13125
13273
  const CommandContext = React.createContext(null);
13126
13274
  const useCommand = () => React.useContext(CommandContext) ?? standalone$2;
13127
13275
  /** Hands a node to a forwarded ref and to one of the component's own. */
13128
- function setRefs$2(node, own, forwarded) {
13276
+ function setRefs$3(node, own, forwarded) {
13129
13277
  own.current = node;
13130
13278
  if (typeof forwarded === 'function')
13131
13279
  forwarded(node);
@@ -13242,7 +13390,7 @@ const CommandParts = React.forwardRef(({ children, label, query: controlledQuery
13242
13390
  registerHandler,
13243
13391
  };
13244
13392
  const { rootRef } = engine;
13245
- const rootRefs = React.useCallback((node) => setRefs$2(node, rootRef, ref), [rootRef, ref]);
13393
+ const rootRefs = React.useCallback((node) => setRefs$3(node, rootRef, ref), [rootRef, ref]);
13246
13394
  return (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(CommandContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
13247
13395
  });
13248
13396
  CommandParts.displayName = 'CommandParts';
@@ -13262,7 +13410,7 @@ const CommandInput = React.forwardRef(({ isLoading, iconLeft, iconLeftName, onKe
13262
13410
  return () => reportLoading(false);
13263
13411
  }, [reportLoading, isLoading]);
13264
13412
  const { inputRef } = engine;
13265
- const inputRefs = React.useCallback((node) => setRefs$2(node, inputRef, ref), [inputRef, ref]);
13413
+ const inputRefs = React.useCallback((node) => setRefs$3(node, inputRef, ref), [inputRef, ref]);
13266
13414
  const { expanded, highlighted } = engine;
13267
13415
  // A name the caller gave the field wins over the Command's `label`.
13268
13416
  const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
@@ -18743,6 +18891,25 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
18743
18891
  });
18744
18892
  Autocomplete.displayName = 'Autocomplete';
18745
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
+
18746
18913
  const validComboboxSizes = ['small', 'medium', 'large'];
18747
18914
  // What the status region announces: the `Combobox.Empty` content, as the one
18748
18915
  // item it's handed while that content is on screen because of the user's
@@ -18791,7 +18958,7 @@ const standalone$1 = {
18791
18958
  const ComboboxContext = React.createContext(null);
18792
18959
  const useCombobox = () => React.useContext(ComboboxContext) ?? standalone$1;
18793
18960
  /** Hands a node to a forwarded ref and to one of the component's own. */
18794
- function setRefs$1(node, own, forwarded) {
18961
+ function setRefs$2(node, own, forwarded) {
18795
18962
  own.current = node;
18796
18963
  if (typeof forwarded === 'function')
18797
18964
  forwarded(node);
@@ -18923,7 +19090,7 @@ const ComboboxParts = React.forwardRef(({ children, below, onSettle, open, defau
18923
19090
  appendToBody,
18924
19091
  };
18925
19092
  const { rootRef } = engine;
18926
- const rootRefs = React.useCallback((node) => setRefs$1(node, rootRef, ref), [rootRef, ref]);
19093
+ const rootRefs = React.useCallback((node) => setRefs$2(node, rootRef, ref), [rootRef, ref]);
18927
19094
  const comboboxElement = (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(ComboboxContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
18928
19095
  const fieldElement = below === undefined ? (comboboxElement) : (jsxRuntime.jsxs("div", { children: [comboboxElement, jsxRuntime.jsx("div", { onPointerDownCapture: engine.markInside, onMouseDown: engine.keepFocus, children: below })] }));
18929
19096
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
@@ -18956,7 +19123,7 @@ const ComboboxInput = React.forwardRef(({ id, isLoading, controlSize, onKeyDown,
18956
19123
  return () => reportLoading(false);
18957
19124
  }, [reportLoading, isLoading]);
18958
19125
  const { inputRef } = engine;
18959
- const inputRefs = React.useCallback((node) => setRefs$1(node, inputRef, ref), [inputRef, ref]);
19126
+ const inputRefs = React.useCallback((node) => setRefs$2(node, inputRef, ref), [inputRef, ref]);
18960
19127
  const { expanded, highlighted } = engine;
18961
19128
  // An `id` of the field's own reaches the label from a layout effect, so
18962
19129
  // in server markup, and the first render, the label's `for` still names
@@ -19135,7 +19302,7 @@ ComboboxEmpty.displayName = 'ComboboxEmpty';
19135
19302
  const toItem = (entry) => typeof entry === 'string' ? { value: entry, label: entry } : entry;
19136
19303
  const labelOf = (item) => item.label ?? item.value;
19137
19304
  /** One value or several, as a list: one value is a list of none or one. */
19138
- const asList = (value) => value == null ? [] : Array.isArray(value) ? value : [value];
19305
+ const asList$1 = (value) => value == null ? [] : Array.isArray(value) ? value : [value];
19139
19306
  /**
19140
19307
  * The Combobox given `data`. It renders the parts itself, filters the items
19141
19308
  * by label as the user types, and keeps the value, so a pick goes through
@@ -19167,9 +19334,9 @@ const ComboboxFromData = React.forwardRef((props, ref) => {
19167
19334
  return item ? labelOf(item) : picked;
19168
19335
  };
19169
19336
  const controlled = value !== undefined;
19170
- const [ownValue, setOwnValue] = React.useState(() => asList(defaultValue));
19337
+ const [ownValue, setOwnValue] = React.useState(() => asList$1(defaultValue));
19171
19338
  const matchText = useLocaleMatcher();
19172
- const picked = controlled ? asList(value) : ownValue;
19339
+ const picked = controlled ? asList$1(value) : ownValue;
19173
19340
  const change = (next) => {
19174
19341
  if (!controlled)
19175
19342
  setOwnValue(next);
@@ -19221,7 +19388,7 @@ const ComboboxFromData = React.forwardRef((props, ref) => {
19221
19388
  };
19222
19389
  // The picks, for the field's description, since its text is the search.
19223
19390
  const tagsId = React.useId();
19224
- const tagIds = picked.map((_, index) => `${tagsId}-${index}`);
19391
+ const ids = tagIds(tagsId, picked);
19225
19392
  // Each group where `data` first names it, and the items with none
19226
19393
  // together where the first of them is.
19227
19394
  const sections = new Map();
@@ -19235,14 +19402,11 @@ const ComboboxFromData = React.forwardRef((props, ref) => {
19235
19402
  if (!next)
19236
19403
  settle();
19237
19404
  onOpenChange?.(next);
19238
- }, below: multiple
19239
- ? 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))) }))
19240
- : 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,
19241
19406
  // The browser's check reads the field's text, and only a pick
19242
19407
  // fills it for good, so it's required until there is one.
19243
- required: required && picked.length === 0, "aria-required": required || undefined, form: form, "aria-describedby": multiple && picked.length > 0 ? tagIds.join(' ') : undefined, placeholder: placeholder, value: query ??
19244
- (multiple || picked.length === 0 ? '' : labelFor(picked[0])), onChange: event => setQuery(event.target.value), onBlur: settle }), inline ? list : jsxRuntime.jsx(ComboboxMenu, { children: list }), name &&
19245
- (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 })] }));
19246
19410
  });
19247
19411
  ComboboxFromData.displayName = 'ComboboxFromData';
19248
19412
  /**
@@ -19315,51 +19479,946 @@ const Combobox = withSubComponents(ComboboxRoot, {
19315
19479
  Empty: ComboboxEmpty,
19316
19480
  }, 'Combobox');
19317
19481
 
19318
- // An IME that's composing owns every key: Enter confirms what it composed,
19319
- // the arrows move through its candidates, and Escape cancels it. Safari can
19320
- // say so with `keyCode` 229 rather than `isComposing` (#1102).
19321
- 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
+ }
19322
19522
  /**
19323
- * The `Taginput` component provides a tag/chip input field for managing multiple tags.
19324
- *
19325
- * @function
19326
- * @param {TaginputProps} props - Props for the Taginput component.
19327
- * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the input element.
19328
- * @returns {JSX.Element} The rendered taginput component.
19329
- *
19330
- * @example
19331
- * // Basic usage
19332
- * <Taginput
19333
- * placeholder="Add a tag..."
19334
- * onChange={(tags) => console.log(tags)}
19335
- * />
19336
- *
19337
- * @example
19338
- * // With autocomplete
19339
- * <Taginput
19340
- * data={['React', 'Vue', 'Angular', 'Svelte']}
19341
- * allowNew={false}
19342
- * placeholder="Select frameworks..."
19343
- * />
19344
- */
19345
- const Taginput = React.forwardRef(({
19346
- // Field props
19347
- 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) => {
19348
- const disabled = useFieldsetDisabled(disabledProp);
19349
- const insideField = useInsideField();
19350
- const insideControl = useInsideControl();
19351
- const ownField = rendersOwnField('Taginput', {
19352
- insideField,
19353
- insideControl,
19354
- label,
19355
- message,
19356
- horizontal,
19357
- fieldClassName,
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
+ }
19358
19543
  });
19359
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
19360
- const { classPrefix } = useConfig();
19361
- const defaultIconLibrary = useIconLibrary();
19362
- const formatNumber = useScopedNumberFormat();
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;
19570
+ };
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,
19597
+ });
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);
19602
+ React.useEffect(() => {
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,
19665
+ }
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();
19363
20422
  const matchText = useLocaleMatcher();
19364
20423
  const resolvedLibrary = iconLibraryProp || defaultIconLibrary || 'fa';
19365
20424
  const pcn = (...args) => prefixedClassNames(classPrefix, ...args);
@@ -20253,7 +21312,7 @@ const rgbChannels = (color) => {
20253
21312
  /** Whether a press should start a drag: any touch or pen, a mouse's main button. */
20254
21313
  const startsDrag = (event) => event.pointerType !== 'mouse' || event.button === 0;
20255
21314
  /** Hands a node to a forwarded ref and to one of the part's own. */
20256
- function setRefs(node, own, forwarded) {
21315
+ function setRefs$1(node, own, forwarded) {
20257
21316
  own.current = node;
20258
21317
  if (typeof forwarded === 'function')
20259
21318
  forwarded(node);
@@ -20385,7 +21444,7 @@ const ColorPickerArea = React.forwardRef(({ className, style, ...props }, ref) =
20385
21444
  const hiddenOf = (which) => touch || keyed || axis === which ? undefined : true;
20386
21445
  if (!picker)
20387
21446
  return null;
20388
- return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
21447
+ return (jsxRuntime.jsx("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
20389
21448
  ...style,
20390
21449
  '--color-picker-hue': Math.round(color.h),
20391
21450
  '--color-picker-rgb': rgbChannels(color),
@@ -20459,7 +21518,7 @@ const ChannelSlider = React.forwardRef(({ channel, className, style, ...props },
20459
21518
  };
20460
21519
  if (!picker)
20461
21520
  return null;
20462
- return (jsxRuntime.jsxs("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
21521
+ return (jsxRuntime.jsxs("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
20463
21522
  ...style,
20464
21523
  '--color-picker-hue': Math.round(color.h),
20465
21524
  '--color-picker-rgb': rgbChannels(color),
@@ -20583,7 +21642,7 @@ const ColorPickerSwatches = React.forwardRef(({ className, swatches: ownSwatches
20583
21642
  };
20584
21643
  if (!picker || list.length === 0)
20585
21644
  return null;
20586
- return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(groupClass, bulmaHelperClasses, className), role: "radiogroup", "aria-label": labels.swatches, "aria-disabled": disabled || undefined, ...rest, children: list.map((entry, i) => {
21645
+ return (jsxRuntime.jsx("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(groupClass, bulmaHelperClasses, className), role: "radiogroup", "aria-label": labels.swatches, "aria-disabled": disabled || undefined, ...rest, children: list.map((entry, i) => {
20587
21646
  const swatch = colorOf(i);
20588
21647
  const isChecked = i === checked;
20589
21648
  // The check is drawn in black or white with a ring of the other, so
@@ -20951,61 +22010,6 @@ fieldClassName, controlClassName,
20951
22010
  });
20952
22011
  TextArea.displayName = 'TextArea';
20953
22012
 
20954
- /**
20955
- * Whether a key is Alt+ArrowDown or Alt+ArrowUp, which open and close a
20956
- * picker's popover. The calendar and the time wheels leave both to it rather
20957
- * than reading them as plain arrows.
20958
- */
20959
- const isPopoverToggleKey = (e) => e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp');
20960
- const DEFAULT_PICKER_LABELS = {
20961
- prevMonth: 'Previous month',
20962
- nextMonth: 'Next month',
20963
- prevYear: 'Previous year',
20964
- nextYear: 'Next year',
20965
- chooseDate: 'Choose date',
20966
- chooseMonth: 'Choose month',
20967
- chooseYear: 'Choose year',
20968
- rangeStart: 'Start date',
20969
- rangeEnd: 'End date',
20970
- rangePreviewEnd: 'Choose as end date',
20971
- rangeSeparator: '–',
20972
- chooseDateRange: 'Choose date range',
20973
- hours: 'hours',
20974
- minutes: 'minutes',
20975
- seconds: 'seconds',
20976
- ampm: 'AM/PM',
20977
- increaseHours: 'Increase hours',
20978
- decreaseHours: 'Decrease hours',
20979
- increaseMinutes: 'Increase minutes',
20980
- decreaseMinutes: 'Decrease minutes',
20981
- increaseSeconds: 'Increase seconds',
20982
- decreaseSeconds: 'Decrease seconds',
20983
- toggleAmPm: 'Toggle AM/PM',
20984
- chooseTime: 'Choose time',
20985
- chooseDateTime: 'Choose date and time',
20986
- now: 'Now',
20987
- today: 'Today',
20988
- clear: 'Clear',
20989
- cancel: 'Cancel',
20990
- ok: 'OK',
20991
- reset: 'Reset',
20992
- done: 'Done',
20993
- time: 'Time',
20994
- };
20995
- /**
20996
- * Merge user-supplied label overrides with the defaults. A key set to
20997
- * `undefined` keeps its default, so a locale table that is missing a string
20998
- * still names its control instead of leaving it with no accessible name.
20999
- */
21000
- const mergeLabels = (overrides) => {
21001
- const merged = { ...DEFAULT_PICKER_LABELS };
21002
- for (const [key, value] of Object.entries(overrides ?? {})) {
21003
- if (value !== undefined)
21004
- merged[key] = value;
21005
- }
21006
- return merged;
21007
- };
21008
-
21009
22013
  function isSameDay(a, b) {
21010
22014
  return (a.getFullYear() === b.getFullYear() &&
21011
22015
  a.getMonth() === b.getMonth() &&
@@ -22259,186 +23263,80 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
22259
23263
  'is-disabled': disabled,
22260
23264
  'is-other-month': otherMonth,
22261
23265
  });
22262
- const display = !otherMonth || nearbyMonthDays;
22263
- return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "aria-describedby": describedBy, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
22264
- // Reached by Tab while the focused day is disabled, or
22265
- // by pointer: keys move on from here. A nearby month's
22266
- // day waits for its click, which turns the grid.
22267
- if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
22268
- onFocusedDateChange(cell.date);
22269
- }
22270
- },
22271
- // Mouseover rather than mouseenter: browsers send it to a
22272
- // disabled day as well, where React holds mouseenter back,
22273
- // so the preview leaves the last enabled day behind.
22274
- onMouseOver: anchor ? () => setHoverDate(cell.date) : undefined, onClick: () => {
22275
- if (disabled)
22276
- return;
22277
- onFocusedDateChange(cell.date);
22278
- if (rangeMode)
22279
- pickRangeDay(cell.date);
22280
- else
22281
- onSelect?.(cell.date);
22282
- }, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
22283
- }), weekClass) })] })), view === 'months' && (jsxRuntime.jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsxRuntime.jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
22284
- const date = makeDate(focusedYear, month);
22285
- const disabled = disabledMonths[month];
22286
- const isSelected = !!value && isSameMonth(value, date);
22287
- const isFocused = month === tabStopMonth;
22288
- const isCurrent = isSameMonth(date, today);
22289
- const cellClass = prefixedClassNames(classPrefix, 'dateinput-month-cell', {
22290
- 'is-selected': isSelected,
22291
- 'is-today': isCurrent,
22292
- 'is-disabled': disabled,
22293
- });
22294
- return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": isCurrent ? 'date' : undefined, "aria-label": computedMonthNames[month], "data-focused": isFocused ? 'true' : undefined, disabled: disabled, className: cellClass, onFocus: () => {
22295
- // Reached by Tab while the focused month is disabled,
22296
- // or by pointer: keys move on from here.
22297
- if (month !== focusedDate.getMonth()) {
22298
- onFocusedDateChange(clampDate(date, min, max));
22299
- }
22300
- }, onClick: () => {
22301
- onFocusedDateChange(clampDate(date, min, max));
22302
- onSelect?.(date);
22303
- }, children: monthCellNames[month] }, month));
22304
- }) }, row[0]))) })), view === 'years' && (jsxRuntime.jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
22305
- const isFocused = year === tabStopYear;
22306
- const isToday = year === todayYear;
22307
- // As navigation the focused year is the current one. As the
22308
- // selection surface the value's year is selected and focus roams.
22309
- const isSelected = isYearGranularity
22310
- ? !!value && value.getFullYear() === year
22311
- : isFocused;
22312
- const disabled = disabledYears.has(year);
22313
- const cellCls = prefixedClassNames(classPrefix, 'dateinput-year-cell', {
22314
- 'is-selected': isSelected,
22315
- 'is-today': isToday,
22316
- 'is-disabled': disabled,
22317
- });
22318
- return (jsxRuntime.jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: year === listTabStop ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
22319
- // As navigation, focusing a year by key or pointer only
22320
- // moves the tab stop: nothing is committed until a year is
22321
- // picked. As the selection surface it is reached by Tab
22322
- // while the focused year is disabled, or by pointer, and
22323
- // keys move on from here.
22324
- if (!isYearGranularity) {
22325
- setNavYear(year);
22326
- }
22327
- else if (year !== focusedYear) {
22328
- onFocusedDateChange(clampDate(makeDate(year), min, max));
22329
- }
22330
- }, onClick: () => isYearGranularity
22331
- ? handleYearPick(year)
22332
- : handleYearSelect(year), children: year }, year));
22333
- }) })), rangeMode && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { id: rangeStartId, hidden: true, children: t.rangeStart }), jsxRuntime.jsx("span", { id: rangeEndId, hidden: true, children: t.rangeEnd }), jsxRuntime.jsx("span", { id: rangePreviewEndId, hidden: true, children: t.rangePreviewEnd }), jsxRuntime.jsx("div", { id: rangeSummaryId, "aria-live": "polite", style: visuallyHidden, children: rangeSummary })] }))] }));
22334
- };
22335
- Calendar.displayName = 'Calendar';
22336
-
22337
- const isBrowser = typeof window !== 'undefined';
22338
- 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, }) => {
22339
- const panelRef = React.useRef(null);
22340
- const resolveContainer = usePortalContainer();
22341
- // Read as the trap turns on, once the panel's content has rendered.
22342
- const initialFocusRef = React.useMemo(() => initialFocusSelector === undefined
22343
- ? undefined
22344
- : {
22345
- get current() {
22346
- return (panelRef.current?.querySelector(initialFocusSelector) ?? null);
22347
- },
22348
- }, [initialFocusSelector]);
22349
- const resolved = useAnchoredPosition(anchorRef, panelRef, {
22350
- active: isOpen,
22351
- position,
22352
- fixed: appendToBody,
22353
- // The gap comes from `--bulma-picker-popover-offset` as a margin, in place
22354
- // or portaled, so the coordinates leave it out and `auto` reads it off
22355
- // the panel.
22356
- offset: 0,
22357
- styledGap: marginGap,
22358
- });
22359
- // Read when a press lands outside or a key arrives, so both listeners stay
22360
- // put while `onClose` changes identity, as an inline one does on every
22361
- // render of the field.
22362
- const onCloseRef = useLatestRef(onClose);
22363
- React.useEffect(() => {
22364
- if (!isOpen || !closeOnClickOutside || !isBrowser)
22365
- return undefined;
22366
- // Listen on `pointerdown` (rather than `mousedown`) so touch starts
22367
- // outside the popover dismiss it on mobile — including swipe-from-outside
22368
- // gestures, where the synthetic mouse event would otherwise lag a few
22369
- // hundred ms behind the actual touch.
22370
- const handler = (e) => {
22371
- const path = e.composedPath();
22372
- if (panelRef.current && path.includes(panelRef.current))
22373
- return;
22374
- if (anchorRef.current && path.includes(anchorRef.current))
22375
- return;
22376
- onCloseRef.current();
22377
- };
22378
- document.addEventListener('pointerdown', handler);
22379
- return () => document.removeEventListener('pointerdown', handler);
22380
- }, [isOpen, closeOnClickOutside, anchorRef, onCloseRef]);
22381
- // Escape closes it, and so does Alt+ArrowUp, the combobox's key for it,
22382
- // which the calendar and the time wheels inside leave to it. Escape closes
22383
- // it from anywhere, but the key is only the picker's when it was pressed in
22384
- // the panel or the field: then it's marked used, so a Modal or Sidebar
22385
- // around the picker stays open. From anywhere else, such as a dialog opened
22386
- // over the picker, the key goes on to whatever has focus.
22387
- React.useEffect(() => {
22388
- if (!isOpen || !closeOnEscape || !isBrowser)
22389
- return undefined;
22390
- const handler = (e) => {
22391
- if (e.key === 'Escape') {
22392
- if (isEventInside(e, panelRef.current) ||
22393
- isEventInside(e, anchorRef.current)) {
22394
- e.preventDefault();
22395
- }
22396
- onCloseRef.current();
22397
- }
22398
- else if (e.altKey && e.key === 'ArrowUp') {
22399
- e.preventDefault();
22400
- e.stopPropagation();
22401
- onCloseRef.current();
22402
- }
22403
- };
22404
- document.addEventListener('keydown', handler);
22405
- return () => document.removeEventListener('keydown', handler);
22406
- }, [isOpen, closeOnEscape, anchorRef, onCloseRef]);
22407
- useFocusTrap(panelRef, {
22408
- active: isOpen && trapFocus,
22409
- initialFocusRef,
22410
- restoreFocus: restoreFocusRef ?? true,
22411
- });
22412
- const panelClass = usePrefixedClassNames('picker-popover', {
22413
- 'is-active': isOpen,
22414
- [`is-${resolved.position}`]: true,
22415
- 'is-portal': appendToBody,
22416
- });
22417
- if (!isOpen)
22418
- return null;
22419
- const style = appendToBody
22420
- ? {
22421
- position: 'fixed',
22422
- top: resolved.top,
22423
- left: resolved.left,
22424
- }
22425
- : {};
22426
- 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,
22427
- // An Escape pressed in the panel is the picker's, and the listener
22428
- // above claims it. A Dropdown menu around the picker hears Escape in a
22429
- // React handler, which runs before that listener, so the panel claims
22430
- // it here too, where React gets to it first.
22431
- onKeyDown: e => {
22432
- if (closeOnEscape && e.key === 'Escape')
22433
- e.preventDefault();
22434
- }, children: children }));
22435
- if (appendToBody && isBrowser) {
22436
- const target = resolveContainer();
22437
- return reactDom.createPortal(jsxRuntime.jsx(LocalePortalScope, { target: target, scope: localeScope, children: panel }), target);
22438
- }
22439
- return panel;
23266
+ const display = !otherMonth || nearbyMonthDays;
23267
+ return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "aria-describedby": describedBy, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
23268
+ // Reached by Tab while the focused day is disabled, or
23269
+ // by pointer: keys move on from here. A nearby month's
23270
+ // day waits for its click, which turns the grid.
23271
+ if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
23272
+ onFocusedDateChange(cell.date);
23273
+ }
23274
+ },
23275
+ // Mouseover rather than mouseenter: browsers send it to a
23276
+ // disabled day as well, where React holds mouseenter back,
23277
+ // so the preview leaves the last enabled day behind.
23278
+ onMouseOver: anchor ? () => setHoverDate(cell.date) : undefined, onClick: () => {
23279
+ if (disabled)
23280
+ return;
23281
+ onFocusedDateChange(cell.date);
23282
+ if (rangeMode)
23283
+ pickRangeDay(cell.date);
23284
+ else
23285
+ onSelect?.(cell.date);
23286
+ }, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
23287
+ }), weekClass) })] })), view === 'months' && (jsxRuntime.jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsxRuntime.jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
23288
+ const date = makeDate(focusedYear, month);
23289
+ const disabled = disabledMonths[month];
23290
+ const isSelected = !!value && isSameMonth(value, date);
23291
+ const isFocused = month === tabStopMonth;
23292
+ const isCurrent = isSameMonth(date, today);
23293
+ const cellClass = prefixedClassNames(classPrefix, 'dateinput-month-cell', {
23294
+ 'is-selected': isSelected,
23295
+ 'is-today': isCurrent,
23296
+ 'is-disabled': disabled,
23297
+ });
23298
+ return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": isCurrent ? 'date' : undefined, "aria-label": computedMonthNames[month], "data-focused": isFocused ? 'true' : undefined, disabled: disabled, className: cellClass, onFocus: () => {
23299
+ // Reached by Tab while the focused month is disabled,
23300
+ // or by pointer: keys move on from here.
23301
+ if (month !== focusedDate.getMonth()) {
23302
+ onFocusedDateChange(clampDate(date, min, max));
23303
+ }
23304
+ }, onClick: () => {
23305
+ onFocusedDateChange(clampDate(date, min, max));
23306
+ onSelect?.(date);
23307
+ }, children: monthCellNames[month] }, month));
23308
+ }) }, row[0]))) })), view === 'years' && (jsxRuntime.jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
23309
+ const isFocused = year === tabStopYear;
23310
+ const isToday = year === todayYear;
23311
+ // As navigation the focused year is the current one. As the
23312
+ // selection surface the value's year is selected and focus roams.
23313
+ const isSelected = isYearGranularity
23314
+ ? !!value && value.getFullYear() === year
23315
+ : isFocused;
23316
+ const disabled = disabledYears.has(year);
23317
+ const cellCls = prefixedClassNames(classPrefix, 'dateinput-year-cell', {
23318
+ 'is-selected': isSelected,
23319
+ 'is-today': isToday,
23320
+ 'is-disabled': disabled,
23321
+ });
23322
+ return (jsxRuntime.jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: year === listTabStop ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
23323
+ // As navigation, focusing a year by key or pointer only
23324
+ // moves the tab stop: nothing is committed until a year is
23325
+ // picked. As the selection surface it is reached by Tab
23326
+ // while the focused year is disabled, or by pointer, and
23327
+ // keys move on from here.
23328
+ if (!isYearGranularity) {
23329
+ setNavYear(year);
23330
+ }
23331
+ else if (year !== focusedYear) {
23332
+ onFocusedDateChange(clampDate(makeDate(year), min, max));
23333
+ }
23334
+ }, onClick: () => isYearGranularity
23335
+ ? handleYearPick(year)
23336
+ : handleYearSelect(year), children: year }, year));
23337
+ }) })), rangeMode && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { id: rangeStartId, hidden: true, children: t.rangeStart }), jsxRuntime.jsx("span", { id: rangeEndId, hidden: true, children: t.rangeEnd }), jsxRuntime.jsx("span", { id: rangePreviewEndId, hidden: true, children: t.rangePreviewEnd }), jsxRuntime.jsx("div", { id: rangeSummaryId, "aria-live": "polite", style: visuallyHidden, children: rangeSummary })] }))] }));
22440
23338
  };
22441
- PickerPopover.displayName = 'PickerPopover';
23339
+ Calendar.displayName = 'Calendar';
22442
23340
 
22443
23341
  /**
22444
23342
  * Segment-map utility for manual keyboard entry on a date / time picker input.
@@ -23192,79 +24090,6 @@ function supportsInputType(type) {
23192
24090
  return supported;
23193
24091
  }
23194
24092
 
23195
- /**
23196
- * A picker's locale: its own `locale`, else the one a `LocaleProvider` around
23197
- * it sets, else `undefined`, which leaves `Intl` to use the runtime's.
23198
- */
23199
- function usePickerLocale(own) {
23200
- const scope = useLocaleScope();
23201
- return own || scope?.locale;
23202
- }
23203
- const NO_ROOT_ATTRS = {};
23204
- /**
23205
- * A picker's language and direction inside a `LocaleProvider`, by the rule a
23206
- * nested provider follows: its `lang` is its own `locale`, else the
23207
- * provider's, and its `dir` is the `dir` passed to it, else its own locale's
23208
- * direction when it has one, else the provider's. Outside a provider it
23209
- * changes nothing. A `locale` `Intl` refuses counts as none here, since the
23210
- * picker's own formatting reports it.
23211
- */
23212
- function usePickerLocaleScope(ownLocale, ownDir) {
23213
- const scope = useLocaleScope();
23214
- return React.useMemo(() => {
23215
- if (!scope)
23216
- return { rootAttrs: NO_ROOT_ATTRS };
23217
- let own;
23218
- try {
23219
- own = canonicalLocale(ownLocale);
23220
- }
23221
- catch {
23222
- own = undefined;
23223
- }
23224
- const dir = ownDir || (own ? localeDirection(own) : scope.dir);
23225
- return {
23226
- rootAttrs: own ? { lang: own, dir } : NO_ROOT_ATTRS,
23227
- panel: { locale: own || scope.locale, dir },
23228
- };
23229
- }, [scope, ownLocale, ownDir]);
23230
- }
23231
- /**
23232
- * Whether a picker's `dayNames` replace the calendar's own, which `Calendar`
23233
- * does only for a full week of seven.
23234
- */
23235
- function hasOwnDayNames(dayNames) {
23236
- return dayNames !== undefined && dayNames.length === 7;
23237
- }
23238
- /**
23239
- * The day a picker's week starts on: its own `firstDayOfWeek`, else, inside a
23240
- * `LocaleProvider`, the first day of the locale the picker uses, else Sunday.
23241
- * Outside a provider it stays Sunday whatever the locale, as it always has,
23242
- * and so does a picker inside one with no locale from either, since the
23243
- * runtime's own differs between the server and the browser.
23244
- *
23245
- * It also stays Sunday for a picker that brings its own `dayNames`. The
23246
- * calendar shows those exactly as given, in calendar order, and before the
23247
- * provider an unset `firstDayOfWeek` was always Sunday, so names written
23248
- * Sunday first would sit a day or more off their columns if the week moved
23249
- * under them.
23250
- */
23251
- function usePickerFirstDayOfWeek(own, locale, hasDayNames) {
23252
- const inProvider = useLocaleScope() !== null;
23253
- return React.useMemo(() => {
23254
- if (own !== undefined)
23255
- return own;
23256
- if (!inProvider || !locale || hasDayNames)
23257
- return 0;
23258
- try {
23259
- return localeFirstDay(canonicalLocale(locale));
23260
- }
23261
- catch {
23262
- // A tag Intl refuses: the picker's own formatting says so.
23263
- return 0;
23264
- }
23265
- }, [own, inProvider, locale, hasDayNames]);
23266
- }
23267
-
23268
24093
  const DEFAULT_FORMATS = {
23269
24094
  day: DEFAULT_DATE_FORMAT,
23270
24095
  month: DEFAULT_MONTH_FORMAT,
@@ -27011,6 +27836,421 @@ function ClientOnly({ children, fallback = null }) {
27011
27836
  return jsxRuntime.jsx(jsxRuntime.Fragment, { children: typeof children === 'function' ? children() : children });
27012
27837
  }
27013
27838
 
27839
+ const FrameContext = React.createContext(null);
27840
+ /** A width prop as CSS: a number is pixels. */
27841
+ const cssLength = (value) => typeof value === 'number' ? `${value}px` : value;
27842
+ /**
27843
+ * Hands `node` to a part's own ref and to the one forwarded to it. A
27844
+ * forwarded callback that returns a cleanup, as React 19 allows, has it held
27845
+ * in `cleanup` and run on detach in place of a call with `null`, on React 18
27846
+ * as well.
27847
+ */
27848
+ function setRefs(node, own, forwarded, cleanup) {
27849
+ own.current = node;
27850
+ if (typeof forwarded === 'function') {
27851
+ if (node === null) {
27852
+ const detach = cleanup.current;
27853
+ cleanup.current = null;
27854
+ if (detach)
27855
+ detach();
27856
+ else
27857
+ forwarded(null);
27858
+ return;
27859
+ }
27860
+ const returned = forwarded(node);
27861
+ cleanup.current =
27862
+ typeof returned === 'function' ? returned : null;
27863
+ }
27864
+ else if (forwarded) {
27865
+ forwarded.current = node;
27866
+ }
27867
+ }
27868
+ /**
27869
+ * Warns when a part has an `id` the frame couldn't read, because it isn't a
27870
+ * direct child, so the skip link or the burger points at the wrong one.
27871
+ */
27872
+ function warnOnUnreadId(part, id, expected) {
27873
+ if (id && id !== expected) {
27874
+ warnOnce(`AppFrame:${part}-id`, `[bestax-bulma] <AppFrame.${part}> has an id <AppFrame> can't read, ` +
27875
+ `so ${part === 'Main' ? 'the skip link' : "the burger's aria-controls"} ` +
27876
+ `misses it. Put AppFrame.${part} directly inside AppFrame.`);
27877
+ }
27878
+ }
27879
+ /**
27880
+ * The top bar of an AppFrame, which sticks while the page scrolls.
27881
+ *
27882
+ * @function
27883
+ * @param {AppFrameHeaderProps} props - Props for AppFrame.Header.
27884
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<header>`.
27885
+ * @returns {JSX.Element} The rendered header.
27886
+ */
27887
+ const AppFrameHeader = React.forwardRef(({ className, children, ...props }, ref) => {
27888
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
27889
+ const headerClass = usePrefixedClassNames('app-frame-header');
27890
+ return (jsxRuntime.jsx("header", { ref: ref, className: classNames(headerClass, bulmaHelperClasses, className), ...rest, children: children }));
27891
+ });
27892
+ AppFrameHeader.displayName = 'AppFrame.Header';
27893
+ /**
27894
+ * The navigation of an AppFrame: a column beside the page from the breakpoint up, and a drawer below it.
27895
+ *
27896
+ * @function
27897
+ * @param {AppFrameNavProps} props - Props for AppFrame.Nav.
27898
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<nav>`.
27899
+ * @returns {JSX.Element} The rendered nav, and the drawer's backdrop.
27900
+ */
27901
+ const AppFrameNav = React.forwardRef(({ className, style, width, overlay = true, closeOnEscape = true, closeOnClickOutside = true, closeOnNavigate = true, onClick, onPointerDownCapture, children, ...props }, ref) => {
27902
+ const frame = React.useContext(FrameContext);
27903
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
27904
+ const navClass = usePrefixedClassNames('app-frame-nav');
27905
+ const closeClass = usePrefixedClassNames('app-frame-close');
27906
+ const backdropClass = usePrefixedClassNames('app-frame-backdrop');
27907
+ const activeClass = usePrefixedClassNames('is-active');
27908
+ const navRef = React.useRef(null);
27909
+ const backdropRef = React.useRef(null);
27910
+ const navCleanupRef = React.useRef(null);
27911
+ const navRefs = React.useCallback((node) => setRefs(node, navRef, ref, navCleanupRef), [ref]);
27912
+ const navId = rest.id || frame?.state.navId;
27913
+ if (frame)
27914
+ warnOnUnreadId('Nav', rest.id, frame.state.navId);
27915
+ // The drawer is open only below the breakpoint. Above it the nav is a
27916
+ // column, and none of what follows applies.
27917
+ const isDrawer = !!frame?.state.isDrawer;
27918
+ const open = isDrawer && !!frame?.state.navOpen;
27919
+ // The open drawer is Bulma's active state, on the nav as on a navbar's
27920
+ // menu, and on its backdrop.
27921
+ const shown = { [activeClass]: !!frame?.state.navOpen };
27922
+ const modal = open && overlay;
27923
+ const close = () => frame?.state.setNavOpen(false);
27924
+ // The rest of the frame goes inert: the header, the page, the aside and
27925
+ // the footer, but not the backdrop, which takes the presses outside the
27926
+ // drawer rather than let them through to what's under it. Not the whole
27927
+ // page, since a Modal or a toast renders beside the frame, often into an
27928
+ // element that's there before the drawer opens, and a Modal opened from
27929
+ // the drawer has to stay usable. First, so that as the drawer closes it
27930
+ // lets go before focus goes back to the burger: an inert element can't
27931
+ // take focus.
27932
+ useInertSiblings(navRef, modal, false, backdropRef);
27933
+ const isTopmost = useOverlayStack(open, overlay);
27934
+ useScrollLock(modal);
27935
+ // Tab stays inside while the drawer is the topmost modal overlay, as in
27936
+ // Sidebar, so a Modal opened from the drawer holds Tab until it closes.
27937
+ React.useEffect(() => {
27938
+ if (!modal)
27939
+ return undefined;
27940
+ return trapTab(() => navRef.current, window, () => isTopmost('modal'));
27941
+ }, [modal, isTopmost]);
27942
+ useOverlayEscape(open && closeOnEscape, isTopmost, close);
27943
+ // Without an overlay there's no backdrop: a press outside the drawer
27944
+ // closes it, as Popover's does, while it's the topmost overlay, so a
27945
+ // press in a Modal opened over it is the Modal's. A press on a control
27946
+ // that names the nav in its `aria-controls`, such as the burger, is left
27947
+ // to that control, which toggles it.
27948
+ const markInsidePress = useOutsidePress(navRef, open && !overlay && closeOnClickOutside, e => {
27949
+ const id = navRef.current?.id;
27950
+ const controlsNav = (node) => !!node
27951
+ .getAttribute?.('aria-controls')
27952
+ ?.split(/\s+/)
27953
+ .includes(id);
27954
+ if (!isTopmost() || e.composedPath().some(controlsNav))
27955
+ return;
27956
+ close();
27957
+ });
27958
+ // Focus moves to the drawer's first tab stop, the close button, as it
27959
+ // opens, and back to whatever had it as it closes, unless it has left
27960
+ // the drawer. After a press outside, the press's own mousedown then takes
27961
+ // it wherever the press goes. When the window widened, the drawer is the
27962
+ // column again, and focus on its links stays; the close button hides
27963
+ // there, so focus on it, or dropped from it to <body>, goes back too.
27964
+ const isDrawerRef = useLatestRef(isDrawer);
27965
+ useOverlayFocus(navRef, open, {
27966
+ modal: overlay,
27967
+ restore: () => {
27968
+ if (isDrawerRef.current)
27969
+ return true;
27970
+ const nav = navRef.current;
27971
+ const active = getActiveElementInTree(nav);
27972
+ return (!nav.contains(active) || !!active?.classList.contains(closeClass));
27973
+ },
27974
+ });
27975
+ const handleClick = (e) => {
27976
+ onClick?.(e);
27977
+ if (!open ||
27978
+ !closeOnNavigate ||
27979
+ e.button !== 0 ||
27980
+ e.metaKey ||
27981
+ e.ctrlKey ||
27982
+ e.shiftKey ||
27983
+ e.altKey) {
27984
+ return;
27985
+ }
27986
+ // Not skipped for a click whose default was prevented: a router's
27987
+ // link prevents it to navigate without a page load. Only a link the nav
27988
+ // holds: one in a Modal portaled from it is the Modal's.
27989
+ const link = e.target.closest('a[href]');
27990
+ if (!link || !navRef.current?.contains(link))
27991
+ return;
27992
+ if (link.hasAttribute('download'))
27993
+ return;
27994
+ const target = link.getAttribute('target');
27995
+ if (target && target !== '_self')
27996
+ return;
27997
+ close();
27998
+ };
27999
+ const navStyle = width === undefined
28000
+ ? style
28001
+ : {
28002
+ ...style,
28003
+ '--bulma-app-frame-nav-width': cssLength(width),
28004
+ };
28005
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("nav", { ref: navRefs, className: classNames(navClass, shown, bulmaHelperClasses, className), style: navStyle, onClick: handleClick, onPointerDownCapture: e => {
28006
+ onPointerDownCapture?.(e);
28007
+ markInsidePress(e);
28008
+ }, ...rest, id: navId, "aria-label": rest['aria-label'] ||
28009
+ (rest['aria-labelledby'] ? undefined : frame?.labels.nav), children: [frame && (jsxRuntime.jsx(Delete, { className: closeClass, size: "medium", ariaLabel: frame.labels.closeNav, onClick: close })), children] }), frame && overlay && (jsxRuntime.jsx("div", { ref: backdropRef, className: classNames(backdropClass, shown), "aria-hidden": "true", onClick: closeOnClickOutside
28010
+ ? () => {
28011
+ // Not under an overlay opened over the drawer.
28012
+ if (isTopmost())
28013
+ close();
28014
+ }
28015
+ : undefined }))] }));
28016
+ });
28017
+ AppFrameNav.displayName = 'AppFrame.Nav';
28018
+ /**
28019
+ * The page's main content in an AppFrame, which the skip link moves focus to.
28020
+ *
28021
+ * @function
28022
+ * @param {AppFrameMainProps} props - Props for AppFrame.Main.
28023
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<main>`.
28024
+ * @returns {JSX.Element} The rendered main.
28025
+ */
28026
+ const AppFrameMain = React.forwardRef(({ className, children, ...props }, ref) => {
28027
+ const frame = React.useContext(FrameContext);
28028
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
28029
+ const mainClass = usePrefixedClassNames('app-frame-main');
28030
+ if (frame)
28031
+ warnOnUnreadId('Main', rest.id, frame.mainId);
28032
+ return (jsxRuntime.jsx("main", { ref: ref, tabIndex: -1, className: classNames(mainClass, bulmaHelperClasses, className), ...rest, id: rest.id || frame?.mainId, children: children }));
28033
+ });
28034
+ AppFrameMain.displayName = 'AppFrame.Main';
28035
+ /**
28036
+ * Content beside the page in an AppFrame, which moves under it below the breakpoint.
28037
+ *
28038
+ * @function
28039
+ * @param {AppFrameAsideProps} props - Props for AppFrame.Aside.
28040
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<aside>`.
28041
+ * @returns {JSX.Element} The rendered aside.
28042
+ */
28043
+ const AppFrameAside = React.forwardRef(({ className, style, width, children, ...props }, ref) => {
28044
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
28045
+ const asideClass = usePrefixedClassNames('app-frame-aside');
28046
+ const asideStyle = width === undefined
28047
+ ? style
28048
+ : {
28049
+ ...style,
28050
+ '--bulma-app-frame-aside-width': cssLength(width),
28051
+ };
28052
+ return (jsxRuntime.jsx("aside", { ref: ref, className: classNames(asideClass, bulmaHelperClasses, className), style: asideStyle, ...rest, children: children }));
28053
+ });
28054
+ AppFrameAside.displayName = 'AppFrame.Aside';
28055
+ /**
28056
+ * The bottom of an AppFrame, under the page.
28057
+ *
28058
+ * @function
28059
+ * @param {AppFrameFooterProps} props - Props for AppFrame.Footer.
28060
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<footer>`.
28061
+ * @returns {JSX.Element} The rendered footer.
28062
+ */
28063
+ const AppFrameFooter = React.forwardRef(({ className, children, ...props }, ref) => {
28064
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
28065
+ const footerClass = usePrefixedClassNames('app-frame-footer');
28066
+ return (jsxRuntime.jsx("footer", { ref: ref, className: classNames(footerClass, bulmaHelperClasses, className), ...rest, children: children }));
28067
+ });
28068
+ AppFrameFooter.displayName = 'AppFrame.Footer';
28069
+ /**
28070
+ * The button that opens the drawer, or collapses the column from the breakpoint up: Navbar.Burger, wired to the frame.
28071
+ *
28072
+ * @function
28073
+ * @param {AppFrameBurgerProps} props - Props for AppFrame.Burger.
28074
+ * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
28075
+ * @returns {JSX.Element} The rendered burger.
28076
+ */
28077
+ const AppFrameBurger = React.forwardRef(({ className, onClick, ...props }, ref) => {
28078
+ // Outside a frame it's a Navbar.Burger that controls nothing.
28079
+ const frame = React.useContext(FrameContext);
28080
+ const burgerClass = usePrefixedClassNames('app-frame-burger');
28081
+ const state = frame?.state;
28082
+ // Until the frame knows whether the nav is a drawer, the burger says
28083
+ // nothing it can't stand behind: no `aria-expanded`, and its bars. It can't
28084
+ // act before then anyway, since that's before hydration. Outside a frame
28085
+ // it's a plain Navbar.Burger.
28086
+ const known = !!frame?.modeKnown;
28087
+ const expanded = known && !!state && (state.isDrawer ? state.navOpen : !state.navCollapsed);
28088
+ return (jsxRuntime.jsx(NavbarBurger, { ref: ref, ...props, className: classNames(burgerClass, className), "aria-label": props['aria-label'] || frame?.labels.burger, active: expanded, "aria-expanded": frame && !known ? null : expanded, "aria-controls": state?.navId, onClick: e => {
28089
+ onClick?.(e);
28090
+ if (e.defaultPrevented || !frame)
28091
+ return;
28092
+ // Safari, and Firefox on a Mac, don't focus a button that's pressed.
28093
+ // Focused, it's the element the drawer hands focus back to.
28094
+ e.currentTarget.focus();
28095
+ frame.state.toggleNav();
28096
+ } }));
28097
+ });
28098
+ AppFrameBurger.displayName = 'AppFrame.Burger';
28099
+ const isMain = (child) => React.isValidElement(child) && child.type === AppFrameMain;
28100
+ const isNav = (child) => React.isValidElement(child) && child.type === AppFrameNav;
28101
+ /**
28102
+ * The `AppFrame` component lays out an app's pages: a header across the top, a navigation column down the side, the page, and an aside and a footer, with the nav a drawer behind a burger on narrow screens.
28103
+ *
28104
+ * The header and the column stick while the document scrolls, and a skip
28105
+ * link comes first. The layout is CSS, so the server renders it as the
28106
+ * browser does. It holds two states, each yours to control: whether the
28107
+ * drawer is open, and whether the column is collapsed. `AppFrame.Burger`
28108
+ * and `useAppFrame()` toggle whichever applies at the current width.
28109
+ *
28110
+ * @function
28111
+ * @param {AppFrameProps} props - Props for the AppFrame component.
28112
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the root `<div>`.
28113
+ * @returns {JSX.Element} The rendered frame.
28114
+ *
28115
+ * @example
28116
+ * <AppFrame>
28117
+ * <AppFrame.Header>
28118
+ * <Navbar aria-label="Site">
28119
+ * <Navbar.Brand>
28120
+ * <Navbar.Item href="/">Acme</Navbar.Item>
28121
+ * <AppFrame.Burger />
28122
+ * </Navbar.Brand>
28123
+ * </Navbar>
28124
+ * </AppFrame.Header>
28125
+ * <AppFrame.Nav>
28126
+ * <Menu>…</Menu>
28127
+ * </AppFrame.Nav>
28128
+ * <AppFrame.Main>…</AppFrame.Main>
28129
+ * </AppFrame>
28130
+ */
28131
+ const AppFrameRoot = React.forwardRef(({ className, textColor, bgColor, breakpoint = 'desktop', navOpen: navOpenProp, defaultNavOpen = false, onNavOpenChange, navCollapsed: navCollapsedProp, defaultNavCollapsed = false, onNavCollapsedChange, skipLink = true, labels, children, ...props }, ref) => {
28132
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
28133
+ color: textColor,
28134
+ backgroundColor: bgColor,
28135
+ ...props,
28136
+ });
28137
+ const rootRef = React.useRef(null);
28138
+ const rootCleanupRef = React.useRef(null);
28139
+ const rootRefs = React.useCallback((node) => setRefs(node, rootRef, ref, rootCleanupRef), [ref]);
28140
+ const [openState, setOpenState] = React.useState(defaultNavOpen);
28141
+ const [collapsedState, setCollapsedState] = React.useState(defaultNavCollapsed);
28142
+ const navOpen = navOpenProp ?? openState;
28143
+ const navCollapsed = navCollapsedProp ?? collapsedState;
28144
+ const openControlled = navOpenProp !== undefined;
28145
+ const collapsedControlled = navCollapsedProp !== undefined;
28146
+ const onNavOpenChangeRef = useLatestRef(onNavOpenChange);
28147
+ const onNavCollapsedChangeRef = useLatestRef(onNavCollapsedChange);
28148
+ const setNavOpen = React.useCallback((open) => {
28149
+ onNavOpenChangeRef.current?.(open);
28150
+ if (!openControlled)
28151
+ setOpenState(open);
28152
+ }, [openControlled, onNavOpenChangeRef]);
28153
+ const setNavCollapsed = React.useCallback((collapsed) => {
28154
+ onNavCollapsedChangeRef.current?.(collapsed);
28155
+ if (!collapsedControlled)
28156
+ setCollapsedState(collapsed);
28157
+ }, [collapsedControlled, onNavCollapsedChangeRef]);
28158
+ // The layout switches in the stylesheet. The script needs the width
28159
+ // only to know which state the burger toggles, and to close the drawer
28160
+ // once the window is at or past the breakpoint.
28161
+ const wide = useBulmaBreakpoint(breakpoint);
28162
+ const isDrawer = wide === false;
28163
+ const isWide = wide === true;
28164
+ React.useEffect(() => {
28165
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- the width is known only in the browser, and the drawer closes in answer to it
28166
+ if (isWide && navOpen)
28167
+ setNavOpen(false);
28168
+ }, [isWide, navOpen, setNavOpen]);
28169
+ const toggleNav = React.useCallback(() => {
28170
+ if (isDrawer)
28171
+ setNavOpen(!navOpen);
28172
+ else
28173
+ setNavCollapsed(!navCollapsed);
28174
+ }, [isDrawer, navOpen, navCollapsed, setNavOpen, setNavCollapsed]);
28175
+ // The ids of the parts the skip link and the burger point at, read off
28176
+ // the children so the server renders them too.
28177
+ const baseId = React.useId();
28178
+ const parts = flattenChildren(children);
28179
+ const mainId = parts.find(isMain)?.props.id || `${baseId}-main`;
28180
+ const navId = parts.find(isNav)?.props.id || `${baseId}-nav`;
28181
+ const { skipLink: skipText, nav, closeNav, burger } = labels ?? {};
28182
+ const value = React.useMemo(() => ({
28183
+ state: {
28184
+ navOpen,
28185
+ setNavOpen,
28186
+ navCollapsed,
28187
+ setNavCollapsed,
28188
+ isDrawer,
28189
+ toggleNav,
28190
+ navId,
28191
+ },
28192
+ labels: {
28193
+ skipLink: skipText || 'Skip to main content',
28194
+ nav: nav || 'Sections',
28195
+ closeNav: closeNav || 'Close navigation',
28196
+ burger: burger || undefined,
28197
+ },
28198
+ mainId,
28199
+ modeKnown: wide !== null,
28200
+ }), [
28201
+ navOpen,
28202
+ setNavOpen,
28203
+ navCollapsed,
28204
+ setNavCollapsed,
28205
+ isDrawer,
28206
+ toggleNav,
28207
+ navId,
28208
+ skipText,
28209
+ nav,
28210
+ closeNav,
28211
+ burger,
28212
+ mainId,
28213
+ wide,
28214
+ ]);
28215
+ // Its header and footer are the page's banner and contentinfo only
28216
+ // outside `main` and sectioning content, as HTML-AAM maps them.
28217
+ React.useEffect(() => {
28218
+ const host = rootRef.current?.parentElement?.closest('main, article, aside, nav, section');
28219
+ if (host) {
28220
+ warnOnce('AppFrame:nested', `[bestax-bulma] <AppFrame> sits inside a <${host.localName}>, where ` +
28221
+ "its header and footer are no longer the page's banner and " +
28222
+ 'contentinfo landmarks. Put AppFrame at the top of the page.');
28223
+ }
28224
+ }, []);
28225
+ const rootClass = usePrefixedClassNames('app-frame', {
28226
+ [`is-${breakpoint}`]: true,
28227
+ 'is-nav-collapsed': navCollapsed,
28228
+ });
28229
+ const skipClass = usePrefixedClassNames('app-frame-skip-link');
28230
+ return (jsxRuntime.jsxs("div", { ref: rootRefs, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: [skipLink && (jsxRuntime.jsx("a", { className: skipClass, href: `#${mainId}`, children: value.labels.skipLink })), jsxRuntime.jsx(FrameContext.Provider, { value: value, children: children })] }));
28231
+ });
28232
+ /**
28233
+ * The frame's state, for a control of your own inside it: whether the
28234
+ * drawer is open and the column collapsed, setters that go through the
28235
+ * frame's callbacks, and `toggleNav()`, which does what the burger does.
28236
+ * Outside an AppFrame it's `null`, as the library's other context hooks
28237
+ * are, so a control that can sit outside one renders a fallback or nothing.
28238
+ *
28239
+ * @function useAppFrame
28240
+ * @returns The frame's state, or `null` outside an AppFrame.
28241
+ */
28242
+ function useAppFrame() {
28243
+ return React.useContext(FrameContext)?.state ?? null;
28244
+ }
28245
+ const AppFrame = withSubComponents(AppFrameRoot, {
28246
+ Header: AppFrameHeader,
28247
+ Nav: AppFrameNav,
28248
+ Main: AppFrameMain,
28249
+ Aside: AppFrameAside,
28250
+ Footer: AppFrameFooter,
28251
+ Burger: AppFrameBurger,
28252
+ }, 'AppFrame');
28253
+
27014
28254
  /**
27015
28255
  * The `.stack` element behind Stack, Group, Center and Flex.
27016
28256
  *
@@ -28020,6 +29260,13 @@ const Stack = React.forwardRef(function Stack(props, ref) {
28020
29260
  Stack.displayName = 'Stack';
28021
29261
 
28022
29262
  exports.Affix = Affix;
29263
+ exports.AppFrame = AppFrame;
29264
+ exports.AppFrameAside = AppFrameAside;
29265
+ exports.AppFrameBurger = AppFrameBurger;
29266
+ exports.AppFrameFooter = AppFrameFooter;
29267
+ exports.AppFrameHeader = AppFrameHeader;
29268
+ exports.AppFrameMain = AppFrameMain;
29269
+ exports.AppFrameNav = AppFrameNav;
28023
29270
  exports.Autocomplete = Autocomplete;
28024
29271
  exports.Avatar = Avatar;
28025
29272
  exports.Avatars = Avatars;
@@ -28233,6 +29480,7 @@ exports.ToastContainer = ToastContainer;
28233
29480
  exports.ToggleButton = ToggleButton;
28234
29481
  exports.Tooltip = Tooltip;
28235
29482
  exports.Tr = Tr;
29483
+ exports.TreeSelect = TreeSelect;
28236
29484
  exports.TreeView = TreeView;
28237
29485
  exports.TreeViewItem = TreeViewItem;
28238
29486
  exports.UnorderedList = UnorderedList;
@@ -28250,6 +29498,7 @@ exports.radioSizes = radioSizes;
28250
29498
  exports.switchColors = switchColors;
28251
29499
  exports.switchSizes = switchSizes;
28252
29500
  exports.toast = toast;
29501
+ exports.useAppFrame = useAppFrame;
28253
29502
  exports.useBulmaClasses = useBulmaClasses;
28254
29503
  exports.useClassPrefix = useClassPrefix;
28255
29504
  exports.useColorClasses = useColorClasses;