@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 +441 -111
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +442 -112
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/Dialog.d.ts +6 -0
- package/dist/types/components/Dropdown.d.ts +2 -1
- package/dist/types/components/Modal.d.ts +25 -1
- package/dist/types/components/Navbar.d.ts +14 -7
- package/dist/types/elements/Button.d.ts +3 -1
- package/dist/types/elements/Buttons.d.ts +2 -2
- package/dist/types/elements/LinkButton.d.ts +2 -2
- package/dist/types/helpers/portal.d.ts +15 -0
- package/dist/types/helpers/scrollLock.d.ts +9 -0
- package/dist/types/helpers/useIsHydrated.d.ts +12 -0
- package/package.json +1 -1
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:
|
|
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
|
-
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
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
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
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
|
-
|
|
3778
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
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
|
|
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
|
|
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
|
/**
|