@allxsmith/bestax-bulma 5.18.1 → 5.19.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.
Files changed (76) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/constants.cjs +67 -6
  4. package/dist/constants.cjs.map +1 -1
  5. package/dist/constants.d.cts +30 -6
  6. package/dist/constants.esm.js +65 -7
  7. package/dist/constants.esm.js.map +1 -1
  8. package/dist/extras.css +1 -1
  9. package/dist/extras.css.map +1 -1
  10. package/dist/index.cjs +733 -85
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.esm.js +731 -86
  13. package/dist/index.esm.js.map +1 -1
  14. package/dist/types/components/Avatar.d.ts +3 -0
  15. package/dist/types/components/Card.d.ts +2 -0
  16. package/dist/types/components/Carousel.d.ts +8 -2
  17. package/dist/types/components/Dropdown.d.ts +6 -1
  18. package/dist/types/components/Menu.d.ts +6 -1
  19. package/dist/types/components/Modal.d.ts +2 -0
  20. package/dist/types/components/Navbar.d.ts +24 -3
  21. package/dist/types/components/Sidebar.d.ts +2 -0
  22. package/dist/types/components/Steps.d.ts +5 -1
  23. package/dist/types/components/Toast.d.ts +15 -4
  24. package/dist/types/elements/Delete.d.ts +9 -1
  25. package/dist/types/elements/Link.d.ts +7 -1
  26. package/dist/types/elements/Notification.d.ts +45 -7
  27. package/dist/types/elements/Tag.d.ts +7 -1
  28. package/dist/types/elements/Td.d.ts +21 -2
  29. package/dist/types/form/DateInputBase.d.ts +9 -1
  30. package/dist/types/form/DateTimeInputBase.d.ts +11 -4
  31. package/dist/types/form/TimeInputBase.d.ts +4 -2
  32. package/dist/types/helpers/Theme.d.ts +6 -1
  33. package/dist/types/helpers/bulmaClassHelpers.d.ts +30 -6
  34. package/dist/types/helpers/buttonType.d.ts +19 -0
  35. package/dist/types/helpers/positionStacks.d.ts +33 -0
  36. package/dist/types/helpers/shadowDom.d.ts +54 -0
  37. package/dist/types/helpers/statusRegion.d.ts +61 -0
  38. package/dist/types/helpers/useBulmaClasses.d.ts +1 -1
  39. package/dist/types/helpers/useOtherClasses.d.ts +61 -5
  40. package/dist/types-cjs/components/Avatar.d.ts +3 -0
  41. package/dist/types-cjs/components/Card.d.ts +2 -0
  42. package/dist/types-cjs/components/Carousel.d.ts +8 -2
  43. package/dist/types-cjs/components/Dropdown.d.ts +6 -1
  44. package/dist/types-cjs/components/Menu.d.ts +6 -1
  45. package/dist/types-cjs/components/Modal.d.ts +2 -0
  46. package/dist/types-cjs/components/Navbar.d.ts +24 -3
  47. package/dist/types-cjs/components/Sidebar.d.ts +2 -0
  48. package/dist/types-cjs/components/Steps.d.ts +5 -1
  49. package/dist/types-cjs/components/Toast.d.ts +15 -4
  50. package/dist/types-cjs/elements/Delete.d.ts +9 -1
  51. package/dist/types-cjs/elements/Link.d.ts +7 -1
  52. package/dist/types-cjs/elements/Notification.d.ts +45 -7
  53. package/dist/types-cjs/elements/Tag.d.ts +7 -1
  54. package/dist/types-cjs/elements/Td.d.ts +21 -2
  55. package/dist/types-cjs/form/DateInputBase.d.ts +9 -1
  56. package/dist/types-cjs/form/DateTimeInputBase.d.ts +11 -4
  57. package/dist/types-cjs/form/TimeInputBase.d.ts +4 -2
  58. package/dist/types-cjs/helpers/Theme.d.ts +6 -1
  59. package/dist/types-cjs/helpers/bulmaClassHelpers.d.ts +30 -6
  60. package/dist/types-cjs/helpers/buttonType.d.ts +19 -0
  61. package/dist/types-cjs/helpers/positionStacks.d.ts +33 -0
  62. package/dist/types-cjs/helpers/shadowDom.d.ts +54 -0
  63. package/dist/types-cjs/helpers/statusRegion.d.ts +61 -0
  64. package/dist/types-cjs/helpers/useBulmaClasses.d.ts +1 -1
  65. package/dist/types-cjs/helpers/useOtherClasses.d.ts +61 -5
  66. package/dist/versions/bestax-no-dark-mode.css +1 -1
  67. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  68. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  69. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  70. package/dist/versions/bestax-no-helpers.css +1 -1
  71. package/dist/versions/bestax-no-helpers.css.map +1 -1
  72. package/dist/versions/bestax-prefixed.css +1 -1
  73. package/dist/versions/bestax-prefixed.css.map +1 -1
  74. package/package.json +1 -1
  75. package/src/scss/form/_dateinput.scss +36 -10
  76. package/src/scss/form/_timeinput.scss +30 -9
