@allxsmith/bestax-bulma 5.34.3 → 5.35.1

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