@allxsmith/bestax-bulma 5.34.3 → 5.35.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 (70) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/Loading.js +19 -6
  4. package/dist/esm/components/Loading.js.map +1 -1
  5. package/dist/esm/components/Menu.js +5 -1
  6. package/dist/esm/components/Menu.js.map +1 -1
  7. package/dist/esm/components/Modal.js +22 -31
  8. package/dist/esm/components/Modal.js.map +1 -1
  9. package/dist/esm/components/Sidebar.js +19 -6
  10. package/dist/esm/components/Sidebar.js.map +1 -1
  11. package/dist/esm/components/Steps.js +14 -13
  12. package/dist/esm/components/Steps.js.map +1 -1
  13. package/dist/esm/components/Toast.js +18 -7
  14. package/dist/esm/components/Toast.js.map +1 -1
  15. package/dist/esm/components/Tooltip.js +3 -3
  16. package/dist/esm/components/Tooltip.js.map +1 -1
  17. package/dist/esm/elements/Loader.js +2 -1
  18. package/dist/esm/elements/Loader.js.map +1 -1
  19. package/dist/esm/form/Checkbox.js +37 -4
  20. package/dist/esm/form/Checkbox.js.map +1 -1
  21. package/dist/esm/form/Slider.js +5 -4
  22. package/dist/esm/form/Slider.js.map +1 -1
  23. package/dist/esm/form/Taginput.js +21 -9
  24. package/dist/esm/form/Taginput.js.map +1 -1
  25. package/dist/esm/form/_pickerInternals/PickerPopover.js +9 -1
  26. package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
  27. package/dist/esm/form/_pickerInternals/useSegmentedEntry.js +5 -4
  28. package/dist/esm/form/_pickerInternals/useSegmentedEntry.js.map +1 -1
  29. package/dist/esm/helpers/overlayStack.js +49 -0
  30. package/dist/esm/helpers/overlayStack.js.map +1 -0
  31. package/dist/esm/helpers/useLatestRef.js +33 -0
  32. package/dist/esm/helpers/useLatestRef.js.map +1 -0
  33. package/dist/extras.css +1 -1
  34. package/dist/extras.css.map +1 -1
  35. package/dist/index.cjs +240 -92
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/types/components/Loading.d.ts +13 -3
  38. package/dist/types/components/Menu.d.ts +14 -1
  39. package/dist/types/components/Modal.d.ts +7 -4
  40. package/dist/types/components/Sidebar.d.ts +8 -3
  41. package/dist/types/components/Steps.d.ts +27 -5
  42. package/dist/types/components/Toast.d.ts +5 -2
  43. package/dist/types/elements/Loader.d.ts +2 -1
  44. package/dist/types/form/Checkbox.d.ts +11 -0
  45. package/dist/types/form/Taginput.d.ts +5 -1
  46. package/dist/types/helpers/overlayStack.d.ts +15 -0
  47. package/dist/types/helpers/useLatestRef.d.ts +21 -0
  48. package/dist/types-cjs/components/Loading.d.ts +13 -3
  49. package/dist/types-cjs/components/Menu.d.ts +14 -1
  50. package/dist/types-cjs/components/Modal.d.ts +7 -4
  51. package/dist/types-cjs/components/Sidebar.d.ts +8 -3
  52. package/dist/types-cjs/components/Steps.d.ts +27 -5
  53. package/dist/types-cjs/components/Toast.d.ts +5 -2
  54. package/dist/types-cjs/elements/Loader.d.ts +2 -1
  55. package/dist/types-cjs/form/Checkbox.d.ts +11 -0
  56. package/dist/types-cjs/form/Taginput.d.ts +5 -1
  57. package/dist/types-cjs/helpers/overlayStack.d.ts +15 -0
  58. package/dist/types-cjs/helpers/useLatestRef.d.ts +21 -0
  59. package/dist/versions/bestax-no-dark-mode.css +1 -1
  60. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  61. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  62. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  63. package/dist/versions/bestax-no-helpers.css +1 -1
  64. package/dist/versions/bestax-no-helpers.css.map +1 -1
  65. package/dist/versions/bestax-prefixed.css +1 -1
  66. package/dist/versions/bestax-prefixed.css.map +1 -1
  67. package/package.json +1 -1
  68. package/src/scss/components/_steps.scss +96 -38
  69. package/src/scss/elements/_loader.scss +20 -0
  70. package/src/scss/form/_checkbox.scss +28 -1
