@allxsmith/bestax-bulma 5.14.0 → 5.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -1776,14 +1776,46 @@ const isBrowser$1 = (win, doc) => typeof win !== 'undefined' && typeof doc !== '
1776
1776
  *
1777
1777
  * @function
1778
1778
  * @param {DropdownProps} props - Props for the Dropdown component.
1779
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the root dropdown element.
1779
1780
  * @returns {JSX.Element} The rendered dropdown.
1780
1781
  * @see {@link https://bulma.io/documentation/components/dropdown/ | Bulma Dropdown documentation}
1781
1782
  */
1782
- const DropdownComponent = ({ label, children, className, menuClassName, active: activeProp, up, right, hoverable, disabled, onActiveChange, closeOnClick = true, id, ...props }) => {
1783
+ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, children, className, menuClassName, active: activeProp, up, right, hoverable, disabled, onActiveChange, closeOnClick = true, id, ...props }, ref) {
1783
1784
  const [active, setActive] = React.useState(!!activeProp);
1784
1785
  const dropdownRef = React.useRef(null);
1785
1786
  const triggerRef = React.useRef(null);
1786
1787
  const pendingFocusRef = React.useRef(null);
1788
+ // Cleanup returned by a consumer's callback ref on attach, held until detach.
1789
+ const consumerCleanupRef = React.useRef(null);
1790
+ const setRefs = React.useCallback((node) => {
1791
+ dropdownRef.current =
1792
+ node;
1793
+ if (typeof ref === 'function') {
1794
+ // React 19 lets a callback ref return a cleanup function and detaches
1795
+ // by running it instead of calling the ref with `null`; React 18
1796
+ // discards the return value entirely. Returning it from here would be
1797
+ // a React-19-only contract, so instead we hold the cleanup and run it
1798
+ // ourselves on detach — a consumer's cleanup ref then behaves the same
1799
+ // on both majors of the CI matrix.
1800
+ if (node === null) {
1801
+ const consumerCleanup = consumerCleanupRef.current;
1802
+ consumerCleanupRef.current = null;
1803
+ if (consumerCleanup) {
1804
+ consumerCleanup();
1805
+ }
1806
+ else {
1807
+ ref(null);
1808
+ }
1809
+ return;
1810
+ }
1811
+ const cleanup = ref(node);
1812
+ consumerCleanupRef.current =
1813
+ typeof cleanup === 'function' ? cleanup : null;
1814
+ }
1815
+ else if (ref) {
1816
+ ref.current = node;
1817
+ }
1818
+ }, [ref]);
1787
1819
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
1788
1820
  // Generate Bulma classes with prefix
1789
1821
  const bulmaClasses = usePrefixedClassNames('dropdown', {
@@ -1951,8 +1983,8 @@ const DropdownComponent = ({ label, children, className, menuClassName, active:
1951
1983
  }
1952
1984
  };
1953
1985
  const dropdownClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
1954
- return (jsxRuntime.jsxs("div", { className: dropdownClasses, ref: dropdownRef, id: id, "data-testid": "dropdown-root", ...rest, children: [jsxRuntime.jsx("div", { className: usePrefixedClassNames('dropdown-trigger'), children: jsxRuntime.jsxs("button", { ref: triggerRef, className: buttonClass, "aria-haspopup": "true", "aria-controls": id ? `${id}-menu` : undefined, "aria-expanded": active, onClick: handleToggle, onKeyDown: handleTriggerKeyDown, disabled: disabled, type: "button", children: [jsxRuntime.jsx("span", { children: label }), jsxRuntime.jsx("span", { className: usePrefixedClassNames('icon', 'is-small'), "aria-hidden": "true", children: jsxRuntime.jsx("i", { className: "fas fa-angle-down" }) })] }) }), jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('dropdown-menu'), menuClassName), id: id ? `${id}-menu` : undefined, role: "menu", "data-testid": "dropdown-menu", onKeyDown: handleMenuKeyDown, children: jsxRuntime.jsx("div", { className: usePrefixedClassNames('dropdown-content'), onClick: handleMenuClick, tabIndex: -1, children: children }) })] }));
1955
- };
1986
+ return (jsxRuntime.jsxs("div", { className: dropdownClasses, ref: setRefs, id: id, "data-testid": "dropdown-root", ...rest, children: [jsxRuntime.jsx("div", { className: usePrefixedClassNames('dropdown-trigger'), children: jsxRuntime.jsxs("button", { ref: triggerRef, className: buttonClass, "aria-haspopup": "true", "aria-controls": id ? `${id}-menu` : undefined, "aria-expanded": active, onClick: handleToggle, onKeyDown: handleTriggerKeyDown, disabled: disabled, type: "button", children: [jsxRuntime.jsx("span", { children: label }), jsxRuntime.jsx("span", { className: usePrefixedClassNames('icon', 'is-small'), "aria-hidden": "true", children: jsxRuntime.jsx("i", { className: "fas fa-angle-down" }) })] }) }), jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('dropdown-menu'), menuClassName), id: id ? `${id}-menu` : undefined, role: "menu", "data-testid": "dropdown-menu", onKeyDown: handleMenuKeyDown, children: jsxRuntime.jsx("div", { className: usePrefixedClassNames('dropdown-content'), onClick: handleMenuClick, tabIndex: -1, children: children }) })] }));
1987
+ });
1956
1988
  /**
1957
1989
  * Bulma Dropdown item.
1958
1990
  *
@@ -2108,6 +2140,105 @@ const Message = withSubComponents(MessageComponent, {
2108
2140
  Body: MessageBody,
2109
2141
  }, 'Message');
2110
2142
 
2143
+ // Ref-counted across every caller (e.g. a Dialog rendering its own Modal)
2144
+ // so overlapping/nested locks don't fight over which one restores `overflow`.
2145
+ // Every overlay that locks body scroll — Modal, Dialog, Sidebar, Loading —
2146
+ // goes through here; a component setting `document.body.style.overflow`
2147
+ // itself would clear a lock another overlay still needs.
2148
+ let lockCount = 0;
2149
+ let originalOverflow = '';
2150
+ /**
2151
+ * Locks (and ref-counted-ly unlocks) `document.body` scrolling while `active`
2152
+ * is true. Safe to call from multiple components at once — the body scroll
2153
+ * is only restored once every active caller has released its lock.
2154
+ *
2155
+ * @function useScrollLock
2156
+ * @param active - Whether this caller wants the body scroll locked.
2157
+ */
2158
+ function useScrollLock(active) {
2159
+ React.useEffect(() => {
2160
+ if (!active)
2161
+ return undefined;
2162
+ lockCount++;
2163
+ if (lockCount === 1) {
2164
+ originalOverflow = document.body.style.overflow;
2165
+ document.body.style.overflow = 'hidden';
2166
+ }
2167
+ return () => {
2168
+ lockCount--;
2169
+ if (lockCount === 0) {
2170
+ document.body.style.overflow = originalOverflow;
2171
+ }
2172
+ };
2173
+ }, [active]);
2174
+ }
2175
+
2176
+ /**
2177
+ * Resolves a portal target: an `HTMLElement` is used directly, a non-empty
2178
+ * `string` is treated as a `document.querySelector` selector (falling back to
2179
+ * `document.body` when it matches nothing), and any falsy value — `undefined`
2180
+ * or the empty string — resolves to `document.body`.
2181
+ *
2182
+ * The empty string is deliberately treated as "no target" rather than passed
2183
+ * through: `document.querySelector('')` throws a `SyntaxError`, and callers
2184
+ * building a selector from state can easily hand us `''`.
2185
+ *
2186
+ * @function resolvePortalContainer
2187
+ * @param container - The requested portal target, if any.
2188
+ * @returns The resolved DOM node to portal into.
2189
+ */
2190
+ function resolvePortalContainer(container) {
2191
+ if (!container) {
2192
+ return document.body;
2193
+ }
2194
+ if (typeof container === 'string') {
2195
+ return (document.querySelector(container) ?? document.body);
2196
+ }
2197
+ return container;
2198
+ }
2199
+
2200
+ // "Are we past hydration?" as a store that never changes: React reads the
2201
+ // server snapshot both while server-rendering and while hydrating, and the
2202
+ // client snapshot from the first post-hydration render onward.
2203
+ const subscribeToNothing = () => () => { };
2204
+ const getClientSnapshot = () => true;
2205
+ const getServerSnapshot = () => false;
2206
+ /**
2207
+ * Reports whether the component has passed hydration.
2208
+ *
2209
+ * Returns `false` during server rendering and during the hydrating client
2210
+ * render, then `true` from the commit that follows. Use it to defer
2211
+ * DOM-only behaviour (such as portalling) past hydration so the first client
2212
+ * render matches the server markup instead of tripping hydration recovery.
2213
+ *
2214
+ * @function useIsHydrated
2215
+ * @returns `true` once the hydrating render has completed, otherwise `false`.
2216
+ */
2217
+ function useIsHydrated() {
2218
+ return React.useSyncExternalStore(subscribeToNothing, getClientSnapshot, getServerSnapshot);
2219
+ }
2220
+
2221
+ /**
2222
+ * Every currently active modal, in the order they opened. Only the last entry
2223
+ * — the topmost modal — reacts to Escape and Tab, so one keypress can't
2224
+ * dismiss a whole stack (e.g. a `Dialog` opened on top of a `Modal`).
2225
+ */
2226
+ const activeModalStack = [];
2227
+ /**
2228
+ * Controls the modal can hand focus to. Disabled and hidden controls are
2229
+ * excluded because `focus()` on them is a no-op, which would leave focus
2230
+ * outside the modal.
2231
+ */
2232
+ const FOCUSABLE_SELECTOR$1 = 'button:not(:disabled), [href], input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), [tabindex]';
2233
+ /**
2234
+ * The modal's tab stops, in document order. The selector alone is not the
2235
+ * tabbable set — `[href]` and `button` match regardless of `tabindex`, so an
2236
+ * `<a href tabIndex={-1}>` would otherwise be treated as a tab stop and let
2237
+ * Tab escape the modal when it sorts last. `el.tabIndex` is the browser's own
2238
+ * resolved value, so filtering on it drops every negative index (not just
2239
+ * `-1`) without a second selector to keep in sync.
2240
+ */
2241
+ const getTabbable = (node) => Array.from(node.querySelectorAll(FOCUSABLE_SELECTOR$1)).filter(el => el.tabIndex >= 0);
2111
2242
  /**
2112
2243
  * Modal.Background - Renders the modal background overlay.
2113
2244
  *
@@ -2238,7 +2369,7 @@ const ModalClose = ({ className, size = 'large', variant = 'delete', ...props })
2238
2369
  *
2239
2370
  * @see {@link https://bulma.io/documentation/components/modal/ | Bulma Modal documentation}
2240
2371
  */
