@allxsmith/bestax-bulma 5.14.0 → 5.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +613 -151
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +614 -152
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/Avatar.d.ts +39 -5
- package/dist/types/components/Avatars.d.ts +1 -1
- package/dist/types/components/Dialog.d.ts +6 -0
- package/dist/types/components/Dropdown.d.ts +2 -1
- package/dist/types/components/Menu.d.ts +35 -10
- package/dist/types/components/Modal.d.ts +25 -1
- package/dist/types/components/Navbar.d.ts +77 -18
- package/dist/types/components/Reveal.d.ts +18 -24
- package/dist/types/elements/Button.d.ts +25 -14
- package/dist/types/elements/Buttons.d.ts +2 -2
- package/dist/types/elements/Link.d.ts +33 -9
- package/dist/types/elements/LinkButton.d.ts +33 -4
- package/dist/types/helpers/polymorphic.d.ts +105 -0
- 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/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/scss/CLAUDE.md +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -1189,6 +1189,21 @@ const ColumnsComponent = ({ className, textColor, color: _fieldColor, bgColor, i
|
|
|
1189
1189
|
};
|
|
1190
1190
|
const Columns = withSubComponents(ColumnsComponent, { Column }, 'Columns');
|
|
1191
1191
|
|
|
1192
|
+
/**
|
|
1193
|
+
* Whether an `as` target is a custom element rather than a built-in tag.
|
|
1194
|
+
*
|
|
1195
|
+
* HTML requires a custom element's name to contain a hyphen, and no built-in
|
|
1196
|
+
* element name has one, so this is exact rather than a heuristic. It matters
|
|
1197
|
+
* because a custom element is an intrinsic STRING — `typeof as === 'string'` is
|
|
1198
|
+
* true — while its props are whatever a consumer declared through
|
|
1199
|
+
* `React.JSX.IntrinsicElements`, the way this package declares `<ion-icon>`.
|
|
1200
|
+
* A runtime backstop that filters built-in attributes must leave those alone,
|
|
1201
|
+
* or it strips props the derived type just promised to forward.
|
|
1202
|
+
*/
|
|
1203
|
+
function isCustomElement(as) {
|
|
1204
|
+
return typeof as === 'string' && as.includes('-');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1192
1207
|
const avatarColors = [
|
|
1193
1208
|
'primary',
|
|
1194
1209
|
'link',
|
|
@@ -1257,6 +1272,7 @@ function DefaultAvatarIcon() {
|
|
|
1257
1272
|
*
|
|
1258
1273
|
* @function
|
|
1259
1274
|
* @param {AvatarProps} props - Props for the Avatar component.
|
|
1275
|
+
* @param {React.Ref} ref - Forwarded ref to the element `as` renders.
|
|
1260
1276
|
* @returns {JSX.Element} The rendered avatar element.
|
|
1261
1277
|
*
|
|
1262
1278
|
* @example
|
|
@@ -1264,7 +1280,8 @@ function DefaultAvatarIcon() {
|
|
|
1264
1280
|
* @example
|
|
1265
1281
|
* <Avatar name="Grace Hopper" />
|
|
1266
1282
|
*/
|
|
1267
|
-
const Avatar = (
|
|
1283
|
+
const Avatar = React.forwardRef(function Avatar(avatarProps, ref) {
|
|
1284
|
+
const { className, src, alt, name, initials, icon, size, shape = 'circle', color, as, href, target, rel, imageProps, style, ...props } = avatarProps;
|
|
1268
1285
|
// Tracks the src that failed to load. A src change clears the latch during
|
|
1269
1286
|
// render (React's "reset state when props change" pattern) so a previously
|
|
1270
1287
|
// failed src is retried when switched back to. The img is additionally
|
|
@@ -1316,10 +1333,10 @@ const Avatar = ({ className, src, alt, name, initials, icon, size, shape = 'circ
|
|
|
1316
1333
|
// decorative (aria-hidden) and it could render nameless.
|
|
1317
1334
|
const isInteractive = Tag === 'a' ||
|
|
1318
1335
|
Tag === 'button' ||
|
|
1319
|
-
(typeof Tag !== 'string' && href != null);
|
|
1336
|
+
((typeof Tag !== 'string' || isCustomElement(Tag)) && href != null);
|
|
1320
1337
|
// Only forward link attributes when rendering an anchor or a custom (non-DOM)
|
|
1321
1338
|
// component; a plain `as="div"` must not receive a stray `href`/`target`/`rel`.
|
|
1322
|
-
const isLinkLike = Tag === 'a' || typeof Tag !== 'string';
|
|
1339
|
+
const isLinkLike = Tag === 'a' || typeof Tag !== 'string' || isCustomElement(Tag);
|
|
1323
1340
|
const linkProps = isLinkLike ? { href, target, rel } : {};
|
|
1324
1341
|
// alt coalesces with ?? (not ||) so an explicit alt="" survives as the
|
|
1325
1342
|
// standard decorative marker instead of being overridden by name.
|
|
@@ -1342,11 +1359,11 @@ const Avatar = ({ className, src, alt, name, initials, icon, size, shape = 'circ
|
|
|
1342
1359
|
// A clickable avatar inside a form must not submit it; default the native
|
|
1343
1360
|
// button type (an explicit type passed through rest still wins).
|
|
1344
1361
|
const buttonTypeProps = Tag === 'button' ? { type: 'button' } : {};
|
|
1345
|
-
return (jsxRuntime.jsxs(Tag, { className: combinedClasses, style: { ...sizeStyle, ...style }, ...buttonTypeProps, ...linkProps, ...a11yProps, ...rest, children: [showImage && (jsxRuntime.jsx("img", { ...imageProps, ref: imgRef, src: src, alt: accessibleName ?? '', onError: e => {
|
|
1362
|
+
return (jsxRuntime.jsxs(Tag, { ref: ref, className: combinedClasses, style: { ...sizeStyle, ...style }, ...buttonTypeProps, ...linkProps, ...a11yProps, ...rest, children: [showImage && (jsxRuntime.jsx("img", { ...imageProps, ref: imgRef, src: src, alt: accessibleName ?? '', onError: e => {
|
|
1346
1363
|
imageProps?.onError?.(e);
|
|
1347
1364
|
setErroredSrc(src);
|
|
1348
1365
|
} }, src)), showInitials && (jsxRuntime.jsx("span", { className: initialsClass, children: resolvedInitials })), showIcon && icon, showDefaultIcon && jsxRuntime.jsx(DefaultAvatarIcon, {})] }));
|
|
1349
|
-
};
|
|
1366
|
+
});
|
|
1350
1367
|
Avatar.displayName = 'Avatar';
|
|
1351
1368
|
|
|
1352
1369
|
/**
|
|
@@ -1776,14 +1793,46 @@ const isBrowser$1 = (win, doc) => typeof win !== 'undefined' && typeof doc !== '
|
|
|
1776
1793
|
*
|
|
1777
1794
|
* @function
|
|
1778
1795
|
* @param {DropdownProps} props - Props for the Dropdown component.
|
|
1796
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the root dropdown element.
|
|
1779
1797
|
* @returns {JSX.Element} The rendered dropdown.
|
|
1780
1798
|
* @see {@link https://bulma.io/documentation/components/dropdown/ | Bulma Dropdown documentation}
|
|
1781
1799
|
*/
|
|
1782
|
-
const DropdownComponent = ({ label, children, className, menuClassName, active: activeProp, up, right, hoverable, disabled, onActiveChange, closeOnClick = true, id, ...props })
|
|
1800
|
+
const DropdownComponent = React.forwardRef(function DropdownComponent({ label, children, className, menuClassName, active: activeProp, up, right, hoverable, disabled, onActiveChange, closeOnClick = true, id, ...props }, ref) {
|
|
1783
1801
|
const [active, setActive] = React.useState(!!activeProp);
|
|
1784
1802
|
const dropdownRef = React.useRef(null);
|
|
1785
1803
|
const triggerRef = React.useRef(null);
|
|
1786
1804
|
const pendingFocusRef = React.useRef(null);
|
|
1805
|
+
// Cleanup returned by a consumer's callback ref on attach, held until detach.
|
|
1806
|
+
const consumerCleanupRef = React.useRef(null);
|
|
1807
|
+
const setRefs = React.useCallback((node) => {
|
|
1808
|
+
dropdownRef.current =
|
|
1809
|
+
node;
|
|
1810
|
+
if (typeof ref === 'function') {
|
|
1811
|
+
// React 19 lets a callback ref return a cleanup function and detaches
|
|
1812
|
+
// by running it instead of calling the ref with `null`; React 18
|
|
1813
|
+
// discards the return value entirely. Returning it from here would be
|
|
1814
|
+
// a React-19-only contract, so instead we hold the cleanup and run it
|
|
1815
|
+
// ourselves on detach — a consumer's cleanup ref then behaves the same
|
|
1816
|
+
// on both majors of the CI matrix.
|
|
1817
|
+
if (node === null) {
|
|
1818
|
+
const consumerCleanup = consumerCleanupRef.current;
|
|
1819
|
+
consumerCleanupRef.current = null;
|
|
1820
|
+
if (consumerCleanup) {
|
|
1821
|
+
consumerCleanup();
|
|
1822
|
+
}
|
|
1823
|
+
else {
|
|
1824
|
+
ref(null);
|
|
1825
|
+
}
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
const cleanup = ref(node);
|
|
1829
|
+
consumerCleanupRef.current =
|
|
1830
|
+
typeof cleanup === 'function' ? cleanup : null;
|
|
1831
|
+
}
|
|
1832
|
+
else if (ref) {
|
|
1833
|
+
ref.current = node;
|
|
1834
|
+
}
|
|
1835
|
+
}, [ref]);
|
|
1787
1836
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
1788
1837
|
// Generate Bulma classes with prefix
|
|
1789
1838
|
const bulmaClasses = usePrefixedClassNames('dropdown', {
|
|
@@ -1951,8 +2000,8 @@ const DropdownComponent = ({ label, children, className, menuClassName, active:
|
|
|
1951
2000
|
}
|
|
1952
2001
|
};
|
|
1953
2002
|
const dropdownClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
1954
|
-
return (jsxRuntime.jsxs("div", { className: dropdownClasses, ref:
|
|
1955
|
-
};
|
|
2003
|
+
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 }) })] }));
|
|
2004
|
+
});
|
|
1956
2005
|
/**
|
|
1957
2006
|
* Bulma Dropdown item.
|
|
1958
2007
|
*
|
|
@@ -2027,13 +2076,23 @@ const MenuList = ({ className, children, ...props }) => {
|
|
|
2027
2076
|
*
|
|
2028
2077
|
* @function
|
|
2029
2078
|
* @param {MenuItemProps} props - Props for the MenuItem component.
|
|
2079
|
+
* @param {React.Ref} ref - Forwarded ref to the inner element `as` renders.
|
|
2030
2080
|
* @returns {JSX.Element} The rendered menu item.
|
|
2031
2081
|
*/
|
|
2032
|
-
const MenuItem = (
|
|
2033
|
-
const {
|
|
2082
|
+
const MenuItem = React.forwardRef(function MenuItem(itemProps, ref) {
|
|
2083
|
+
const { className, children, active, as: Component = 'a', 'data-testid': testId, style, id, title, role, tabIndex, ...rest } = itemProps;
|
|
2084
|
+
const { bulmaHelperClasses, rest: forwarded } = useBulmaClasses(rest);
|
|
2085
|
+
// `href` reaches an anchor or a custom component (which owns its own prop
|
|
2086
|
+
// contract), but not another intrinsic tag — `<span href>` is invalid HTML.
|
|
2087
|
+
// The same rule Avatar applies through `isLinkLike` and Button through its
|
|
2088
|
+
// intrinsic-only strip. Before #641 `href` was an own prop re-applied only
|
|
2089
|
+
// for `as="a"`; deriving it from `as` must not quietly widen that.
|
|
2090
|
+
const isLinkLike = Component === 'a' ||
|
|
2091
|
+
typeof Component !== 'string' ||
|
|
2092
|
+
isCustomElement(Component);
|
|
2093
|
+
const { href: _href, ...withoutHref } = forwarded;
|
|
2094
|
+
const linkProps = isLinkLike ? forwarded : withoutHref;
|
|
2034
2095
|
const itemClass = classNames({ [usePrefixedClassNames('is-active')]: active }, bulmaHelperClasses);
|
|
2035
|
-
// Standard <li> props
|
|
2036
|
-
const { style, id, title, role, tabIndex, ...linkProps } = bulmaRest;
|
|
2037
2096
|
// Split children into label and nested MenuList(s)
|
|
2038
2097
|
const labelChildren = [];
|
|
2039
2098
|
const nestedMenuLists = [];
|
|
@@ -2045,15 +2104,9 @@ const MenuItem = ({ className, children, active, href, as: Component = 'a', 'dat
|
|
|
2045
2104
|
labelChildren.push(child);
|
|
2046
2105
|
}
|
|
2047
2106
|
});
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
}
|
|
2052
|
-
if (Object.prototype.hasOwnProperty.call(rest, 'to')) {
|
|
2053
|
-
linkProps.to = rest.to;
|
|
2054
|
-
}
|
|
2055
|
-
return (jsxRuntime.jsxs("li", { className: className, "data-testid": testId, style: style, id: id, title: title, role: role, tabIndex: tabIndex, children: [jsxRuntime.jsx(Component, { className: itemClass, ...linkProps, children: labelChildren }), nestedMenuLists] }));
|
|
2056
|
-
};
|
|
2107
|
+
return (jsxRuntime.jsxs("li", { className: className, "data-testid": testId, style: style, id: id, title: title, role: role, tabIndex: tabIndex, children: [jsxRuntime.jsx(Component, { ref: ref, className: itemClass, ...linkProps, children: labelChildren }), nestedMenuLists] }));
|
|
2108
|
+
});
|
|
2109
|
+
MenuItem.displayName = 'MenuItem';
|
|
2057
2110
|
// Attach static subcomponents
|
|
2058
2111
|
const Menu = withSubComponents(MenuComponent, {
|
|
2059
2112
|
Label: MenuLabel,
|
|
@@ -2108,6 +2161,105 @@ const Message = withSubComponents(MessageComponent, {
|
|
|
2108
2161
|
Body: MessageBody,
|
|
2109
2162
|
}, 'Message');
|
|
2110
2163
|
|
|
2164
|
+
// Ref-counted across every caller (e.g. a Dialog rendering its own Modal)
|
|
2165
|
+
// so overlapping/nested locks don't fight over which one restores `overflow`.
|
|
2166
|
+
// Every overlay that locks body scroll — Modal, Dialog, Sidebar, Loading —
|
|
2167
|
+
// goes through here; a component setting `document.body.style.overflow`
|
|
2168
|
+
// itself would clear a lock another overlay still needs.
|
|
2169
|
+
let lockCount = 0;
|
|
2170
|
+
let originalOverflow = '';
|
|
2171
|
+
/**
|
|
2172
|
+
* Locks (and ref-counted-ly unlocks) `document.body` scrolling while `active`
|
|
2173
|
+
* is true. Safe to call from multiple components at once — the body scroll
|
|
2174
|
+
* is only restored once every active caller has released its lock.
|
|
2175
|
+
*
|
|
2176
|
+
* @function useScrollLock
|
|
2177
|
+
* @param active - Whether this caller wants the body scroll locked.
|
|
2178
|
+
*/
|
|
2179
|
+
function useScrollLock(active) {
|
|
2180
|
+
React.useEffect(() => {
|
|
2181
|
+
if (!active)
|
|
2182
|
+
return undefined;
|
|
2183
|
+
lockCount++;
|
|
2184
|
+
if (lockCount === 1) {
|
|
2185
|
+
originalOverflow = document.body.style.overflow;
|
|
2186
|
+
document.body.style.overflow = 'hidden';
|
|
2187
|
+
}
|
|
2188
|
+
return () => {
|
|
2189
|
+
lockCount--;
|
|
2190
|
+
if (lockCount === 0) {
|
|
2191
|
+
document.body.style.overflow = originalOverflow;
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
}, [active]);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/**
|
|
2198
|
+
* Resolves a portal target: an `HTMLElement` is used directly, a non-empty
|
|
2199
|
+
* `string` is treated as a `document.querySelector` selector (falling back to
|
|
2200
|
+
* `document.body` when it matches nothing), and any falsy value — `undefined`
|
|
2201
|
+
* or the empty string — resolves to `document.body`.
|
|
2202
|
+
*
|
|
2203
|
+
* The empty string is deliberately treated as "no target" rather than passed
|
|
2204
|
+
* through: `document.querySelector('')` throws a `SyntaxError`, and callers
|
|
2205
|
+
* building a selector from state can easily hand us `''`.
|
|
2206
|
+
*
|
|
2207
|
+
* @function resolvePortalContainer
|
|
2208
|
+
* @param container - The requested portal target, if any.
|
|
2209
|
+
* @returns The resolved DOM node to portal into.
|
|
2210
|
+
*/
|
|
2211
|
+
function resolvePortalContainer(container) {
|
|
2212
|
+
if (!container) {
|
|
2213
|
+
return document.body;
|
|
2214
|
+
}
|
|
2215
|
+
if (typeof container === 'string') {
|
|
2216
|
+
return (document.querySelector(container) ?? document.body);
|
|
2217
|
+
}
|
|
2218
|
+
return container;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// "Are we past hydration?" as a store that never changes: React reads the
|
|
2222
|
+
// server snapshot both while server-rendering and while hydrating, and the
|
|
2223
|
+
// client snapshot from the first post-hydration render onward.
|
|
2224
|
+
const subscribeToNothing = () => () => { };
|
|
2225
|
+
const getClientSnapshot = () => true;
|
|
2226
|
+
const getServerSnapshot = () => false;
|
|
2227
|
+
/**
|
|
2228
|
+
* Reports whether the component has passed hydration.
|
|
2229
|
+
*
|
|
2230
|
+
* Returns `false` during server rendering and during the hydrating client
|
|
2231
|
+
* render, then `true` from the commit that follows. Use it to defer
|
|
2232
|
+
* DOM-only behaviour (such as portalling) past hydration so the first client
|
|
2233
|
+
* render matches the server markup instead of tripping hydration recovery.
|
|
2234
|
+
*
|
|
2235
|
+
* @function useIsHydrated
|
|
2236
|
+
* @returns `true` once the hydrating render has completed, otherwise `false`.
|
|
2237
|
+
*/
|
|
2238
|
+
function useIsHydrated() {
|
|
2239
|
+
return React.useSyncExternalStore(subscribeToNothing, getClientSnapshot, getServerSnapshot);
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
/**
|
|
2243
|
+
* Every currently active modal, in the order they opened. Only the last entry
|
|
2244
|
+
* — the topmost modal — reacts to Escape and Tab, so one keypress can't
|
|
2245
|
+
* dismiss a whole stack (e.g. a `Dialog` opened on top of a `Modal`).
|
|
2246
|
+
*/
|
|
2247
|
+
const activeModalStack = [];
|
|
2248
|
+
/**
|
|
2249
|
+
* Controls the modal can hand focus to. Disabled and hidden controls are
|
|
2250
|
+
* excluded because `focus()` on them is a no-op, which would leave focus
|
|
2251
|
+
* outside the modal.
|
|
2252
|
+
*/
|
|
2253
|
+
const FOCUSABLE_SELECTOR$1 = 'button:not(:disabled), [href], input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), [tabindex]';
|
|
2254
|
+
/**
|
|
2255
|
+
* The modal's tab stops, in document order. The selector alone is not the
|
|
2256
|
+
* tabbable set — `[href]` and `button` match regardless of `tabindex`, so an
|
|
2257
|
+
* `<a href tabIndex={-1}>` would otherwise be treated as a tab stop and let
|
|
2258
|
+
* Tab escape the modal when it sorts last. `el.tabIndex` is the browser's own
|
|
2259
|
+
* resolved value, so filtering on it drops every negative index (not just
|
|
2260
|
+
* `-1`) without a second selector to keep in sync.
|
|
2261
|
+
*/
|
|
2262
|
+
const getTabbable = (node) => Array.from(node.querySelectorAll(FOCUSABLE_SELECTOR$1)).filter(el => el.tabIndex >= 0);
|
|
2111
2263
|
/**
|
|
2112
2264
|
* Modal.Background - Renders the modal background overlay.
|
|
2113
2265
|
*
|
|
@@ -2238,7 +2390,7 @@ const ModalClose = ({ className, size = 'large', variant = 'delete', ...props })
|
|
|
2238
2390
|
*
|
|
2239
2391
|
* @see {@link https://bulma.io/documentation/components/modal/ | Bulma Modal documentation}
|
|
2240
2392
|
*/
|
|
2241
|
-
const ModalRoot = ({ active, isActive, onClose, className, textColor, bgColor, modalCardTitle, modalCardFoot, type, children, ...props })
|
|
2393
|
+
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
2394
|
const { classPrefix } = useConfig();
|
|
2243
2395
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
2244
2396
|
color: textColor,
|
|
@@ -2247,6 +2399,133 @@ const ModalRoot = ({ active, isActive, onClose, className, textColor, bgColor, m
|
|
|
2247
2399
|
});
|
|
2248
2400
|
// Support both active and isActive props
|
|
2249
2401
|
const isModalActive = active ?? isActive ?? false;
|
|
2402
|
+
const modalRootRef = React.useRef(null);
|
|
2403
|
+
// Cleanup returned by a consumer's callback ref on attach, held until detach.
|
|
2404
|
+
const consumerCleanupRef = React.useRef(null);
|
|
2405
|
+
// The forwarded ref and the internal one must both see the node: focus
|
|
2406
|
+
// management, the scroll lock and the topmost-modal check all read
|
|
2407
|
+
// `modalRootRef`, while consumers expect their own ref to resolve.
|
|
2408
|
+
const combinedRef = React.useCallback((node) => {
|
|
2409
|
+
modalRootRef.current =
|
|
2410
|
+
node;
|
|
2411
|
+
if (typeof ref === 'function') {
|
|
2412
|
+
// React 19 lets a callback ref return a cleanup function and detaches
|
|
2413
|
+
// by running it instead of calling the ref with `null`; React 18
|
|
2414
|
+
// discards the return value entirely. Returning it from here would be
|
|
2415
|
+
// a React-19-only contract, so instead we hold the cleanup and run it
|
|
2416
|
+
// ourselves on detach — a consumer's cleanup ref then behaves the same
|
|
2417
|
+
// on both majors of the CI matrix. Same shape as `Dropdown`.
|
|
2418
|
+
if (node === null) {
|
|
2419
|
+
const consumerCleanup = consumerCleanupRef.current;
|
|
2420
|
+
consumerCleanupRef.current = null;
|
|
2421
|
+
if (consumerCleanup) {
|
|
2422
|
+
consumerCleanup();
|
|
2423
|
+
}
|
|
2424
|
+
else {
|
|
2425
|
+
ref(null);
|
|
2426
|
+
}
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const cleanup = ref(node);
|
|
2430
|
+
consumerCleanupRef.current =
|
|
2431
|
+
typeof cleanup === 'function' ? cleanup : null;
|
|
2432
|
+
}
|
|
2433
|
+
else if (ref) {
|
|
2434
|
+
ref.current = node;
|
|
2435
|
+
}
|
|
2436
|
+
}, [ref]);
|
|
2437
|
+
const previouslyFocusedRef = React.useRef(null);
|
|
2438
|
+
const stackTokenRef = React.useRef({});
|
|
2439
|
+
const generatedTitleId = React.useId();
|
|
2440
|
+
// A portal has no server-rendered counterpart, so the server render and the
|
|
2441
|
+
// hydrating render have to stay inline and only move into the portal once
|
|
2442
|
+
// we're past hydration.
|
|
2443
|
+
const onClient = useIsHydrated();
|
|
2444
|
+
// Moving into the portal remounts the modal's subtree, so any effect holding
|
|
2445
|
+
// a DOM node from before the move is holding a detached one. Effects that
|
|
2446
|
+
// touch the modal's nodes key on this so they re-run against the new tree.
|
|
2447
|
+
const isPortaled = Boolean(portal) && onClient;
|
|
2448
|
+
useScrollLock(isModalActive && lockScroll);
|
|
2449
|
+
// Record open order so keyboard handling only applies to the topmost modal.
|
|
2450
|
+
React.useEffect(() => {
|
|
2451
|
+
if (!isModalActive)
|
|
2452
|
+
return undefined;
|
|
2453
|
+
const token = stackTokenRef.current;
|
|
2454
|
+
activeModalStack.push(token);
|
|
2455
|
+
return () => {
|
|
2456
|
+
const index = activeModalStack.indexOf(token);
|
|
2457
|
+
if (index !== -1)
|
|
2458
|
+
activeModalStack.splice(index, 1);
|
|
2459
|
+
};
|
|
2460
|
+
}, [isModalActive]);
|
|
2461
|
+
// Escape closes; Tab cycles inside the modal. Both only for the topmost one.
|
|
2462
|
+
React.useEffect(() => {
|
|
2463
|
+
if (!isModalActive)
|
|
2464
|
+
return undefined;
|
|
2465
|
+
const handleKeyDown = (e) => {
|
|
2466
|
+
const node = modalRootRef.current;
|
|
2467
|
+
if (activeModalStack[activeModalStack.length - 1] !== stackTokenRef.current) {
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
if (e.key === 'Escape') {
|
|
2471
|
+
if (closeOnEscape)
|
|
2472
|
+
onClose?.();
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
if (e.key !== 'Tab' || !node)
|
|
2476
|
+
return;
|
|
2477
|
+
// Keep Tab within the modal — `aria-modal` hides the rest of the page
|
|
2478
|
+
// from assistive technology, so the keyboard order has to agree.
|
|
2479
|
+
const focusable = getTabbable(node);
|
|
2480
|
+
const activeElement = document.activeElement;
|
|
2481
|
+
if (focusable.length === 0) {
|
|
2482
|
+
e.preventDefault();
|
|
2483
|
+
node.focus();
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
const first = focusable[0];
|
|
2487
|
+
const last = focusable[focusable.length - 1];
|
|
2488
|
+
const outside = !node.contains(activeElement);
|
|
2489
|
+
if (e.shiftKey) {
|
|
2490
|
+
if (outside || activeElement === first) {
|
|
2491
|
+
e.preventDefault();
|
|
2492
|
+
last.focus();
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
else if (outside || activeElement === last) {
|
|
2496
|
+
e.preventDefault();
|
|
2497
|
+
first.focus();
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
2501
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
2502
|
+
}, [isModalActive, closeOnEscape, onClose]);
|
|
2503
|
+
// Move focus into the modal on open, restore it on close
|
|
2504
|
+
React.useEffect(() => {
|
|
2505
|
+
if (!isModalActive)
|
|
2506
|
+
return undefined;
|
|
2507
|
+
previouslyFocusedRef.current = document.activeElement;
|
|
2508
|
+
const node = modalRootRef.current;
|
|
2509
|
+
const focusable = node ? getTabbable(node)[0] : undefined;
|
|
2510
|
+
(focusable ?? node)?.focus();
|
|
2511
|
+
return () => {
|
|
2512
|
+
// Only hand focus back if this modal still owns it: closing a background
|
|
2513
|
+
// modal must not pull focus out of one that is still open on top. A
|
|
2514
|
+
// removed subtree leaves focus on <body>, which still counts as ours.
|
|
2515
|
+
const activeElement = document.activeElement;
|
|
2516
|
+
if (activeElement &&
|
|
2517
|
+
activeElement !== document.body &&
|
|
2518
|
+
!node?.contains(activeElement)) {
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
previouslyFocusedRef.current?.focus?.();
|
|
2522
|
+
};
|
|
2523
|
+
// `isPortaled` flips once when a hydrated portal modal moves out of the
|
|
2524
|
+
// inline tree; re-running rebinds focus onto the remounted nodes. The
|
|
2525
|
+
// cleanup above restores focus to the pre-open element first (the detached
|
|
2526
|
+
// subtree has left focus on <body>), so the re-run re-records the same
|
|
2527
|
+
// element and restore-on-close still lands in the right place.
|
|
2528
|
+
}, [isModalActive, isPortaled]);
|
|
2250
2529
|
// Check if children contain compound components
|
|
2251
2530
|
const hasCompoundComponents = React.Children.toArray(children).some(child => React.isValidElement(child) &&
|
|
2252
2531
|
(child.type === ModalBackground ||
|
|
@@ -2259,20 +2538,35 @@ const ModalRoot = ({ active, isActive, onClose, className, textColor, bgColor, m
|
|
|
2259
2538
|
});
|
|
2260
2539
|
const deleteClass = usePrefixedClassNames('delete');
|
|
2261
2540
|
const modalClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
2541
|
+
const resolvedRole = role ?? (isModalActive ? 'dialog' : undefined);
|
|
2542
|
+
const resolvedAriaModal = ariaModalProp ??
|
|
2543
|
+
(isModalActive && resolvedRole !== 'presentation' ? 'true' : undefined);
|
|
2544
|
+
// A dialog needs an accessible name: the legacy card title is wired up
|
|
2545
|
+
// automatically, compound users pass their own aria-label/aria-labelledby
|
|
2546
|
+
// (both arrive in `rest`, which is spread last and so wins).
|
|
2547
|
+
const titleId = modalCardTitle ? generatedTitleId : undefined;
|
|
2548
|
+
let modalElement;
|
|
2262
2549
|
// If using compound components, render children as-is
|
|
2263
2550
|
if (hasCompoundComponents) {
|
|
2264
|
-
|
|
2551
|
+
modalElement = (jsxRuntime.jsx("div", { className: modalClasses, ref: combinedRef, role: resolvedRole, "aria-modal": resolvedAriaModal, tabIndex: -1, ...rest, "data-testid": "modal", children: children }));
|
|
2265
2552
|
}
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
};
|
|
2553
|
+
else {
|
|
2554
|
+
// Legacy API: EXPLICIT type wins; fallback to auto detection if not provided
|
|
2555
|
+
let isModalCard;
|
|
2556
|
+
if (type === 'card')
|
|
2557
|
+
isModalCard = true;
|
|
2558
|
+
else if (type === 'content')
|
|
2559
|
+
isModalCard = false;
|
|
2560
|
+
else
|
|
2561
|
+
isModalCard = !!modalCardTitle || !!modalCardFoot;
|
|
2562
|
+
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" }))] }));
|
|
2563
|
+
}
|
|
2564
|
+
if (isPortaled) {
|
|
2565
|
+
const target = resolvePortalContainer(typeof portal === 'boolean' ? undefined : portal);
|
|
2566
|
+
return reactDom.createPortal(modalElement, target);
|
|
2567
|
+
}
|
|
2568
|
+
return modalElement;
|
|
2569
|
+
});
|
|
2276
2570
|
const Modal = withSubComponents(ModalRoot, {
|
|
2277
2571
|
Background: ModalBackground,
|
|
2278
2572
|
Content: ModalContent,
|
|
@@ -2286,10 +2580,11 @@ const NavbarDropdownContext = React.createContext(null);
|
|
|
2286
2580
|
*
|
|
2287
2581
|
* @function
|
|
2288
2582
|
* @param {NavbarProps} props - Props for the Navbar component.
|
|
2583
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the root `<nav>` element.
|
|
2289
2584
|
* @returns {JSX.Element} The rendered navbar.
|
|
2290
2585
|
* @see {@link https://bulma.io/documentation/components/navbar/ | Bulma Navbar documentation}
|
|
2291
2586
|
*/
|
|
2292
|
-
const NavbarComponent = ({ className, textColor, bgColor, color, transparent, fixed, children, ...props })
|
|
2587
|
+
const NavbarComponent = React.forwardRef(function NavbarComponent({ className, textColor, bgColor, color, transparent, fixed, children, ...props }, ref) {
|
|
2293
2588
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
2294
2589
|
color: textColor,
|
|
2295
2590
|
backgroundColor: bgColor,
|
|
@@ -2302,8 +2597,8 @@ const NavbarComponent = ({ className, textColor, bgColor, color, transparent, fi
|
|
|
2302
2597
|
[`is-fixed-${fixed}`]: fixed,
|
|
2303
2598
|
});
|
|
2304
2599
|
const navbarClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
2305
|
-
return (jsxRuntime.jsx("nav", { className: navbarClasses, role: "navigation", "aria-label": "main navigation", ...rest, children: children }));
|
|
2306
|
-
};
|
|
2600
|
+
return (jsxRuntime.jsx("nav", { ref: ref, className: navbarClasses, role: "navigation", "aria-label": "main navigation", ...rest, children: children }));
|
|
2601
|
+
});
|
|
2307
2602
|
/**
|
|
2308
2603
|
* For logo and branding (left side)
|
|
2309
2604
|
*
|
|
@@ -2323,33 +2618,38 @@ const NavbarBrand = ({ className, children, textColor, ...props }) => {
|
|
|
2323
2618
|
*
|
|
2324
2619
|
* @function
|
|
2325
2620
|
* @param {NavbarItemProps} props - Props for the NavbarItem component.
|
|
2621
|
+
* @param {React.Ref} ref - Forwarded ref to the element `as` renders.
|
|
2326
2622
|
* @returns {JSX.Element} The rendered item.
|
|
2327
2623
|
*/
|
|
2328
|
-
const NavbarItem = (
|
|
2624
|
+
const NavbarItem = React.forwardRef(function NavbarItem(itemProps, ref) {
|
|
2625
|
+
const { className, as: Component = 'a', active, textColor, bgColor, children, ...props } = itemProps;
|
|
2329
2626
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
2330
2627
|
color: textColor,
|
|
2331
2628
|
backgroundColor: bgColor,
|
|
2332
2629
|
...props,
|
|
2333
2630
|
});
|
|
2334
|
-
return (jsxRuntime.jsx(Component, { className: classNames(usePrefixedClassNames('navbar-item', {
|
|
2631
|
+
return (jsxRuntime.jsx(Component, { ref: ref, className: classNames(usePrefixedClassNames('navbar-item', {
|
|
2335
2632
|
'is-active': active,
|
|
2336
2633
|
}), bulmaHelperClasses, className), ...rest, children: children }));
|
|
2337
|
-
};
|
|
2634
|
+
});
|
|
2635
|
+
NavbarItem.displayName = 'NavbarItem';
|
|
2338
2636
|
/**
|
|
2339
2637
|
* Responsive menu toggle (mobile)
|
|
2340
2638
|
*
|
|
2341
2639
|
* @function
|
|
2342
2640
|
* @param {NavbarBurgerProps} props - Props for the NavbarBurger component.
|
|
2641
|
+
* @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the burger button element.
|
|
2343
2642
|
* @returns {JSX.Element} The rendered burger.
|
|
2344
2643
|
*/
|
|
2345
|
-
const NavbarBurger = ({ className, active, children, ...props })
|
|
2644
|
+
const NavbarBurger = React.forwardRef(function NavbarBurger({ className, active, children, ...props }, ref) {
|
|
2346
2645
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
2347
2646
|
...props,
|
|
2348
2647
|
});
|
|
2349
|
-
return (jsxRuntime.jsxs("button", { type: "button", className: classNames(usePrefixedClassNames('navbar-burger', {
|
|
2648
|
+
return (jsxRuntime.jsxs("button", { ref: ref, type: "button", className: classNames(usePrefixedClassNames('navbar-burger', {
|
|
2350
2649
|
'is-active': active,
|
|
2351
2650
|
}), 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
|
-
};
|
|
2651
|
+
});
|
|
2652
|
+
NavbarBurger.displayName = 'NavbarBurger';
|
|
2353
2653
|
/**
|
|
2354
2654
|
* Collapsible content (contains `Navbar.Start` and `Navbar.End`)
|
|
2355
2655
|
*
|
|
@@ -2396,9 +2696,11 @@ const NavbarEnd = ({ className, children, ...props }) => {
|
|
|
2396
2696
|
*
|
|
2397
2697
|
* @function
|
|
2398
2698
|
* @param {NavbarLinkProps} props - Props for the NavbarLink component.
|
|
2699
|
+
* @param {React.Ref} ref - Forwarded ref to the element `as` renders.
|
|
2399
2700
|
* @returns {JSX.Element} The rendered navbar link.
|
|
2400
2701
|
*/
|
|
2401
|
-
const NavbarLink = (
|
|
2702
|
+
const NavbarLink = React.forwardRef(function NavbarLink(linkProps, ref) {
|
|
2703
|
+
const { className, as: Component = 'a', arrowless, textColor, bgColor, children, ...props } = linkProps;
|
|
2402
2704
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
2403
2705
|
color: textColor,
|
|
2404
2706
|
backgroundColor: bgColor,
|
|
@@ -2407,8 +2709,16 @@ const NavbarLink = ({ className, as: Component = 'a', arrowless, textColor, bgCo
|
|
|
2407
2709
|
const dropdownContext = React.useContext(NavbarDropdownContext);
|
|
2408
2710
|
const hasHref = rest.href !== undefined;
|
|
2409
2711
|
const isNativeInteractive = Component === 'button' || hasHref;
|
|
2712
|
+
// Guarded like Button's disabled blocker: these run on whatever `as` renders,
|
|
2713
|
+
// and a custom target's callback need not take a DOM event. One declaring
|
|
2714
|
+
// `onKeyDown: (k: {key: string}) => void` reaches `e.preventDefault()` and
|
|
2715
|
+
// throws. Composing the caller's handler is unconditional; acting on the
|
|
2716
|
+
// event is not ours to do when the payload is foreign.
|
|
2717
|
+
const isDomEvent = (e) => typeof e?.preventDefault === 'function';
|
|
2410
2718
|
const handleKeyDown = (e) => {
|
|
2411
2719
|
rest.onKeyDown?.(e);
|
|
2720
|
+
if (!isDomEvent(e))
|
|
2721
|
+
return;
|
|
2412
2722
|
if (!dropdownContext || e.defaultPrevented)
|
|
2413
2723
|
return;
|
|
2414
2724
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
@@ -2427,11 +2737,13 @@ const NavbarLink = ({ className, as: Component = 'a', arrowless, textColor, bgCo
|
|
|
2427
2737
|
// click semantics (navigation / the caller's handler).
|
|
2428
2738
|
const handleClick = (e) => {
|
|
2429
2739
|
rest.onClick?.(e);
|
|
2740
|
+
if (!isDomEvent(e))
|
|
2741
|
+
return;
|
|
2430
2742
|
if (!dropdownContext || isNativeInteractive || e.defaultPrevented)
|
|
2431
2743
|
return;
|
|
2432
2744
|
dropdownContext.toggle();
|
|
2433
2745
|
};
|
|
2434
|
-
return (jsxRuntime.jsx(Component, { className: classNames(usePrefixedClassNames('navbar-link', {
|
|
2746
|
+
return (jsxRuntime.jsx(Component, { ref: ref, className: classNames(usePrefixedClassNames('navbar-link', {
|
|
2435
2747
|
'is-arrowless': arrowless,
|
|
2436
2748
|
}), bulmaHelperClasses, className), ...rest, ...(dropdownContext && {
|
|
2437
2749
|
'aria-haspopup': 'true',
|
|
@@ -2443,15 +2755,17 @@ const NavbarLink = ({ className, as: Component = 'a', arrowless, textColor, bgCo
|
|
|
2443
2755
|
onClick: handleClick,
|
|
2444
2756
|
}),
|
|
2445
2757
|
}), children: children }));
|
|
2446
|
-
};
|
|
2758
|
+
});
|
|
2759
|
+
NavbarLink.displayName = 'NavbarLink';
|
|
2447
2760
|
/**
|
|
2448
2761
|
* Dropdown parent (with options for hover, up, right, active)
|
|
2449
2762
|
*
|
|
2450
2763
|
* @function
|
|
2451
2764
|
* @param {NavbarDropdownProps} props - Props for the NavbarDropdown component.
|
|
2765
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the dropdown container element.
|
|
2452
2766
|
* @returns {JSX.Element} The rendered dropdown.
|
|
2453
2767
|
*/
|
|
2454
|
-
const NavbarDropdown = ({ className, right, up, hoverable, active: activeProp, onActiveChange, children, ...props })
|
|
2768
|
+
const NavbarDropdown = React.forwardRef(function NavbarDropdown({ className, right, up, hoverable, active: activeProp, onActiveChange, children, ...props }, ref) {
|
|
2455
2769
|
const [active, setActive] = React.useState(!!activeProp);
|
|
2456
2770
|
React.useEffect(() => {
|
|
2457
2771
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncing to controlled prop
|
|
@@ -2472,13 +2786,14 @@ const NavbarDropdown = ({ className, right, up, hoverable, active: activeProp, o
|
|
|
2472
2786
|
setActive(false);
|
|
2473
2787
|
onActiveChange?.(false);
|
|
2474
2788
|
};
|
|
2475
|
-
return (jsxRuntime.jsx(NavbarDropdownContext.Provider, { value: { active, toggle, close }, children: jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('navbar-item', 'has-dropdown', {
|
|
2789
|
+
return (jsxRuntime.jsx(NavbarDropdownContext.Provider, { value: { active, toggle, close }, children: jsxRuntime.jsx("div", { ref: ref, className: classNames(usePrefixedClassNames('navbar-item', 'has-dropdown', {
|
|
2476
2790
|
'has-dropdown-up': up,
|
|
2477
2791
|
'is-right': right,
|
|
2478
2792
|
'is-hoverable': hoverable,
|
|
2479
2793
|
'is-active': active,
|
|
2480
2794
|
}), className), ...props, children: children }) }));
|
|
2481
|
-
};
|
|
2795
|
+
});
|
|
2796
|
+
NavbarDropdown.displayName = 'NavbarDropdown';
|
|
2482
2797
|
/**
|
|
2483
2798
|
* Dropdown menu container
|
|
2484
2799
|
*
|
|
@@ -3248,16 +3563,10 @@ const Loading = ({ active = false, isFullPage = false, size, color, canCancel =
|
|
|
3248
3563
|
document.addEventListener('keydown', handleKeyDown);
|
|
3249
3564
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
3250
3565
|
}, [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]);
|
|
3566
|
+
// Prevent body scroll when full page loading is active. Ref-counted through
|
|
3567
|
+
// the shared helper so an overlapping Modal/Dialog/Sidebar doesn't have its
|
|
3568
|
+
// lock cleared when this one releases.
|
|
3569
|
+
useScrollLock(isFullPage && active);
|
|
3261
3570
|
if (!active) {
|
|
3262
3571
|
return null;
|
|
3263
3572
|
}
|
|
@@ -3748,6 +4057,8 @@ const Steps = withSubComponents(StepsComponent, { Step }, 'Steps');
|
|
|
3748
4057
|
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
4058
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
3750
4059
|
const sidebarRef = React.useRef(null);
|
|
4060
|
+
// Cleanup returned by a consumer's callback ref on attach, held until detach.
|
|
4061
|
+
const consumerCleanupRef = React.useRef(null);
|
|
3751
4062
|
const resolvedFullwidth = isFullwidth ?? fullWidth ?? false;
|
|
3752
4063
|
// Close handler
|
|
3753
4064
|
const handleClose = React.useCallback(() => {
|
|
@@ -3773,17 +4084,9 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
3773
4084
|
document.addEventListener('keydown', handleKeyDown);
|
|
3774
4085
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
3775
4086
|
}, [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]);
|
|
4087
|
+
// Prevent body scroll when sidebar is open. Ref-counted through the shared
|
|
4088
|
+
// helper so an overlapping Modal/Dialog/Loading doesn't unlock underneath.
|
|
4089
|
+
useScrollLock(isOpen && overlay);
|
|
3787
4090
|
// Focus trap (basic - focus sidebar when opened)
|
|
3788
4091
|
React.useEffect(() => {
|
|
3789
4092
|
if (isOpen) {
|
|
@@ -3805,7 +4108,28 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
3805
4108
|
sidebarRef.current =
|
|
3806
4109
|
node;
|
|
3807
4110
|
if (typeof ref === 'function') {
|
|
3808
|
-
ref
|
|
4111
|
+
// React 19 lets a callback ref return a cleanup function and detaches by
|
|
4112
|
+
// running it instead of calling the ref with `null`; React 18 discards the
|
|
4113
|
+
// return value entirely. Returning it from here would be a React-19-only
|
|
4114
|
+
// contract, so we hold the cleanup and run it ourselves on detach — the
|
|
4115
|
+
// same shape as `Dropdown`, `Modal`, `Dialog`, `Toast` and `Carousel`.
|
|
4116
|
+
// The form controls that merge a forwarded ref still discard the cleanup
|
|
4117
|
+
// and detach with `ref(null)`, so this is not yet library-wide — grep
|
|
4118
|
+
// `typeof ref === 'function'` for the current split.
|
|
4119
|
+
if (node === null) {
|
|
4120
|
+
const consumerCleanup = consumerCleanupRef.current;
|
|
4121
|
+
consumerCleanupRef.current = null;
|
|
4122
|
+
if (consumerCleanup) {
|
|
4123
|
+
consumerCleanup();
|
|
4124
|
+
}
|
|
4125
|
+
else {
|
|
4126
|
+
ref(null);
|
|
4127
|
+
}
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
const cleanup = ref(node);
|
|
4131
|
+
consumerCleanupRef.current =
|
|
4132
|
+
typeof cleanup === 'function' ? cleanup : null;
|
|
3809
4133
|
}
|
|
3810
4134
|
else if (ref) {
|
|
3811
4135
|
ref.current = node;
|
|
@@ -3922,6 +4246,8 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
|
|
|
3922
4246
|
const [isVisible, setIsVisible] = React.useState(true);
|
|
3923
4247
|
const [isPaused, setIsPaused] = React.useState(false);
|
|
3924
4248
|
const toastRef = React.useRef(null);
|
|
4249
|
+
// Cleanup returned by a consumer's callback ref on attach, held until detach.
|
|
4250
|
+
const consumerCleanupRef = React.useRef(null);
|
|
3925
4251
|
const handleClose = React.useCallback(() => {
|
|
3926
4252
|
setIsVisible(false);
|
|
3927
4253
|
onClose?.();
|
|
@@ -3992,27 +4318,41 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
|
|
|
3992
4318
|
const toastActionClass = usePrefixedClassNames('toast-action');
|
|
3993
4319
|
const toastButtonClass = usePrefixedClassNames('button');
|
|
3994
4320
|
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) => {
|
|
4321
|
+
const setRef = React.useCallback((node) => {
|
|
4008
4322
|
toastRef.current = node;
|
|
4009
4323
|
if (typeof ref === 'function') {
|
|
4010
|
-
ref
|
|
4324
|
+
// React 19 lets a callback ref return a cleanup function and detaches by
|
|
4325
|
+
// running it instead of calling the ref with `null`; React 18 discards the
|
|
4326
|
+
// return value entirely. Returning it from here would be a React-19-only
|
|
4327
|
+
// contract, so we hold the cleanup and run it ourselves on detach — the
|
|
4328
|
+
// same shape as `Dropdown`, `Modal`, `Dialog`, `Sidebar` and `Carousel`.
|
|
4329
|
+
// The form controls that merge a forwarded ref still discard the cleanup
|
|
4330
|
+
// and detach with `ref(null)`, so this is not yet library-wide — grep
|
|
4331
|
+
// `typeof ref === 'function'` for the current split. Memoized because an
|
|
4332
|
+
// unstable ref identity makes React detach and re-attach on every render,
|
|
4333
|
+
// which would run that cleanup each time.
|
|
4334
|
+
if (node === null) {
|
|
4335
|
+
const consumerCleanup = consumerCleanupRef.current;
|
|
4336
|
+
consumerCleanupRef.current = null;
|
|
4337
|
+
if (consumerCleanup) {
|
|
4338
|
+
consumerCleanup();
|
|
4339
|
+
}
|
|
4340
|
+
else {
|
|
4341
|
+
ref(null);
|
|
4342
|
+
}
|
|
4343
|
+
return;
|
|
4344
|
+
}
|
|
4345
|
+
const cleanup = ref(node);
|
|
4346
|
+
consumerCleanupRef.current =
|
|
4347
|
+
typeof cleanup === 'function' ? cleanup : null;
|
|
4011
4348
|
}
|
|
4012
4349
|
else if (ref) {
|
|
4013
4350
|
ref.current = node;
|
|
4014
4351
|
}
|
|
4015
|
-
};
|
|
4352
|
+
}, [ref]);
|
|
4353
|
+
if (!isVisible) {
|
|
4354
|
+
return null;
|
|
4355
|
+
}
|
|
4016
4356
|
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
4357
|
e.stopPropagation();
|
|
4018
4358
|
handleClose();
|
|
@@ -4028,7 +4368,7 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
|
|
|
4028
4368
|
}
|
|
4029
4369
|
const toastContent = jsxRuntime.jsx("div", { className: containerClasses, children: toastElement });
|
|
4030
4370
|
if (typeof document !== 'undefined') {
|
|
4031
|
-
return reactDom.createPortal(toastContent,
|
|
4371
|
+
return reactDom.createPortal(toastContent, resolvePortalContainer(container));
|
|
4032
4372
|
}
|
|
4033
4373
|
return null;
|
|
4034
4374
|
});
|
|
@@ -4168,9 +4508,6 @@ const ToastContainer = ({ position = 'top-right', }) => {
|
|
|
4168
4508
|
}) }), document.body);
|
|
4169
4509
|
};
|
|
4170
4510
|
|
|
4171
|
-
// Ref-counted body scroll lock for chained/overlapping dialogs
|
|
4172
|
-
let _scrollLockCount = 0;
|
|
4173
|
-
let _originalOverflow = '';
|
|
4174
4511
|
/**
|
|
4175
4512
|
* The `Dialog` component provides ready-made confirm and alert dialogs, so a destructive action stays one `await dialog.confirm()` call away.
|
|
4176
4513
|
*
|
|
@@ -4202,9 +4539,11 @@ let _originalOverflow = '';
|
|
|
4202
4539
|
* onCancel={() => setShowConfirm(false)}
|
|
4203
4540
|
* />
|
|
4204
4541
|
*/
|
|
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) => {
|
|
4542
|
+
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
4543
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
4207
4544
|
const dialogRef = React.useRef(null);
|
|
4545
|
+
// Cleanup returned by a consumer's callback ref on attach, held until detach.
|
|
4546
|
+
const consumerCleanupRef = React.useRef(null);
|
|
4208
4547
|
const confirmRef = React.useRef(null);
|
|
4209
4548
|
const cancelRef = React.useRef(null);
|
|
4210
4549
|
// Handle cancel
|
|
@@ -4223,48 +4562,47 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
|
|
|
4223
4562
|
handleCancel();
|
|
4224
4563
|
}
|
|
4225
4564
|
}, [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
|
|
4565
|
+
// Moving into the portal remounts the Modal's subtree — these buttons
|
|
4566
|
+
// included — so the focus effect below has to re-run against the new
|
|
4567
|
+
// nodes, exactly as the Modal's own focus effect does.
|
|
4568
|
+
const isHydrated = useIsHydrated();
|
|
4569
|
+
const isPortaled = Boolean(portal) && isHydrated;
|
|
4570
|
+
// Focus management — Dialog picks a specific button rather than the
|
|
4571
|
+
// Modal's default (first focusable/root); this effect runs after the
|
|
4572
|
+
// inner Modal's own focus-on-open effect, so it wins.
|
|
4239
4573
|
React.useEffect(() => {
|
|
4240
4574
|
if (isOpen) {
|
|
4241
4575
|
const buttonToFocus = focusCancel && showCancel ? cancelRef.current : confirmRef.current;
|
|
4242
4576
|
buttonToFocus?.focus();
|
|
4243
4577
|
}
|
|
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]);
|
|
4578
|
+
}, [isOpen, focusCancel, showCancel, isPortaled]);
|
|
4262
4579
|
// Use combined ref
|
|
4263
4580
|
const combinedRef = React.useCallback((node) => {
|
|
4264
4581
|
dialogRef.current =
|
|
4265
4582
|
node;
|
|
4266
4583
|
if (typeof ref === 'function') {
|
|
4267
|
-
ref
|
|
4584
|
+
// React 19 lets a callback ref return a cleanup function and detaches by
|
|
4585
|
+
// running it instead of calling the ref with `null`; React 18 discards the
|
|
4586
|
+
// return value entirely. Returning it from here would be a React-19-only
|
|
4587
|
+
// contract, so we hold the cleanup and run it ourselves on detach — the
|
|
4588
|
+
// same shape as `Dropdown`, `Modal`, `Sidebar`, `Toast` and `Carousel`.
|
|
4589
|
+
// The form controls that merge a forwarded ref still discard the cleanup
|
|
4590
|
+
// and detach with `ref(null)`, so this is not yet library-wide — grep
|
|
4591
|
+
// `typeof ref === 'function'` for the current split.
|
|
4592
|
+
if (node === null) {
|
|
4593
|
+
const consumerCleanup = consumerCleanupRef.current;
|
|
4594
|
+
consumerCleanupRef.current = null;
|
|
4595
|
+
if (consumerCleanup) {
|
|
4596
|
+
consumerCleanup();
|
|
4597
|
+
}
|
|
4598
|
+
else {
|
|
4599
|
+
ref(null);
|
|
4600
|
+
}
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
const cleanup = ref(node);
|
|
4604
|
+
consumerCleanupRef.current =
|
|
4605
|
+
typeof cleanup === 'function' ? cleanup : null;
|
|
4268
4606
|
}
|
|
4269
4607
|
else if (ref) {
|
|
4270
4608
|
ref.current = node;
|
|
@@ -4307,7 +4645,7 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
|
|
|
4307
4645
|
if (!isOpen) {
|
|
4308
4646
|
return null;
|
|
4309
4647
|
}
|
|
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 })] })] })] }));
|
|
4648
|
+
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
4649
|
});
|
|
4312
4650
|
Dialog.displayName = 'Dialog';
|
|
4313
4651
|
let dialogListeners = new Set();
|
|
@@ -4390,14 +4728,16 @@ const validButtonColors = [...validColors, 'text', 'ghost'];
|
|
|
4390
4728
|
*
|
|
4391
4729
|
* @function
|
|
4392
4730
|
* @param {ButtonProps} props - Props for the Button component.
|
|
4393
|
-
* @
|
|
4731
|
+
* @param {React.Ref} ref - Forwarded ref to the element `as` renders.
|
|
4732
|
+
* @returns {JSX.Element} The rendered button, anchor, or custom element.
|
|
4394
4733
|
* @see {@link https://bulma.io/documentation/elements/button/ | Bulma Button documentation}
|
|
4395
4734
|
*/
|
|
4396
|
-
const Button = (
|
|
4735
|
+
const Button = React.forwardRef(function Button(props, ref) {
|
|
4736
|
+
const { 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, ...bulmaProps } = props;
|
|
4397
4737
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
4398
4738
|
color: textColor,
|
|
4399
4739
|
backgroundColor: bgColor,
|
|
4400
|
-
...
|
|
4740
|
+
...bulmaProps,
|
|
4401
4741
|
});
|
|
4402
4742
|
// Generate Bulma classes with prefix
|
|
4403
4743
|
const bulmaClasses = usePrefixedClassNames('button', {
|
|
@@ -4419,16 +4759,97 @@ const Button = ({ color, size, isLight, isRounded, isLoading, isStatic, isFullwi
|
|
|
4419
4759
|
// Combine prefixed Bulma classes with unprefixed user className and prefixed helper classes
|
|
4420
4760
|
const buttonClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
4421
4761
|
if (Component !== 'button') {
|
|
4422
|
-
//
|
|
4423
|
-
//
|
|
4424
|
-
//
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4762
|
+
// Strip the form-control attributes a link-like element cannot carry, so a
|
|
4763
|
+
// JavaScript consumer or a spread object doesn't put them on the DOM. The
|
|
4764
|
+
// types no longer allow them through; this is the runtime backstop.
|
|
4765
|
+
//
|
|
4766
|
+
// Only for INTRINSIC tags. A custom component (a router Link, ...) owns its
|
|
4767
|
+
// prop contract, and `ComponentPropsWithoutRef<T>` promises the caller that
|
|
4768
|
+
// its props reach it. Stripping names by their spelling alone broke that
|
|
4769
|
+
// promise silently: `<Button as={Custom} name="x" />` type-checked while
|
|
4770
|
+
// `Custom` never received the `name` it requires.
|
|
4771
|
+
//
|
|
4772
|
+
// `autoFocus` and `type` are deliberately NOT stripped: both are valid on
|
|
4773
|
+
// targets this list was filtering them from. `autoFocus` is a global
|
|
4774
|
+
// attribute (React decides per element whether to honour it, which is not
|
|
4775
|
+
// ours to pre-empt), and `type` on an anchor is the MIME hint that
|
|
4776
|
+
// `ButtonProps<'a'>` now types it as.
|
|
4777
|
+
//
|
|
4778
|
+
// The types already answer this for a TypeScript caller: `ButtonProps<T>`
|
|
4779
|
+
// derives from `ComponentPropsWithoutRef<T>`, so only what the target
|
|
4780
|
+
// accepts can be passed. What is left is a runtime backstop for JavaScript
|
|
4781
|
+
// consumers and spread objects.
|
|
4782
|
+
//
|
|
4783
|
+
// Keep it to the two cases where a stray attribute does something. The
|
|
4784
|
+
// submit-overrides belong to a submit control and never make sense on the
|
|
4785
|
+
// anchor path. `disabled` is the one that is VISIBLE: Bulma styles
|
|
4786
|
+
// `.button[disabled]` (background, border, shadow, opacity), so letting it
|
|
4787
|
+
// through to a `<span>` greys the element out — main stripped it, and not
|
|
4788
|
+
// stripping it would be a silent visual change on a types-only release.
|
|
4789
|
+
//
|
|
4790
|
+
// Everything else (`name`, `value`, `form`) is inert where it does not
|
|
4791
|
+
// belong, and enumerating which tags own it is what kept going wrong:
|
|
4792
|
+
// first all nine names on every non-button tag (so `<Button as="input"
|
|
4793
|
+
// name="query" value="Search">` submitted nothing), then a form-control
|
|
4794
|
+
// allowlist that still lost `formAction` on an input and `value` on an
|
|
4795
|
+
// option. Those now ride on the types instead.
|
|
4796
|
+
//
|
|
4797
|
+
// A custom component is exempt entirely — it owns its prop contract, and
|
|
4798
|
+
// `ComponentPropsWithoutRef<T>` promises the caller its props arrive.
|
|
4799
|
+
//
|
|
4800
|
+
// The elements that own `disabled`, per the HTML spec. A closed set, unlike
|
|
4801
|
+
// the open-ended question of which tag owns which attribute.
|
|
4802
|
+
const SUBMIT_OVERRIDES = [
|
|
4803
|
+
'formAction',
|
|
4804
|
+
'formEncType',
|
|
4805
|
+
'formMethod',
|
|
4806
|
+
'formNoValidate',
|
|
4807
|
+
'formTarget',
|
|
4808
|
+
];
|
|
4809
|
+
const OWNS_DISABLED = [
|
|
4810
|
+
'button',
|
|
4811
|
+
'fieldset',
|
|
4812
|
+
'input',
|
|
4813
|
+
'optgroup',
|
|
4814
|
+
'option',
|
|
4815
|
+
'select',
|
|
4816
|
+
'textarea',
|
|
4817
|
+
];
|
|
4818
|
+
// Two independent filters. They are not exclusive — an `<a>` owns neither
|
|
4819
|
+
// the submit-overrides nor `disabled`, so it takes both — and deriving them
|
|
4820
|
+
// from one destructure, or from one if/else, is what broke each of them in
|
|
4821
|
+
// turn: first the overrides came off every intrinsic (an `as="input"` lost
|
|
4822
|
+
// `formAction`), then scoping them to the anchor let `disabled` back onto
|
|
4823
|
+
// it.
|
|
4824
|
+
const forwardedRest = { ...rest };
|
|
4825
|
+
if (typeof Component === 'string' && !isCustomElement(Component)) {
|
|
4826
|
+
// Meaningless anywhere but a submit control, and an anchor is not one.
|
|
4827
|
+
if (Component === 'a') {
|
|
4828
|
+
for (const key of SUBMIT_OVERRIDES)
|
|
4829
|
+
delete forwardedRest[key];
|
|
4830
|
+
}
|
|
4831
|
+
// Visible via Bulma's `.button[disabled]`, so withheld from any element
|
|
4832
|
+
// that does not own it.
|
|
4833
|
+
if (!OWNS_DISABLED.includes(Component))
|
|
4834
|
+
delete forwardedRest.disabled;
|
|
4835
|
+
}
|
|
4836
|
+
return (jsxRuntime.jsx(Component, { ref: ref, className: buttonClasses, href: href, target: target, rel: rel, "aria-disabled": isDisabled, tabIndex: isDisabled ? -1 : undefined, onClick: isDisabled
|
|
4837
|
+
? // Guarded, because this handler is installed into whatever `as`
|
|
4838
|
+
// renders and a custom component's `onClick` need not be a DOM
|
|
4839
|
+
// event handler at all. One declaring `onClick: (v: string) =>
|
|
4840
|
+
// void` calls this with a string, and an unguarded
|
|
4841
|
+
// `e.preventDefault()` throws `is not a function`. Blocking the
|
|
4842
|
+
// real event is the point; a foreign payload is simply not ours
|
|
4843
|
+
// to act on.
|
|
4844
|
+
(e) => {
|
|
4845
|
+
if (typeof e?.preventDefault === 'function')
|
|
4846
|
+
e.preventDefault();
|
|
4847
|
+
}
|
|
4848
|
+
: onClick, ...forwardedRest, children: children }));
|
|
4429
4849
|
}
|
|
4430
|
-
return (jsxRuntime.jsx("button", { className: buttonClasses, disabled: isDisabled, onClick: onClick, ...rest, children: children }));
|
|
4431
|
-
};
|
|
4850
|
+
return (jsxRuntime.jsx("button", { ref: ref, className: buttonClasses, disabled: isDisabled, onClick: onClick, ...rest, children: children }));
|
|
4851
|
+
});
|
|
4852
|
+
Button.displayName = 'Button';
|
|
4432
4853
|
|
|
4433
4854
|
/**
|
|
4434
4855
|
* Individual carousel item/slide.
|
|
@@ -4487,6 +4908,8 @@ const DefaultNextIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
|
|
|
4487
4908
|
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
4909
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
4489
4910
|
const carouselRef = React.useRef(null);
|
|
4911
|
+
// Cleanup returned by a consumer's callback ref on attach, held until detach.
|
|
4912
|
+
const consumerCleanupRef = React.useRef(null);
|
|
4490
4913
|
const containerRef = React.useRef(null);
|
|
4491
4914
|
const [internalValue, setInternalValue] = React.useState(0);
|
|
4492
4915
|
const [isPaused, setIsPaused] = React.useState(false);
|
|
@@ -4672,7 +5095,28 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
4672
5095
|
carouselRef.current =
|
|
4673
5096
|
node;
|
|
4674
5097
|
if (typeof ref === 'function') {
|
|
4675
|
-
ref
|
|
5098
|
+
// React 19 lets a callback ref return a cleanup function and detaches by
|
|
5099
|
+
// running it instead of calling the ref with `null`; React 18 discards the
|
|
5100
|
+
// return value entirely. Returning it from here would be a React-19-only
|
|
5101
|
+
// contract, so we hold the cleanup and run it ourselves on detach — the
|
|
5102
|
+
// same shape as `Dropdown`, `Modal`, `Dialog`, `Sidebar` and `Toast`.
|
|
5103
|
+
// The form controls that merge a forwarded ref still discard the cleanup
|
|
5104
|
+
// and detach with `ref(null)`, so this is not yet library-wide — grep
|
|
5105
|
+
// `typeof ref === 'function'` for the current split.
|
|
5106
|
+
if (node === null) {
|
|
5107
|
+
const consumerCleanup = consumerCleanupRef.current;
|
|
5108
|
+
consumerCleanupRef.current = null;
|
|
5109
|
+
if (consumerCleanup) {
|
|
5110
|
+
consumerCleanup();
|
|
5111
|
+
}
|
|
5112
|
+
else {
|
|
5113
|
+
ref(null);
|
|
5114
|
+
}
|
|
5115
|
+
return;
|
|
5116
|
+
}
|
|
5117
|
+
const cleanup = ref(node);
|
|
5118
|
+
consumerCleanupRef.current =
|
|
5119
|
+
typeof cleanup === 'function' ? cleanup : null;
|
|
4676
5120
|
}
|
|
4677
5121
|
else if (ref) {
|
|
4678
5122
|
ref.current = node;
|
|
@@ -4790,7 +5234,8 @@ function usePrefersReducedMotion() {
|
|
|
4790
5234
|
* ))}
|
|
4791
5235
|
* </Reveal>
|
|
4792
5236
|
*/
|
|
4793
|
-
|
|
5237
|
+
function RevealImpl(revealProps) {
|
|
5238
|
+
const { animation = 'fade-up', delay = 0, duration = 600, threshold = 0.15, once = true, as: Component = 'div', cascade = false, cascadeInterval = 80, className, style, children, ...props } = revealProps;
|
|
4794
5239
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
4795
5240
|
const classPrefix = useClassPrefix();
|
|
4796
5241
|
const prefersReducedMotion = usePrefersReducedMotion();
|
|
@@ -4867,12 +5312,16 @@ const Reveal = ({ animation = 'fade-up', delay = 0, duration = 600, threshold =
|
|
|
4867
5312
|
return (jsxRuntime.jsx("span", { className: itemAnimationClasses, style: itemStyle, children: child }, index));
|
|
4868
5313
|
})
|
|
4869
5314
|
: children;
|
|
4870
|
-
// Scroll observation needs a
|
|
4871
|
-
//
|
|
4872
|
-
//
|
|
4873
|
-
//
|
|
4874
|
-
//
|
|
4875
|
-
//
|
|
5315
|
+
// Scroll observation needs a DOM node this component OWNS — the observer and
|
|
5316
|
+
// the animation classes both key off it. An intrinsic tag is that node, so
|
|
5317
|
+
// the ref goes straight on it. A component passed via `as` is not: whatever
|
|
5318
|
+
// it renders is its own business, and it may render several elements or
|
|
5319
|
+
// none. So it gets wrapped in an observed `div`.
|
|
5320
|
+
//
|
|
5321
|
+
// Not a statement about ref forwarding. Several components in this library
|
|
5322
|
+
// forward refs (#641 added four more), and that changes nothing here — a
|
|
5323
|
+
// forwarded ref would still point at the target's node rather than at one
|
|
5324
|
+
// this component controls.
|
|
4876
5325
|
if (typeof Component === 'string') {
|
|
4877
5326
|
// A plain createElement call sidesteps the combinatorial JSX prop types
|
|
4878
5327
|
// for `ref` across every possible intrinsic tag `as` could be.
|
|
@@ -4884,7 +5333,9 @@ const Reveal = ({ animation = 'fade-up', delay = 0, duration = 600, threshold =
|
|
|
4884
5333
|
}, content);
|
|
4885
5334
|
}
|
|
4886
5335
|
return (jsxRuntime.jsx("div", { ref: setNode, className: combinedClasses, style: wrapperStyle, children: jsxRuntime.jsx(Component, { ...rest, children: content }) }));
|
|
4887
|
-
}
|
|
5336
|
+
}
|
|
5337
|
+
const Reveal = RevealImpl;
|
|
5338
|
+
Reveal.displayName = 'Reveal';
|
|
4888
5339
|
|
|
4889
5340
|
/**
|
|
4890
5341
|
* The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
|
|
@@ -4937,6 +5388,7 @@ const Box = ({ className, textColor, color, bgColor, hasShadow = true, children,
|
|
|
4937
5388
|
*
|
|
4938
5389
|
* @function
|
|
4939
5390
|
* @param {LinkButtonProps} props - Props for the LinkButton component.
|
|
5391
|
+
* @param {React.Ref} ref - Forwarded ref to the element `as` renders.
|
|
4940
5392
|
* @returns {JSX.Element} The rendered link-styled button element.
|
|
4941
5393
|
*
|
|
4942
5394
|
* @example
|
|
@@ -4947,11 +5399,18 @@ const Box = ({ className, textColor, color, bgColor, hasShadow = true, children,
|
|
|
4947
5399
|
* // Underline variant with color
|
|
4948
5400
|
* <LinkButton variant="underline" color="primary">Learn more</LinkButton>
|
|
4949
5401
|
*/
|
|
4950
|
-
const LinkButton = ({ variant = 'text', color, className, ...props })
|
|
5402
|
+
const LinkButton = React.forwardRef(function LinkButton({ variant = 'text', color, className, ...props }, ref) {
|
|
4951
5403
|
const buttonColor = variant === 'underline' ? 'text' : variant;
|
|
4952
5404
|
const prefixedClasses = usePrefixedClassNames('link-button', color && `link-button-${color}`, variant === 'underline' && 'link-button-underline');
|
|
4953
|
-
return (jsxRuntime.jsx(Button
|
|
4954
|
-
|
|
5405
|
+
return (jsxRuntime.jsx(Button
|
|
5406
|
+
// LinkButton is polymorphic through to Button, which resolves the element
|
|
5407
|
+
// from `as`. TS cannot infer that through the spread below.
|
|
5408
|
+
, {
|
|
5409
|
+
// LinkButton is polymorphic through to Button, which resolves the element
|
|
5410
|
+
// from `as`. TS cannot infer that through the spread below.
|
|
5411
|
+
ref: ref, color: buttonColor, className: classNames(prefixedClasses, className), ...props }));
|
|
5412
|
+
});
|
|
5413
|
+
LinkButton.displayName = 'LinkButton';
|
|
4955
5414
|
|
|
4956
5415
|
const validButtonsSizes = ['small', 'medium', 'large'];
|
|
4957
5416
|
/**
|
|
@@ -5221,10 +5680,12 @@ const Image = ({ as, className, textColor, color, bgColor, size, isRounded, isRe
|
|
|
5221
5680
|
*
|
|
5222
5681
|
* @function
|
|
5223
5682
|
* @param {LinkProps} props - Props for the Link component.
|
|
5224
|
-
* @
|
|
5683
|
+
* @param {React.Ref} ref - Forwarded ref to the element `as` renders.
|
|
5684
|
+
* @returns {JSX.Element} The rendered anchor, or whatever `as` names.
|
|
5225
5685
|
* @see {@link https://bulma.io/documentation/elements/content/ | Bulma Content documentation}
|
|
5226
5686
|
*/
|
|
5227
|
-
const Link = (
|
|
5687
|
+
const Link = React.forwardRef(function Link(linkProps, ref) {
|
|
5688
|
+
const { className, textColor, bgColor, isActive, as: Component = 'a', children, ...props } = linkProps;
|
|
5228
5689
|
/**
|
|
5229
5690
|
* Generates Bulma helper classes and separates out remaining props.
|
|
5230
5691
|
*/
|
|
@@ -5237,8 +5698,9 @@ const Link = ({ className, textColor, bgColor, isActive, as: Component = 'a', ch
|
|
|
5237
5698
|
'is-active': isActive,
|
|
5238
5699
|
});
|
|
5239
5700
|
const linkClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
5240
|
-
return (jsxRuntime.jsx(Component, { className: linkClasses || undefined, ...rest, children: children }));
|
|
5241
|
-
};
|
|
5701
|
+
return (jsxRuntime.jsx(Component, { ref: ref, className: linkClasses || undefined, ...rest, children: children }));
|
|
5702
|
+
});
|
|
5703
|
+
Link.displayName = 'Link';
|
|
5242
5704
|
|
|
5243
5705
|
/**
|
|
5244
5706
|
* The `ListItem` component renders a styled list item (`<li>`) element with Bulma helper class integration.
|