package/dist/index.cjs CHANGED
@@ -3581,6 +3581,10 @@ const STRIP_FROM_NON_LINK = { href: true };
3581
3581
  /**
3582
3582
  * MenuItem supports `as` prop for custom link components, e.g., react-router-dom Link.
3583
3583
  *
3584
+ * Give each item an `href`, a router link through `as`, or `as="button"`, so
3585
+ * the keyboard can reach it: an `<a>` with no `href` isn't a link, and Tab
3586
+ * skips it.
3587
+ *
3584
3588
  * @function
3585
3589
  * @param {MenuItemProps} props - Props for the MenuItem component.
3586
3590
  * @param {React.Ref} ref - Forwarded ref to the inner element `as` renders.
@@ -3623,7 +3627,7 @@ const MenuItem = React.forwardRef(function MenuItem(itemProps, ref) {
3623
3627
  labelChildren.push(child);
3624
3628
  }
3625
3629
  });
3626
- return (jsxRuntime.jsxs("li", { className: className, "data-testid": testId, style: style, id: id, title: title, role: role, tabIndex: tabIndex, children: [jsxRuntime.jsx(Component, { ref: ref, className: itemClass || undefined, ...linkProps, ...(Component === 'button'
3630
+ return (jsxRuntime.jsxs("li", { className: className, "data-testid": testId, style: style, id: id, title: title, role: role, tabIndex: tabIndex, children: [jsxRuntime.jsx(Component, { ref: ref, className: itemClass || undefined, "aria-current": active ? 'page' : undefined, ...linkProps, ...(Component === 'button'
3627
3631
  ? { type: buttonType(linkProps.type) }
3628
3632
  : {}), children: labelChildren }), nestedMenuLists] }));
3629
3633
  });
@@ -3834,12 +3838,85 @@ function Portal({ children, container, disabled = false }) {
3834
3838
  return reactDom.createPortal(children, resolveContainer(container));
3835
3839
  }
3836
3840
 
3841
+ // INTERNAL: deliberately not exported from src/index.ts.
3842
+ /**
3843
+ * `useLayoutEffect` in the browser, `useEffect` on the server. A server
3844
+ * render runs neither, and React 18 warns about a layout effect there, so the
3845
+ * server is handed the plain one.
3846
+ */
3847
+ const useClientLayoutEffect = typeof document === 'undefined' ? React.useEffect : React.useLayoutEffect;
3848
+
3849
+ // INTERNAL: deliberately not exported from src/index.ts.
3850
+ /**
3851
+ * A ref holding the value from the latest commit, for a listener that is
3852
+ * added once and calls whatever callback it was given most recently.
3853
+ *
3854
+ * A listener whose effect lists a callback prop is taken off and added again
3855
+ * each time that callback changes, and an inline one changes on every render
3856
+ * of its parent. React can render between two listeners for the same
3857
+ * keypress, and the browser runs neither a listener taken off mid-event nor
3858
+ * one added mid-event, so that keypress never reaches it (#1088). Reading the
3859
+ * callback from here keeps the listener where it is.
3860
+ *
3861
+ * It's updated before paint on each commit, so a listener reads the props of
3862
+ * the render that's on screen.
3863
+ *
3864
+ * @function useLatestRef
3865
+ * @param value - The value to keep current, usually a callback prop.
3866
+ * @returns A ref whose `current` is `value` as of the latest commit.
3867
+ */
3868
+ function useLatestRef(value) {
3869
+ const ref = React.useRef(value);
3870
+ useClientLayoutEffect(() => {
3871
+ ref.current = value;
3872
+ });
3873
+ return ref;
3874
+ }
3875
+
3876
+ // INTERNAL: deliberately not exported from src/index.ts.
3837
3877
  /**
3838
- * Every currently active modal, in the order they opened. Only the last entry
3839
- * — the topmost modal — reacts to Escape and Tab, so one keypress can't
3840
- * dismiss a whole stack (e.g. a `Dialog` opened on top of a `Modal`).
3878
+ * The open overlays that take part, in the order they opened. Only the last
3879
+ * one answers Escape, so one keypress closes one layer. It's touched in
3880
+ * effects and key handlers, never while rendering, so a server render leaves
3881
+ * it alone.
3841
3882
  */
