@allxsmith/bestax-bulma 5.13.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 +516 -143
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +517 -144
- 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/components/Panel.d.ts +5 -4
- package/dist/types/elements/Button.d.ts +3 -1
- package/dist/types/elements/Buttons.d.ts +2 -2
- package/dist/types/elements/Icon.d.ts +30 -7
- package/dist/types/elements/IconText.d.ts +4 -4
- package/dist/types/elements/LinkButton.d.ts +2 -2
- package/dist/types/form/Control.d.ts +4 -4
- 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
|
*
|
|
@@ -2788,8 +3068,37 @@ function getIconClasses(library, name, variant, features) {
|
|
|
2788
3068
|
const Icon = ({ className, textColor, bgColor, name, library, variant, features, libraryFeatures, // Deprecated but maintained for backward compatibility
|
|
2789
3069
|
size, ariaLabel = 'icon', style, icon, // Capture and exclude the deprecated 'icon' prop from DOM
|
|
2790
3070
|
color: _color, // Exclude 'color' prop if passed directly
|
|
2791
|
-
containerClassName, ...restProps }) => {
|
|
2792
|
-
//
|
|
3071
|
+
containerClassName, children, ...restProps }) => {
|
|
3072
|
+
// Get the default icon library from context, fallback to 'fa' if not set
|
|
3073
|
+
const defaultLibrary = useIconLibrary();
|
|
3074
|
+
/**
|
|
3075
|
+
* Generates Bulma helper classes and separates out remaining props.
|
|
3076
|
+
* Note: variant, features, and libraryFeatures are excluded from props spread
|
|
3077
|
+
*/
|
|
3078
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
3079
|
+
color: textColor,
|
|
3080
|
+
backgroundColor: bgColor,
|
|
3081
|
+
...restProps,
|
|
3082
|
+
});
|
|
3083
|
+
// Hoisted unconditionally to respect rules-of-hooks; the branches below
|
|
3084
|
+
// pick which result to consume.
|
|
3085
|
+
const defaultIconClasses = usePrefixedClassNames('icon', {
|
|
3086
|
+
[`is-${size}`]: size,
|
|
3087
|
+
});
|
|
3088
|
+
const sizeModifierClass = usePrefixedClassNames(size ? `is-${size}` : undefined);
|
|
3089
|
+
const bulmaClasses = containerClassName
|
|
3090
|
+
? containerClassName
|
|
3091
|
+
: defaultIconClasses;
|
|
3092
|
+
const iconContainerClasses = classNames(bulmaClasses, containerClassName && size ? sizeModifierClass : undefined, bulmaHelperClasses, className);
|
|
3093
|
+
if (children !== undefined) {
|
|
3094
|
+
// `IconChildrenProps`: render the caller's node (an inline SVG, a `react-icons`
|
|
3095
|
+
// component, …) in place of a class-based glyph. Library/variant/features don't apply.
|
|
3096
|
+
return (jsxRuntime.jsx("span", { className: iconContainerClasses, "aria-label": ariaLabel, style: style, ...rest, children: children }));
|
|
3097
|
+
}
|
|
3098
|
+
// `name` is guaranteed once `children` is absent (`IconProps` is a discriminated union of
|
|
3099
|
+
// the two, and `IconChildrenProps['children']` excludes `undefined` so `children={undefined}`
|
|
3100
|
+
// can't slip past into this branch) — the cast only matters for legacy callers that bypass
|
|
3101
|
+
// the type and rely solely on the deprecated `icon` prop below.
|
|
2793
3102
|
let finalName = name;
|
|
2794
3103
|
if (!name && icon) {
|
|
2795
3104
|
// If icon prop is provided instead of name, try to parse it
|
|
@@ -2808,31 +3117,16 @@ containerClassName, ...restProps }) => {
|
|
|
2808
3117
|
}
|
|
2809
3118
|
}
|
|
2810
3119
|
}
|
|
2811
|
-
// Get the default icon library from context, fallback to 'fa' if not set
|
|
2812
|
-
const defaultLibrary = useIconLibrary();
|
|
2813
3120
|
const finalLibrary = library || defaultLibrary || 'fa';
|
|
2814
3121
|
// Normalize a redundant leading library prefix (e.g. "fa-check" -> "check" when the
|
|
2815
3122
|
// library is 'fa'), so `name` behaves identically with or without the prefix.
|
|
2816
3123
|
finalName = stripRedundantLibraryPrefix(finalName, finalLibrary);
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
backgroundColor: bgColor,
|
|
2824
|
-
...restProps,
|
|
2825
|
-
});
|
|
2826
|
-
// Hoisted unconditionally to respect rules-of-hooks; the ternaries below
|
|
2827
|
-
// pick which result to consume.
|
|
2828
|
-
const defaultIconClasses = usePrefixedClassNames('icon', {
|
|
2829
|
-
[`is-${size}`]: size,
|
|
2830
|
-
});
|
|
2831
|
-
const sizeModifierClass = usePrefixedClassNames(size ? `is-${size}` : undefined);
|
|
2832
|
-
const bulmaClasses = containerClassName
|
|
2833
|
-
? containerClassName
|
|
2834
|
-
: defaultIconClasses;
|
|
2835
|
-
const iconContainerClasses = classNames(bulmaClasses, containerClassName && size ? sizeModifierClass : undefined, bulmaHelperClasses, className);
|
|
3124
|
+
if (!finalName) {
|
|
3125
|
+
// No glyph to name. Unreachable through the public type, but a plain-JS caller (or one
|
|
3126
|
+
// that casts) can land here, and building a class off an absent name produced a bogus
|
|
3127
|
+
// `fa-undefined` glyph. Render the bare container instead, matching the `children` branch.
|
|
3128
|
+
return (jsxRuntime.jsx("span", { className: iconContainerClasses, "aria-label": ariaLabel, style: style, ...rest }));
|
|
3129
|
+
}
|
|
2836
3130
|
// Backward compatibility: if libraryFeatures is provided, parse it for variant and features
|
|
2837
3131
|
let finalVariant = variant;
|
|
2838
3132
|
let finalFeatures = features;
|
|
@@ -2951,8 +3245,9 @@ const PanelTabs = ({ className, children, ...props }) => (jsxRuntime.jsx("p", {
|
|
|
2951
3245
|
*/
|
|
2952
3246
|
const PanelBlock = ({ className, active, children, ...props }) => (jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('panel-block', { 'is-active': active }), className), ...props, children: children }));
|
|
2953
3247
|
/**
|
|
2954
|
-
* Icon wrapper with panel styling (renders as `<span class="panel-icon"
|
|
2955
|
-
*
|
|
3248
|
+
* Icon wrapper with panel styling (renders as `<span class="panel-icon">`, containing an
|
|
3249
|
+
* `<i/>` when a `name` is given, or the custom node passed as `children`).
|
|
3250
|
+
* Accepts all Icon props (`name`, `variant`, `features`, etc.), or `children` for a custom node.
|
|
2956
3251
|
*
|
|
2957
3252
|
* @function
|
|
2958
3253
|
* @param {PanelIconProps} props - Props for the PanelIcon component.
|
|
@@ -3233,16 +3528,10 @@ const Loading = ({ active = false, isFullPage = false, size, color, canCancel =
|
|
|
3233
3528
|
document.addEventListener('keydown', handleKeyDown);
|
|
3234
3529
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
3235
3530
|
}, [active, canCancel, onCancel]);
|
|
3236
|
-
// Prevent body scroll when full page loading is active
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
return () => {
|
|
3241
|
-
document.body.style.overflow = '';
|
|
3242
|
-
};
|
|
3243
|
-
}
|
|
3244
|
-
return undefined;
|
|
3245
|
-
}, [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);
|
|
3246
3535
|
if (!active) {
|
|
3247
3536
|
return null;
|
|
3248
3537
|
}
|
|
@@ -3733,6 +4022,8 @@ const Steps = withSubComponents(StepsComponent, { Step }, 'Steps');
|
|
|
3733
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) => {
|
|
3734
4023
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
3735
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);
|
|
3736
4027
|
const resolvedFullwidth = isFullwidth ?? fullWidth ?? false;
|
|
3737
4028
|
// Close handler
|
|
3738
4029
|
const handleClose = React.useCallback(() => {
|
|
@@ -3758,17 +4049,9 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
3758
4049
|
document.addEventListener('keydown', handleKeyDown);
|
|
3759
4050
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
3760
4051
|
}, [isOpen, escapeClose, handleClose]);
|
|
3761
|
-
// Prevent body scroll when sidebar is open
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
const originalOverflow = document.body.style.overflow;
|
|
3765
|
-
document.body.style.overflow = 'hidden';
|
|
3766
|
-
return () => {
|
|
3767
|
-
document.body.style.overflow = originalOverflow;
|
|
3768
|
-
};
|
|
3769
|
-
}
|
|
3770
|
-
return undefined;
|
|
3771
|
-
}, [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);
|
|
3772
4055
|
// Focus trap (basic - focus sidebar when opened)
|
|
3773
4056
|
React.useEffect(() => {
|
|
3774
4057
|
if (isOpen) {
|
|
@@ -3790,7 +4073,28 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
3790
4073
|
sidebarRef.current =
|
|
3791
4074
|
node;
|
|
3792
4075
|
if (typeof ref === 'function') {
|
|
3793
|
-
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;
|
|
3794
4098
|
}
|
|
3795
4099
|
else if (ref) {
|
|
3796
4100
|
ref.current = node;
|
|
@@ -3907,6 +4211,8 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
|
|
|
3907
4211
|
const [isVisible, setIsVisible] = React.useState(true);
|
|
3908
4212
|
const [isPaused, setIsPaused] = React.useState(false);
|
|
3909
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);
|
|
3910
4216
|
const handleClose = React.useCallback(() => {
|
|
3911
4217
|
setIsVisible(false);
|
|
3912
4218
|
onClose?.();
|
|
@@ -3977,27 +4283,41 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
|
|
|
3977
4283
|
const toastActionClass = usePrefixedClassNames('toast-action');
|
|
3978
4284
|
const toastButtonClass = usePrefixedClassNames('button');
|
|
3979
4285
|
const toastCloseClass = usePrefixedClassNames('delete', 'is-small');
|
|
3980
|
-
|
|
3981
|
-
return null;
|
|
3982
|
-
}
|
|
3983
|
-
const resolveContainer = () => {
|
|
3984
|
-
if (container) {
|
|
3985
|
-
if (typeof container === 'string') {
|
|
3986
|
-
return (document.querySelector(container) || document.body);
|
|
3987
|
-
}
|
|
3988
|
-
return container;
|
|
3989
|
-
}
|
|
3990
|
-
return document.body;
|
|
3991
|
-
};
|
|
3992
|
-
const setRef = (node) => {
|
|
4286
|
+
const setRef = React.useCallback((node) => {
|
|
3993
4287
|
toastRef.current = node;
|
|
3994
4288
|
if (typeof ref === 'function') {
|
|
3995
|
-
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;
|
|
3996
4313
|
}
|
|
3997
4314
|
else if (ref) {
|
|
3998
4315
|
ref.current = node;
|
|
3999
4316
|
}
|
|
4000
|
-
};
|
|
4317
|
+
}, [ref]);
|
|
4318
|
+
if (!isVisible) {
|
|
4319
|
+
return null;
|
|
4320
|
+
}
|
|
4001
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 => {
|
|
4002
4322
|
e.stopPropagation();
|
|
4003
4323
|
handleClose();
|
|
@@ -4013,7 +4333,7 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
|
|
|
4013
4333
|
}
|
|
4014
4334
|
const toastContent = jsxRuntime.jsx("div", { className: containerClasses, children: toastElement });
|
|
4015
4335
|
if (typeof document !== 'undefined') {
|
|
4016
|
-
return reactDom.createPortal(toastContent,
|
|
4336
|
+
return reactDom.createPortal(toastContent, resolvePortalContainer(container));
|
|
4017
4337
|
}
|
|
4018
4338
|
return null;
|
|
4019
4339
|
});
|
|
@@ -4153,9 +4473,6 @@ const ToastContainer = ({ position = 'top-right', }) => {
|
|
|
4153
4473
|
}) }), document.body);
|
|
4154
4474
|
};
|
|
4155
4475
|
|
|
4156
|
-
// Ref-counted body scroll lock for chained/overlapping dialogs
|
|
4157
|
-
let _scrollLockCount = 0;
|
|
4158
|
-
let _originalOverflow = '';
|
|
4159
4476
|
/**
|
|
4160
4477
|
* The `Dialog` component provides ready-made confirm and alert dialogs, so a destructive action stays one `await dialog.confirm()` call away.
|
|
4161
4478
|
*
|
|
@@ -4187,9 +4504,11 @@ let _originalOverflow = '';
|
|
|
4187
4504
|
* onCancel={() => setShowConfirm(false)}
|
|
4188
4505
|
* />
|
|
4189
4506
|
*/
|
|
4190
|
-
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) => {
|
|
4191
4508
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
4192
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);
|
|
4193
4512
|
const confirmRef = React.useRef(null);
|
|
4194
4513
|
const cancelRef = React.useRef(null);
|
|
4195
4514
|
// Handle cancel
|
|
@@ -4208,48 +4527,47 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
|
|
|
4208
4527
|
handleCancel();
|
|
4209
4528
|
}
|
|
4210
4529
|
}, [canCancel, handleCancel]);
|
|
4211
|
-
//
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
};
|
|
4220
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
4221
|
-
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
4222
|
-
}, [isOpen, canCancel, handleCancel]);
|
|
4223
|
-
// 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.
|
|
4224
4538
|
React.useEffect(() => {
|
|
4225
4539
|
if (isOpen) {
|
|
4226
4540
|
const buttonToFocus = focusCancel && showCancel ? cancelRef.current : confirmRef.current;
|
|
4227
4541
|
buttonToFocus?.focus();
|
|
4228
4542
|
}
|
|
4229
|
-
}, [isOpen, focusCancel, showCancel]);
|
|
4230
|
-
// Prevent body scroll (ref-counted so chained dialogs work correctly)
|
|
4231
|
-
React.useEffect(() => {
|
|
4232
|
-
if (isOpen) {
|
|
4233
|
-
_scrollLockCount++;
|
|
4234
|
-
if (_scrollLockCount === 1) {
|
|
4235
|
-
_originalOverflow = document.body.style.overflow;
|
|
4236
|
-
document.body.style.overflow = 'hidden';
|
|
4237
|
-
}
|
|
4238
|
-
return () => {
|
|
4239
|
-
_scrollLockCount--;
|
|
4240
|
-
if (_scrollLockCount === 0) {
|
|
4241
|
-
document.body.style.overflow = _originalOverflow;
|
|
4242
|
-
}
|
|
4243
|
-
};
|
|
4244
|
-
}
|
|
4245
|
-
return undefined;
|
|
4246
|
-
}, [isOpen]);
|
|
4543
|
+
}, [isOpen, focusCancel, showCancel, isPortaled]);
|
|
4247
4544
|
// Use combined ref
|
|
4248
4545
|
const combinedRef = React.useCallback((node) => {
|
|
4249
4546
|
dialogRef.current =
|
|
4250
4547
|
node;
|
|
4251
4548
|
if (typeof ref === 'function') {
|
|
4252
|
-
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;
|
|
4253
4571
|
}
|
|
4254
4572
|
else if (ref) {
|
|
4255
4573
|
ref.current = node;
|
|
@@ -4292,7 +4610,7 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
|
|
|
4292
4610
|
if (!isOpen) {
|
|
4293
4611
|
return null;
|
|
4294
4612
|
}
|
|
4295
|
-
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 })] })] })] }));
|
|
4296
4614
|
});
|
|
4297
4615
|
Dialog.displayName = 'Dialog';
|
|
4298
4616
|
let dialogListeners = new Set();
|
|
@@ -4375,10 +4693,11 @@ const validButtonColors = [...validColors, 'text', 'ghost'];
|
|
|
4375
4693
|
*
|
|
4376
4694
|
* @function
|
|
4377
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.
|
|
4378
4697
|
* @returns {JSX.Element} The rendered button or anchor element.
|
|
4379
4698
|
* @see {@link https://bulma.io/documentation/elements/button/ | Bulma Button documentation}
|
|
4380
4699
|
*/
|
|
4381
|
-
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) {
|
|
4382
4701
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
4383
4702
|
color: textColor,
|
|
4384
4703
|
backgroundColor: bgColor,
|
|
@@ -4408,12 +4727,13 @@ const Button = ({ color, size, isLight, isRounded, isLoading, isStatic, isFullwi
|
|
|
4408
4727
|
// native/custom link-like elements (an <a>, a router Link, ...) don't
|
|
4409
4728
|
// receive button-only HTML attributes.
|
|
4410
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;
|
|
4411
|
-
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
|
|
4412
4731
|
? (e) => e.preventDefault()
|
|
4413
4732
|
: onClick, ...anchorRest, children: children }));
|
|
4414
4733
|
}
|
|
4415
|
-
return (jsxRuntime.jsx("button", { className: buttonClasses, disabled: isDisabled, onClick: onClick, ...rest, children: children }));
|
|
4416
|
-
};
|
|
4734
|
+
return (jsxRuntime.jsx("button", { ref: ref, className: buttonClasses, disabled: isDisabled, onClick: onClick, ...rest, children: children }));
|
|
4735
|
+
});
|
|
4736
|
+
Button.displayName = 'Button';
|
|
4417
4737
|
|
|
4418
4738
|
/**
|
|
4419
4739
|
* Individual carousel item/slide.
|
|
@@ -4472,6 +4792,8 @@ const DefaultNextIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
|
|
|
4472
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) => {
|
|
4473
4793
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
4474
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);
|
|
4475
4797
|
const containerRef = React.useRef(null);
|
|
4476
4798
|
const [internalValue, setInternalValue] = React.useState(0);
|
|
4477
4799
|
const [isPaused, setIsPaused] = React.useState(false);
|
|
@@ -4657,7 +4979,28 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
4657
4979
|
carouselRef.current =
|
|
4658
4980
|
node;
|
|
4659
4981
|
if (typeof ref === 'function') {
|
|
4660
|
-
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;
|
|
4661
5004
|
}
|
|
4662
5005
|
else if (ref) {
|
|
4663
5006
|
ref.current = node;
|
|
@@ -4922,6 +5265,7 @@ const Box = ({ className, textColor, color, bgColor, hasShadow = true, children,
|
|
|
4922
5265
|
*
|
|
4923
5266
|
* @function
|
|
4924
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.
|
|
4925
5269
|
* @returns {JSX.Element} The rendered link-styled button element.
|
|
4926
5270
|
*
|
|
4927
5271
|
* @example
|
|
@@ -4932,11 +5276,12 @@ const Box = ({ className, textColor, color, bgColor, hasShadow = true, children,
|
|
|
4932
5276
|
* // Underline variant with color
|
|
4933
5277
|
* <LinkButton variant="underline" color="primary">Learn more</LinkButton>
|
|
4934
5278
|
*/
|
|
4935
|
-
const LinkButton = ({ variant = 'text', color, className, ...props })
|
|
5279
|
+
const LinkButton = React.forwardRef(function LinkButton({ variant = 'text', color, className, ...props }, ref) {
|
|
4936
5280
|
const buttonColor = variant === 'underline' ? 'text' : variant;
|
|
4937
5281
|
const prefixedClasses = usePrefixedClassNames('link-button', color && `link-button-${color}`, variant === 'underline' && 'link-button-underline');
|
|
4938
|
-
return (jsxRuntime.jsx(Button, { color: buttonColor, className: classNames(prefixedClasses, className), ...props }));
|
|
4939
|
-
};
|
|
5282
|
+
return (jsxRuntime.jsx(Button, { ref: ref, color: buttonColor, className: classNames(prefixedClasses, className), ...props }));
|
|
5283
|
+
});
|
|
5284
|
+
LinkButton.displayName = 'LinkButton';
|
|
4940
5285
|
|
|
4941
5286
|
const validButtonsSizes = ['small', 'medium', 'large'];
|
|
4942
5287
|
/**
|
|
@@ -5127,6 +5472,16 @@ const Figure = withSubComponents(FigureComponent, {
|
|
|
5127
5472
|
Caption: FigureCaption,
|
|
5128
5473
|
}, 'Figure');
|
|
5129
5474
|
|
|
5475
|
+
/**
|
|
5476
|
+
* Distinguishes an `IconProps` object from a plain custom node (an inline SVG, a `react-icons`
|
|
5477
|
+
* component, …) passed to a slot that accepts either.
|
|
5478
|
+
*/
|
|
5479
|
+
function isIconProps$1(value) {
|
|
5480
|
+
return (typeof value === 'object' &&
|
|
5481
|
+
value !== null &&
|
|
5482
|
+
!React.isValidElement(value) &&
|
|
5483
|
+
('name' in value || 'children' in value));
|
|
5484
|
+
}
|
|
5130
5485
|
/**
|
|
5131
5486
|
* The `IconText` component provides a Bulma-styled horizontal arrangement of one or more `Icon` components and optional text.
|
|
5132
5487
|
*
|
|
@@ -5146,7 +5501,9 @@ const IconTextComponent = ({ className, textColor, color, bgColor, iconProps, ch
|
|
|
5146
5501
|
});
|
|
5147
5502
|
const bulmaClasses = usePrefixedClassNames('icon-text');
|
|
5148
5503
|
const iconTextClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
5149
|
-
return (jsxRuntime.jsx("span", { className: iconTextClasses, ...rest, children: items ? (items.map((item, index) => (jsxRuntime.jsxs(React.Fragment, { children: [
|
|
5504
|
+
return (jsxRuntime.jsx("span", { className: iconTextClasses, ...rest, children: items ? (items.map((item, index) => (jsxRuntime.jsxs(React.Fragment, { children: [item.iconProps &&
|
|
5505
|
+
(isIconProps$1(item.iconProps) ? (jsxRuntime.jsx(Icon, { ...item.iconProps })) : (jsxRuntime.jsx(Icon, { children: item.iconProps }))), item.text && jsxRuntime.jsx("span", { children: item.text })] }, index)))) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [iconProps &&
|
|
5506
|
+
(isIconProps$1(iconProps) ? (jsxRuntime.jsx(Icon, { ...iconProps })) : (jsxRuntime.jsx(Icon, { children: iconProps }))), children && jsxRuntime.jsx("span", { children: children })] })) }));
|
|
5150
5507
|
};
|
|
5151
5508
|
const IconText = withSubComponents(IconTextComponent, { Icon }, 'IconText');
|
|
5152
5509
|
|
|
@@ -6057,6 +6414,16 @@ const Checkbox = React.forwardRef(({ color, size, className, children, textColor
|
|
|
6057
6414
|
});
|
|
6058
6415
|
Checkbox.displayName = 'Checkbox';
|
|
6059
6416
|
|
|
6417
|
+
/**
|
|
6418
|
+
* Distinguishes an `IconProps` object from a plain custom node (an inline SVG, a `react-icons`
|
|
6419
|
+
* component, …) passed to a slot that accepts either.
|
|
6420
|
+
*/
|
|
6421
|
+
function isIconProps(value) {
|
|
6422
|
+
return (typeof value === 'object' &&
|
|
6423
|
+
value !== null &&
|
|
6424
|
+
!React.isValidElement(value) &&
|
|
6425
|
+
('name' in value || 'children' in value));
|
|
6426
|
+
}
|
|
6060
6427
|
const allowedColors = [...validColors, 'inherit', 'current'];
|
|
6061
6428
|
/**
|
|
6062
6429
|
* The `Control` component is a Bulma-styled wrapper for form controls (`Input`, `Select`, `TextArea`, etc.), supporting icons (left/right), loading state, expansion, size, and Bulma helper props for layout and color.
|
|
@@ -6088,15 +6455,19 @@ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isL
|
|
|
6088
6455
|
backgroundColor: safeBgColor,
|
|
6089
6456
|
...restProps,
|
|
6090
6457
|
});
|
|
6091
|
-
// Prepare icon props for the shortcut
|
|
6092
|
-
|
|
6458
|
+
// Prepare icon props for the shortcut. A truthiness test (not `!== undefined`)
|
|
6459
|
+
// so a falsy node from the idiomatic `iconLeft={cond && <Node/>}` / `iconLeft={maybe ?? null}`
|
|
6460
|
+
// pattern counts as "no icon" — otherwise `false`/`null` would reserve the icon column and
|
|
6461
|
+
// mount an empty `.icon` span. A falsy `iconLeft` also correctly falls through to the
|
|
6462
|
+
// `iconLeftName` shortcut, matching the pre-node behavior.
|
|
6463
|
+
const leftIconValue = iconLeft ||
|
|
6093
6464
|
(iconLeftName
|
|
6094
6465
|
? {
|
|
6095
6466
|
name: iconLeftName,
|
|
6096
6467
|
size: iconLeftSize,
|
|
6097
6468
|
}
|
|
6098
6469
|
: undefined);
|
|
6099
|
-
const
|
|
6470
|
+
const rightIconValue = iconRight ||
|
|
6100
6471
|
(iconRightName
|
|
6101
6472
|
? {
|
|
6102
6473
|
name: iconRightName,
|
|
@@ -6104,15 +6475,17 @@ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isL
|
|
|
6104
6475
|
}
|
|
6105
6476
|
: undefined);
|
|
6106
6477
|
const mainClass = usePrefixedClassNames('control', {
|
|
6107
|
-
'has-icons-left': hasIconsLeft || !!
|
|
6108
|
-
'has-icons-right': hasIconsRight || !!
|
|
6478
|
+
'has-icons-left': hasIconsLeft || !!leftIconValue,
|
|
6479
|
+
'has-icons-right': hasIconsRight || !!rightIconValue,
|
|
6109
6480
|
'is-loading': isLoading,
|
|
6110
6481
|
'is-expanded': isExpanded,
|
|
6111
6482
|
[`is-${size}`]: !!size,
|
|
6112
6483
|
});
|
|
6113
6484
|
const controlClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
6114
6485
|
// --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
|
|
6115
|
-
return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [children,
|
|
6486
|
+
return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [children, leftIconValue &&
|
|
6487
|
+
(isIconProps(leftIconValue) ? (jsxRuntime.jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
|
|
6488
|
+
(isIconProps(rightIconValue) ? (jsxRuntime.jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
|
|
6116
6489
|
});
|
|
6117
6490
|
Control.displayName = 'Control';
|
|
6118
6491
|
|