package/dist/index.esm.js CHANGED
@@ -427,10 +427,59 @@ const validViewports = [
427
427
  */
428
428
  const validFloats = ['left', 'right'];
429
429
  /**
430
- * Valid Bulma overflow classes.
431
- * @example 'clipped'
430
+ * Valid Bulma overflow values for one axis, taken by `overflowX` and
431
+ * `overflowY`. Each renders `is-overflow-x-<value>` or `is-overflow-y-<value>`.
432
+ * @example 'auto', 'hidden', 'scroll'
432
433
  */
433
- const validOverflows = ['clipped'];
434
+ const validAxisOverflows = [
435
+ 'auto',
436
+ 'clip',
437
+ 'hidden',
438
+ 'scroll',
439
+ 'visible',
440
+ ];
441
+ /**
442
+ * Valid Bulma overflow values for `overflow`.
443
+ *
444
+ * `clipped` renders `is-clipped`, the helper `overflow` started with. Every
445
+ * other value renders `is-overflow-<value>`.
446
+ * @example 'clipped', 'auto', 'hidden'
447
+ */
448
+ const validOverflows = ['clipped', ...validAxisOverflows];
449
+ /**
450
+ * Valid Bulma position values, taken by `pos`. Each renders
451
+ * `is-position-<value>`.
452
+ * @example 'relative', 'absolute', 'sticky'
453
+ */
454
+ const validPositions = [
455
+ 'absolute',
456
+ 'fixed',
457
+ 'relative',
458
+ 'static',
459
+ 'sticky',
460
+ ];
461
+ /**
462
+ * Valid Bulma aspect ratios, taken by `aspectRatio`. Each renders
463
+ * `is-aspect-ratio-<value>`.
464
+ * @example '1by1', '4by3', '16by9'
465
+ */
466
+ const validAspectRatios = [
467
+ '1by1',
468
+ '5by4',
469
+ '4by3',
470
+ '3by2',
471
+ '5by3',
472
+ '16by9',
473
+ '2by1',
474
+ '3by1',
475
+ '4by5',
476
+ '3by4',
477
+ '2by3',
478
+ '3by5',
479
+ '9by16',
480
+ '1by2',
481
+ '1by3',
482
+ ];
434
483
  /**
435
484
  * Valid Bulma interaction classes.
436
485
  * @example 'unselectable', 'clickable'
@@ -458,10 +507,19 @@ const cursorClasses = {
458
507
  help: 'is-cursor-help',
459
508
  };
460
509
  /**
461
- * Valid Bulma border-radius helper classes.
462
- * @example 'radiusless'
510
+ * Valid Bulma border-radius helper values.
511
+ *
512
+ * `radiusless` renders `is-radiusless` and removes the radius. The sizes
513
+ * render `has-radius-<value>` and set one from Bulma's radius scale.
514
+ * @example 'radiusless', 'small', 'rounded'
463
515
  */
464
- const validRadii = ['radiusless'];
516
+ const validRadii = [
517
+ 'radiusless',
518
+ 'small',
519
+ 'normal',
520
+ 'large',
521
+ 'rounded',
522
+ ];
465
523
  /**
466
524
  * Valid Bulma shadow helper classes.
467
525
  * @example 'shadowless'
@@ -922,7 +980,7 @@ const useFlexboxClasses = (props) => {
922
980
  /**
923
981
  * A hook that generates miscellaneous Bulma helper classes (float, overflow,
924
982
  * overlay, interaction, cursor, radius, shadow, responsive, skeleton,
925
- * clearfix, relative, and full height).
983
+ * clearfix, position, full height, and aspect ratio).
926
984
  *
927
985
  * @function useOtherClasses
928
986
  * @param props - Miscellaneous Bulma helper props.
@@ -933,15 +991,32 @@ const useFlexboxClasses = (props) => {
933
991
  */
934
992
  const useOtherClasses = (props) => {
935
993
  const { classPrefix } = useConfig();
936
- const { float, overflow, overlay, interaction, cursor, radius, shadow, responsive, skeleton, clearfix, relative, fullHeight, } = props;
994
+ const { float, overflow, overflowX, overflowY, overlay, interaction, cursor, radius, shadow, responsive, skeleton, clearfix, pos, relative, fullHeight, aspectRatio, } = props;
937
995
  return useMemo(() => {
938
996
  const { classes, addPrefixedClass, addClassNoViewport } = createBulmaClassHelpers(classPrefix);
939
997
  // Other Helpers (no viewport support)
940
998
  if (float) {
941
999
  addClassNoViewport('is-pulled', float, validFloats);
942
1000
  }
943
- if (overflow) {
944
- addClassNoViewport('is', overflow, validOverflows);
1001
+ // Bulma's overflow helpers are all `!important` at the same specificity
1002
+ // and ordered by value rather than by axis, so a both-axes class beside an
1003
+ // axis class would be settled by which value comes later in the
1004
+ // stylesheet. Once an axis prop is set, both axes are written per axis
1005
+ // instead: the axis prop wins its own, and `overflow` fills in the other,
1006
+ // `clipped` being `overflow: hidden`.
1007
+ const axisX = validAxisOverflows.find(v => v === overflowX);
1008
+ const axisY = validAxisOverflows.find(v => v === overflowY);
1009
+ if (axisX || axisY) {
1010
+ const rest = overflow === 'clipped' ? 'hidden' : overflow;
1011
+ addClassNoViewport('is-overflow-x', axisX ?? rest, validAxisOverflows);
1012
+ addClassNoViewport('is-overflow-y', axisY ?? rest, validAxisOverflows);
1013
+ }
1014
+ else if (overflow === 'clipped') {
1015
+ // The older helper, which keeps its own class.
1016
+ addPrefixedClass('is-clipped');
1017
+ }
1018
+ else {
1019
+ addClassNoViewport('is-overflow', overflow, validAxisOverflows);
945
1020
  }
946
1021
  if (overlay) {
947
1022
  addPrefixedClass('is-overlay');
@@ -952,8 +1027,13 @@ const useOtherClasses = (props) => {
952
1027
  if (cursor && validCursors.includes(cursor)) {
953
1028
  addPrefixedClass(cursorClasses[cursor]);
954
1029
  }
955
- if (radius) {
956
- addClassNoViewport('is', radius, validRadii);
1030
+ // `radiusless` is an `is-` helper that removes the radius; the sizes are
1031
+ // Bulma's `has-radius-` helpers.
1032
+ if (radius === 'radiusless') {
1033
+ addPrefixedClass('is-radiusless');
1034
+ }
1035
+ else {
1036
+ addClassNoViewport('has-radius', radius, validRadii);
957
1037
  }
958
1038
  if (shadow) {
959
1039
  addClassNoViewport('is', shadow, validShadows);
@@ -969,19 +1049,26 @@ const useOtherClasses = (props) => {
969
1049
  if (clearfix) {
970
1050
  addPrefixedClass('is-clearfix');
971
1051
  }
972
- // Position Relative Helper
973
- if (relative) {
1052
+ // Position Helpers. A valid `pos` decides the position on its own, so a
1053
+ // `relative` beside it cannot add a second, conflicting one.
1054
+ if (pos && validPositions.includes(pos)) {
1055
+ addPrefixedClass(`is-position-${pos}`);
1056
+ }
1057
+ else if (relative) {
974
1058
  addPrefixedClass('is-relative');
975
1059
  }
976
1060
  // Full Height Helper
977
1061
  if (fullHeight) {
978
1062
  addPrefixedClass('is-full-height');
979
1063
  }
1064
+ addClassNoViewport('is-aspect-ratio', aspectRatio, validAspectRatios);
980
1065
  return classNames(classes);
981
1066
  }, [
982
1067
  classPrefix,
983
1068
  float,
984
1069
  overflow,
1070
+ overflowX,
1071
+ overflowY,
985
1072
  overlay,
986
1073
  interaction,
987
1074
  cursor,
@@ -990,8 +1077,10 @@ const useOtherClasses = (props) => {
990
1077
  responsive,
991
1078
  skeleton,
992
1079
  clearfix,
1080
+ pos,
993
1081
  relative,
994
1082
  fullHeight,
1083
+ aspectRatio,
995
1084
  ]);
996
1085
  };
997
1086
 
@@ -1022,7 +1111,7 @@ const useOtherClasses = (props) => {
1022
1111
  * // rest: { className: 'custom-class' }
1023
1112
  */
1024
1113
  const useBulmaClasses = (props) => {
1025
- const { color, backgroundColor, colorShade, backgroundColorShade, m, mt, mr, mb, ml, mx, my, p, pt, pr, pb, pl, px, py, textSize, textAlign, textTransform, textWeight, fontFamily, display, visibility, flexDirection, flexWrap, justifyContent, alignContent, alignItems, alignSelf, flexGrow, flexShrink, float, overflow, overlay, interaction, cursor, radius, shadow, responsive, viewport, displayMobile, displayTablet, displayTabletOnly, displayTouch, displayDesktop, displayDesktopOnly, displayWidescreen, displayWidescreenOnly, displayFullhd, textSizeMobile, textSizeTablet, textSizeDesktop, textSizeWidescreen, textSizeFullhd, textAlignMobile, textAlignTablet, textAlignDesktop, textAlignWidescreen, textAlignFullhd, visibilityMobile, visibilityTablet, visibilityTabletOnly, visibilityTouch, visibilityDesktop, visibilityDesktopOnly, visibilityWidescreen, visibilityWidescreenOnly, visibilityFullhd, skeleton, clearfix, relative, fullHeight, ...rest } = props;
1114
+ const { color, backgroundColor, colorShade, backgroundColorShade, m, mt, mr, mb, ml, mx, my, p, pt, pr, pb, pl, px, py, textSize, textAlign, textTransform, textWeight, fontFamily, display, visibility, flexDirection, flexWrap, justifyContent, alignContent, alignItems, alignSelf, flexGrow, flexShrink, float, overflow, overflowX, overflowY, overlay, interaction, cursor, radius, shadow, responsive, viewport, displayMobile, displayTablet, displayTabletOnly, displayTouch, displayDesktop, displayDesktopOnly, displayWidescreen, displayWidescreenOnly, displayFullhd, textSizeMobile, textSizeTablet, textSizeDesktop, textSizeWidescreen, textSizeFullhd, textAlignMobile, textAlignTablet, textAlignDesktop, textAlignWidescreen, textAlignFullhd, visibilityMobile, visibilityTablet, visibilityTabletOnly, visibilityTouch, visibilityDesktop, visibilityDesktopOnly, visibilityWidescreen, visibilityWidescreenOnly, visibilityFullhd, skeleton, clearfix, pos, relative, fullHeight, aspectRatio, ...rest } = props;
1026
1115
  const colorClasses = useColorClasses({
1027
1116
  color,
1028
1117
  colorShade,
@@ -1110,6 +1199,8 @@ const useBulmaClasses = (props) => {
1110
1199
  const otherClasses = useOtherClasses({
1111
1200
  float,
1112
1201
  overflow,
1202
+ overflowX,
1203
+ overflowY,
1113
1204
  overlay,
1114
1205
  interaction,
1115
1206
  cursor,
@@ -1118,8 +1209,10 @@ const useBulmaClasses = (props) => {
1118
1209
  responsive,
1119
1210
  skeleton,
1120
1211
  clearfix,
1212
+ pos,
1121
1213
  relative,
1122
1214
  fullHeight,
1215
+ aspectRatio,
1123
1216
  });
1124
1217
  const bulmaHelperClasses = useMemo(() => classNames(colorClasses, spacingClasses, typographyClasses, visibilityClasses, flexboxClasses, otherClasses), [
1125
1218
  colorClasses,
@@ -1235,6 +1328,27 @@ const ColumnsComponent = ({ className, textColor, color: _fieldColor, bgColor, i
1235
1328
  };
1236
1329
  const Columns = withSubComponents(ColumnsComponent, { Column }, 'Columns');
1237
1330
 
1331
+ /**
1332
+ * The `type` to render on a `<button>` a component defaults to `button`: the
1333
+ * caller's own value when it is `button`, `submit` or `reset`, and `button`
1334
+ * otherwise.
1335
+ *
1336
+ * A missing `type` is not the only way a button ends up submitting the form
1337
+ * around it. HTML reads an invalid value, such as the anchor's MIME-type
1338
+ * `type="text/html"`, as submit too, and a props spread carrying
1339
+ * `type: undefined` removes the attribute outright. Both can arrive through a
1340
+ * spread or an untyped caller the component's own types never see, so this
1341
+ * checks the value rather than trusting where it came from.
1342
+ *
1343
+ * Call it AFTER the forwarded props are spread and pass it the forwarded
1344
+ * `type`, so its result is the attribute that renders.
1345
+ */
1346
+ function buttonType(given) {
1347
+ return given === 'submit' || given === 'reset' || given === 'button'
1348
+ ? given
1349
+ : 'button';
1350
+ }
1351
+
1238
1352
  /**
1239
1353
  * Whether an `as` target is a custom element rather than a built-in tag.
1240
1354
  *
@@ -1532,7 +1646,7 @@ const Avatar = forwardRef(function Avatar(avatarProps, ref) {
1532
1646
  // The decorative opt-out never applies to an interactive avatar — a link or
1533
1647
  // button must always expose an accessible name.
1534
1648
  const isDecorative = alt === '' && !isInteractive;
1535
- const a11yProps = showImage
1649
+ const a11yDefaults = showImage
1536
1650
  ? isInteractive && !accessibleName
1537
1651
  ? // The img alt normally names the control; with no alt/name (an API
1538
1652
  // returning only a photo URL) the link/button would be nameless.
@@ -1544,10 +1658,18 @@ const Avatar = forwardRef(function Avatar(avatarProps, ref) {
1544
1658
  ...(isInteractive ? {} : { role: 'img' }),
1545
1659
  'aria-label': accessibleName || name || 'Avatar',
1546
1660
  };
1547
- // A clickable avatar inside a form must not submit it; default the native
1548
- // button type (an explicit type passed through rest still wins).
1549
- const buttonTypeProps = Tag === 'button' ? { type: 'button' } : {};
1550
- return (jsxs(Tag, { ref: ref, className: combinedClasses, style: { ...sizeStyle, ...style }, ...buttonTypeProps, ...linkProps, ...a11yProps, ...rest, children: [showImage && (jsx("img", { ...imageProps, ref: imgRef, src: src, alt: accessibleName ?? '', onError: e => {
1661
+ // Each default yields to a value the caller passed, and is applied after
1662
+ // `rest` reading through it rather than spread before it. React treats an
1663
+ // `undefined` attribute as "remove it", and a spread carrying the key with no
1664
+ // value is how that arrives, so a default spread first was erased by it: an
1665
+ // `aria-label: undefined` left a button avatar nameless, and `role` and
1666
+ // `aria-hidden` went the same way. The button `type` below has the same
1667
+ // shape (#690).
1668
+ const a11yProps = Object.fromEntries(Object.entries(a11yDefaults).map(([key, fallback]) => [
1669
+ key,
1670
+ rest[key] ?? fallback,
1671
+ ]));
1672
+ return (jsxs(Tag, { ref: ref, className: combinedClasses, style: { ...sizeStyle, ...style }, ...linkProps, ...rest, ...a11yProps, ...(Tag === 'button' ? { type: buttonType(rest.type) } : {}), children: [showImage && (jsx("img", { ...imageProps, ref: imgRef, src: src, alt: accessibleName ?? '', onError: e => {
1551
1673
  imageProps?.onError?.(e);
1552
1674
  setErroredSrc(src);
1553
1675
  } }, src)), showInitials && (jsx("span", { className: initialsClass, children: resolvedInitials })), showIcon && icon, showDefaultIcon && jsx(DefaultAvatarIcon, {})] }));
@@ -1978,7 +2100,7 @@ const CardHeaderIcon = ({ className, children, color, bgColor, textColor, ...pro
1978
2100
  backgroundColor: bgColor,
1979
2101
  ...props,
1980
2102
  });
1981
- return (jsx("button", { className: classNames(usePrefixedClassNames('card-header-icon'), bulmaHelperClasses, className), "aria-label": props['aria-label'] || 'more options', ...rest, children: children }));
2103
+ return (jsx("button", { className: classNames(usePrefixedClassNames('card-header-icon'), bulmaHelperClasses, className), ...rest, "aria-label": rest['aria-label'] || 'more options', type: buttonType(rest.type), children: children }));
1982
2104
  };
1983
2105
  /**
1984
2106
  * Card image compound component. Wraps children in a `.card-image` element.
@@ -2047,7 +2169,6 @@ const CardFooterItem = ({ as = 'span', className, children, color, bgColor, text
2047
2169
  }
2048
2170
  const forwarded = omitAttrs(rest, STRIP_FROM_NON_ANCHOR$1);
2049
2171
  if (as === 'button') {
2050
- const forwardedType = forwarded.type;
2051
2172
  return (jsx("button", { className: itemClasses, ...forwarded,
2052
2173
  // A footer item button must not submit an enclosing form by default —
2053
2174
  // `<button>` defaults to `type="submit"`, and a Save/Cancel action row
@@ -2057,13 +2178,9 @@ const CardFooterItem = ({ as = 'span', className, children, color, bgColor, text
2057
2178
  // typed here as the `<a>` MIME string (`STRIP_FROM_NON_ANCHOR` above
2058
2179
  // says why it is not withheld), so `as="button" type="text/html"`
2059
2180
  // compiles, and HTML's INVALID-value default for a button's `type` is
2060
- // submit too. So anything that is not one of the three native button
2061
- // types falls back to `button` rather than being forwarded.
2062
- type: forwardedType === 'submit' ||
2063
- forwardedType === 'reset' ||
2064
- forwardedType === 'button'
2065
- ? forwardedType
2066
- : 'button', children: children }));
2181
+ // submit too. `buttonType` keeps `button`, `submit` and `reset` and
2182
+ // turns anything else into `button`.
2183
+ type: buttonType(forwarded.type), children: children }));
2067
2184
  }
2068
2185
  return (jsx("span", { className: itemClasses, ...omitAttrs(forwarded, STRIP_FROM_NON_BUTTON), children: children }));
2069
2186
  };
@@ -2081,6 +2198,72 @@ const Card = withSubComponents(CardComponent, {
2081
2198
  FooterItem: CardFooterItem,
2082
2199
  }, 'Card');
2083
2200
 
2201
+ /**
2202
+ * Event and focus checks that still hold when a component renders inside a
2203
+ * shadow root.
2204
+ *
2205
+ * Code outside a shadow root cannot see into it. A listener on `document`
2206
+ * reads an event from inside one with its `target` set to the shadow host,
2207
+ * and `document.activeElement` names the host instead of the element that has
2208
+ * focus. A component asking either question from `document` then takes a
2209
+ * click on its own menu for an outside click, or cannot tell which of its
2210
+ * items is focused. A web component or a sandboxed preview puts a component
2211
+ * in exactly that position.
2212
+ *
2213
+ * Focus comes in two questions, and each has its own function here. "Is focus
2214
+ * inside me, and on which of my elements?" is `getActiveElementInTree`. "Which
2215
+ * element has focus, wherever it is?", the one to record before moving focus
2216
+ * away and restore to later, is `getDeepestActiveElement`.
2217
+ */
2218
+ /**
2219
+ * Whether `event` happened inside `node`. The event's composed path still
2220
+ * holds the element that was really clicked, where a `document` listener
2221
+ * reads the shadow host as the event's `target`.
2222
+ *
2223
+ * @param event - An event read by a listener outside `node`, usually on `document`.
2224
+ * @param node - The component's element, or nothing while it is unmounted.
2225
+ * @returns True when `node` is on the event's path.
2226
+ */
2227
+ function isEventInside(event, node) {
2228
+ return !!node && event.composedPath().includes(node);
2229
+ }
2230
+ /**
2231
+ * The focused element as `node`'s own tree sees it: its shadow root's
2232
+ * `activeElement` when `node` is inside one, `document.activeElement` when it
2233
+ * is not. The result is comparable with `node` and its descendants, which
2234
+ * live in that same tree, so use it to ask whether focus is inside `node`.
2235
+ * Focus inside a shadow root nested within `node` reads as that root's host,
2236
+ * which `node` contains.
2237
+ *
2238
+ * A shadow root with nothing focused inside reports `null`, and a detached
2239
+ * node has no document above it. Both fall back to `document.activeElement`,
2240
+ * so focus elsewhere on the page still reads as outside `node`.
2241
+ *
2242
+ * @param node - An element of the component, or nothing while it is unmounted.
2243
+ * @returns The focused element in `node`'s tree, or the document's.
2244
+ */
2245
+ function getActiveElementInTree(node) {
2246
+ const root = node?.getRootNode();
2247
+ return root?.activeElement ?? document.activeElement;
2248
+ }
2249
+ /**
2250
+ * The element that really has focus, followed down from
2251
+ * `document.activeElement` through every open shadow root on the way. Use it
2252
+ * to record where focus was before moving it, so it can be restored to that
2253
+ * element later: the tree a component renders into need not be the tree its
2254
+ * opener sits in. A closed shadow root cannot be entered, so focus inside one
2255
+ * reads as its host.
2256
+ *
2257
+ * @returns The focused element, or `null` when the document has none.
2258
+ */
2259
+ function getDeepestActiveElement() {
2260
+ let active = document.activeElement;
2261
+ while (active?.shadowRoot?.activeElement) {
2262
+ active = active.shadowRoot.activeElement;
2263
+ }
2264
+ return active;
2265
+ }
2266
+
2084
2267
  /**
2085
2268
  * Checks if code is running in a browser environment.
2086
2269
  * @deprecated Dropdown's internal SSR guard, exported by accident; it will
@@ -2161,8 +2344,7 @@ const DropdownComponent = forwardRef(function DropdownComponent({ label, childre
2161
2344
  if (!isBrowser$1(window, document))
2162
2345
  return;
2163
2346
  const handleClick = (e) => {
2164
- /* istanbul ignore next: dropdownRef.current is never null while the listener is attached */
2165
- if (!dropdownRef.current?.contains(e.target)) {
2347
+ if (!isEventInside(e, dropdownRef.current)) {
2166
2348
  setActive(false);
2167
2349
  onActiveChange?.(false);
2168
2350
  }
@@ -2178,10 +2360,16 @@ const DropdownComponent = forwardRef(function DropdownComponent({ label, childre
2178
2360
  setActive(newActive);
2179
2361
  onActiveChange?.(newActive);
2180
2362
  };
2181
- const handleMenuClick = () => {
2363
+ const handleMenuClick = (e) => {
2182
2364
  if (closeOnClick) {
2183
2365
  setActive(false);
2184
2366
  onActiveChange?.(false);
2367
+ // Closing hides the item that has focus, and the browser drops focus
2368
+ // to the page when that happens. Hand it back to the trigger, as
2369
+ // Escape does. An item that moved focus somewhere else keeps it there.
2370
+ if (e.currentTarget.contains(getActiveElementInTree(e.currentTarget))) {
2371
+ triggerRef.current?.focus();
2372
+ }
2185
2373
  }
2186
2374
  };
2187
2375
  const getMenuItems = () => {
@@ -2242,6 +2430,11 @@ const DropdownComponent = forwardRef(function DropdownComponent({ label, childre
2242
2430
  case 'Enter':
2243
2431
  case ' ':
2244
2432
  e.preventDefault();
2433
+ // A held key sends a keydown per auto-repeat. Running an item hands
2434
+ // focus back here, so without this a held Enter would reopen the
2435
+ // menu and run whichever item it focused next.
2436
+ if (e.repeat)
2437
+ break;
2245
2438
  if (!active) {
2246
2439
  pendingFocusRef.current = 'first';
2247
2440
  setActive(true);
@@ -2279,7 +2472,7 @@ const DropdownComponent = forwardRef(function DropdownComponent({ label, childre
2279
2472
  const items = getMenuItems();
2280
2473
  if (!items.length)
2281
2474
  return;
2282
- const currentIndex = items.indexOf(document.activeElement);
2475
+ const currentIndex = items.indexOf(getActiveElementInTree(e.currentTarget));
2283
2476
  switch (e.key) {
2284
2477
  case 'ArrowDown': {
2285
2478
  e.preventDefault();
@@ -2400,9 +2593,7 @@ const DropdownItem = ((itemProps) => {
2400
2593
  // carrying `role: undefined` would otherwise erase the default, and an
2401
2594
  // item with no role drops out of the menu and its arrow-key order.
2402
2595
  role: forwarded.role ?? 'menuitem', ...(Component === 'button'
2403
- ? {
2404
- type: forwarded.type ?? 'button',
2405
- }
2596
+ ? { type: buttonType(forwarded.type) }
2406
2597
  : {}), children: children }));
2407
2598
  });
2408
2599
  /**
@@ -2511,7 +2702,9 @@ const MenuItem = forwardRef(function MenuItem(itemProps, ref) {
2511
2702
  labelChildren.push(child);
2512
2703
  }
2513
2704
  });
2514
- return (jsxs("li", { className: className, "data-testid": testId, style: style, id: id, title: title, role: role, tabIndex: tabIndex, children: [jsx(Component, { ref: ref, className: itemClass || undefined, ...linkProps, children: labelChildren }), nestedMenuLists] }));
2705
+ return (jsxs("li", { className: className, "data-testid": testId, style: style, id: id, title: title, role: role, tabIndex: tabIndex, children: [jsx(Component, { ref: ref, className: itemClass || undefined, ...linkProps, ...(Component === 'button'
2706
+ ? { type: buttonType(linkProps.type) }
2707
+ : {}), children: labelChildren }), nestedMenuLists] }));
2515
2708
  });
2516
2709
  MenuItem.displayName = 'MenuItem';
2517
2710
  // Attach static subcomponents
@@ -2767,7 +2960,7 @@ const ModalCard = withSubComponents(ModalCardComponent, {
2767
2960
  */
2768
2961
  const ModalClose = ({ className, size = 'large', variant = 'delete', ...props }) => {
2769
2962
  const classes = classNames(usePrefixedClassNames(variant === 'delete' ? 'delete' : 'modal-close', variant === 'floating' && size && { [`is-${size}`]: true }), className);
2770
- return (jsx("button", { className: classes, "aria-label": "close", type: "button", ...props }));
2963
+ return (jsx("button", { className: classes, ...props, "aria-label": props['aria-label'] || 'close', type: buttonType(props.type) }));
2771
2964
  };
2772
2965
  /**
2773
2966
  * The `Modal` component provides an empty, accessible overlay for arbitrary content — for a ready-made confirm or alert, reach for `Dialog` instead.
@@ -2884,7 +3077,7 @@ const ModalRoot = forwardRef(function ModalRoot({ active, isActive, onClose, cla
2884
3077
  // Keep Tab within the modal — `aria-modal` hides the rest of the page
2885
3078
  // from assistive technology, so the keyboard order has to agree.
2886
3079
  const focusable = getTabbable(node);
2887
- const activeElement = document.activeElement;
3080
+ const activeElement = getActiveElementInTree(node);
2888
3081
  if (focusable.length === 0) {
2889
3082
  e.preventDefault();
2890
3083
  node.focus();
@@ -2911,7 +3104,10 @@ const ModalRoot = forwardRef(function ModalRoot({ active, isActive, onClose, cla
2911
3104
  useEffect(() => {
2912
3105
  if (!isModalActive)
2913
3106
  return undefined;
2914
- previouslyFocusedRef.current = document.activeElement;
3107
+ // The element that had focus, wherever it sits: a portaled modal renders
3108
+ // under `document.body` while its opener can be inside a shadow root.
3109
+ previouslyFocusedRef.current =
3110
+ getDeepestActiveElement();
2915
3111
  const node = modalRootRef.current;
2916
3112
  const focusable = node ? getTabbable(node)[0] : undefined;
2917
3113
  (focusable ?? node)?.focus();
@@ -2919,7 +3115,7 @@ const ModalRoot = forwardRef(function ModalRoot({ active, isActive, onClose, cla
2919
3115
  // Only hand focus back if this modal still owns it: closing a background
2920
3116
  // modal must not pull focus out of one that is still open on top. A
2921
3117
  // removed subtree leaves focus on <body>, which still counts as ours.
2922
- const activeElement = document.activeElement;
3118
+ const activeElement = getActiveElementInTree(node);
2923
3119
  if (activeElement &&
2924
3120
  activeElement !== document.body &&
2925
3121
  !node?.contains(activeElement)) {
@@ -3037,7 +3233,7 @@ const NavbarItem = forwardRef(function NavbarItem(itemProps, ref) {
3037
3233
  });
3038
3234
  return (jsx(Component, { ref: ref, className: classNames(usePrefixedClassNames('navbar-item', {
3039
3235
  'is-active': active,
3040
- }), bulmaHelperClasses, className), ...rest, children: children }));
3236
+ }), bulmaHelperClasses, className), ...rest, ...(Component === 'button' ? { type: buttonType(rest.type) } : {}), children: children }));
3041
3237
  });
3042
3238
  NavbarItem.displayName = 'NavbarItem';
3043
3239
  /**
@@ -3053,9 +3249,9 @@ const NavbarBurger = forwardRef(function NavbarBurger({ className, active, textC
3053
3249
  color: textColor ?? color,
3054
3250
  ...props,
3055
3251
  });
3056
- return (jsxs("button", { ref: ref, type: "button", className: classNames(usePrefixedClassNames('navbar-burger', {
3252
+ return (jsxs("button", { ref: ref, className: classNames(usePrefixedClassNames('navbar-burger', {
3057
3253
  'is-active': active,
3058
- }), bulmaHelperClasses, className), "aria-label": props['aria-label'] || 'menu', "aria-expanded": props['aria-expanded'] ?? !!active, ...rest, children: [jsx("span", { "aria-hidden": "true" }), jsx("span", { "aria-hidden": "true" }), jsx("span", { "aria-hidden": "true" }), jsx("span", { "aria-hidden": "true" }), children] }));
3254
+ }), bulmaHelperClasses, className), ...rest, "aria-label": rest['aria-label'] || 'menu', "aria-expanded": rest['aria-expanded'] ?? !!active, type: buttonType(rest.type), children: [jsx("span", { "aria-hidden": "true" }), jsx("span", { "aria-hidden": "true" }), jsx("span", { "aria-hidden": "true" }), jsx("span", { "aria-hidden": "true" }), children] }));
3059
3255
  });
3060
3256
  NavbarBurger.displayName = 'NavbarBurger';
3061
3257
  /**
@@ -3185,7 +3381,7 @@ const NavbarLink = forwardRef(function NavbarLink(linkProps, ref) {
3185
3381
  tabIndex: 0,
3186
3382
  onClick: handleClick,
3187
3383
  }),
3188
- }), children: children }));
3384
+ }), ...(Component === 'button' ? { type: buttonType(rest.type) } : {}), children: children }));
3189
3385
  });
3190
3386
  NavbarLink.displayName = 'NavbarLink';
3191
3387
  /**
@@ -4509,7 +4705,7 @@ const StepsComponent = ({ value = 0, items, size, color, hasMarker = true, anima
4509
4705
  }
4510
4706
  return null;
4511
4707
  };
4512
- return (jsxs("div", { className: combinedClasses, ...rest, children: [jsx("ul", { className: listClasses, children: renderSteps() }), hasNavigation && (jsxs("div", { className: stepsNavigationClass, children: [jsx("button", { className: prevButtonClass, disabled: value === 0, onClick: onPrev ?? (() => onStepClick?.(value - 1)), children: prevLabel ?? 'Previous' }), jsx("button", { className: nextButtonClass, disabled: value === totalSteps - 1, onClick: onNext ?? (() => onStepClick?.(value + 1)), children: nextLabel ?? 'Next' })] }))] }));
4708
+ return (jsxs("div", { className: combinedClasses, ...rest, children: [jsx("ul", { className: listClasses, children: renderSteps() }), hasNavigation && (jsxs("div", { className: stepsNavigationClass, children: [jsx("button", { type: "button", className: prevButtonClass, disabled: value === 0, onClick: onPrev ?? (() => onStepClick?.(value - 1)), children: prevLabel ?? 'Previous' }), jsx("button", { type: "button", className: nextButtonClass, disabled: value === totalSteps - 1, onClick: onNext ?? (() => onStepClick?.(value + 1)), children: nextLabel ?? 'Next' })] }))] }));
4513
4709
  };
4514
4710
  // Attach Step as static property
4515
4711
  const Steps = withSubComponents(StepsComponent, { Step }, 'Steps');
@@ -4678,7 +4874,7 @@ const SidebarTitle = ({ className, children, ...props }) => {
4678
4874
  */
4679
4875
  const SidebarClose = ({ className, children, ...props }) => {
4680
4876
  const closeClass = usePrefixedClassNames('sidebar-close');
4681
- return (jsx("button", { type: "button", className: classNames(closeClass, className), "aria-label": "Close", ...props, children: children }));
4877
+ return (jsx("button", { className: classNames(closeClass, className), ...props, "aria-label": props['aria-label'] || 'Close', type: buttonType(props.type), children: children }));
4682
4878
  };
4683
4879
  /**
4684
4880
  * Main content area of the sidebar.
@@ -4711,6 +4907,48 @@ const Sidebar = withSubComponents(SidebarComponent, {
4711
4907
  Footer: SidebarFooter,
4712
4908
  }, 'Sidebar');
4713
4909
 
4910
+ /**
4911
+ * Splits the items a container shows into one stack per position in use. An
4912
+ * item goes to its own position when it has one and to the container's
4913
+ * otherwise. A position with no items gets no stack.
4914
+ *
4915
+ * Stacks come back in `order`, so their order in the page follows the screen
4916
+ * rather than which position happened to be used first, and a stack keeps its
4917
+ * place while the others come and go. A position missing from `order` sorts
4918
+ * after the known ones.
4919
+ *
4920
+ * @function groupIntoPositionStacks
4921
+ * @param items - The items the container shows, in the order they were shown.
4922
+ * @param positionOf - Reads an item's own position, if it has one.
4923
+ * @param containerPosition - Where an item without a position of its own goes.
4924
+ * @param order - Every known position, in the order stacks should render.
4925
+ * @returns One stack per position in use.
4926
+ */
4927
+ function groupIntoPositionStacks(items, positionOf, containerPosition, order) {
4928
+ const byPosition = new Map();
4929
+ for (const item of items) {
4930
+ const position = positionOf(item) ?? containerPosition;
4931
+ const stack = byPosition.get(position);
4932
+ if (stack) {
4933
+ stack.push(item);
4934
+ }
4935
+ else {
4936
+ byPosition.set(position, [item]);
4937
+ }
4938
+ }
4939
+ const rank = (position) => {
4940
+ const index = order.indexOf(position);
4941
+ return index === -1 ? order.length : index;
4942
+ };
4943
+ // Array.prototype.sort is stable, so unknown positions keep the order they
4944
+ // were first used in.
4945
+ return Array.from(byPosition, ([position, stackItems]) => ({
4946
+ key: position === containerPosition ? 'container' : `at-${position}`,
4947
+ position,
4948
+ items: stackItems,
4949
+ })).sort((a, b) => rank(a.position) - rank(b.position));
4950
+ }
4951
+
4714
4952
  /**
4715
4953
  * The `Toast` component provides brief notification messages with optional action and cancel buttons.
4716
4954
  *
@@ -4766,7 +5004,7 @@ const Toast = forwardRef(({ message, type = 'default', actionType, position = 't
4766
5004
  if (!dismissible || !isVisible)
4767
5005
  return undefined;
4768
5006
  const handleDocumentClick = (e) => {
4769
- if (toastRef.current && !toastRef.current.contains(e.target)) {
5007
+ if (toastRef.current && !isEventInside(e, toastRef.current)) {
4770
5008
  handleClose();
4771
5009
  }
4772
5010
  };
@@ -4987,25 +5225,38 @@ const toast = {
4987
5225
  return () => toastListeners.delete(listener);
4988
5226
  },
4989
5227
  };
5228
+ // The order ToastContainer renders its stacks in: across the top of the
5229
+ // screen, then across the bottom.
5230
+ const toastStackOrder = [
5231
+ 'top-left',
5232
+ 'top-center',
5233
+ 'top-right',
5234
+ 'bottom-left',
5235
+ 'bottom-center',
5236
+ 'bottom-right',
5237
+ ];
4990
5238
  /**
4991
5239
  * Container component for rendering programmatic toasts.
4992
- * Place once at your app root to enable the toast API.
5240
+ * Place once at your app root to enable the toast API. A toast shown with a
5241
+ * `position` appears there, and one shown without goes to the container's
5242
+ * `position`.
4993
5243
  *
4994
5244
  * @function
4995
5245
  * @param {{ position?: ToastPosition }} props - Container props.
4996
5246
  * @returns {JSX.Element | null} The rendered toast container, or null if empty.
4997
5247
  */
4998
- const ToastContainer = ({ position = 'top-right', }) => {
5248
+ const ToastContainer = ({ position = 'top-right' }) => {
4999
5249
  // Starts from the toasts already showing instead of an empty list, then
5000
5250
  // follows changes.
5001
5251
  const toastList = useSyncExternalStore(toast.subscribe, getVisibleToasts, getServerToasts);
5002
5252
  if (typeof document === 'undefined' || toastList.length === 0) {
5003
5253
  return null;
5004
5254
  }
5005
- return createPortal(jsx("div", { className: `toast-container is-${position}`, children: toastList.map(t => {
5255
+ const stacks = groupIntoPositionStacks(toastList, t => t.props.position, position, toastStackOrder);
5256
+ return createPortal(stacks.map(stack => (jsx("div", { className: `toast-container is-${stack.position}`, children: stack.items.map(t => {
5006
5257
  const { queue: _queue, ...toastProps } = t.props;
5007
- return (jsx(Toast, { ...toastProps, position: position, onClose: () => toast.close(t.id) }, t.id));
5008
- }) }), document.body);
5258
+ return (jsx(Toast, { ...toastProps, position: stack.position, onClose: () => toast.close(t.id) }, t.id));
5259
+ }) }, stack.key))), document.body);
5009
5260
  };
5010
5261
 
5011
5262
  /**
@@ -5556,7 +5807,8 @@ const Carousel = forwardRef(({ value: controlledValue, autoplay = false, interva
5556
5807
  // Keyboard navigation
5557
5808
  useEffect(() => {
5558
5809
  const handleKeyDown = (e) => {
5559
- if (!carouselRef.current?.contains(document.activeElement))
5810
+ const node = carouselRef.current;
5811
+ if (!node?.contains(getActiveElementInTree(node)))
5560
5812
  return;
5561
5813
  switch (e.key) {
5562
5814
  case 'ArrowLeft':
@@ -5689,7 +5941,7 @@ const Carousel = forwardRef(({ value: controlledValue, autoplay = false, interva
5689
5941
  transition: isDragging || skipTransition
5690
5942
  ? 'none'
5691
5943
  : 'transform 0.3s ease-in-out',
5692
- }, onTransitionEnd: handleTransitionEnd, children: renderSlides() }), arrow && itemCount > 1 && (jsxs(Fragment, { children: [jsx(Button, { className: carouselArrowPrevClass, onClick: goToPrev, isDisabled: !canGoPrev, "aria-label": "Previous slide", children: iconPrev ? (jsx(Icon, { name: iconPrev, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })) : (jsx(DefaultPrevIcon, {})) }), jsx(Button, { className: carouselArrowNextClass, onClick: goToNext, isDisabled: !canGoNext, "aria-label": "Next slide", children: iconNext ? (jsx(Icon, { name: iconNext, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })) : (jsx(DefaultNextIcon, {})) })] }))] }), indicator && itemCount > 1 && (jsx("div", { className: carouselIndicatorClass, role: "tablist", children: items.map((_, index) => (jsx(Button, { className: prefixedClassNames(classPrefix, 'indicator-item', {
5944
+ }, onTransitionEnd: handleTransitionEnd, children: renderSlides() }), arrow && itemCount > 1 && (jsxs(Fragment, { children: [jsx(Button, { type: "button", className: carouselArrowPrevClass, onClick: goToPrev, isDisabled: !canGoPrev, "aria-label": "Previous slide", children: iconPrev ? (jsx(Icon, { name: iconPrev, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })) : (jsx(DefaultPrevIcon, {})) }), jsx(Button, { type: "button", className: carouselArrowNextClass, onClick: goToNext, isDisabled: !canGoNext, "aria-label": "Next slide", children: iconNext ? (jsx(Icon, { name: iconNext, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })) : (jsx(DefaultNextIcon, {})) })] }))] }), indicator && itemCount > 1 && (jsx("div", { className: carouselIndicatorClass, role: "tablist", children: items.map((_, index) => (jsx(Button, { type: "button", className: prefixedClassNames(classPrefix, 'indicator-item', {
5693
5945
  'is-active': index === activeIndex,
5694
5946
  }), onClick: () => goToSlide(index), role: "tab", "aria-selected": index === activeIndex, "aria-label": `Go to slide ${index + 1}` }, index))) }))] }));
5695
5947
  });
@@ -5997,12 +6249,14 @@ const Content = ({ className, textColor, color, bgColor, size, children, ...prop
5997
6249
  /**
5998
6250
  * The `Delete` component provides a Bulma-styled close/delete button for dismissing modals, notifications, tags, messages, and more.
5999
6251
  *
6252
+ * It renders `type="button"`, so a delete button inside a form does not submit it.
6253
+ *
6000
6254
  * @function
6001
6255
  * @param {DeleteProps} props - Props for the Delete component.
6002
6256
  * @returns {JSX.Element} The rendered delete button.
6003
6257
  * @see {@link https://bulma.io/documentation/elements/delete/ | Bulma Delete documentation}
6004
6258
  */
6005
- const Delete = ({ className, textColor, color, bgColor, onClick, size, ariaLabel = 'Close', disabled = false, ...props }) => {
6259
+ const Delete = ({ className, textColor, color, bgColor, onClick, size, ariaLabel, disabled = false, ...props }) => {
6006
6260
  /**
6007
6261
  * Generates Bulma helper classes and separates out remaining props.
6008
6262
  */
@@ -6016,7 +6270,7 @@ const Delete = ({ className, textColor, color, bgColor, onClick, size, ariaLabel
6016
6270
  'is-disabled': disabled,
6017
6271
  });
6018
6272
  const classes = classNames(bulmaClasses, bulmaHelperClasses, className);
6019
- return (jsx("button", { className: classes, onClick: onClick, "aria-label": ariaLabel, disabled: disabled, type: "button", ...rest }));
6273
+ return (jsx("button", { className: classes, onClick: onClick, disabled: disabled, ...rest, "aria-label": rest['aria-label'] || ariaLabel || 'Close', type: buttonType(rest.type) }));
6020
6274
  };
6021
6275
 
6022
6276
  /**
@@ -6238,7 +6492,7 @@ const Link = forwardRef(function Link(linkProps, ref) {
6238
6492
  'is-active': isActive,
6239
6493
  });
6240
6494
  const linkClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
6241
- return (jsx(Component, { ref: ref, className: linkClasses || undefined, ...rest, children: children }));
6495
+ return (jsx(Component, { ref: ref, className: linkClasses || undefined, ...rest, ...(Component === 'button' ? { type: buttonType(rest.type) } : {}), children: children }));
6242
6496
  });
6243
6497
  Link.displayName = 'Link';
6244
6498
 
@@ -6263,6 +6517,281 @@ const ListItem = ({ className, textColor, bgColor, children, ...props }) => {
6263
6517
  return (jsx("li", { className: itemClasses || undefined, ...rest, children: children }));
6264
6518
  };
6265
6519
 
6520
+ /**
6521
+ * How long after an item appears its announcement is written, in ms. The wait
6522
+ * lets a screen reader register a region the container has only just mounted,
6523
+ * and items shown in quick succession are written, and read out, together.
6524
+ */
6525
+ const announceDelay = 100;
6526
+ /**
6527
+ * How long an announcement stays in the region once written, in ms. That's
6528
+ * long enough for a screen reader to pick it up. Clearing it after keeps a
6529
+ * second copy of the item's text from sitting in the page, where someone
6530
+ * reading through the page, or a test looking the text up, would find it
6531
+ * again.
6532
+ */
6533
+ const announcementLifetime = 1000;
6534
+ const noAnnouncements = [];
6535
+ const nothingWritten = [];
6536
+ const noAnnouncementsYet = {
6537
+ key: 0,
6538
+ waiting: noAnnouncements,
6539
+ written: nothingWritten,
6540
+ };
6541
+ // Hides the region with inline styles rather than a class, so it needs no
6542
+ // stylesheet, including the helper classes a modular Bulma build can leave
6543
+ // out. Clipping it, rather than using `display` or `visibility`, keeps it in
6544
+ // the accessibility tree. The declarations match the `extras-sr-only` mixin,
6545
+ // plus `clip-path`, which replaces the deprecated `clip`. The negative margin
6546
+ // pulls the 1px box back inside the page, so it doesn't add scrollable
6547
+ // overflow.
6548
+ const visuallyHidden = {
6549
+ position: 'absolute',
6550
+ width: '1px',
6551
+ height: '1px',
6552
+ padding: 0,
6553
+ margin: '-1px',
6554
+ overflow: 'hidden',
6555
+ clip: 'rect(0, 0, 0, 0)',
6556
+ clipPath: 'inset(50%)',
6557
+ whiteSpace: 'nowrap',
6558
+ border: 0,
6559
+ };
6560
+ /**
6561
+ * Works out what a status region says. Each item is announced once. Its
6562
+ * announcement is written `announceDelay` after it appears, or after the last
6563
+ * item to join it when several appear in quick succession, alongside whatever
6564
+ * the region still says. Once written, it stays for `announcementLifetime`,
6565
+ * whatever is written after it and whether or not its item goes in the
6566
+ * meantime. An item that goes before its announcement is written isn't
6567
+ * announced.
6568
+ *
6569
+ * Nothing is written in the commit that mounts the region or the item. A
6570
+ * screen reader reliably announces a polite live region whose content changes
6571
+ * after the region is already in the page, and not always one that arrives
6572
+ * with its content. By the time `describe` runs, an item's rendered text can
6573
+ * be read.
6574
+ *
6575
+ * @function useAnnouncements
6576
+ * @param items - The items to announce, in the order they were shown.
6577
+ * @param describe - The text to announce for an item, or nothing to skip it.
6578
+ * It runs once the item is on screen, and it must keep its identity between
6579
+ * renders (a module-level function or a `useCallback`), or every render
6580
+ * re-checks the items.
6581
+ * @returns The announcements to render, in the order they were written.
6582
+ */
6583
+ function useAnnouncements(items, describe) {
6584
+ // The ids of the items on screen as of the last change, every one of which
6585
+ // has been looked at for an announcement already.
6586
+ const onScreenIdsRef = useRef(new Set());
6587
+ // Clears that haven't run yet. They outlive the batch that started them, so
6588
+ // they're only stopped when the region unmounts.
6589
+ const clearTimersRef = useRef(new Set());
6590
+ const [announcements, setAnnouncements] = useState(noAnnouncementsYet);
6591
+ // Runs after the commit that puts an item on screen, which is the first
6592
+ // point its rendered text can be read.
6593
+ useEffect(() => {
6594
+ const announced = onScreenIdsRef.current;
6595
+ const appeared = items.filter(item => !announced.has(item.id));
6596
+ // Items that have gone drop out here too. Ids are never reused, so
6597
+ // forgetting them can't bring an old item back.
6598
+ onScreenIdsRef.current = new Set(items.map(item => item.id));
6599
+ const next = [];
6600
+ for (const item of appeared) {
6601
+ const text = describe(item);
6602
+ if (text)
6603
+ next.push({ id: item.id, text });
6604
+ }
6605
+ if (next.length > 0) {
6606
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- an item's rendered text can only be read once it's on screen
6607
+ setAnnouncements(current => ({
6608
+ ...current,
6609
+ key: current.key + 1,
6610
+ // Announcements still waiting go out with the new ones. What's
6611
+ // already written stays put.
6612
+ waiting: [...current.waiting, ...next],
6613
+ }));
6614
+ }
6615
+ }, [items, describe]);
6616
+ // The write only touches the batch it was started for: a newer batch can
6617
+ // be set before the render that cancels this timer. Its clear is started
6618
+ // by the write itself, so it's timed from the write without waiting on a
6619
+ // render, and a newer batch arriving doesn't cut short what's written.
6620
+ const { key } = announcements;
6621
+ useEffect(() => {
6622
+ if (key === noAnnouncementsYet.key)
6623
+ return undefined;
6624
+ const clearTimers = clearTimersRef.current;
6625
+ const write = setTimeout(() => {
6626
+ // Whether an item is on screen is decided here, once. An item that
6627
+ // went before its announcement was due isn't announced, and one that
6628
+ // goes after keeps its announcement for the rest of its lifetime, when
6629
+ // the region holds the only copy of its text rather than a second one.
6630
+ const onScreen = onScreenIdsRef.current;
6631
+ setAnnouncements(current => current.key === key
6632
+ ? {
6633
+ ...current,
6634
+ waiting: noAnnouncements,
6635
+ written: [
6636
+ ...current.written,
6637
+ ...current.waiting
6638
+ .filter(announcement => onScreen.has(announcement.id))
6639
+ .map(announcement => ({ ...announcement, batch: key })),
6640
+ ],
6641
+ }
6642
+ : current);
6643
+ // eslint-disable-next-line @eslint-react/web-api-no-leaked-timeout -- it has to outlive this effect when a newer batch arrives, so the unmount effect below clears it
6644
+ const clear = setTimeout(() => {
6645
+ clearTimers.delete(clear);
6646
+ setAnnouncements(current => {
6647
+ const written = current.written.filter(announcement => announcement.batch !== key);
6648
+ return written.length === current.written.length
6649
+ ? current
6650
+ : { ...current, written };
6651
+ });
6652
+ }, announcementLifetime);
6653
+ clearTimers.add(clear);
6654
+ }, announceDelay);
6655
+ return () => clearTimeout(write);
6656
+ }, [key]);
6657
+ useEffect(() => {
6658
+ const clearTimers = clearTimersRef.current;
6659
+ return () => {
6660
+ clearTimers.forEach(clearTimeout);
6661
+ clearTimers.clear();
6662
+ };
6663
+ }, []);
6664
+ return announcements.written;
6665
+ }
6666
+ /**
6667
+ * Reads an element out roughly the way a screen reader would: its text, with
6668
+ * an element's `aria-label`, or an image's `alt`, standing in for what's
6669
+ * inside it, a break between block-level elements, and parts that are
6670
+ * `aria-hidden`, `hidden` or inline-styled `display: none` left out. It isn't
6671
+ * the full accessible name computation, so `aria-labelledby`, CSS-generated
6672
+ * content, content hidden by a stylesheet and other ways of naming or hiding
6673
+ * content aren't followed.
6674
+ *
6675
+ * @function spokenText
6676
+ * @param element - The element to read.
6677
+ * @returns Its text, with runs of whitespace collapsed and the ends trimmed.
6678
+ */
6679
+ function spokenText(element) {
6680
+ return readAloud(element).replace(/\s+/g, ' ').trim();
6681
+ }
6682
+ // Elements a browser displays as blocks by default, and `br`. A screen reader
6683
+ // pauses between them, so their text shouldn't run into the text beside them.
6684
+ // They're known by tag rather than by computed `display`, which would depend
6685
+ // on a stylesheet and on layout.
6686
+ const blockLevelTags = new Set([
6687
+ 'ADDRESS',
6688
+ 'ARTICLE',
6689
+ 'ASIDE',
6690
+ 'BLOCKQUOTE',
6691
+ 'BR',
6692
+ 'CAPTION',
6693
+ 'DD',
6694
+ 'DETAILS',
6695
+ 'DIALOG',
6696
+ 'DIV',
6697
+ 'DL',
6698
+ 'DT',
6699
+ 'FIELDSET',
6700
+ 'FIGCAPTION',
6701
+ 'FIGURE',
6702
+ 'FOOTER',
6703
+ 'FORM',
6704
+ 'H1',
6705
+ 'H2',
6706
+ 'H3',
6707
+ 'H4',
6708
+ 'H5',
6709
+ 'H6',
6710
+ 'HEADER',
6711
+ 'HGROUP',
6712
+ 'HR',
6713
+ 'LEGEND',
6714
+ 'LI',
6715
+ 'MAIN',
6716
+ 'MENU',
6717
+ 'NAV',
6718
+ 'OL',
6719
+ 'P',
6720
+ 'PRE',
6721
+ 'SEARCH',
6722
+ 'SECTION',
6723
+ 'SUMMARY',
6724
+ 'TABLE',
6725
+ 'TBODY',
6726
+ 'TD',
6727
+ 'TFOOT',
6728
+ 'TH',
6729
+ 'THEAD',
6730
+ 'TR',
6731
+ 'UL',
6732
+ ]);
6733
+ /**
6734
+ * Whether an element is left out of what a screen reader reads.
6735
+ *
6736
+ * @function isHidden
6737
+ * @param element - The element to check.
6738
+ * @returns True for `aria-hidden="true"`, the `hidden` attribute, and an
6739
+ * inline `display: none`, on an HTML, SVG or any other element with an
6740
+ * inline style.
6741
+ */
6742
+ const isHidden = (element) => element.getAttribute('aria-hidden') === 'true' ||
6743
+ element.hasAttribute('hidden') ||
6744
+ element.style?.display === 'none';
6745
+ /**
6746
+ * The text `spokenText` reads for an element, before its whitespace is
6747
+ * tidied.
6748
+ *
6749
+ * @function readAloud
6750
+ * @param element - The element to read.
6751
+ * @returns Its text.
6752
+ */
6753
+ function readAloud(element) {
6754
+ if (isHidden(element))
6755
+ return '';
6756
+ const name = element.getAttribute('aria-label') ||
6757
+ (element.tagName === 'IMG' ? element.getAttribute('alt') : null);
6758
+ const text = name ||
6759
+ Array.from(element.childNodes, child => {
6760
+ if (child instanceof Element)
6761
+ return readAloud(child);
6762
+ return child.nodeType === Node.TEXT_NODE ? child.textContent : '';
6763
+ }).join('');
6764
+ // A name stands in for the element's content, and a block's content stands
6765
+ // apart from what's around it, so both are padded to keep them from running
6766
+ // into the neighboring text. An inline run, like `<span>Up</span>` then
6767
+ // `<span>loaded</span>`, still reads as one word.
6768
+ return name || blockLevelTags.has(element.tagName) ? ` ${text} ` : text;
6769
+ }
6770
+ /**
6771
+ * A visually hidden, polite `status` live region that a programmatic
6772
+ * container announces its items through. The container renders it from the
6773
+ * moment it mounts, whether or not it has anything to show, so the region is
6774
+ * already in the page when an announcement is written into it.
6775
+ *
6776
+ * The announcements are this component's own state, so writing and clearing
6777
+ * them re-renders the region and leaves the container's items alone. Each one
6778
+ * is its own keyed node, so a re-render that leaves the announcements alone
6779
+ * leaves the region's content alone too, and nothing is read out twice.
6780
+ *
6781
+ * @function StatusRegion
6782
+ * @param props - The items to announce, and the text to announce for each.
6783
+ * @returns The region.
6784
+ */
6785
+ function StatusRegion({ items, describe, }) {
6786
+ const announcements = useAnnouncements(items, describe);
6787
+ // `role="status"` makes a region atomic unless it says otherwise, which
6788
+ // reads the whole region on every change. Each announcement is its own
6789
+ // node, and only additions are relevant by default, so with atomic off a
6790
+ // screen reader reads just what was written, and removing one announcement
6791
+ // doesn't read out the ones still there.
6792
+ return (jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "false", style: visuallyHidden, children: announcements.map(announcement => (jsx("div", { children: announcement.text }, announcement.id))) }));
6793
+ }
6794
+
6266
6795
  /**
6267
6796
  * The `Notification` component is a Bulma-styled alert/message area for providing feedback, warnings, or information to users.
6268
6797
  *
@@ -6286,7 +6815,7 @@ const Notification = ({ className, color, textColor, isLight, hasDelete, onDelet
6286
6815
  });
6287
6816
  const deleteClasses = usePrefixedClassNames('delete');
6288
6817
  const notificationClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
6289
- return (jsxs("div", { className: notificationClasses, ...rest, children: [hasDelete && (jsx("button", { className: deleteClasses, onClick: onDelete, "aria-label": "Close notification" })), children] }));
6818
+ return (jsxs("div", { className: notificationClasses, ...rest, children: [hasDelete && (jsx("button", { type: "button", className: deleteClasses, onClick: onDelete, "aria-label": "Close notification" })), children] }));
6290
6819
  };
6291
6820
  let notificationId = 0;
6292
6821
  const notificationListeners = new Set();
@@ -6305,9 +6834,9 @@ const getServerNotifications = () => noNotifications;
6305
6834
  // updates alone, so notifications raised before it mounted still appear. It is
6306
6835
  // replaced rather than mutated, and only when listeners are notified, because
6307
6836
  // useSyncExternalStore needs the same array back between changes. An empty
6308
- // list is `noNotifications` itself, so a container that hydrates with nothing
6309
- // to show reads the same snapshot the server did and has no reason to render
6310
- // again.
6837
+ // list is `noNotifications` itself, so an empty store reads the same snapshot
6838
+ // on the client as on the server, and a notify that leaves nothing showing
6839
+ // when nothing was showing doesn't render the container again.
6311
6840
  let visibleNotifications = noNotifications;
6312
6841
  const getVisibleNotifications = () => visibleNotifications;
6313
6842
  const notifyNotificationListeners = () => {
@@ -6418,20 +6947,32 @@ const notification = {
6418
6947
  return () => notificationListeners.delete(listener);
6419
6948
  },
6420
6949
  };
6950
+ /**
6951
+ * Whether a notification of this color announces itself as an alert, rather
6952
+ * than politely through NotificationContainer's status region.
6953
+ *
6954
+ * @function isUrgentColor
6955
+ * @param {NotificationOptions['color']} color - The notification's color.
6956
+ * @returns {boolean} True for `danger` and `warning`.
6957
+ */
6958
+ const isUrgentColor = (color) => color === 'danger' || color === 'warning';
6421
6959
  /**
6422
6960
  * Single auto-dismissing notification item used by NotificationContainer.
6423
6961
  *
6424
6962
  * @function
6425
- * @param {{ instance: NotificationInstance; onClose: (id: string) => void }} props - Component props.
6963
+ * @param {{ instance: NotificationInstance; onClose: (id: string) => void; registerMessage: (id: string, node: HTMLElement | null) => void }} props - Component props.
6426
6964
  * @returns {JSX.Element} The rendered notification item.
6427
6965
  */
6428
- const NotificationItem = ({ instance, onClose }) => {
6966
+ const NotificationItem = ({ instance, onClose, registerMessage }) => {
6429
6967
  const { message, color, isLight, duration = 3000, hasDelete = true, indefinite = false, pauseOnHover = true, } = instance.options;
6430
6968
  const [isPaused, setIsPaused] = useState(false);
6431
- const urgent = color === 'danger' || color === 'warning';
6969
+ const urgent = isUrgentColor(color);
6432
6970
  const handleClose = useCallback(() => {
6433
6971
  onClose(instance.id);
6434
6972
  }, [onClose, instance.id]);
6973
+ const messageRef = useCallback((node) => {
6974
+ registerMessage(instance.id, node);
6975
+ }, [registerMessage, instance.id]);
6435
6976
  // Auto-close timer
6436
6977
  useEffect(() => {
6437
6978
  if (indefinite || duration === 0 || isPaused)
@@ -6447,27 +6988,30 @@ const NotificationItem = ({ instance, onClose }) => {
6447
6988
  if (pauseOnHover)
6448
6989
  setIsPaused(false);
6449
6990
  }, [pauseOnHover]);
6450
- return (jsx(Notification, { color: color, isLight: isLight, hasDelete: hasDelete, onDelete: handleClose, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, style: { pointerEvents: 'auto' }, children: urgent ? (jsx("span", { role: "alert", "aria-live": "assertive", children: message })) : (jsx("span", { role: "status", "aria-live": "polite", children: message })) }));
6451
- };
6991
+ return (jsx(Notification, { color: color, isLight: isLight, hasDelete: hasDelete, onDelete: handleClose, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, style: { pointerEvents: 'auto' }, children: urgent ? (jsx("span", { role: "alert", "aria-live": "assertive", children: message })) : (jsx("span", { ref: messageRef, children: message })) }));
6992
+ };
6993
+ // The order NotificationContainer renders its stacks in: across the top of the
6994
+ // screen, then across the bottom.
6995
+ const notificationStackOrder = [
6996
+ 'top-left',
6997
+ 'top',
6998
+ 'top-right',
6999
+ 'bottom-left',
7000
+ 'bottom',
7001
+ 'bottom-right',
7002
+ ];
6452
7003
  /**
6453
- * Container component for rendering programmatic notifications.
6454
- * Place once at your app root to enable the notification API.
7004
+ * Inline style that fixes a stack of notifications to its place on the screen.
6455
7005
  *
6456
7006
  * @function
6457
- * @param {{ position?: NotificationPosition }} props - Container props.
6458
- * @returns {JSX.Element | null} The rendered notification container, or null if empty.
7007
+ * @param {NotificationPosition} position - Where the stack sits.
7008
+ * @returns {React.CSSProperties} The stack's style.
6459
7009
  */
6460
- const NotificationContainer = ({ position = 'top-right' }) => {
6461
- // Starts from the notifications already showing instead of an empty list,
6462
- // then follows changes.
6463
- const items = useSyncExternalStore(notification.subscribe, getVisibleNotifications, getServerNotifications);
6464
- if (typeof document === 'undefined' || items.length === 0) {
6465
- return null;
6466
- }
7010
+ const notificationStackStyle = (position) => {
6467
7011
  const isBottom = position.startsWith('bottom');
6468
7012
  const isCenter = position === 'top' || position === 'bottom';
6469
7013
  const isRight = position.endsWith('right');
6470
- const containerStyle = {
7014
+ return {
6471
7015
  position: 'fixed',
6472
7016
  zIndex: 100,
6473
7017
  display: 'flex',
@@ -6483,7 +7027,71 @@ const NotificationContainer = ({ position = 'top-right' }) => {
6483
7027
  ? { right: 0, alignItems: 'flex-end' }
6484
7028
  : { left: 0, alignItems: 'flex-start' }),
6485
7029
  };
6486
- return createPortal(jsx("div", { style: containerStyle, children: items.map(item => (jsx(NotificationItem, { instance: item, onClose: notification.close }, item.id))) }), document.body);
7030
+ };
7031
+ /**
7032
+ * Container component for rendering programmatic notifications.
7033
+ * Place once at your app root to enable the notification API. A notification
7034
+ * shown with a `position` appears there, and one shown without goes to the
7035
+ * container's `position`, so the container renders a stack for each position
7036
+ * in use.
7037
+ *
7038
+ * It keeps a visually hidden `role="status"` live region in the page from the
7039
+ * moment it mounts, even while nothing is showing, and announces every
7040
+ * notification other than `danger` and `warning` through it, a moment after
7041
+ * the notification appears. Those notifications carry no `role="status"` of
7042
+ * their own, so `getByRole('status')` finds the region, not the notification.
7043
+ * The text stays in the region briefly, even if the notification closes in
7044
+ * the meantime, and while both are up the page holds two copies of it. A
7045
+ * polite notification that closes before its announcement is written, a
7046
+ * moment after it appears, isn't announced at all. `danger` and `warning`
7047
+ * notifications announce themselves as assertive alerts. The region is hidden
7048
+ * with inline styles, so it needs no stylesheet.
7049
+ *
7050
+ * @function
7051
+ * @param {{ position?: NotificationPosition }} props - Container props.
7052
+ * @returns {JSX.Element | null} The rendered notification container, or null on the server and while hydrating.
7053
+ */
7054
+ const NotificationContainer = ({ position = 'top-right' }) => {
7055
+ // Starts from the notifications already showing instead of an empty list,
7056
+ // then follows changes.
7057
+ const items = useSyncExternalStore(notification.subscribe, getVisibleNotifications, getServerNotifications);
7058
+ // A portal needs a document, and it has no server-rendered counterpart, so
7059
+ // the server render and the hydrating one render nothing, and the container
7060
+ // portals in from the render after hydration.
7061
+ const isHydrated = useIsHydrated();
7062
+ // The notifications the status region announces. Danger and warning
7063
+ // announce themselves.
7064
+ const politeItems = useMemo(() => items.filter(item => !isUrgentColor(item.options.color)), [items]);
7065
+ // Each polite notification's message element, so the text the region
7066
+ // announces is the text on screen, whatever the message renders.
7067
+ const messageNodesRef = useRef(new Map());
7068
+ const registerMessage = useCallback((id, node) => {
7069
+ if (node) {
7070
+ messageNodesRef.current.set(id, node);
7071
+ }
7072
+ else {
7073
+ messageNodesRef.current.delete(id);
7074
+ }
7075
+ }, []);
7076
+ // StatusRegion only describes polite notifications that are on screen, and
7077
+ // each of those registered its message element when it mounted. The
7078
+ // fallback is for a notification announced without being rendered, should
7079
+ // the container ever render fewer than it announces: a plain-text message
7080
+ // still says itself, and anything else is skipped.
7081
+ const describe = useCallback((item) => {
7082
+ const node = messageNodesRef.current.get(item.id);
7083
+ /* istanbul ignore if: every polite notification on screen registers its message element when it mounts */
7084
+ if (!node) {
7085
+ const { message } = item.options;
7086
+ return typeof message === 'string' ? message : null;
7087
+ }
7088
+ return spokenText(node);
7089
+ }, []);
7090
+ if (typeof document === 'undefined' || !isHydrated) {
7091
+ return null;
7092
+ }
7093
+ const stacks = groupIntoPositionStacks(items, item => item.options.position, position, notificationStackOrder);
7094
+ return createPortal(jsxs(Fragment, { children: [stacks.map(stack => (jsx("div", { style: notificationStackStyle(stack.position), children: stack.items.map(item => (jsx(NotificationItem, { instance: item, onClose: notification.close, registerMessage: registerMessage }, item.id))) }, stack.key))), jsx(StatusRegion, { items: politeItems, describe: describe })] }), document.body);
6487
7095
  };
6488
7096
 
6489
7097
  /**
@@ -6735,7 +7343,26 @@ const Tfoot = ({ className, children, ...props }) => {
6735
7343
  return (jsx("tfoot", { className: tfootClasses || undefined, ...rest, children: children }));
6736
7344
  };
6737
7345
 
6738
- /** Valid Bulma color values for table cells. */
7346
+ /**
7347
+ * The values the table `color` prop accepts, as a readonly tuple.
7348
+ *
7349
+ * `TableColor` is typed from it, and `Tr`, `Th` and `Td` all take a
7350
+ * `TableColor`, so the tuple and those props list the same values. Map over
7351
+ * it to build a color picker, or check a value that arrives at runtime before
7352
+ * passing it in: the components add no color class for a value outside the
7353
+ * tuple.
7354
+ *
7355
+ * @example
7356
+ * import { Tr, Td, validTableColors } from '@allxsmith/bestax-bulma';
7357
+ *
7358
+ * <Tr>
7359
+ * {validTableColors.map(color => (
7360
+ * <Td key={color} color={color}>
7361
+ * {color}
7362
+ * </Td>
7363
+ * ))}
7364
+ * </Tr>;
7365
+ */
6739
7366
  const validTableColors = [
6740
7367
  'primary',
6741
7368
  'link',
@@ -6892,7 +7519,7 @@ const Tag = ({ className, color, size, isLight, isRounded, isDelete, isHoverable
6892
7519
  });
6893
7520
  const tagClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
6894
7521
  if (isDelete) {
6895
- return (jsx("button", { className: tagClasses, onClick: onDelete, "aria-label": "Delete tag", ...rest }));
7522
+ return (jsx("button", { className: tagClasses, onClick: onDelete, ...rest, "aria-label": rest['aria-label'] || 'Delete tag', type: buttonType(rest.type) }));
6896
7523
  }
6897
7524
  return (jsx("span", { className: tagClasses, ...rest, children: children }));
6898
7525
  };
@@ -9074,8 +9701,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
9074
9701
  if (!isActive)
9075
9702
  return undefined;
9076
9703
  const handleClickOutside = (e) => {
9077
- if (containerRef.current &&
9078
- !containerRef.current.contains(e.target)) {
9704
+ if (containerRef.current && !isEventInside(e, containerRef.current)) {
9079
9705
  setIsActive(false);
9080
9706
  setHighlightedIndex(-1);
9081
9707
  }
@@ -13235,9 +13861,18 @@ const bulmaVarPropMap = Object.fromEntries(bulmaCssVars
13235
13861
  * real 0 keeps what
13236
13862
  * people see and makes it valid CSS. Keyed by the helper values, so adding
13237
13863
  * one means saying what it writes.
13864
+ *
13865
+ * The sizes write nothing and only add their class. Their classes read the
13866
+ * radius variables, so `normal` would point `--bulma-radius` at itself, which
13867
+ * is invalid and computes to 0, and `rounded` would turn every control inside
13868
+ * the Theme into a pill.
13238
13869
  */
13239
13870
  const radiusHelperVars = {
13240
13871
  radiusless: '0',
13872
+ small: undefined,
13873
+ normal: undefined,
13874
+ large: undefined,
13875
+ rounded: undefined,
13241
13876
  };
13242
13877
  /**
13243
13878
  * What `radius` writes to `--bulma-radius` on a Theme, or `undefined` when it
@@ -13264,7 +13899,7 @@ const themeRadiusVar = (radius) => {
13264
13899
  warnOnce('Theme:radius-variable', `[bestax-bulma] <Theme radius="${radius}">: setting --bulma-radius ` +
13265
13900
  'through the radius prop is deprecated and will stop working in a ' +
13266
13901
  'future major version. On Theme, as on every other component, radius ' +
13267
- `is the "${validRadii.join('", "')}" helper. Set the variable with ` +
13902
+ `is the border radius helper ("${validRadii.join('", "')}"). Set the variable with ` +
13268
13903
  `bulmaVars={{ '--bulma-radius': '${radius}' }} instead.`);
13269
13904
  return radius;
13270
13905
  };
@@ -13350,6 +13985,16 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
13350
13985
  const radiusHelper = validRadii.includes(radius)
13351
13986
  ? radius
13352
13987
  : undefined;
13988
+ // A radius size only adds its class, and a root Theme renders no wrapper to
13989
+ // carry one, so on `isRoot` it does nothing at all. Say so, as the other
13990
+ // radius routes that do not do what they look like do.
13991
+ if (isRoot && radiusHelper && radiusHelperVars[radiusHelper] === undefined) {
13992
+ warnOnce('Theme:root-radius-size', `[bestax-bulma] <Theme isRoot radius="${radiusHelper}">: a root Theme ` +
13993
+ `has no wrapper element for has-radius-${radiusHelper}, and the ` +
13994
+ 'radius sizes set no variable, so this does nothing. To round one ' +
13995
+ `element, put radius="${radiusHelper}" on it. To change the radius ` +
13996
+ "across the page, set bulmaVars={{ '--bulma-radius': '…' }} instead.");
13997
+ }
13353
13998
  // Extract Bulma variable props from restProps
13354
13999
  const { bulmaVarProps, otherProps } = useMemo(() => {
13355
14000
  const varProps = {};
@@ -13800,5 +14445,5 @@ const Section = ({ size, className, children, color, bgColor, textColor, style,
13800
14445
  return (jsx("section", { className: sectionClasses, style: mergeBulmaStyles(bulmaHelperStyles, style), ...rest, children: children }));
13801
14446
  };
13802
14447
 
13803
- export { Autocomplete, Avatar, Avatars, Badge, Block, Box, Breadcrumb, Button, Buttons, Card, Carousel, CarouselItem, Cell, Checkbox, Checkboxes, Code, Collapse, Column, Columns, ConfigProvider, Container, Content, Control, DateInput, DateInputBase, DateTimeInput, DateTimeInputBase, Delete, Dialog, DialogContainer, Divider, Dropdown, DropdownDivider, DropdownItem, Emphasis, Field, FieldBody, FieldLabel, Figure, File, Footer, Grid, Hero, HeroBody, HeroFoot, HeroHead, Icon, IconText, Image, Input, InputBase, Level, LevelItem, LevelLeft, LevelRight, Link, LinkButton, ListItem, Loading, Media, MediaContent, MediaLeft, MediaRight, Menu, MenuItem, MenuLabel, MenuList, Message, Modal, Navbar, NavbarBrand, NavbarBurger, NavbarDivider, NavbarDropdown, NavbarDropdownMenu, NavbarEnd, NavbarItem, NavbarLink, NavbarMenu, NavbarStart, Notification, NotificationContainer, NumberInput, Numberinput, OrderedList, Pagination, PaginationEllipsis, PaginationLink, PaginationList, PaginationNext, PaginationPrevious, Panel, PanelBlock, PanelButtonBlock, PanelCheckboxBlock, PanelHeading, PanelIcon, PanelInputBlock, PanelTabs, Paragraph, Pre, Progress, Radio, Radios, Rate, Reveal, Section, Select, SelectBase, Sidebar, Skeleton, Slider, Span, Step, Steps, Strong, SubTitle, Switch, Tab, TabContentItem, TabItem, TabList, Table, Tabs, TabsContent, Tag, TagInput, Taginput, Tags, Tbody, Td, TextArea, TextAreaBase, Tfoot, Th, Thead, Theme, TimeInput, TimeInputBase, Title, Toast, ToastContainer, Tooltip, Tr, UnorderedList, checkboxColors, checkboxSizes, classNames, createPrefixedClassNames, dialog, isBrowser$1 as isBrowser, mergeBulmaStyles, notification, prefixedClassNames, radioColors, radioSizes, switchColors, switchSizes, toast, useBulmaClasses, useClassPrefix, useColorClasses, useColorStyles, useConfig, useFlexboxClasses, useIconLibrary, useInsideControl, useInsideField, useOtherClasses, usePrefixedClass, usePrefixedClassNames, useSpacingClasses, useTypographyClasses, useVisibilityClasses, validAlignContents, validAlignItems, validAlignSelfs, validAlignments$1 as validAlignments, validColorShades, validColors, validCursors, validDisplays, validFlexDirections, validFlexGrowShrink, validFlexWraps, validFloats, validFontFamilies, validInteractions, validJustifyContents, validOverflows, validRadii, validResponsives, validSchemeColors, validShadows, validSizes$1 as validSizes, validTableColors, validTextSizes, validTextTransforms, validTextWeights, validViewports, validVisibilities };
14448
+ export { Autocomplete, Avatar, Avatars, Badge, Block, Box, Breadcrumb, Button, Buttons, Card, Carousel, CarouselItem, Cell, Checkbox, Checkboxes, Code, Collapse, Column, Columns, ConfigProvider, Container, Content, Control, DateInput, DateInputBase, DateTimeInput, DateTimeInputBase, Delete, Dialog, DialogContainer, Divider, Dropdown, DropdownDivider, DropdownItem, Emphasis, Field, FieldBody, FieldLabel, Figure, File, Footer, Grid, Hero, HeroBody, HeroFoot, HeroHead, Icon, IconText, Image, Input, InputBase, Level, LevelItem, LevelLeft, LevelRight, Link, LinkButton, ListItem, Loading, Media, MediaContent, MediaLeft, MediaRight, Menu, MenuItem, MenuLabel, MenuList, Message, Modal, Navbar, NavbarBrand, NavbarBurger, NavbarDivider, NavbarDropdown, NavbarDropdownMenu, NavbarEnd, NavbarItem, NavbarLink, NavbarMenu, NavbarStart, Notification, NotificationContainer, NumberInput, Numberinput, OrderedList, Pagination, PaginationEllipsis, PaginationLink, PaginationList, PaginationNext, PaginationPrevious, Panel, PanelBlock, PanelButtonBlock, PanelCheckboxBlock, PanelHeading, PanelIcon, PanelInputBlock, PanelTabs, Paragraph, Pre, Progress, Radio, Radios, Rate, Reveal, Section, Select, SelectBase, Sidebar, Skeleton, Slider, Span, Step, Steps, Strong, SubTitle, Switch, Tab, TabContentItem, TabItem, TabList, Table, Tabs, TabsContent, Tag, TagInput, Taginput, Tags, Tbody, Td, TextArea, TextAreaBase, Tfoot, Th, Thead, Theme, TimeInput, TimeInputBase, Title, Toast, ToastContainer, Tooltip, Tr, UnorderedList, checkboxColors, checkboxSizes, classNames, createPrefixedClassNames, dialog, isBrowser$1 as isBrowser, mergeBulmaStyles, notification, prefixedClassNames, radioColors, radioSizes, switchColors, switchSizes, toast, useBulmaClasses, useClassPrefix, useColorClasses, useColorStyles, useConfig, useFlexboxClasses, useIconLibrary, useInsideControl, useInsideField, useOtherClasses, usePrefixedClass, usePrefixedClassNames, useSpacingClasses, useTypographyClasses, useVisibilityClasses, validAlignContents, validAlignItems, validAlignSelfs, validAlignments$1 as validAlignments, validAspectRatios, validAxisOverflows, validColorShades, validColors, validCursors, validDisplays, validFlexDirections, validFlexGrowShrink, validFlexWraps, validFloats, validFontFamilies, validInteractions, validJustifyContents, validOverflows, validPositions, validRadii, validResponsives, validSchemeColors, validShadows, validSizes$1 as validSizes, validTableColors, validTextSizes, validTextTransforms, validTextWeights, validViewports, validVisibilities };
13804
14449
  //# sourceMappingURL=index.esm.js.map