3842
- const activeModalStack = [];
3883
+ const openOverlays = [];
3884
+ /**
3885
+ * Holds this overlay's place in the stack of open overlays while `inStack`
3886
+ * is true, from the effect after it opens until the one after it closes.
3887
+ * Opening again puts it back on top.
3888
+ *
3889
+ * Returns a check for whether nothing in the stack sits above this overlay,
3890
+ * to call when a key arrives. For an overlay in the stack, that's whether it
3891
+ * is the topmost one. For one that isn't, such as a Toast, it's whether no
3892
+ * overlay is open at all.
3893
+ *
3894
+ * @function useOverlayStack
3895
+ * @param inStack - Whether this overlay is open and takes part.
3896
+ * @returns A check for whether nothing in the stack is above this overlay.
3897
+ */
3898
+ function useOverlayStack(inStack) {
3899
+ const tokenRef = React.useRef({});
3900
+ React.useEffect(() => {
3901
+ if (!inStack)
3902
+ return undefined;
3903
+ const token = tokenRef.current;
3904
+ openOverlays.push(token);
3905
+ // Takes out this effect's own push. Should it ever be gone already, the
3906
+ // stack is left as it is: splicing at -1 would take the topmost overlay
3907
+ // instead.
3908
+ return () => {
3909
+ const index = openOverlays.indexOf(token);
3910
+ if (index !== -1)
3911
+ openOverlays.splice(index, 1);
3912
+ };
3913
+ }, [inStack]);
3914
+ return React.useCallback(() => {
3915
+ const top = openOverlays[openOverlays.length - 1];
3916
+ return top === undefined || top === tokenRef.current;
3917
+ }, []);
3918
+ }
3919
+
3843
3920
  /**
3844
3921
  * Controls the modal can hand focus to. Disabled and hidden controls are
3845
3922
  * excluded because `focus()` on them is a no-op, which would leave focus
@@ -3998,8 +4075,8 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
3998
4075
  // Cleanup returned by a consumer's callback ref on attach, held until detach.
3999
4076
  const consumerCleanupRef = React.useRef(null);
4000
4077
  // The forwarded ref and the internal one must both see the node: focus
4001
- // management, the scroll lock and the topmost-modal check all read
4002
- // `modalRootRef`, while consumers expect their own ref to resolve.
4078
+ // management and the Tab trap read `modalRootRef`, while consumers expect
4079
+ // their own ref to resolve.
4003
4080
  const combinedRef = React.useCallback((node) => {
4004
4081
  modalRootRef.current =
4005
4082
  node;
@@ -4030,7 +4107,6 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
4030
4107
  }
4031
4108
  }, [ref]);
4032
4109
  const previouslyFocusedRef = React.useRef(null);
4033
- const stackTokenRef = React.useRef({});
4034
4110
  const generatedTitleId = React.useId();
4035
4111
  // A portal has no server-rendered counterpart, so the server render and the
4036
4112
  // hydrating render have to stay inline and only move into the portal once
@@ -4048,35 +4124,31 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
4048
4124
  // to it moves nothing, so it mustn't re-run the effects either.
4049
4125
  const defaultTarget = isPortaled && portal === true ? portalContainer : undefined;
4050
4126
  useScrollLock(isModalActive && lockScroll);
4051
- // Record open order so keyboard handling only applies to the topmost modal.
4052
- React.useEffect(() => {
4053
- if (!isModalActive)
4054
- return undefined;
4055
- const token = stackTokenRef.current;
4056
- activeModalStack.push(token);
4057
- return () => {
4058
- const index = activeModalStack.indexOf(token);
4059
- if (index !== -1)
4060
- activeModalStack.splice(index, 1);
4061
- };
4062
- }, [isModalActive]);
4063
- // Escape closes; Tab cycles inside the modal. Both only for the topmost one.
4064
- // The listener is on `window`, where a keydown arrives last, so everything
4065
- // inside the modal has had the key first: React's handlers, and `document`
4066
- // listeners added after this one, such as a date picker's.
4127
+ // Read when Escape arrives, so the key listener stays put while `onClose`
4128
+ // changes identity, as an inline one does on every render of the parent.
4129
+ const onCloseRef = useLatestRef(onClose);
4130
+ // Joins the stack of open overlays, so the keyboard handling below only
4131
+ // applies while this modal is the topmost overlay.
4132
+ const isTopmost = useOverlayStack(isModalActive);
4133
+ // Escape closes; Tab cycles inside the modal. Both only for the topmost
4134
+ // overlay. The listener is on `window`, where a keydown arrives last, so
4135
+ // everything inside the modal has had the key first: React's handlers, and
4136
+ // `document` listeners added after this one, such as a date picker's.
4067
4137
  React.useEffect(() => {
4068
4138
  if (!isModalActive)
4069
4139
  return undefined;
4070
4140
  const handleKeyDown = (e) => {
4071
4141
  const node = modalRootRef.current;
4072
- if (activeModalStack[activeModalStack.length - 1] !== stackTokenRef.current) {
4142
+ if (!isTopmost())
4073
4143
  return;
4074
- }
4075
4144
  if (e.key === 'Escape') {
4076
4145
  // Something inside already used this Escape, such as a popup that
4077
- // closed on it, so the modal waits for the next one.
4078
- if (closeOnEscape && !e.defaultPrevented)
4079
- onClose?.();
4146
+ // closed on it, so the modal waits for the next one. One the modal
4147
+ // closes on is marked used, so nothing under it acts on it as well.
4148
+ if (closeOnEscape && onCloseRef.current && !e.defaultPrevented) {
4149
+ e.preventDefault();
4150
+ onCloseRef.current();
4151
+ }
4080
4152
  return;
4081
4153
  }
4082
4154
  // A trap inside the modal, such as a portaled popup's, already moved
@@ -4108,7 +4180,7 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
4108
4180
  };
4109
4181
  window.addEventListener('keydown', handleKeyDown);
4110
4182
  return () => window.removeEventListener('keydown', handleKeyDown);
4111
- }, [isModalActive, closeOnEscape, onClose]);
4183
+ }, [isModalActive, closeOnEscape, onCloseRef, isTopmost]);
4112
4184
  // Move focus into the modal on open, restore it on close
4113
4185
  React.useEffect(() => {
4114
4186
  if (!isModalActive)
@@ -5105,14 +5177,6 @@ const Tabs = withSubComponents(TabsComponent, {
5105
5177
  Content: TabsContent,
5106
5178
  }, 'Tabs');
5107
5179
 
5108
- // INTERNAL: deliberately not exported from src/index.ts.
5109
- /**
5110
- * `useLayoutEffect` in the browser, `useEffect` on the server. A server
5111
- * render runs neither, and React 18 warns about a layout effect there, so the
5112
- * server is handed the plain one.
5113
- */
5114
- const useClientLayoutEffect = typeof document === 'undefined' ? React.useEffect : React.useLayoutEffect;
5115
-
5116
5180
  // Not exported, so it stays out of the public API that `index.ts` re-exports.
5117
5181
  const SegmentedControlContext = React.createContext(null);
5118
5182
  /** Sorts mounted segments into the order they sit in the page. */
@@ -5387,19 +5451,30 @@ const Loading = ({ active = false, isFullPage = false, size, color, canCancel =
5387
5451
  onCancel();
5388
5452
  }
5389
5453
  };