2241
- const ModalRoot = ({ active, isActive, onClose, className, textColor, bgColor, modalCardTitle, modalCardFoot, type, children, ...props }) => {
2372
+ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClose, className, textColor, bgColor, modalCardTitle, modalCardFoot, type, children, closeOnEscape = true, lockScroll = true, portal = false, role, 'aria-modal': ariaModalProp, ...props }, ref) {
2242
2373
  const { classPrefix } = useConfig();
2243
2374
  const { bulmaHelperClasses, rest } = useBulmaClasses({
2244
2375
  color: textColor,
@@ -2247,6 +2378,133 @@ const ModalRoot = ({ active, isActive, onClose, className, textColor, bgColor, m
2247
2378
  });
2248
2379
  // Support both active and isActive props
2249
2380
  const isModalActive = active ?? isActive ?? false;
2381
+ const modalRootRef = React.useRef(null);
2382
+ // Cleanup returned by a consumer's callback ref on attach, held until detach.
2383
+ const consumerCleanupRef = React.useRef(null);
2384
+ // The forwarded ref and the internal one must both see the node: focus
2385
+ // management, the scroll lock and the topmost-modal check all read
2386
+ // `modalRootRef`, while consumers expect their own ref to resolve.
2387
+ const combinedRef = React.useCallback((node) => {
2388
+ modalRootRef.current =
2389
+ node;
2390
+ if (typeof ref === 'function') {
2391
+ // React 19 lets a callback ref return a cleanup function and detaches
2392
+ // by running it instead of calling the ref with `null`; React 18
2393
+ // discards the return value entirely. Returning it from here would be
2394
+ // a React-19-only contract, so instead we hold the cleanup and run it
2395
+ // ourselves on detach — a consumer's cleanup ref then behaves the same
2396
+ // on both majors of the CI matrix. Same shape as `Dropdown`.
2397
+ if (node === null) {
2398
+ const consumerCleanup = consumerCleanupRef.current;
2399
+ consumerCleanupRef.current = null;
2400
+ if (consumerCleanup) {
2401
+ consumerCleanup();
2402
+ }
2403
+ else {
2404
+ ref(null);
2405
+ }
2406
+ return;
2407
+ }
2408
+ const cleanup = ref(node);
2409
+ consumerCleanupRef.current =
2410
+ typeof cleanup === 'function' ? cleanup : null;
2411
+ }
2412
+ else if (ref) {
2413
+ ref.current = node;
2414
+ }
2415
+ }, [ref]);
2416
+ const previouslyFocusedRef = React.useRef(null);
2417
+ const stackTokenRef = React.useRef({});
2418
+ const generatedTitleId = React.useId();
2419
+ // A portal has no server-rendered counterpart, so the server render and the
2420
+ // hydrating render have to stay inline and only move into the portal once
2421
+ // we're past hydration.
2422
+ const onClient = useIsHydrated();
2423
+ // Moving into the portal remounts the modal's subtree, so any effect holding
2424
+ // a DOM node from before the move is holding a detached one. Effects that
2425
+ // touch the modal's nodes key on this so they re-run against the new tree.
2426
+ const isPortaled = Boolean(portal) && onClient;
2427
+ useScrollLock(isModalActive && lockScroll);
2428
+ // Record open order so keyboard handling only applies to the topmost modal.
2429
+ React.useEffect(() => {
2430
+ if (!isModalActive)
2431
+ return undefined;
2432
+ const token = stackTokenRef.current;
2433
+ activeModalStack.push(token);
2434
+ return () => {
2435
+ const index = activeModalStack.indexOf(token);
2436
+ if (index !== -1)
2437
+ activeModalStack.splice(index, 1);
2438
+ };
2439
+ }, [isModalActive]);
2440
+ // Escape closes; Tab cycles inside the modal. Both only for the topmost one.
2441
+ React.useEffect(() => {
2442
+ if (!isModalActive)
2443
+ return undefined;
2444
+ const handleKeyDown = (e) => {
2445
+ const node = modalRootRef.current;
2446
+ if (activeModalStack[activeModalStack.length - 1] !== stackTokenRef.current) {
2447
+ return;
2448
+ }
2449
+ if (e.key === 'Escape') {
2450
+ if (closeOnEscape)
2451
+ onClose?.();
2452
+ return;
2453
+ }
2454
+ if (e.key !== 'Tab' || !node)
2455
+ return;
2456
+ // Keep Tab within the modal — `aria-modal` hides the rest of the page
2457
+ // from assistive technology, so the keyboard order has to agree.
2458
+ const focusable = getTabbable(node);
2459
+ const activeElement = document.activeElement;
2460
+ if (focusable.length === 0) {
2461
+ e.preventDefault();
2462
+ node.focus();
2463
+ return;
2464
+ }
2465
+ const first = focusable[0];
2466
+ const last = focusable[focusable.length - 1];
2467
+ const outside = !node.contains(activeElement);
2468
+ if (e.shiftKey) {
2469
+ if (outside || activeElement === first) {
2470
+ e.preventDefault();
2471
+ last.focus();
2472
+ }
2473
+ }
2474
+ else if (outside || activeElement === last) {
2475
+ e.preventDefault();
2476
+ first.focus();
2477
+ }
2478
+ };
2479
+ document.addEventListener('keydown', handleKeyDown);
2480
+ return () => document.removeEventListener('keydown', handleKeyDown);
2481
+ }, [isModalActive, closeOnEscape, onClose]);
2482
+ // Move focus into the modal on open, restore it on close
2483
+ React.useEffect(() => {
2484
+ if (!isModalActive)
2485
+ return undefined;
2486
+ previouslyFocusedRef.current = document.activeElement;
2487
+ const node = modalRootRef.current;
2488
+ const focusable = node ? getTabbable(node)[0] : undefined;
2489
+ (focusable ?? node)?.focus();
2490
+ return () => {
2491
+ // Only hand focus back if this modal still owns it: closing a background
2492
+ // modal must not pull focus out of one that is still open on top. A
2493
+ // removed subtree leaves focus on <body>, which still counts as ours.
2494
+ const activeElement = document.activeElement;
2495
+ if (activeElement &&
2496
+ activeElement !== document.body &&
2497
+ !node?.contains(activeElement)) {
2498
+ return;
2499
+ }
2500
+ previouslyFocusedRef.current?.focus?.();
2501
+ };
2502
+ // `isPortaled` flips once when a hydrated portal modal moves out of the
2503
+ // inline tree; re-running rebinds focus onto the remounted nodes. The
2504
+ // cleanup above restores focus to the pre-open element first (the detached
2505
+ // subtree has left focus on <body>), so the re-run re-records the same
2506
+ // element and restore-on-close still lands in the right place.
2507
+ }, [isModalActive, isPortaled]);
2250
2508
  // Check if children contain compound components
2251
2509
  const hasCompoundComponents = React.Children.toArray(children).some(child => React.isValidElement(child) &&
2252
2510
  (child.type === ModalBackground ||
@@ -2259,20 +2517,35 @@ const ModalRoot = ({ active, isActive, onClose, className, textColor, bgColor, m
2259
2517
  });
2260
2518
  const deleteClass = usePrefixedClassNames('delete');
2261
2519
  const modalClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
2520
+ const resolvedRole = role ?? (isModalActive ? 'dialog' : undefined);
2521
+ const resolvedAriaModal = ariaModalProp ??
2522
+ (isModalActive && resolvedRole !== 'presentation' ? 'true' : undefined);
2523
+ // A dialog needs an accessible name: the legacy card title is wired up
2524
+ // automatically, compound users pass their own aria-label/aria-labelledby
2525
+ // (both arrive in `rest`, which is spread last and so wins).
2526
+ const titleId = modalCardTitle ? generatedTitleId : undefined;
2527
+ let modalElement;
2262
2528
  // If using compound components, render children as-is
2263
2529
  if (hasCompoundComponents) {
2264
- return (jsxRuntime.jsx("div", { className: modalClasses, ...rest, "data-testid": "modal", children: children }));
2265
- }
2266
- // Legacy API: EXPLICIT type wins; fallback to auto detection if not provided
2267
- let isModalCard;
2268
- if (type === 'card')
2269
- isModalCard = true;
2270
- else if (type === 'content')
2271
- isModalCard = false;
2272
- else
2273
- isModalCard = !!modalCardTitle || !!modalCardFoot;
2274
- return (jsxRuntime.jsxs("div", { className: modalClasses, ...rest, "data-testid": "modal", children: [jsxRuntime.jsx("div", { className: prefixedClassNames(classPrefix, 'modal-background'), onClick: onClose, "data-testid": "modal-background" }), isModalCard ? (jsxRuntime.jsxs("div", { className: prefixedClassNames(classPrefix, 'modal-card'), children: [modalCardTitle && (jsxRuntime.jsxs("header", { className: prefixedClassNames(classPrefix, 'modal-card-head'), children: [jsxRuntime.jsx("p", { className: prefixedClassNames(classPrefix, 'modal-card-title'), children: modalCardTitle }), onClose && (jsxRuntime.jsx("button", { className: deleteClass, "aria-label": "close", onClick: onClose, type: "button", "data-testid": "modal-close" }))] })), jsxRuntime.jsx("section", { className: prefixedClassNames(classPrefix, 'modal-card-body'), "data-testid": "modal-body", children: children }), modalCardFoot && (jsxRuntime.jsx("footer", { className: prefixedClassNames(classPrefix, 'modal-card-foot'), children: modalCardFoot }))] })) : (jsxRuntime.jsx("div", { className: prefixedClassNames(classPrefix, 'modal-content'), "data-testid": "modal-content", children: children })), (!isModalCard || (!modalCardTitle && onClose)) && onClose && (jsxRuntime.jsx("button", { className: prefixedClassNames(classPrefix, 'modal-close', 'is-large'), "aria-label": "close", onClick: onClose, type: "button", "data-testid": "modal-close-float" }))] }));
2275
- };
2530
+ modalElement = (jsxRuntime.jsx("div", { className: modalClasses, ref: combinedRef, role: resolvedRole, "aria-modal": resolvedAriaModal, tabIndex: -1, ...rest, "data-testid": "modal", children: children }));
2531
+ }
2532
+ else {
2533
+ // Legacy API: EXPLICIT type wins; fallback to auto detection if not provided
2534
+ let isModalCard;
2535
+ if (type === 'card')
2536
+ isModalCard = true;
2537
+ else if (type === 'content')
2538
+ isModalCard = false;
2539
+ else
2540
+ isModalCard = !!modalCardTitle || !!modalCardFoot;
2541
+ modalElement = (jsxRuntime.jsxs("div", { className: modalClasses, ref: combinedRef, role: resolvedRole, "aria-modal": resolvedAriaModal, "aria-labelledby": titleId, tabIndex: -1, ...rest, "data-testid": "modal", children: [jsxRuntime.jsx("div", { className: prefixedClassNames(classPrefix, 'modal-background'), onClick: onClose, "data-testid": "modal-background" }), isModalCard ? (jsxRuntime.jsxs("div", { className: prefixedClassNames(classPrefix, 'modal-card'), children: [modalCardTitle && (jsxRuntime.jsxs("header", { className: prefixedClassNames(classPrefix, 'modal-card-head'), children: [jsxRuntime.jsx("p", { id: titleId, className: prefixedClassNames(classPrefix, 'modal-card-title'), children: modalCardTitle }), onClose && (jsxRuntime.jsx("button", { className: deleteClass, "aria-label": "close", onClick: onClose, type: "button", "data-testid": "modal-close" }))] })), jsxRuntime.jsx("section", { className: prefixedClassNames(classPrefix, 'modal-card-body'), "data-testid": "modal-body", children: children }), modalCardFoot && (jsxRuntime.jsx("footer", { className: prefixedClassNames(classPrefix, 'modal-card-foot'), children: modalCardFoot }))] })) : (jsxRuntime.jsx("div", { className: prefixedClassNames(classPrefix, 'modal-content'), "data-testid": "modal-content", children: children })), (!isModalCard || (!modalCardTitle && onClose)) && onClose && (jsxRuntime.jsx("button", { className: prefixedClassNames(classPrefix, 'modal-close', 'is-large'), "aria-label": "close", onClick: onClose, type: "button", "data-testid": "modal-close-float" }))] }));
2542
+ }
2543
+ if (isPortaled) {
2544
+ const target = resolvePortalContainer(typeof portal === 'boolean' ? undefined : portal);
2545
+ return reactDom.createPortal(modalElement, target);
2546
+ }
2547
+ return modalElement;
2548
+ });
2276
2549
  const Modal = withSubComponents(ModalRoot, {
2277
2550
  Background: ModalBackground,
2278
2551
  Content: ModalContent,
@@ -2286,10 +2559,11 @@ const NavbarDropdownContext = React.createContext(null);
2286
2559
  *
2287
2560
  * @function
2288
2561
  * @param {NavbarProps} props - Props for the Navbar component.
2562
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the root `<nav>` element.
2289
2563
  * @returns {JSX.Element} The rendered navbar.
2290
2564
  * @see {@link https://bulma.io/documentation/components/navbar/ | Bulma Navbar documentation}
2291
2565
  */
2292
- const NavbarComponent = ({ className, textColor, bgColor, color, transparent, fixed, children, ...props }) => {
2566
+ const NavbarComponent = React.forwardRef(function NavbarComponent({ className, textColor, bgColor, color, transparent, fixed, children, ...props }, ref) {
2293
2567
  const { bulmaHelperClasses, rest } = useBulmaClasses({
2294
2568
  color: textColor,
2295
2569
  backgroundColor: bgColor,
@@ -2302,8 +2576,8 @@ const NavbarComponent = ({ className, textColor, bgColor, color, transparent, fi
2302
2576
  [`is-fixed-${fixed}`]: fixed,
2303
2577
  });
2304
2578
  const navbarClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
2305
- return (jsxRuntime.jsx("nav", { className: navbarClasses, role: "navigation", "aria-label": "main navigation", ...rest, children: children }));
2306
- };
2579
+ return (jsxRuntime.jsx("nav", { ref: ref, className: navbarClasses, role: "navigation", "aria-label": "main navigation", ...rest, children: children }));
2580
+ });
2307
2581
  /**
2308
2582
  * For logo and branding (left side)
2309
2583
  *
@@ -2340,16 +2614,18 @@ const NavbarItem = ({ className, as: Component = 'a', active, textColor, bgColor
2340
2614
  *
2341
2615
  * @function
2342
2616
  * @param {NavbarBurgerProps} props - Props for the NavbarBurger component.
2617
+ * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the burger button element.
2343
2618
  * @returns {JSX.Element} The rendered burger.
2344
2619
  */
2345
- const NavbarBurger = ({ className, active, children, ...props }) => {
2620
+ const NavbarBurger = React.forwardRef(function NavbarBurger({ className, active, children, ...props }, ref) {
2346
2621
  const { bulmaHelperClasses, rest } = useBulmaClasses({
2347
2622
  ...props,
2348
2623
  });
2349
- return (jsxRuntime.jsxs("button", { type: "button", className: classNames(usePrefixedClassNames('navbar-burger', {
2624
+ return (jsxRuntime.jsxs("button", { ref: ref, type: "button", className: classNames(usePrefixedClassNames('navbar-burger', {
2350
2625
  'is-active': active,
2351
2626
  }), bulmaHelperClasses, className), "aria-label": props['aria-label'] || 'menu', "aria-expanded": props['aria-expanded'] ?? !!active, ...rest, children: [jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), children] }));
2352
- };
2627
+ });
2628
+ NavbarBurger.displayName = 'NavbarBurger';
2353
2629
  /**
2354
2630
  * Collapsible content (contains `Navbar.Start` and `Navbar.End`)
2355
2631
  *
@@ -2396,9 +2672,10 @@ const NavbarEnd = ({ className, children, ...props }) => {
2396
2672
  *
2397
2673
  * @function
2398
2674
  * @param {NavbarLinkProps} props - Props for the NavbarLink component.
2675
+ * @param {React.Ref<HTMLAnchorElement | HTMLButtonElement>} ref - Forwarded ref to the rendered link or button element.
2399
2676
  * @returns {JSX.Element} The rendered navbar link.
2400
2677
  */
2401
- const NavbarLink = ({ className, as: Component = 'a', arrowless, textColor, bgColor, children, ...props }) => {
2678
+ const NavbarLink = React.forwardRef(function NavbarLink({ className, as: Component = 'a', arrowless, textColor, bgColor, children, ...props }, ref) {
2402
2679
  const { bulmaHelperClasses, rest } = useBulmaClasses({
2403
2680
  color: textColor,
2404
2681
  backgroundColor: bgColor,
@@ -2431,7 +2708,7 @@ const NavbarLink = ({ className, as: Component = 'a', arrowless, textColor, bgCo
2431
2708
  return;
2432
2709
  dropdownContext.toggle();
2433
2710
  };
2434
- return (jsxRuntime.jsx(Component, { className: classNames(usePrefixedClassNames('navbar-link', {
2711
+ return (jsxRuntime.jsx(Component, { ref: ref, className: classNames(usePrefixedClassNames('navbar-link', {
2435
2712
  'is-arrowless': arrowless,
2436
2713
  }), bulmaHelperClasses, className), ...rest, ...(dropdownContext && {
2437
2714
  'aria-haspopup': 'true',
@@ -2443,15 +2720,17 @@ const NavbarLink = ({ className, as: Component = 'a', arrowless, textColor, bgCo
2443
2720
  onClick: handleClick,
2444
2721
  }),
2445
2722
  }), children: children }));
2446
- };
2723
+ });
2724
+ NavbarLink.displayName = 'NavbarLink';
2447
2725
  /**
2448
2726
  * Dropdown parent (with options for hover, up, right, active)
2449
2727
  *
2450
2728
  * @function
2451
2729
  * @param {NavbarDropdownProps} props - Props for the NavbarDropdown component.
2730
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the dropdown container element.
2452
2731
  * @returns {JSX.Element} The rendered dropdown.
2453
2732
  */
2454
- const NavbarDropdown = ({ className, right, up, hoverable, active: activeProp, onActiveChange, children, ...props }) => {
2733
+ const NavbarDropdown = React.forwardRef(function NavbarDropdown({ className, right, up, hoverable, active: activeProp, onActiveChange, children, ...props }, ref) {
2455
2734
  const [active, setActive] = React.useState(!!activeProp);
2456
2735
  React.useEffect(() => {
2457
2736
  // eslint-disable-next-line react-hooks/set-state-in-effect -- syncing to controlled prop
@@ -2472,13 +2751,14 @@ const NavbarDropdown = ({ className, right, up, hoverable, active: activeProp, o
2472
2751
  setActive(false);
2473
2752
  onActiveChange?.(false);
2474
2753
  };
2475
- return (jsxRuntime.jsx(NavbarDropdownContext.Provider, { value: { active, toggle, close }, children: jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('navbar-item', 'has-dropdown', {
2754
+ return (jsxRuntime.jsx(NavbarDropdownContext.Provider, { value: { active, toggle, close }, children: jsxRuntime.jsx("div", { ref: ref, className: classNames(usePrefixedClassNames('navbar-item', 'has-dropdown', {
2476
2755
  'has-dropdown-up': up,
2477
2756
  'is-right': right,
2478
2757
  'is-hoverable': hoverable,
2479
2758
  'is-active': active,
2480
2759
  }), className), ...props, children: children }) }));
2481
- };
2760
+ });
2761
+ NavbarDropdown.displayName = 'NavbarDropdown';
2482
2762
  /**
2483
2763
  * Dropdown menu container
2484
2764
  *
@@ -3248,16 +3528,10 @@ const Loading = ({ active = false, isFullPage = false, size, color, canCancel =
3248
3528
  document.addEventListener('keydown', handleKeyDown);
3249
3529
  return () => document.removeEventListener('keydown', handleKeyDown);
3250
3530
  }, [active, canCancel, onCancel]);
3251
- // Prevent body scroll when full page loading is active
3252
- React.useEffect(() => {
3253
- if (isFullPage && active) {
3254
- document.body.style.overflow = 'hidden';
3255
- return () => {
3256
- document.body.style.overflow = '';
3257
- };
3258
- }
3259
- return undefined;
3260
- }, [isFullPage, active]);
3531
+ // Prevent body scroll when full page loading is active. Ref-counted through
3532
+ // the shared helper so an overlapping Modal/Dialog/Sidebar doesn't have its
3533
+ // lock cleared when this one releases.
3534
+ useScrollLock(isFullPage && active);
3261
3535
  if (!active) {
3262
3536
  return null;
3263
3537
  }
@@ -3748,6 +4022,8 @@ const Steps = withSubComponents(StepsComponent, { Step }, 'Steps');
3748
4022
  const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left', width = '260px', isFullwidth, fullWidth, overlay = true, overlayClose = true, escapeClose = true, canCancel = true, children, inline = false, className, style, ...props }, ref) => {
3749
4023
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
3750
4024
  const sidebarRef = React.useRef(null);
4025
+ // Cleanup returned by a consumer's callback ref on attach, held until detach.
4026
+ const consumerCleanupRef = React.useRef(null);
3751
4027
  const resolvedFullwidth = isFullwidth ?? fullWidth ?? false;
3752
4028
  // Close handler
3753
4029
  const handleClose = React.useCallback(() => {
@@ -3773,17 +4049,9 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
3773
4049
  document.addEventListener('keydown', handleKeyDown);
3774
4050
  return () => document.removeEventListener('keydown', handleKeyDown);
3775
4051
  }, [isOpen, escapeClose, handleClose]);
3776
- // Prevent body scroll when sidebar is open
3777
- React.useEffect(() => {
3778
- if (isOpen && overlay) {
3779
- const originalOverflow = document.body.style.overflow;
3780
- document.body.style.overflow = 'hidden';
3781
- return () => {
3782
- document.body.style.overflow = originalOverflow;
3783
- };
3784
- }
3785
- return undefined;
3786
- }, [isOpen, overlay]);
4052
+ // Prevent body scroll when sidebar is open. Ref-counted through the shared
4053
+ // helper so an overlapping Modal/Dialog/Loading doesn't unlock underneath.
4054
+ useScrollLock(isOpen && overlay);
3787
4055
  // Focus trap (basic - focus sidebar when opened)
3788
4056
  React.useEffect(() => {
3789
4057
  if (isOpen) {
@@ -3805,7 +4073,28 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
3805
4073
  sidebarRef.current =
3806
4074
  node;
3807
4075
  if (typeof ref === 'function') {
3808
- ref(node);
4076
+ // React 19 lets a callback ref return a cleanup function and detaches by
4077
+ // running it instead of calling the ref with `null`; React 18 discards the
4078
+ // return value entirely. Returning it from here would be a React-19-only
4079
+ // contract, so we hold the cleanup and run it ourselves on detach — the
4080
+ // same shape as `Dropdown`, `Modal`, `Dialog`, `Toast` and `Carousel`.
4081
+ // The form controls that merge a forwarded ref still discard the cleanup
4082
+ // and detach with `ref(null)`, so this is not yet library-wide — grep
4083
+ // `typeof ref === 'function'` for the current split.
4084
+ if (node === null) {
4085
+ const consumerCleanup = consumerCleanupRef.current;
4086
+ consumerCleanupRef.current = null;
4087
+ if (consumerCleanup) {
4088
+ consumerCleanup();
4089
+ }
4090
+ else {
4091
+ ref(null);
4092
+ }
4093
+ return;
4094
+ }
4095
+ const cleanup = ref(node);
4096
+ consumerCleanupRef.current =
4097
+ typeof cleanup === 'function' ? cleanup : null;
3809
4098
  }
3810
4099
  else if (ref) {
3811
4100
  ref.current = node;
@@ -3922,6 +4211,8 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
3922
4211
  const [isVisible, setIsVisible] = React.useState(true);
3923
4212
  const [isPaused, setIsPaused] = React.useState(false);
3924
4213
  const toastRef = React.useRef(null);
4214
+ // Cleanup returned by a consumer's callback ref on attach, held until detach.
4215
+ const consumerCleanupRef = React.useRef(null);
3925
4216
  const handleClose = React.useCallback(() => {
3926
4217
  setIsVisible(false);
3927
4218
  onClose?.();
@@ -3992,27 +4283,41 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
3992
4283
  const toastActionClass = usePrefixedClassNames('toast-action');
3993
4284
  const toastButtonClass = usePrefixedClassNames('button');
3994
4285
  const toastCloseClass = usePrefixedClassNames('delete', 'is-small');
3995
- if (!isVisible) {
3996
- return null;
3997
- }
3998
- const resolveContainer = () => {
3999
- if (container) {
4000
- if (typeof container === 'string') {
4001
- return (document.querySelector(container) || document.body);
4002
- }
4003
- return container;
4004
- }
4005
- return document.body;
4006
- };
4007
- const setRef = (node) => {
4286
+ const setRef = React.useCallback((node) => {
4008
4287
  toastRef.current = node;
4009
4288
  if (typeof ref === 'function') {
4010
- ref(node);
4289
+ // React 19 lets a callback ref return a cleanup function and detaches by
4290
+ // running it instead of calling the ref with `null`; React 18 discards the
4291
+ // return value entirely. Returning it from here would be a React-19-only
4292
+ // contract, so we hold the cleanup and run it ourselves on detach — the
4293
+ // same shape as `Dropdown`, `Modal`, `Dialog`, `Sidebar` and `Carousel`.
4294
+ // The form controls that merge a forwarded ref still discard the cleanup
4295
+ // and detach with `ref(null)`, so this is not yet library-wide — grep
4296
+ // `typeof ref === 'function'` for the current split. Memoized because an
4297
+ // unstable ref identity makes React detach and re-attach on every render,
4298
+ // which would run that cleanup each time.
4299
+ if (node === null) {
4300
+ const consumerCleanup = consumerCleanupRef.current;
4301
+ consumerCleanupRef.current = null;
4302
+ if (consumerCleanup) {
4303
+ consumerCleanup();
4304
+ }
4305
+ else {
4306
+ ref(null);
4307
+ }
4308
+ return;
4309
+ }
4310
+ const cleanup = ref(node);
4311
+ consumerCleanupRef.current =
4312
+ typeof cleanup === 'function' ? cleanup : null;
4011
4313
  }
4012
4314
  else if (ref) {
4013
4315
  ref.current = node;
4014
4316
  }
4015
- };
4317
+ }, [ref]);
4318
+ if (!isVisible) {
4319
+ return null;
4320
+ }
4016
4321
  const toastElement = (jsxRuntime.jsxs("div", { ref: setRef, className: combinedClasses, role: "alert", "aria-live": type === 'danger' || type === 'warning' ? 'assertive' : 'polite', onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onClick: dismissible ? handleClose : undefined, ...rest, children: [jsxRuntime.jsx("span", { className: toastMessageClass, children: message }), (cancelText || actionText) && (jsxRuntime.jsxs("div", { className: toastActionsClass, children: [cancelText && (jsxRuntime.jsx("span", { className: toastCancelClass, children: jsxRuntime.jsx("button", { type: "button", className: toastButtonClass, onClick: e => {
4017
4322
  e.stopPropagation();
4018
4323
  handleClose();
@@ -4028,7 +4333,7 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
4028
4333
  }
4029
4334
  const toastContent = jsxRuntime.jsx("div", { className: containerClasses, children: toastElement });
4030
4335
  if (typeof document !== 'undefined') {
4031
- return reactDom.createPortal(toastContent, resolveContainer());
4336
+ return reactDom.createPortal(toastContent, resolvePortalContainer(container));
4032
4337
  }
4033
4338
  return null;
4034
4339
  });
@@ -4168,9 +4473,6 @@ const ToastContainer = ({ position = 'top-right', }) => {
4168
4473
  }) }), document.body);
4169
4474
  };
4170
4475
 
4171
- // Ref-counted body scroll lock for chained/overlapping dialogs
4172
- let _scrollLockCount = 0;
4173
- let _originalOverflow = '';
4174
4476
  /**
4175
4477
  * The `Dialog` component provides ready-made confirm and alert dialogs, so a destructive action stays one `await dialog.confirm()` call away.
4176
4478
  *
@@ -4202,9 +4504,11 @@ let _originalOverflow = '';
4202
4504
  * onCancel={() => setShowConfirm(false)}
4203
4505
  * />
4204
4506
  */
4205
- const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', confirmText = 'OK', cancelText = 'Cancel', onConfirm, onCancel, showCancel = true, canCancel = true, focusCancel = false, icon, className, ...props }, ref) => {
4507
+ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', confirmText = 'OK', cancelText = 'Cancel', onConfirm, onCancel, showCancel = true, canCancel = true, focusCancel = false, icon, portal, className, ...props }, ref) => {
4206
4508
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
4207
4509
  const dialogRef = React.useRef(null);
4510
+ // Cleanup returned by a consumer's callback ref on attach, held until detach.
4511
+ const consumerCleanupRef = React.useRef(null);
4208
4512
  const confirmRef = React.useRef(null);
4209
4513
  const cancelRef = React.useRef(null);
4210
4514
  // Handle cancel
@@ -4223,48 +4527,47 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
4223
4527
  handleCancel();
4224
4528
  }
4225
4529
  }, [canCancel, handleCancel]);
4226
- // Handle escape key
4227
- React.useEffect(() => {
4228
- if (!isOpen || !canCancel)
4229
- return undefined;
4230
- const handleKeyDown = (e) => {
4231
- if (e.key === 'Escape') {
4232
- handleCancel();
4233
- }
4234
- };
4235
- document.addEventListener('keydown', handleKeyDown);
4236
- return () => document.removeEventListener('keydown', handleKeyDown);
4237
- }, [isOpen, canCancel, handleCancel]);
4238
- // Focus management
4530
+ // Moving into the portal remounts the Modal's subtree — these buttons
4531
+ // included — so the focus effect below has to re-run against the new
4532
+ // nodes, exactly as the Modal's own focus effect does.
4533
+ const isHydrated = useIsHydrated();
4534
+ const isPortaled = Boolean(portal) && isHydrated;
4535
+ // Focus management Dialog picks a specific button rather than the
4536
+ // Modal's default (first focusable/root); this effect runs after the
4537
+ // inner Modal's own focus-on-open effect, so it wins.
4239
4538
  React.useEffect(() => {
4240
4539
  if (isOpen) {
4241
4540
  const buttonToFocus = focusCancel && showCancel ? cancelRef.current : confirmRef.current;
4242
4541
  buttonToFocus?.focus();
4243
4542
  }
4244
- }, [isOpen, focusCancel, showCancel]);
4245
- // Prevent body scroll (ref-counted so chained dialogs work correctly)
4246
- React.useEffect(() => {
4247
- if (isOpen) {
4248
- _scrollLockCount++;
4249
- if (_scrollLockCount === 1) {
4250
- _originalOverflow = document.body.style.overflow;
4251
- document.body.style.overflow = 'hidden';
4252
- }
4253
- return () => {
4254
- _scrollLockCount--;
4255
- if (_scrollLockCount === 0) {
4256
- document.body.style.overflow = _originalOverflow;
4257
- }
4258
- };
4259
- }
4260
- return undefined;
4261
- }, [isOpen]);
4543
+ }, [isOpen, focusCancel, showCancel, isPortaled]);
4262
4544
  // Use combined ref
4263
4545
  const combinedRef = React.useCallback((node) => {
4264
4546
  dialogRef.current =
4265
4547
  node;
4266
4548
  if (typeof ref === 'function') {
4267
- ref(node);
4549
+ // React 19 lets a callback ref return a cleanup function and detaches by
4550
+ // running it instead of calling the ref with `null`; React 18 discards the
4551
+ // return value entirely. Returning it from here would be a React-19-only
4552
+ // contract, so we hold the cleanup and run it ourselves on detach — the
4553
+ // same shape as `Dropdown`, `Modal`, `Sidebar`, `Toast` and `Carousel`.
4554
+ // The form controls that merge a forwarded ref still discard the cleanup
4555
+ // and detach with `ref(null)`, so this is not yet library-wide — grep
4556
+ // `typeof ref === 'function'` for the current split.
4557
+ if (node === null) {
4558
+ const consumerCleanup = consumerCleanupRef.current;
4559
+ consumerCleanupRef.current = null;
4560
+ if (consumerCleanup) {
4561
+ consumerCleanup();
4562
+ }
4563
+ else {
4564
+ ref(null);
4565
+ }
4566
+ return;
4567
+ }
4568
+ const cleanup = ref(node);
4569
+ consumerCleanupRef.current =
4570
+ typeof cleanup === 'function' ? cleanup : null;
4268
4571
  }
4269
4572
  else if (ref) {
4270
4573
  ref.current = node;
@@ -4307,7 +4610,7 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
4307
4610
  if (!isOpen) {
4308
4611
  return null;
4309
4612
  }
4310
- return (jsxRuntime.jsxs(Modal, { isActive: isOpen, children: [jsxRuntime.jsx(Modal.Background, { onClick: handleBackgroundClick }), jsxRuntime.jsxs("div", { ref: combinedRef, className: combinedClasses, role: "alertdialog", "aria-modal": "true", "aria-labelledby": title ? 'dialog-title' : undefined, "aria-describedby": "dialog-message", ...rest, children: [title && (jsxRuntime.jsxs("div", { className: headerClass, children: [displayIcon && jsxRuntime.jsx("span", { className: iconClass, children: displayIcon }), jsxRuntime.jsx("h3", { id: "dialog-title", className: titleClass, children: title })] })), jsxRuntime.jsx("div", { id: "dialog-message", className: bodyClass, children: message }), jsxRuntime.jsxs("div", { className: footerClass, children: [showCancel && (jsxRuntime.jsx("button", { ref: cancelRef, type: "button", className: cancelButtonClass, onClick: handleCancel, children: cancelText })), jsxRuntime.jsx("button", { ref: confirmRef, type: "button", className: confirmButtonClass, onClick: handleConfirm, children: confirmText })] })] })] }));
4613
+ return (jsxRuntime.jsxs(Modal, { isActive: isOpen, onClose: handleCancel, role: "presentation", portal: portal, children: [jsxRuntime.jsx(Modal.Background, { onClick: handleBackgroundClick }), jsxRuntime.jsxs("div", { ref: combinedRef, className: combinedClasses, role: "alertdialog", "aria-modal": "true", "aria-labelledby": title ? 'dialog-title' : undefined, "aria-describedby": "dialog-message", ...rest, children: [title && (jsxRuntime.jsxs("div", { className: headerClass, children: [displayIcon && jsxRuntime.jsx("span", { className: iconClass, children: displayIcon }), jsxRuntime.jsx("h3", { id: "dialog-title", className: titleClass, children: title })] })), jsxRuntime.jsx("div", { id: "dialog-message", className: bodyClass, children: message }), jsxRuntime.jsxs("div", { className: footerClass, children: [showCancel && (jsxRuntime.jsx("button", { ref: cancelRef, type: "button", className: cancelButtonClass, onClick: handleCancel, children: cancelText })), jsxRuntime.jsx("button", { ref: confirmRef, type: "button", className: confirmButtonClass, onClick: handleConfirm, children: confirmText })] })] })] }));
4311
4614
  });
4312
4615
  Dialog.displayName = 'Dialog';
4313
4616
  let dialogListeners = new Set();
@@ -4390,10 +4693,11 @@ const validButtonColors = [...validColors, 'text', 'ghost'];
4390
4693
  *
4391
4694
  * @function
4392
4695
  * @param {ButtonProps} props - Props for the Button component.
4696
+ * @param {React.Ref<HTMLButtonElement | HTMLAnchorElement>} ref - Forwarded ref to the rendered button or anchor element.
4393
4697
  * @returns {JSX.Element} The rendered button or anchor element.
4394
4698
  * @see {@link https://bulma.io/documentation/elements/button/ | Bulma Button documentation}
4395
4699
  */
4396
- const Button = ({ color, size, isLight, isRounded, isLoading, isStatic, isFullwidth, isFullWidth, isOutlined, isInverted, isFocused, isActive, isHovered, isDisabled, className, children, textColor, bgColor, as: Component = 'button', href, onClick, target, rel, ...props }) => {
4700
+ const Button = React.forwardRef(function Button({ color, size, isLight, isRounded, isLoading, isStatic, isFullwidth, isFullWidth, isOutlined, isInverted, isFocused, isActive, isHovered, isDisabled, className, children, textColor, bgColor, as: Component = 'button', href, onClick, target, rel, ...props }, ref) {
4397
4701
  const { bulmaHelperClasses, rest } = useBulmaClasses({
4398
4702
  color: textColor,
4399
4703
  backgroundColor: bgColor,
@@ -4423,12 +4727,13 @@ const Button = ({ color, size, isLight, isRounded, isLoading, isStatic, isFullwi
4423
4727
  // native/custom link-like elements (an <a>, a router Link, ...) don't
4424
4728
  // receive button-only HTML attributes.
4425
4729
  const { type: _type, disabled: _disabled, form: _form, formAction: _formAction, formEncType: _formEncType, formMethod: _formMethod, formNoValidate: _formNoValidate, formTarget: _formTarget, name: _name, value: _value, autoFocus: _autoFocus, ...anchorRest } = rest;
4426
- return (jsxRuntime.jsx(Component, { className: buttonClasses, href: href, target: target, rel: rel, "aria-disabled": isDisabled, tabIndex: isDisabled ? -1 : undefined, onClick: isDisabled
4730
+ return (jsxRuntime.jsx(Component, { ref: ref, className: buttonClasses, href: href, target: target, rel: rel, "aria-disabled": isDisabled, tabIndex: isDisabled ? -1 : undefined, onClick: isDisabled
4427
4731
  ? (e) => e.preventDefault()
4428
4732
  : onClick, ...anchorRest, children: children }));
4429
4733
  }
4430
- return (jsxRuntime.jsx("button", { className: buttonClasses, disabled: isDisabled, onClick: onClick, ...rest, children: children }));
4431
- };
4734
+ return (jsxRuntime.jsx("button", { ref: ref, className: buttonClasses, disabled: isDisabled, onClick: onClick, ...rest, children: children }));
4735
+ });
4736
+ Button.displayName = 'Button';
4432
4737
 
4433
4738
  /**
4434
4739
  * Individual carousel item/slide.
@@ -4487,6 +4792,8 @@ const DefaultNextIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
4487
4792
  const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, interval = 5000, pauseOnHover = true, repeat = true, hasDrag = true, arrow = true, arrowHover = false, indicator = true, indicatorInside = false, indicatorPosition = 'bottom', indicatorStyle = 'dots', iconPrev, iconNext, iconLibrary, iconVariant, iconSize, iconFeatures, arrowBackground = true, arrowColor, ariaLabel = 'Image carousel', onChange, className, children, ...props }, ref) => {
4488
4793
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
4489
4794
  const carouselRef = React.useRef(null);
4795
+ // Cleanup returned by a consumer's callback ref on attach, held until detach.
4796
+ const consumerCleanupRef = React.useRef(null);
4490
4797
  const containerRef = React.useRef(null);
4491
4798
  const [internalValue, setInternalValue] = React.useState(0);
4492
4799
  const [isPaused, setIsPaused] = React.useState(false);
@@ -4672,7 +4979,28 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
4672
4979
  carouselRef.current =
4673
4980
  node;
4674
4981
  if (typeof ref === 'function') {
4675
- ref(node);
4982
+ // React 19 lets a callback ref return a cleanup function and detaches by
4983
+ // running it instead of calling the ref with `null`; React 18 discards the
4984
+ // return value entirely. Returning it from here would be a React-19-only
4985
+ // contract, so we hold the cleanup and run it ourselves on detach — the
4986
+ // same shape as `Dropdown`, `Modal`, `Dialog`, `Sidebar` and `Toast`.
4987
+ // The form controls that merge a forwarded ref still discard the cleanup
4988
+ // and detach with `ref(null)`, so this is not yet library-wide — grep
4989
+ // `typeof ref === 'function'` for the current split.
4990
+ if (node === null) {
4991
+ const consumerCleanup = consumerCleanupRef.current;
4992
+ consumerCleanupRef.current = null;
4993
+ if (consumerCleanup) {
4994
+ consumerCleanup();
4995
+ }
4996
+ else {
4997
+ ref(null);
4998
+ }
4999
+ return;
5000
+ }
5001
+ const cleanup = ref(node);
5002
+ consumerCleanupRef.current =
5003
+ typeof cleanup === 'function' ? cleanup : null;
4676
5004
  }
4677
5005
  else if (ref) {
4678
5006
  ref.current = node;
@@ -4937,6 +5265,7 @@ const Box = ({ className, textColor, color, bgColor, hasShadow = true, children,
4937
5265
  *
4938
5266
  * @function
4939
5267
  * @param {LinkButtonProps} props - Props for the LinkButton component.
5268
+ * @param {React.Ref<HTMLButtonElement | HTMLAnchorElement>} ref - Forwarded ref to the rendered button or anchor element.
4940
5269
  * @returns {JSX.Element} The rendered link-styled button element.
4941
5270
  *
4942
5271
  * @example
@@ -4947,11 +5276,12 @@ const Box = ({ className, textColor, color, bgColor, hasShadow = true, children,
4947
5276
  * // Underline variant with color
4948
5277
  * <LinkButton variant="underline" color="primary">Learn more</LinkButton>
4949
5278
  */
4950
- const LinkButton = ({ variant = 'text', color, className, ...props }) => {
5279
+ const LinkButton = React.forwardRef(function LinkButton({ variant = 'text', color, className, ...props }, ref) {
4951
5280
  const buttonColor = variant === 'underline' ? 'text' : variant;
4952
5281
  const prefixedClasses = usePrefixedClassNames('link-button', color && `link-button-${color}`, variant === 'underline' && 'link-button-underline');
4953
- return (jsxRuntime.jsx(Button, { color: buttonColor, className: classNames(prefixedClasses, className), ...props }));
4954
- };
5282
+ return (jsxRuntime.jsx(Button, { ref: ref, color: buttonColor, className: classNames(prefixedClasses, className), ...props }));
5283
+ });
5284
+ LinkButton.displayName = 'LinkButton';
4955
5285
 
4956
5286
  const validButtonsSizes = ['small', 'medium', 'large'];
4957
5287
  /**