5454
+ // Read when Escape arrives, so the key listener stays put while `onCancel`
5455
+ // changes identity, as an inline one does on every render of the parent.
5456
+ const onCancelRef = useLatestRef(onCancel);
5457
+ const hasOnCancel = Boolean(onCancel);
5458
+ // A full-page loading covers everything under it, so it joins the stack of
5459
+ // open overlays and takes Escape from them while it's on top. One inside a
5460
+ // container covers only that, so it stays out and waits, as a Toast does,
5461
+ // until no overlay is open.
5462
+ const isTopmost = useOverlayStack(active && isFullPage);
5390
5463
  // Handle escape key. On `window`, as in `Modal`, so a popup on the page has
5391
- // had the key first, and an Escape it closed on doesn't cancel as well.
5464
+ // had the key first, and an Escape it closed on doesn't cancel as well. One
5465
+ // that cancels is marked used, so nothing under it acts on it too.
5392
5466
  React.useEffect(() => {
5393
- if (!active || !canCancel || !onCancel)
5467
+ if (!active || !canCancel || !hasOnCancel)
5394
5468
  return;
5395
5469
  const handleKeyDown = (e) => {
5396
- if (e.key === 'Escape' && !e.defaultPrevented) {
5397
- onCancel();
5398
- }
5470
+ if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
5471
+ return;
5472
+ e.preventDefault();
5473
+ onCancelRef.current?.();
5399
5474
  };
5400
5475
  window.addEventListener('keydown', handleKeyDown);
5401
5476
  return () => window.removeEventListener('keydown', handleKeyDown);
5402
- }, [active, canCancel, onCancel]);
5477
+ }, [active, canCancel, hasOnCancel, onCancelRef, isTopmost]);
5403
5478
  // Prevent body scroll when full page loading is active. Ref-counted through
5404
5479
  // the shared helper so an overlapping Modal/Dialog/Sidebar doesn't have its
5405
5480
  // lock cleared when this one releases.
@@ -6672,10 +6747,9 @@ const Tooltip = ({ label, content, position = 'top', color, size, active: always
6672
6747
  openOnTap,
6673
6748
  });
6674
6749
  // Auto-placement and overflow correction
6675
- React.useLayoutEffect(() => {
6750
+ useClientLayoutEffect(() => {
6676
6751
  const isAuto = position === 'auto';
6677
6752
  if (!isAuto) {
6678
- // eslint-disable-next-line react-hooks/set-state-in-effect -- layout measurement effect resolves placement
6679
6753
  setResolvedPosition(position);
6680
6754
  }
6681
6755
  const showing = isVisible || alwaysActive;
@@ -7657,23 +7731,21 @@ const Step = ({ isActive = false, isCompleted = false, label, icon, clickable =
7657
7731
  const stepsMarkerClass = usePrefixedClassNames('steps-marker');
7658
7732
  const stepsContentClass = usePrefixedClassNames('steps-content');
7659
7733
  const stepsTitleClass = usePrefixedClassNames('steps-title');
7660
- const handleClick = () => {
7661
- if (clickable && onClick) {
7662
- onClick();
7663
- }
7664
- };
7665
- const handleKeyDown = (e) => {
7666
- if (clickable && onClick && (e.key === 'Enter' || e.key === ' ')) {
7667
- e.preventDefault();
7668
- onClick();
7669
- }
7670
- };
7671
7734
  const resolvedCompletedIcon = completedIcon === undefined ? '\u2713' : completedIcon;
7672
7735
  const markerContent = icon ||
7673
7736
  (isCompleted && resolvedCompletedIcon
7674
7737
  ? resolvedCompletedIcon
7675
7738
  : (stepNumber ?? null));
7676
- return (jsxRuntime.jsx("li", { className: stepClasses, onClick: handleClick, onKeyDown: handleKeyDown, tabIndex: clickable ? 0 : undefined, role: clickable ? 'button' : undefined, "aria-current": isActive ? 'step' : undefined, ...rest, children: jsxRuntime.jsxs("div", { className: stepsLinkClass, children: [jsxRuntime.jsx("span", { className: stepsMarkerClass, children: markerContent }), (label || children) && (jsxRuntime.jsx("div", { className: stepsContentClass, children: jsxRuntime.jsx("p", { className: stepsTitleClass, children: label || children }) }))] }) }));
7739
+ const title = label || children;
7740
+ const current = isActive ? 'step' : undefined;
7741
+ // The step stays a list item, and a real button inside it does the
7742
+ // clicking, so the list keeps its items and the button brings Enter, Space
7743
+ // and focus with it. A button holds only phrasing content, so its parts
7744
+ // are spans.
7745
+ if (clickable) {
7746
+ return (jsxRuntime.jsx("li", { className: stepClasses, ...rest, children: jsxRuntime.jsxs("button", { type: "button", className: stepsLinkClass, onClick: () => onClick?.(), "aria-current": current, children: [jsxRuntime.jsx("span", { className: stepsMarkerClass, children: markerContent }), title && (jsxRuntime.jsx("span", { className: stepsContentClass, children: jsxRuntime.jsx("span", { className: stepsTitleClass, children: title }) }))] }) }));
7747
+ }
7748
+ return (jsxRuntime.jsx("li", { className: stepClasses, "aria-current": current, ...rest, children: jsxRuntime.jsxs("div", { className: stepsLinkClass, children: [jsxRuntime.jsx("span", { className: stepsMarkerClass, children: markerContent }), title && (jsxRuntime.jsx("div", { className: stepsContentClass, children: jsxRuntime.jsx("p", { className: stepsTitleClass, children: title }) }))] }) }));
7677
7749
  };
7678
7750
  /**
7679
7751
  * The `Steps` component provides a multi-step progress indicator for wizard flows, checkout processes, or any multi-step workflow.
@@ -7754,7 +7826,10 @@ const StepsComponent = ({ value = 0, items, size, color, hasMarker = true, anima
7754
7826
  }
7755
7827
  return null;
7756
7828
  };
7757
- return (jsxRuntime.jsxs("div", { className: combinedClasses, ...rest, children: [jsxRuntime.jsx("ul", { className: listClasses, children: renderSteps() }), hasNavigation && (jsxRuntime.jsxs("div", { className: stepsNavigationClass, children: [jsxRuntime.jsx("button", { type: "button", className: prevButtonClass, disabled: value === 0, onClick: onPrev ?? (() => onStepClick?.(value - 1)), children: prevLabel ?? 'Previous' }), jsxRuntime.jsx("button", { type: "button", className: nextButtonClass, disabled: value === totalSteps - 1, onClick: onNext ?? (() => onStepClick?.(value + 1)), children: nextLabel ?? 'Next' })] }))] }));
7829
+ // role="list" restates the <ul>'s own role, for Safari: WebKit stops
7830
+ // exposing a list as one once its markers are hidden, as they are here,
7831
+ // unless the role is set explicitly.
7832
+ return (jsxRuntime.jsxs("div", { className: combinedClasses, ...rest, children: [jsxRuntime.jsx("ul", { className: listClasses, role: "list", children: renderSteps() }), hasNavigation && (jsxRuntime.jsxs("div", { className: stepsNavigationClass, children: [jsxRuntime.jsx("button", { type: "button", className: prevButtonClass, disabled: value === 0, onClick: onPrev ?? (() => onStepClick?.(value - 1)), children: prevLabel ?? 'Previous' }), jsxRuntime.jsx("button", { type: "button", className: nextButtonClass, disabled: value === totalSteps - 1, onClick: onNext ?? (() => onStepClick?.(value + 1)), children: nextLabel ?? 'Next' })] }))] }));
7758
7833
  };
7759
7834
  // Attach Step as static property
7760
7835
  const Steps = withSubComponents(StepsComponent, { Step }, 'Steps');
@@ -7808,19 +7883,29 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
7808
7883
  handleClose();
7809
7884
  }
7810
7885
  }, [overlayClose, handleClose]);
7886
+ // Read when Escape arrives, so the key listener stays put while
7887
+ // `onClose` changes identity, as an inline one does on every render of
7888
+ // the parent.
7889
+ const handleCloseRef = useLatestRef(handleClose);
7890
+ // Joins the stack of open overlays, so the sidebar answers Escape only
7891
+ // while it's the topmost overlay, and keeps it from those under it.
7892
+ const isTopmost = useOverlayStack(isOpen);
7893
+ const closesOnEscape = escapeClose && canCancel && Boolean(onClose);
7811
7894
  // Handle escape key. On `window`, as in `Modal`, so everything inside the
7812
- // sidebar has had the key first, and an Escape it used is left to it.
7895
+ // sidebar has had the key first, and an Escape it used is left to it. One
7896
+ // the sidebar closes on is marked used, so nothing under it acts on it.
7813
7897
  React.useEffect(() => {
7814
- if (!isOpen || !escapeClose)
7898
+ if (!isOpen || !closesOnEscape)
7815
7899
  return;
7816
7900
  const handleKeyDown = (e) => {
7817
- if (e.key === 'Escape' && !e.defaultPrevented) {
7818
- handleClose();
7819
- }
7901
+ if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
7902
+ return;
7903
+ e.preventDefault();
7904
+ handleCloseRef.current();
7820
7905
  };
7821
7906
  window.addEventListener('keydown', handleKeyDown);
7822
7907
  return () => window.removeEventListener('keydown', handleKeyDown);
7823
- }, [isOpen, escapeClose, handleClose]);
7908
+ }, [isOpen, closesOnEscape, handleCloseRef, isTopmost]);
7824
7909
  // Prevent body scroll when sidebar is open. Ref-counted through the shared
7825
7910
  // helper so an overlapping Modal/Dialog/Loading doesn't unlock underneath.
7826
7911
  useScrollLock(isOpen && overlay);
@@ -7830,6 +7915,7 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
7830
7915
  React.useEffect(() => {
7831
7916
  if (isOpen) {
7832
7917
  const element = sidebarRef.current;
7918
+ /* istanbul ignore else: the aside renders whenever the sidebar is open, so its ref is set before this runs */
7833
7919
  if (element) {
7834
7920
  // Focus first focusable element or sidebar itself
7835
7921
  const focusable = element.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
@@ -8038,14 +8124,19 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
8038
8124
  onAction?.();
8039
8125
  handleClose();
8040
8126
  }, [onAction, handleClose]);
8127
+ // Read when the timer runs out, a click lands outside or Escape arrives,
8128
+ // so the timer and the listeners stay put while `onClose` changes
8129
+ // identity, as an inline one does on every render of the parent. An
8130
+ // inline one from `ToastContainer` changes whenever another toast shows.
8131
+ const handleCloseRef = useLatestRef(handleClose);
8041
8132
  React.useEffect(() => {
8042
8133
  if (indefinite || duration === 0 || isPaused)
8043
8134
  return undefined;
8044
8135
  const timer = setTimeout(() => {
8045
- handleClose();
8136
+ handleCloseRef.current();
8046
8137
  }, duration);
8047
8138
  return () => clearTimeout(timer);
8048
- }, [duration, indefinite, isPaused, handleClose]);
8139
+ }, [duration, indefinite, isPaused, handleCloseRef]);
8049
8140
  const handleMouseEnter = React.useCallback(() => {
8050
8141
  if (pauseOnHover) {
8051
8142
  setIsPaused(true);
@@ -8063,7 +8154,7 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
8063
8154
  return undefined;
8064
8155
  const handleDocumentClick = (e) => {
8065
8156
  if (toastRef.current && !isEventInside(e, toastRef.current)) {
8066
- handleClose();
8157
+ handleCloseRef.current();
8067
8158
  }
8068
8159
  };
8069
8160
  const raf = requestAnimationFrame(() => {
@@ -8073,20 +8164,24 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
8073
8164
  cancelAnimationFrame(raf);
8074
8165
  document.removeEventListener('click', handleDocumentClick);
8075
8166
  };
8076
- }, [dismissible, isVisible, handleClose]);
8167
+ }, [dismissible, isVisible, handleCloseRef]);
8168
+ // A toast is a notification rather than a layer, so it never joins the
8169
+ // stack of open overlays. Asking the stack tells it whether one is open:
8170
+ // an Escape while one is belongs to the overlay, and the toast waits.
8171
+ const isTopmost = useOverlayStack(false);
8077
8172
  // On `window`, as in `Modal`, so a popup on the page has had the key
8078
8173
  // first, and an Escape it closed on doesn't dismiss the toast as well.
8079
8174
  React.useEffect(() => {
8080
8175
  if (!cancelable || !isVisible)
8081
8176
  return undefined;
8082
8177
  const handleKeyDown = (e) => {
8083
- if (e.key === 'Escape' && !e.defaultPrevented) {
8084
- handleClose();
8178
+ if (e.key === 'Escape' && !e.defaultPrevented && isTopmost()) {
8179
+ handleCloseRef.current();
8085
8180
  }
8086
8181
  };
8087
8182
  window.addEventListener('keydown', handleKeyDown);
8088
8183
  return () => window.removeEventListener('keydown', handleKeyDown);
8089
- }, [cancelable, isVisible, handleClose]);
8184
+ }, [cancelable, isVisible, handleCloseRef, isTopmost]);
8090
8185
  const containerClasses = usePrefixedClassNames('toast-container', {
8091
8186
  [`is-${position}`]: true,
8092
8187
  });
@@ -9754,7 +9849,8 @@ const ListItem = ({ className, textColor, bgColor, children, ...props }) => {
9754
9849
  *
9755
9850
  * The ring is `1em` square, so `textSize` scales it. It is exposed as an
9756
9851
  * indeterminate `progressbar` named by `ariaLabel`, and it stops spinning
9757
- * under `prefers-reduced-motion: reduce` while staying drawn.
9852
+ * under `prefers-reduced-motion: reduce` while staying drawn. In forced
9853
+ * colors its two open sides take the page's color, so it stays open.
9758
9854
  *
9759
9855
  * @function
9760
9856
  * @param {LoaderProps} props - Props for the Loader component.
@@ -10835,11 +10931,39 @@ const checkboxSizes = ['small', 'normal', 'medium', 'large'];
10835
10931
  * <Checkbox value="vue">Vue</Checkbox>
10836
10932
  * </Checkboxes>
10837
10933
  */
10838
- const Checkbox = React.forwardRef(({ color, size, className, children, textColor, disabled, name, value, checked, defaultChecked, onChange, ...props }, ref) => {
10934
+ const Checkbox = React.forwardRef(({ color, size, className, children, textColor, disabled, name, value, checked, defaultChecked, indeterminate, onChange, ...props }, ref) => {
10839
10935
  const { bulmaHelperClasses, rest } = useBulmaClasses({
10840
10936
  color: textColor,
10841
10937
  ...props,
10842
10938
  });
10939
+ // The input's own ref, merged with the forwarded one. A callback ref's
10940
+ // return value is handed back, so React 19 still runs a cleanup it
10941
+ // returns, as it did when the ref went straight to the input.
10942
+ const inputRef = React.useRef(null);
10943
+ const setRefs = React.useCallback((node) => {
10944
+ inputRef.current = node;
10945
+ if (typeof ref === 'function')
10946
+ return ref(node);
10947
+ if (ref)
10948
+ ref.current = node;
10949
+ }, [ref]);
10950
+ // `indeterminate` is a DOM property with no attribute, so it's set on the
10951
+ // input rather than rendered. A click clears it, so this runs after every
10952
+ // render, not only when the prop changes. Unset, it's left alone, so a
10953
+ // ref that sets the property itself still works. `ownsIndeterminateRef`
10954
+ // remembers that this component set it, so a prop that goes back to unset
10955
+ // clears the dash once instead of leaving it on.
10956
+ const ownsIndeterminateRef = React.useRef(false);
10957
+ useClientLayoutEffect(() => {
10958
+ if (indeterminate !== undefined) {
10959
+ ownsIndeterminateRef.current = true;
10960
+ inputRef.current.indeterminate = indeterminate;
10961
+ }
10962
+ else if (ownsIndeterminateRef.current) {
10963
+ ownsIndeterminateRef.current = false;
10964
+ inputRef.current.indeterminate = false;
10965
+ }
10966
+ });
10843
10967
  // Inherit name + selection state from a surrounding <Checkboxes> group.
10844
10968
  // Local props always win over the group (explicit > implicit).
10845
10969
  const group = useCheckboxesGroup();
@@ -10854,6 +10978,10 @@ const Checkbox = React.forwardRef(({ color, size, className, children, textColor
10854
10978
  // the controlled/uncontrolled React warning.
10855
10979
  const effectiveDefaultChecked = groupManaged ? undefined : defaultChecked;
10856
10980
  const handleChange = React.useCallback((e) => {
10981
+ // The click that got here cleared the mixed state. Put it back now,
10982
+ // since a click that changes no state renders nothing to do it.
10983
+ if (indeterminate !== undefined)
10984
+ e.target.indeterminate = indeterminate;
10857
10985
  // Local onChange always fires.
10858
10986
  onChange?.(e);
10859
10987
  // Dispatch to the group additionally — toggle this value's
@@ -10868,7 +10996,7 @@ const Checkbox = React.forwardRef(({ color, size, className, children, textColor
10868
10996
  : currentArr.filter(v => v !== valStr);
10869
10997
  group.onChange(next);
10870
10998
  }
10871
- }, [onChange, group, value]);
10999
+ }, [onChange, group, value, indeterminate]);
10872
11000
  const mainClass = usePrefixedClassNames('styled-checkbox', 'checkbox', {
10873
11001
  [`is-${color}`]: color && checkboxColors.includes(color),
10874
11002
  [`is-${size}`]: size && checkboxSizes.includes(size),
@@ -10876,7 +11004,7 @@ const Checkbox = React.forwardRef(({ color, size, className, children, textColor
10876
11004
  const checkboxClass = classNames(mainClass, bulmaHelperClasses, className);
10877
11005
  const checkClass = usePrefixedClassNames('check');
10878
11006
  const controlLabelClass = usePrefixedClassNames('control-label');
10879
- return (jsxRuntime.jsxs("label", { className: checkboxClass, children: [jsxRuntime.jsx("input", { ref: ref, type: "checkbox", disabled: disabled, name: effectiveName, value: value, checked: effectiveChecked, defaultChecked: effectiveDefaultChecked, onChange: handleChange, ...rest }), jsxRuntime.jsx("span", { className: checkClass }), children && jsxRuntime.jsx("span", { className: controlLabelClass, children: children })] }));
11007
+ return (jsxRuntime.jsxs("label", { className: checkboxClass, children: [jsxRuntime.jsx("input", { ref: setRefs, type: "checkbox", disabled: disabled, name: effectiveName, value: value, checked: effectiveChecked, defaultChecked: effectiveDefaultChecked, onChange: handleChange, ...rest }), jsxRuntime.jsx("span", { className: checkClass }), children && jsxRuntime.jsx("span", { className: controlLabelClass, children: children })] }));
10880
11008
  });
10881
11009
  Checkbox.displayName = 'Checkbox';
10882
11010
 
@@ -12029,7 +12157,7 @@ const Slider = React.forwardRef((props, ref) => {
12029
12157
  }
12030
12158
  return 0;
12031
12159
  }, []);
12032
- React.useLayoutEffect(() => {
12160
+ useClientLayoutEffect(() => {
12033
12161
  if (isVertical)
12034
12162
  return;
12035
12163
  const wrapper = wrapperRef.current;
@@ -12043,7 +12171,7 @@ const Slider = React.forwardRef((props, ref) => {
12043
12171
  setNudgeX(computeNudge(el, wrapper, pct, thumbPx));
12044
12172
  }
12045
12173
  });
12046
- React.useLayoutEffect(() => {
12174
+ useClientLayoutEffect(() => {
12047
12175
  if (isVertical || !range)
12048
12176
  return;
12049
12177
  const wrapper = wrapperRef.current;
@@ -12057,7 +12185,7 @@ const Slider = React.forwardRef((props, ref) => {
12057
12185
  }
12058
12186
  });
12059
12187
  // Detect vertical tooltip overflow: flip to left if no space on right
12060
- React.useLayoutEffect(() => {
12188
+ useClientLayoutEffect(() => {
12061
12189
  if (!isVertical)
12062
12190
  return;
12063
12191
  const wrapper = wrapperRef.current;
@@ -13089,7 +13217,10 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
13089
13217
  const hasInput = inputValue || (openOnFocus && isActive);
13090
13218
  return matchesInput && notAlreadyAdded && hasInput;
13091
13219
  });
13092
- const listOpen = isActive && filteredData.length > 0;
13220
+ // At the tag limit the input isn't rendered, so the list isn't either.
13221
+ // Left open it would be a listbox with no combobox, offering tags past
13222
+ // the limit (#1087).
13223
+ const listOpen = isActive && !isMaxReached && filteredData.length > 0;
13093
13224
  // keepFirst: auto-highlight first item when dropdown opens
13094
13225
  React.useEffect(() => {
13095
13226
  if (keepFirst && isActive && filteredData.length > 0) {
@@ -13103,17 +13234,16 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
13103
13234
  }
13104
13235
  onChange?.(newTags);
13105
13236
  }, [controlledValue, onChange]);
13106
- // Add tag
13237
+ // Add a tag from text typed, pasted or picked from the list. Only
13238
+ // `createTag` turns it into an object.
13107
13239
  const addTag = React.useCallback((value) => {
13108
13240
  if (disabled || readonly)
13109
13241
  return;
13110
- const tagValue = typeof value === 'string' ? value.trim() : value;
13111
- const displayValue = typeof tagValue === 'string' ? tagValue : getDisplayValue(tagValue);
13242
+ const displayValue = value.trim();
13112
13243
  if (!displayValue)
13113
13244
  return;
13114
- // Check max tags
13115
- if (maxTags && tags.length >= maxTags)
13116
- return;
13245
+ // No maxTags check: only the input and the list call this, neither
13246
+ // renders at the limit, and each call adds at most one tag.
13117
13247
  // Check duplicates
13118
13248
  if (!allowDuplicates) {
13119
13249
  const exists = tags.some(tag => getDisplayValue(tag).toLowerCase() === displayValue.toLowerCase());
@@ -13130,7 +13260,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
13130
13260
  if (beforeAdding && !beforeAdding(displayValue))
13131
13261
  return;
13132
13262
  // createTag transformation
13133
- const finalTag = createTag ? createTag(displayValue) : tagValue;
13263
+ const finalTag = createTag ? createTag(displayValue) : displayValue;
13134
13264
  const newTags = [...tags, finalTag];
13135
13265
  updateTags(newTags);
13136
13266
  onAdd?.(finalTag);
@@ -13143,7 +13273,6 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
13143
13273
  disabled,
13144
13274
  readonly,
13145
13275
  tags,
13146
- maxTags,
13147
13276
  allowDuplicates,
13148
13277
  allowNew,
13149
13278
  data,
@@ -13209,6 +13338,17 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
13209
13338
  setIsActive(true);
13210
13339
  }
13211
13340
  }, [openOnFocus, data.length, disabled]);
13341
+ // Reaching the limit dismisses the list the way a click outside does, so
13342
+ // making room later doesn't bring it back unasked. It's an effect, not a
13343
+ // step in addTag, because a click on a suggestion also bubbles to the
13344
+ // container, whose focus on the input reopens the list in that same
13345
+ // event, and because a controlled value can reach the limit too.
13346
+ React.useEffect(() => {
13347
+ if (isMaxReached) {
13348
+ setIsActive(false);
13349
+ setHighlightedIndex(-1);
13350
+ }
13351
+ }, [isMaxReached]);
13212
13352
  // Handle click outside
13213
13353
  React.useEffect(() => {
13214
13354
  if (!isActive)
@@ -15167,7 +15307,15 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
15167
15307
  left: resolved.left,
15168
15308
  }
15169
15309
  : {};
15170
- 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, children: children }));
15310
+ 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,
15311
+ // An Escape pressed in the panel is the picker's, and the listener
15312
+ // above claims it. A Dropdown menu around the picker hears Escape in a
15313
+ // React handler, which runs before that listener, so the panel claims
15314
+ // it here too, where React gets to it first.
15315
+ onKeyDown: e => {
15316
+ if (closeOnEscape && e.key === 'Escape')
15317
+ e.preventDefault();
15318
+ }, children: children }));
15171
15319
  if (appendToBody && isBrowser) {
15172
15320
  return reactDom.createPortal(panel, resolveContainer());
15173
15321
  }
@@ -15581,7 +15729,7 @@ function useSegmentedEntry(params) {
15581
15729
  // any of its effects, so a focus in between is the one handed back.
15582
15730
  const closingRef = React.useRef(false);
15583
15731
  const wasOpenRef = React.useRef(isOpen);
15584
- React.useLayoutEffect(() => {
15732
+ useClientLayoutEffect(() => {
15585
15733
  if (wasOpenRef.current && !isOpen) {
15586
15734
  closingRef.current = true;
15587
15735
  if (!value && isUntouched(text))
@@ -15611,12 +15759,12 @@ function useSegmentedEntry(params) {
15611
15759
  setText(formatFn(next, format, locale));
15612
15760
  }, [commitValue, isAllowed, formatFn, format, locale, setText]);
15613
15761
  // After every segment-driven change, restore the system selection on the
15614
- // active segment's char range. useLayoutEffect runs synchronously before
15762
+ // active segment's char range. A layout effect runs synchronously before
15615
15763
  // paint so the user never sees a stray caret position. We deliberately do
15616
15764
  // NOT gate on document.activeElement: when openOnFocus is true the focus
15617
15765
  // trap moves focus into the popover, but setSelectionRange on an unfocused
15618
15766
  // input is well-defined and the selection becomes visible on refocus.
15619
- React.useLayoutEffect(() => {
15767
+ useClientLayoutEffect(() => {
15620
15768
  if (activeSegmentIdx === null || !segmentMap || !inputRef.current)
15621
15769
  return;
15622
15770
  const seg = segmentMap.segments[activeSegmentIdx];