@allxsmith/bestax-bulma 5.62.0 → 5.64.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 (89) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/EmptyState.js +2 -6
  4. package/dist/esm/components/EmptyState.js.map +1 -1
  5. package/dist/esm/components/ScrollArea.js +7 -7
  6. package/dist/esm/components/ScrollArea.js.map +1 -1
  7. package/dist/esm/components/TableOfContents.js +85 -0
  8. package/dist/esm/components/TableOfContents.js.map +1 -0
  9. package/dist/esm/components/Timeline.js +5 -9
  10. package/dist/esm/components/Timeline.js.map +1 -1
  11. package/dist/esm/components/TreeView.js +3 -7
  12. package/dist/esm/components/TreeView.js.map +1 -1
  13. package/dist/esm/elements/Buttons.js +2 -0
  14. package/dist/esm/elements/Buttons.js.map +1 -1
  15. package/dist/esm/elements/CopyButton.js +132 -0
  16. package/dist/esm/elements/CopyButton.js.map +1 -0
  17. package/dist/esm/elements/DescriptionList.js +1 -5
  18. package/dist/esm/elements/DescriptionList.js.map +1 -1
  19. package/dist/esm/elements/Stat.js +196 -0
  20. package/dist/esm/elements/Stat.js.map +1 -0
  21. package/dist/esm/elements/Stats.js +48 -0
  22. package/dist/esm/elements/Stats.js.map +1 -0
  23. package/dist/esm/elements/statContext.js +10 -0
  24. package/dist/esm/elements/statContext.js.map +1 -0
  25. package/dist/esm/form/Combobox.js +2 -1
  26. package/dist/esm/form/Combobox.js.map +1 -1
  27. package/dist/esm/form/PasswordInput.js +105 -0
  28. package/dist/esm/form/PasswordInput.js.map +1 -0
  29. package/dist/esm/form/PasswordInputBase.js +177 -0
  30. package/dist/esm/form/PasswordInputBase.js.map +1 -0
  31. package/dist/esm/helpers/checkIcon.js +20 -0
  32. package/dist/esm/helpers/checkIcon.js.map +1 -0
  33. package/dist/esm/helpers/isGiven.js +13 -0
  34. package/dist/esm/helpers/isGiven.js.map +1 -0
  35. package/dist/esm/helpers/statusRegion.js +3 -3
  36. package/dist/esm/helpers/useClipboard.js +176 -0
  37. package/dist/esm/helpers/useClipboard.js.map +1 -0
  38. package/dist/esm/helpers/useScrollSpy.js +286 -0
  39. package/dist/esm/helpers/useScrollSpy.js.map +1 -0
  40. package/dist/extras.css +1 -1
  41. package/dist/extras.css.map +1 -1
  42. package/dist/index.cjs +2671 -1544
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.esm.js +8 -0
  45. package/dist/index.esm.js.map +1 -1
  46. package/dist/types/components/TableOfContents.d.ts +87 -0
  47. package/dist/types/elements/Buttons.d.ts +1 -0
  48. package/dist/types/elements/CopyButton.d.ts +105 -0
  49. package/dist/types/elements/Stat.d.ts +242 -0
  50. package/dist/types/elements/Stats.d.ts +40 -0
  51. package/dist/types/elements/statContext.d.ts +18 -0
  52. package/dist/types/form/PasswordInput.d.ts +98 -0
  53. package/dist/types/form/PasswordInputBase.d.ts +91 -0
  54. package/dist/types/helpers/checkIcon.d.ts +10 -0
  55. package/dist/types/helpers/isGiven.d.ts +7 -0
  56. package/dist/types/helpers/useClipboard.d.ts +65 -0
  57. package/dist/types/helpers/useScrollSpy.d.ts +61 -0
  58. package/dist/types/index.d.ts +10 -0
  59. package/dist/types-cjs/components/TableOfContents.d.ts +87 -0
  60. package/dist/types-cjs/elements/Buttons.d.ts +1 -0
  61. package/dist/types-cjs/elements/CopyButton.d.ts +105 -0
  62. package/dist/types-cjs/elements/Stat.d.ts +242 -0
  63. package/dist/types-cjs/elements/Stats.d.ts +40 -0
  64. package/dist/types-cjs/elements/statContext.d.ts +18 -0
  65. package/dist/types-cjs/form/PasswordInput.d.ts +98 -0
  66. package/dist/types-cjs/form/PasswordInputBase.d.ts +91 -0
  67. package/dist/types-cjs/helpers/checkIcon.d.ts +10 -0
  68. package/dist/types-cjs/helpers/isGiven.d.ts +7 -0
  69. package/dist/types-cjs/helpers/useClipboard.d.ts +65 -0
  70. package/dist/types-cjs/helpers/useScrollSpy.d.ts +61 -0
  71. package/dist/types-cjs/index.d.ts +10 -0
  72. package/dist/versions/bestax-no-dark-mode.css +1 -1
  73. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  74. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  75. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  76. package/dist/versions/bestax-no-helpers.css +1 -1
  77. package/dist/versions/bestax-no-helpers.css.map +1 -1
  78. package/dist/versions/bestax-prefixed.css +1 -1
  79. package/dist/versions/bestax-prefixed.css.map +1 -1
  80. package/package.json +1 -1
  81. package/src/scss/CLAUDE.md +3 -0
  82. package/src/scss/_input-trigger.scss +76 -0
  83. package/src/scss/components/_index.scss +1 -0
  84. package/src/scss/components/_tableofcontents.scss +42 -0
  85. package/src/scss/elements/_index.scss +1 -0
  86. package/src/scss/elements/_stat.scss +153 -0
  87. package/src/scss/form/_index.scss +1 -0
  88. package/src/scss/form/_passwordinput.scss +112 -0
  89. package/src/scss/form/_picker-popover.scss +24 -63
package/dist/index.cjs CHANGED
@@ -3937,378 +3937,1125 @@ const IconButton = React.forwardRef(function IconButton(props, ref) {
3937
3937
  });
3938
3938
  IconButton.displayName = 'IconButton';
3939
3939
 
3940
+ // INTERNAL: deliberately not exported from src/index.ts.
3940
3941
  /**
3941
- * The `LinkButton` component renders a `<button>` that visually looks like text or a link.
3942
- *
3943
- * @function
3944
- * @param {LinkButtonProps} props - Props for the LinkButton component.
3945
- * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
3946
- * @returns {JSX.Element} The rendered link-styled button element.
3947
- *
3948
- * @example
3949
- * // Text variant (default)
3950
- * <LinkButton onClick={handleClick}>Click me</LinkButton>
3942
+ * A check mark, 1em square and drawn in `currentColor`, so it takes the text
3943
+ * color around it, the system's in forced colors included. Combobox draws it
3944
+ * beside a chosen item, and CopyButton after a copy. It's decorative: put it
3945
+ * inside something `aria-hidden`.
3951
3946
  *
3952
- * @example
3953
- * // Underline variant with color
3954
- * <LinkButton variant="underline" color="primary">Learn more</LinkButton>
3947
+ * @function CheckIcon
3948
+ * @returns The check mark's `<svg>`.
3955
3949
  */
3956
- const LinkButton = React.forwardRef(function LinkButton({ variant = 'text', color, className, ...props }, ref) {
3957
- const buttonColor = variant === 'underline' ? 'text' : variant;
3958
- const prefixedClasses = usePrefixedClassNames('link-button', color && `link-button-${color}`, variant === 'underline' && 'link-button-underline');
3959
- return (jsxRuntime.jsx(Button
3960
- // LinkButton is polymorphic through to Button, which resolves the element
3961
- // from `as`. TS cannot infer that through the spread below.
3962
- , {
3963
- // LinkButton is polymorphic through to Button, which resolves the element
3964
- // from `as`. TS cannot infer that through the spread below.
3965
- ref: ref, color: buttonColor, className: classNames(prefixedClasses, className), ...props }));
3966
- });
3967
- LinkButton.displayName = 'LinkButton';
3950
+ function CheckIcon$1() {
3951
+ return (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", width: "1em", height: "1em", focusable: "false", children: jsxRuntime.jsx("polyline", { points: "3 8.5 6.5 12 13 4.5" }) }));
3952
+ }
3968
3953
 
3954
+ // "Are we past hydration?" as a store that never changes: React reads the
3955
+ // server snapshot both while server-rendering and while hydrating, and the
3956
+ // client snapshot from the first post-hydration render onward.
3957
+ const subscribeToNothing = () => () => { };
3958
+ const getClientSnapshot = () => true;
3959
+ const getServerSnapshot = () => false;
3969
3960
  /**
3970
- * The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.
3971
- *
3972
- * It's always a `<button type="button">`, so it never submits a form and
3973
- * carries no form value: a setting saved with a form is a `Checkbox` or a
3974
- * `Switch`. A set of toggles that each turn on and off on their own goes in
3975
- * `Buttons hasAddons` with `role="group"` and an `aria-label`. A set where only
3976
- * one can be on is a radio group rather than a row of these: use
3977
- * `SegmentedControl`, or `Radios` for a choice that's sent with a form.
3961
+ * Reports whether the component has passed hydration.
3978
3962
  *
3979
- * @function
3980
- * @param {ToggleButtonProps} props - Props for the ToggleButton component.
3981
- * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
3982
- * @returns {JSX.Element} The rendered toggle button.
3983
- * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/button/ | APG button pattern}
3963
+ * Returns `false` during server rendering and during the hydrating client
3964
+ * render, then `true` from the commit that follows. Use it to defer
3965
+ * DOM-only behaviour (such as portalling) past hydration so the first client
3966
+ * render matches the server markup instead of tripping hydration recovery.
3984
3967
  *
3985
- * @example
3986
- * <ToggleButton onPressedChange={setMuted}>Mute</ToggleButton>
3968
+ * Only a component that hydrates ever sees `false` on the client. One that
3969
+ * mounts later (after a route change, or in an app with no server rendering)
3970
+ * gets `true` on its first render, so it pays for no extra render. It
3971
+ * subscribes to nothing, so no later render flips it back.
3987
3972
  *
3988
- * @example
3989
- * <Buttons hasAddons role="group" aria-label="Text style">
3990
- * <ToggleButton aria-label="Bold" pressed={bold} onPressedChange={setBold}>
3991
- * <Icon name="bold" />
3992
- * </ToggleButton>
3993
- * </Buttons>
3973
+ * @function useIsHydrated
3974
+ * @returns `true` once the hydrating render has completed, otherwise `false`.
3994
3975
  */
3995
- const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', isLoading, 'aria-busy': ariaBusy, 'aria-disabled': ariaDisabled, className, onClick, ...props }, ref) {
3996
- const isControlled = controlledPressed !== undefined;
3997
- const [internalPressed, setInternalPressed] = React.useState(defaultPressed);
3998
- const pressed = controlledPressed ?? internalPressed;
3999
- // Prefixed like Button's own classes, which a hand-written
4000
- // `className="is-selected"` could not be under ConfigProvider classPrefix.
4001
- // Bulma lifts `is-selected` above its neighbours in `.buttons.has-addons`.
4002
- const selectedClass = usePrefixedClassNames({ 'is-selected': pressed });
4003
- // The caller's onClick runs first, so a `preventDefault()` there vetoes
4004
- // the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here
4005
- // through the button's own click, and a disabled button gets no click.
4006
- //
4007
- // A loading one gets the click but drops it. Bulma's spinner only stops a
4008
- // pointer, so without this a key flipped a toggle the mouse couldn't.
4009
- // `disabled` would stop both, but it takes focus away mid-save.
4010
- const handleClick = (event) => {
4011
- if (isLoading)
4012
- return;
4013
- onClick?.(event);
4014
- if (event.defaultPrevented)
4015
- return;
4016
- const next = !pressed;
4017
- onPressedChange?.(next);
4018
- if (!isControlled)
4019
- setInternalPressed(next);
4020
- };
4021
- // The props are spread first, so what a toggle depends on wins over
4022
- // anything a JavaScript caller or a spread object passes along: it stays
4023
- // a button, never submits, reports the state it's in, and says it's busy
4024
- // and won't respond while it's loading. A caller's own `aria-busy` or
4025
- // `aria-disabled` stays when it isn't loading.
4026
- return (jsxRuntime.jsx(Button, { ...props, ref: ref, as: "button", type: "button", isLoading: isLoading, "aria-busy": isLoading || ariaBusy, "aria-disabled": isLoading || ariaDisabled, "aria-pressed": pressed, color: pressed ? color : undefined, className: classNames(selectedClass, className), onClick: handleClick }));
4027
- });
4028
- ToggleButton.displayName = 'ToggleButton';
3976
+ function useIsHydrated() {
3977
+ return React.useSyncExternalStore(subscribeToNothing, getClientSnapshot, getServerSnapshot);
3978
+ }
4029
3979
 
4030
- const validButtonsSizes = ['small', 'medium', 'large'];
4031
3980
  /**
4032
- * The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.
3981
+ * Resolves a portal target: an `HTMLElement` is used directly, a non-empty
3982
+ * `string` is treated as a `document.querySelector` selector (falling back to
3983
+ * `document.body` when it matches nothing), and any falsy value — `undefined`
3984
+ * or the empty string — resolves to `document.body`.
4033
3985
  *
4034
- * @function
4035
- * @param {ButtonsProps} props - Props for the Buttons component.
4036
- * @returns {JSX.Element} The rendered group of buttons.
4037
- * @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}
4038
- */
4039
- const ButtonsComponent = ({ className, textColor, color, bgColor, isCentered, isRight, hasAddons, size, children, ...props }) => {
4040
- const buttonsClasses = usePrefixedClassNames('buttons', {
4041
- 'is-centered': isCentered,
4042
- 'is-right': isRight,
4043
- 'has-addons': hasAddons,
4044
- [`are-${size}`]: size && validButtonsSizes.includes(size),
4045
- });
4046
- /**
4047
- * Generates Bulma helper classes and separates out remaining props.
4048
- */
4049
- const { bulmaHelperClasses, rest } = useBulmaClasses({
4050
- color: textColor ?? color,
4051
- backgroundColor: bgColor,
4052
- ...props,
4053
- });
4054
- const combinedClasses = classNames(buttonsClasses, className, bulmaHelperClasses);
4055
- return (jsxRuntime.jsx("div", { className: combinedClasses, ...rest, children: children }));
4056
- };
4057
- const Buttons = withSubComponents(ButtonsComponent, {
4058
- Button,
4059
- LinkButton,
4060
- ToggleButton,
4061
- IconButton,
4062
- }, 'Buttons');
4063
-
4064
- /**
4065
- * The `Paragraph` component renders a styled `<p>` element with Bulma helper class integration.
3986
+ * The empty string is deliberately treated as "no target" rather than passed
3987
+ * through: `document.querySelector('')` throws a `SyntaxError`, and callers
3988
+ * building a selector from state can easily hand us `''`.
4066
3989
  *
4067
- * @function
4068
- * @param {ParagraphProps} props - Props for the Paragraph component.
4069
- * @returns {JSX.Element} The rendered paragraph element.
3990
+ * @function resolvePortalContainer
3991
+ * @param container - The requested portal target, if any.
3992
+ * @returns The resolved DOM node to portal into.
4070
3993
  */
4071
- const Paragraph = ({ className, textColor, bgColor, children, ...props }) => {
4072
- /**
4073
- * Generates Bulma helper classes and separates out remaining props.
4074
- */
4075
- const { bulmaHelperClasses, rest } = useBulmaClasses({
4076
- color: textColor,
4077
- backgroundColor: bgColor,
4078
- ...props,
4079
- });
4080
- const bulmaClasses = usePrefixedClassNames();
4081
- const paragraphClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
4082
- return (jsxRuntime.jsx("p", { className: paragraphClasses || undefined, ...rest, children: children }));
4083
- };
4084
-
4085
- const validTitleSizes = ['1', '2', '3', '4', '5', '6'];
4086
- const validTitleElements = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'];
3994
+ function resolvePortalContainer(container) {
3995
+ if (!container) {
3996
+ return document.body;
3997
+ }
3998
+ if (typeof container === 'string') {
3999
+ return (document.querySelector(container) ?? document.body);
4000
+ }
4001
+ return container;
4002
+ }
4087
4003
  /**
4088
- * The `Title` component renders a Bulma-styled title (heading), supporting sizes `1`-`6`, spacing, and rendering as any heading or paragraph element (`h1`-`h6`, `p`).
4004
+ * Where a component's portal goes, as a function to call when the portal is
4005
+ * created: the target the component was given, else the `portalContainer` a
4006
+ * `ConfigProvider` sets, else `document.body`, each read as
4007
+ * `resolvePortalContainer` reads it. A component that portals resolves its
4008
+ * target through this, so the provider's default reaches it.
4089
4009
  *
4090
- * @function
4091
- * @param {TitleProps} props - Props for the Title component.
4092
- * @returns {JSX.Element} The rendered title element.
4093
- * @see {@link https://bulma.io/documentation/elements/title/ | Bulma Title documentation}
4094
- */
4095
- const Title = ({ className, size, isSpaced, as = 'h1', hasSkeleton, textColor, bgColor, children, ...props }) => {
4096
- /**
4097
- * Generates Bulma helper classes and separates out remaining props.
4098
- */
4099
- const { bulmaHelperClasses, rest } = useBulmaClasses({
4100
- color: textColor,
4101
- backgroundColor: bgColor,
4102
- ...props,
4103
- });
4104
- // Validate 'as' prop at runtime
4105
- const element = validTitleElements.includes(as) ? as : 'h1';
4106
- // Validate 'size' prop at runtime (numeric sizes normalize to strings)
4107
- const normalized = size == null ? undefined : String(size);
4108
- const validSize = normalized && validTitleSizes.includes(normalized)
4109
- ? normalized
4110
- : undefined;
4111
- const bulmaClasses = usePrefixedClassNames('title', {
4112
- [`is-${validSize}`]: validSize,
4113
- 'is-spaced': isSpaced,
4114
- 'has-skeleton': hasSkeleton,
4115
- });
4116
- const titleClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
4117
- // Determine the tag based on 'element' and 'validSize'
4118
- const Tag = element === 'p' ? 'p' : validSize ? `h${validSize}` : element;
4119
- return (jsxRuntime.jsx(Tag, { className: titleClasses, ...rest, children: children }));
4120
- };
4121
-
4122
- const validEmptyStateColors = [
4123
- 'primary',
4124
- 'link',
4125
- 'info',
4126
- 'success',
4127
- 'warning',
4128
- 'danger',
4129
- ];
4130
- const validEmptyStateHeadings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
4131
- const validEmptyStateSizes = ['small', 'medium', 'large'];
4132
- /**
4133
- * The circle the picture sits in, hidden from assistive tech whatever is in it.
4010
+ * The hook only reads context, so it is safe to call on the server, ahead of
4011
+ * any early return. The function it returns reads the DOM, so call it only
4012
+ * where the portal is created, which the server never reaches.
4134
4013
  *
4135
- * @function
4136
- * @param {EmptyStateIndicatorProps} props - Props for EmptyState.Indicator.
4137
- * @returns {JSX.Element} The rendered indicator.
4014
+ * @function usePortalContainer
4015
+ * @returns A function that takes the component's own target, if any, and
4016
+ * returns the DOM node to portal into.
4138
4017
  */
4139
- const EmptyStateIndicator = ({ className, textColor, bgColor, children, ...props }) => {
4140
- const { bulmaHelperClasses, rest } = useBulmaClasses({
4141
- color: textColor,
4142
- backgroundColor: bgColor,
4143
- ...props,
4144
- });
4145
- const indicatorClass = usePrefixedClassNames('empty-state-indicator');
4146
- // Hidden after the rest of the props, so nothing passed in can expose the
4147
- // picture: the title says what it means.
4148
- return (jsxRuntime.jsx("div", { className: classNames(indicatorClass, bulmaHelperClasses, className), ...rest, "aria-hidden": "true", children: children }));
4149
- };
4150
- EmptyStateIndicator.displayName = 'EmptyState.Indicator';
4018
+ function usePortalContainer() {
4019
+ const { portalContainer } = useConfig();
4020
+ return container => resolvePortalContainer(container || portalContainer);
4021
+ }
4151
4022
  /**
4152
- * The heading, a Bulma `Title` at the level `as` sets, `h2` by default, and at the EmptyState's size.
4023
+ * Reads Modal's `portal` prop (`boolean | string | HTMLElement`) for a
4024
+ * component that takes it: whether the content is in its portal yet, and
4025
+ * where it goes.
4153
4026
  *
4154
- * @function
4155
- * @param {EmptyStateTitleProps} props - Props for EmptyState.Title.
4156
- * @returns {JSX.Element} The rendered heading.
4157
- */
4158
- const EmptyStateTitle = ({ as = 'h2', className, ...props }) => {
4159
- // `size` isn't in the type, but on `Title` it picks the tag, so it's kept
4160
- // off even when it's passed anyway. The level is `as` alone.
4161
- const { size: _size, ...rest } = props;
4162
- const titleClass = usePrefixedClassNames('empty-state-title');
4163
- const level = validEmptyStateHeadings.includes(as) ? as : 'h2';
4164
- return (jsxRuntime.jsx(Title, { as: level, className: classNames(titleClass, className), ...rest }));
4165
- };
4166
- EmptyStateTitle.displayName = 'EmptyState.Title';
4167
- /**
4168
- * A sentence or two under the title, in the color of the text around it.
4027
+ * A portal has no server-rendered counterpart, so such a component renders in
4028
+ * place on the server and while hydrating, and moves into its portal once the
4029
+ * client takes over. Render the content in `Portal` with
4030
+ * `disabled={!isPortaled}` and this `container`. Moving remounts the content,
4031
+ * so an effect that holds one of its nodes depends on `isPortaled`, `portal`
4032
+ * and `defaultTarget`.
4169
4033
  *
4170
- * @function
4171
- * @param {EmptyStateDescriptionProps} props - Props for EmptyState.Description.
4172
- * @returns {JSX.Element} The rendered description.
4034
+ * @function usePortalProp
4035
+ * @param portal - The component's `portal` prop.
4036
+ * @returns Whether it's portaled now, its `container`, and the provider's
4037
+ * target while that places it.
4173
4038
  */
4174
- const EmptyStateDescription = ({ className, ...props }) => {
4175
- const descriptionClass = usePrefixedClassNames('empty-state-description');
4176
- return (jsxRuntime.jsx(Paragraph, { className: classNames(descriptionClass, className), ...props }));
4177
- };
4178
- EmptyStateDescription.displayName = 'EmptyState.Description';
4039
+ function usePortalProp(portal) {
4040
+ const hydrated = useIsHydrated();
4041
+ const { portalContainer } = useConfig();
4042
+ const isPortaled = Boolean(portal) && hydrated;
4043
+ return {
4044
+ isPortaled,
4045
+ container: typeof portal === 'boolean' ? undefined : portal,
4046
+ defaultTarget: isPortaled && portal === true ? portalContainer : undefined,
4047
+ };
4048
+ }
4179
4049
  /**
4180
- * A row of buttons, centered under the text or at the start of the line beside the indicator.
4050
+ * Renders its children into another part of the page, `document.body` unless `container` or a `ConfigProvider` says otherwise, so floating content escapes an ancestor's `overflow`, `transform` or stacking context.
4181
4051
  *
4182
- * @function
4183
- * @param {EmptyStateActionsProps} props - Props for EmptyState.Actions.
4184
- * @returns {JSX.Element} The rendered row of buttons.
4185
- */
4186
- const EmptyStateActions = ({ className, ...props }) => {
4187
- const actionsClass = usePrefixedClassNames('empty-state-actions');
4188
- return jsxRuntime.jsx(Buttons, { className: classNames(actionsClass, className), ...props });
4189
- };
4190
- EmptyStateActions.displayName = 'EmptyState.Actions';
4191
- /** Whether a shorthand prop was given something to render. */
4192
- const isGiven$3 = (node) => node !== undefined &&
4193
- node !== null &&
4194
- typeof node !== 'boolean' &&
4195
- node !== '';
4196
- /**
4197
- * The `EmptyState` component renders the block an app shows where content would be: an empty list, a search with no results, or a first-run screen.
4052
+ * React's server renderer can't render a portal, so `Portal` renders nothing
4053
+ * on the server and during hydration, and mounts its children in the commit
4054
+ * that follows. The first client render then matches the server markup. In an app
4055
+ * without server rendering it portals on the first render.
4198
4056
  *
4199
- * It's a column of a picture in a circle, a heading, a line of text and the
4200
- * actions, with one layout whether it's written with the `icon`, `title` and
4201
- * `description` props or with the parts. The children always render as
4202
- * written, after the parts the props render.
4057
+ * React context, such as `ConfigProvider`'s class prefix, reaches the
4058
+ * portaled children, and their React events bubble through the React tree.
4059
+ * CSS inheritance follows the DOM instead: a scoped (non-`isRoot`) `Theme`
4060
+ * sets its variables on a wrapper element the portaled content has left, so
4061
+ * that content sees the page-level theme, and styles scoped to a shadow root
4062
+ * don't reach `document.body` at all. To keep them, portal into an element
4063
+ * inside the themed area or the shadow root: pass it as `container`, or as
4064
+ * the `portalContainer` of a `ConfigProvider` around that area. The second
4065
+ * also reaches what the library's components portal, such as Popover's
4066
+ * `appendToBody` panel or a `Modal` with `portal`.
4203
4067
  *
4204
- * It has no role and no live region. It arrives already filled and goes away
4205
- * when results come back, so it can't announce a search that matched
4206
- * nothing: keep a `role="status"` region beside the search field for that,
4207
- * in the page the whole time. While the data loads, show a `Skeleton` or a
4208
- * `Loader` instead, so "No projects yet" doesn't flash first, and show a
4209
- * load that failed in a `Message` or a `Notification`.
4068
+ * Inside a `LocaleProvider`, the children keep its `dir` and `lang`. A
4069
+ * container inside the provider's own wrapper takes the children as they
4070
+ * are, and they follow the provider through the page. Any other,
4071
+ * `document.body` included, gets them inside one `<div>` carrying the
4072
+ * provider's values, with `display: contents` so it adds no box. It is
4073
+ * still an element, so content that must be the container's direct child,
4074
+ * a `<tr>` in a `<tbody>`, or that a direct-child selector styles, belongs
4075
+ * in a container inside the provider. Which of the two is settled when the
4076
+ * children mount, so a change of locale updates them in place rather than
4077
+ * remounting them.
4078
+ *
4079
+ * Tab order and focus follow the DOM too. Content portaled from inside a
4080
+ * focus trap is no longer inside the trapped element, so the trap neither
4081
+ * reaches it nor holds focus there: render nested overlays inside the
4082
+ * trapped element, or give them a trap of their own. To trap portaled
4083
+ * content, put `useFocusTrap`'s ref on the element inside the `Portal`.
4210
4084
  *
4211
4085
  * @function
4212
- * @param {EmptyStateProps} props - Props for the EmptyState component.
4213
- * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the root `<div>`.
4214
- * @returns {JSX.Element} The rendered empty state.
4086
+ * @param {PortalProps} props - Props for the Portal component.
4087
+ * @returns The portal, the children in place when `disabled`, or nothing
4088
+ * before hydration.
4215
4089
  *
4216
4090
  * @example
4217
- * <EmptyState
4218
- * icon="folder-open"
4219
- * title="No projects yet"
4220
- * description="Projects you create or join show up here."
4221
- * >
4222
- * <Button color="primary">New project</Button>
4223
- * </EmptyState>
4224
- */
4225
- const EmptyStateRoot = React.forwardRef(({ className, icon, title, titleAs = 'h2', description, children, color, variant = 'light', size = 'normal', align = 'center', textColor, bgColor, ...props }, ref) => {
4226
- const { bulmaHelperClasses, rest } = useBulmaClasses({
4227
- color: textColor,
4228
- backgroundColor: bgColor,
4229
- ...props,
4230
- });
4231
- const rootClass = usePrefixedClassNames('empty-state', {
4232
- [`is-${size}`]: validEmptyStateSizes.includes(size),
4233
- 'is-left': align === 'left',
4234
- [`is-${color}`]: !!color && validEmptyStateColors.includes(color),
4235
- 'is-filled': variant === 'filled',
4236
- });
4237
- // The props' parts first, each only when it's given, then the children
4238
- // exactly as they were written: nothing here looks at them.
4239
- return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: [isGiven$3(icon) && (jsxRuntime.jsx(EmptyStateIndicator, { children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), isGiven$3(title) && (jsxRuntime.jsx(EmptyStateTitle, { as: titleAs, children: title })), isGiven$3(description) && (jsxRuntime.jsx(EmptyStateDescription, { children: description })), children] }));
4240
- });
4241
- const EmptyState = withSubComponents(EmptyStateRoot, {
4242
- Indicator: EmptyStateIndicator,
4243
- Title: EmptyStateTitle,
4244
- Description: EmptyStateDescription,
4245
- Actions: EmptyStateActions,
4246
- }, 'EmptyState');
4247
-
4248
- // INTERNAL: deliberately not exported from src/index.ts.
4249
- /** The major version of the React this bundle is running against. */
4250
- const runningMajor = () => Number(React.version.split('.')[0]);
4251
- /**
4252
- * The prop that makes an element inert, spelled for the React in use, or
4253
- * nothing when `inert` is false.
4091
+ * <Portal>
4092
+ * <div className="box">Rendered at the end of document.body</div>
4093
+ * </Portal>
4254
4094
  *
4255
- * Both majors must render `inert=""`, and each warns on the other's spelling
4256
- * and then drops the attribute. React 19 treats `inert` as a boolean and
4257
- * rejects the empty string. React 18 does not know the attribute, so it
4258
- * passes a string through and rejects `true`.
4095
+ * @example
4096
+ * // Into a dedicated overlay root
4097
+ * <Portal container="#overlays">
4098
+ * <CommandPalette />
4099
+ * </Portal>
4259
4100
  */
4260
- const inertProps = (inert, major = runningMajor()) => {
4261
- if (!inert)
4262
- return {};
4263
- // The cast is for React 18's spelling: this package's types are React 19's,
4264
- // where `inert` is a boolean.
4265
- return (major >= 19
4266
- ? { inert: true }
4267
- : { inert: '' });
4268
- };
4101
+ function Portal({ children, container, disabled = false }) {
4102
+ const hydrated = useIsHydrated();
4103
+ const resolveContainer = usePortalContainer();
4104
+ if (disabled)
4105
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
4106
+ if (!hydrated)
4107
+ return null;
4108
+ const target = resolveContainer(container);
4109
+ return reactDom.createPortal(jsxRuntime.jsx(LocalePortalScope, { target: target, children: children }), target);
4110
+ }
4269
4111
 
4270
- // Context to track MenuList nesting level
4271
- const MenuListLevelContext = React.createContext(0);
4272
4112
  /**
4273
- * The `Menu` component provides Bulma's vertical navigation menu: a simple, accessible sidebar or section menu for your Bulma React UI.
4274
- *
4275
- * @function
4276
- * @param {MenuProps} props - Props for the Menu component.
4277
- * @returns {JSX.Element} The rendered menu.
4278
- * @see {@link https://bulma.io/documentation/components/menu/ | Bulma Menu documentation}
4113
+ * How long after an item appears its announcement is written, in ms. The wait
4114
+ * lets a screen reader register a region the container has only just mounted,
4115
+ * and items shown in quick succession are written, and read out, together.
4279
4116
  */
4280
- const MenuComponent = ({ className, children, ...props }) => {
4281
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
4282
- // Generate Bulma classes with prefix
4283
- const bulmaClasses = usePrefixedClassNames('menu');
4284
- return (jsxRuntime.jsx("aside", { className: classNames(bulmaClasses, bulmaHelperClasses, className), ...rest, children: children }));
4285
- };
4117
+ const announceDelay = 100;
4286
4118
  /**
4287
- * Bulma Menu label component.
4288
- *
4289
- * @function
4290
- * @param {MenuLabelProps} props - Props for the MenuLabel component.
4291
- * @returns {JSX.Element} The rendered menu label.
4119
+ * How long an announcement stays in the region once written, in ms. That's
4120
+ * long enough for a screen reader to pick it up. Clearing it after keeps a
4121
+ * second copy of the item's text from sitting in the page, where someone
4122
+ * reading through the page, or a test looking the text up, would find it
4123
+ * again.
4292
4124
  */
4293
- const MenuLabel = ({ className, children, ...props }) => {
4294
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
4295
- return (jsxRuntime.jsx("p", { className: classNames(usePrefixedClassNames('menu-label'), className, bulmaHelperClasses), ...rest, children: children }));
4125
+ const announcementLifetime = 1000;
4126
+ const noAnnouncements$2 = [];
4127
+ const nothingWritten = [];
4128
+ const noAnnouncementsYet = {
4129
+ key: 0,
4130
+ waiting: noAnnouncements$2,
4131
+ written: nothingWritten,
4132
+ };
4133
+ // Hides the region with inline styles rather than a class, so it needs no
4134
+ // stylesheet, including the helper classes a modular Bulma build can leave
4135
+ // out. Clipping it, rather than using `display` or `visibility`, keeps it in
4136
+ // the accessibility tree. The declarations match the `extras-sr-only` mixin,
4137
+ // plus `clip-path`, which replaces the deprecated `clip`. The negative margin
4138
+ // pulls the 1px box back inside the page, so it doesn't add scrollable
4139
+ // overflow. Exported for other live regions that must work without a
4140
+ // stylesheet, such as a range calendar's.
4141
+ const visuallyHidden = {
4142
+ position: 'absolute',
4143
+ width: '1px',
4144
+ height: '1px',
4145
+ padding: 0,
4146
+ margin: '-1px',
4147
+ overflow: 'hidden',
4148
+ clip: 'rect(0, 0, 0, 0)',
4149
+ clipPath: 'inset(50%)',
4150
+ whiteSpace: 'nowrap',
4151
+ border: 0,
4296
4152
  };
4297
4153
  /**
4298
- * MenuList applies `menu-list` class only at the top level (not for nested lists).
4154
+ * Works out what a status region says. Each item is announced once each time
4155
+ * it appears, so one that goes and comes back is announced again. Its
4156
+ * announcement is written `announceDelay` after it appears, or after the last
4157
+ * item to join it when several appear in quick succession, alongside whatever
4158
+ * the region still says. Once written, it stays for `announcementLifetime`,
4159
+ * whatever is written after it and whether or not its item goes in the
4160
+ * meantime. An item that goes before its announcement is written isn't
4161
+ * announced.
4299
4162
  *
4300
- * Every level renders `role="list"`, which Safari needs: WebKit stops
4301
- * exposing a `<ul>` as a list once its markers are hidden, and Bulma's reset
4302
- * hides them.
4163
+ * Nothing is written in the commit that mounts the region or the item. A
4164
+ * screen reader reliably announces a polite live region whose content changes
4165
+ * after the region is already in the page, and not always one that arrives
4166
+ * with its content. By the time `describe` runs, an item's rendered text can
4167
+ * be read.
4303
4168
  *
4304
- * @function
4305
- * @param {MenuListProps} props - Props for the MenuList component.
4306
- * @returns {JSX.Element} The rendered menu list.
4169
+ * @function useAnnouncements
4170
+ * @param items - The items to announce, in the order they were shown.
4171
+ * @param describe - The text to announce for an item, or nothing to skip it.
4172
+ * It runs once the item is on screen, and it must keep its identity between
4173
+ * renders (a module-level function or a `useCallback`), or every render
4174
+ * re-checks the items.
4175
+ * @returns The announcements to render, in the order they were written.
4307
4176
  */
4308
- const MenuList = ({ className, children, ...props }) => {
4309
- const level = React.useContext(MenuListLevelContext);
4310
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
4311
- const ulClass = classNames(className, bulmaHelperClasses, {
4177
+ function useAnnouncements(items, describe) {
4178
+ // The ids of the items on screen as of the last change, every one of which
4179
+ // has been looked at for an announcement already.
4180
+ const onScreenIdsRef = React.useRef(new Set());
4181
+ // Clears that haven't run yet. They outlive the batch that started them, so
4182
+ // they're only stopped when the region unmounts.
4183
+ const clearTimersRef = React.useRef(new Set());
4184
+ const [announcements, setAnnouncements] = React.useState(noAnnouncementsYet);
4185
+ // Runs after the commit that puts an item on screen, which is the first
4186
+ // point its rendered text can be read.
4187
+ React.useEffect(() => {
4188
+ const announced = onScreenIdsRef.current;
4189
+ const appeared = items.filter(item => !announced.has(item.id));
4190
+ // Items that have gone drop out here too, so one that comes back has
4191
+ // appeared again.
4192
+ onScreenIdsRef.current = new Set(items.map(item => item.id));
4193
+ const next = [];
4194
+ for (const item of appeared) {
4195
+ const text = describe(item);
4196
+ if (text)
4197
+ next.push({ id: item.id, text });
4198
+ }
4199
+ if (next.length > 0) {
4200
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- an item's rendered text can only be read once it's on screen
4201
+ setAnnouncements(current => ({
4202
+ ...current,
4203
+ key: current.key + 1,
4204
+ // Announcements still waiting go out with the new ones, apart from
4205
+ // one whose item came back before it was written, which the new one
4206
+ // replaces. What's already written stays put.
4207
+ waiting: [
4208
+ ...current.waiting.filter(waiting => !next.some(({ id }) => id === waiting.id)),
4209
+ ...next,
4210
+ ],
4211
+ }));
4212
+ }
4213
+ }, [items, describe]);
4214
+ // The write only touches the batch it was started for: a newer batch can
4215
+ // be set before the render that cancels this timer. Its clear is started
4216
+ // by the write itself, so it's timed from the write without waiting on a
4217
+ // render, and a newer batch arriving doesn't cut short what's written.
4218
+ const { key } = announcements;
4219
+ React.useEffect(() => {
4220
+ if (key === noAnnouncementsYet.key)
4221
+ return undefined;
4222
+ const clearTimers = clearTimersRef.current;
4223
+ const write = setTimeout(() => {
4224
+ // Whether an item is on screen is decided here, once. An item that
4225
+ // went before its announcement was due isn't announced, and one that
4226
+ // goes after keeps its announcement for the rest of its lifetime, when
4227
+ // the region holds the only copy of its text rather than a second one.
4228
+ const onScreen = onScreenIdsRef.current;
4229
+ setAnnouncements(current => current.key === key
4230
+ ? {
4231
+ ...current,
4232
+ waiting: noAnnouncements$2,
4233
+ written: [
4234
+ ...current.written,
4235
+ ...current.waiting
4236
+ .filter(announcement => onScreen.has(announcement.id))
4237
+ .map(announcement => ({ ...announcement, batch: key })),
4238
+ ],
4239
+ }
4240
+ : current);
4241
+ // 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
4242
+ const clear = setTimeout(() => {
4243
+ clearTimers.delete(clear);
4244
+ setAnnouncements(current => {
4245
+ const written = current.written.filter(announcement => announcement.batch !== key);
4246
+ return written.length === current.written.length
4247
+ ? current
4248
+ : { ...current, written };
4249
+ });
4250
+ }, announcementLifetime);
4251
+ clearTimers.add(clear);
4252
+ }, announceDelay);
4253
+ return () => clearTimeout(write);
4254
+ }, [key]);
4255
+ React.useEffect(() => {
4256
+ const clearTimers = clearTimersRef.current;
4257
+ return () => {
4258
+ clearTimers.forEach(clearTimeout);
4259
+ clearTimers.clear();
4260
+ };
4261
+ }, []);
4262
+ return announcements.written;
4263
+ }
4264
+ /**
4265
+ * Reads an element out roughly the way a screen reader would: its text, with
4266
+ * an element's `aria-label`, or an image's `alt`, standing in for what's
4267
+ * inside it, a break between block-level elements, and parts that are
4268
+ * `aria-hidden`, `hidden` or inline-styled `display: none` left out. It isn't
4269
+ * the full accessible name computation, so `aria-labelledby`, CSS-generated
4270
+ * content, content hidden by a stylesheet and other ways of naming or hiding
4271
+ * content aren't followed.
4272
+ *
4273
+ * @function spokenText
4274
+ * @param element - The element to read.
4275
+ * @returns Its text, with runs of whitespace collapsed and the ends trimmed.
4276
+ */
4277
+ function spokenText(element) {
4278
+ return readAloud(element).replace(/\s+/g, ' ').trim();
4279
+ }
4280
+ // Elements a browser displays as blocks by default, and `br`. A screen reader
4281
+ // pauses between them, so their text shouldn't run into the text beside them.
4282
+ // They're known by tag rather than by computed `display`, which would depend
4283
+ // on a stylesheet and on layout.
4284
+ const blockLevelTags = new Set([
4285
+ 'ADDRESS',
4286
+ 'ARTICLE',
4287
+ 'ASIDE',
4288
+ 'BLOCKQUOTE',
4289
+ 'BR',
4290
+ 'CAPTION',
4291
+ 'DD',
4292
+ 'DETAILS',
4293
+ 'DIALOG',
4294
+ 'DIV',
4295
+ 'DL',
4296
+ 'DT',
4297
+ 'FIELDSET',
4298
+ 'FIGCAPTION',
4299
+ 'FIGURE',
4300
+ 'FOOTER',
4301
+ 'FORM',
4302
+ 'H1',
4303
+ 'H2',
4304
+ 'H3',
4305
+ 'H4',
4306
+ 'H5',
4307
+ 'H6',
4308
+ 'HEADER',
4309
+ 'HGROUP',
4310
+ 'HR',
4311
+ 'LEGEND',
4312
+ 'LI',
4313
+ 'MAIN',
4314
+ 'MENU',
4315
+ 'NAV',
4316
+ 'OL',
4317
+ 'P',
4318
+ 'PRE',
4319
+ 'SEARCH',
4320
+ 'SECTION',
4321
+ 'SUMMARY',
4322
+ 'TABLE',
4323
+ 'TBODY',
4324
+ 'TD',
4325
+ 'TFOOT',
4326
+ 'TH',
4327
+ 'THEAD',
4328
+ 'TR',
4329
+ 'UL',
4330
+ ]);
4331
+ /**
4332
+ * Whether an element is left out of what a screen reader reads.
4333
+ *
4334
+ * @function isHidden
4335
+ * @param element - The element to check.
4336
+ * @returns True for `aria-hidden="true"`, the `hidden` attribute, and an
4337
+ * inline `display: none`, on an HTML, SVG or any other element with an
4338
+ * inline style.
4339
+ */
4340
+ const isHidden = (element) => element.getAttribute('aria-hidden') === 'true' ||
4341
+ element.hasAttribute('hidden') ||
4342
+ element.style?.display === 'none';
4343
+ /**
4344
+ * The text `spokenText` reads for an element, before its whitespace is
4345
+ * tidied.
4346
+ *
4347
+ * @function readAloud
4348
+ * @param element - The element to read.
4349
+ * @returns Its text.
4350
+ */
4351
+ function readAloud(element) {
4352
+ if (isHidden(element))
4353
+ return '';
4354
+ const name = element.getAttribute('aria-label') ||
4355
+ (element.tagName === 'IMG' ? element.getAttribute('alt') : null);
4356
+ const text = name ||
4357
+ Array.from(element.childNodes, child => {
4358
+ if (child instanceof Element)
4359
+ return readAloud(child);
4360
+ return child.nodeType === Node.TEXT_NODE ? child.textContent : '';
4361
+ }).join('');
4362
+ // A name stands in for the element's content, and a block's content stands
4363
+ // apart from what's around it, so both are padded to keep them from running
4364
+ // into the neighboring text. An inline run, like `<span>Up</span>` then
4365
+ // `<span>loaded</span>`, still reads as one word.
4366
+ return name || blockLevelTags.has(element.tagName) ? ` ${text} ` : text;
4367
+ }
4368
+ /**
4369
+ * A visually hidden, polite `status` live region that a container announces
4370
+ * its items through. The container renders it from the moment it mounts,
4371
+ * whether or not it has anything to show, so the region is already in the
4372
+ * page when an announcement is written into it.
4373
+ *
4374
+ * The announcements are this component's own state, so writing and clearing
4375
+ * them re-renders the region and leaves the container's items alone. Each one
4376
+ * is its own keyed node, so a re-render that leaves the announcements alone
4377
+ * leaves the region's content alone too, and nothing is read out twice.
4378
+ *
4379
+ * @function StatusRegion
4380
+ * @param props - The items to announce, and the text to announce for each.
4381
+ * @returns The region.
4382
+ */
4383
+ function StatusRegion({ items, describe, }) {
4384
+ const announcements = useAnnouncements(items, describe);
4385
+ // `role="status"` makes a region atomic unless it says otherwise, which
4386
+ // reads the whole region on every change. Each announcement is its own
4387
+ // node, and only additions are relevant by default, so with atomic off a
4388
+ // screen reader reads just what was written, and removing one announcement
4389
+ // doesn't read out the ones still there. A node is keyed by its batch as
4390
+ // well as its item, since an item that comes back can be written again
4391
+ // while what it said before is still there.
4392
+ return (jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "false", style: visuallyHidden, children: announcements.map(announcement => (jsxRuntime.jsx("div", { children: announcement.text }, `${announcement.batch}:${announcement.id}`))) }));
4393
+ }
4394
+
4395
+ /** How long `copied` lasts when `duration` doesn't say, in ms. */
4396
+ const defaultDuration = 2000;
4397
+ /**
4398
+ * The longest delay `setTimeout` takes, in ms. A longer one, `Infinity`
4399
+ * included, runs at once.
4400
+ */
4401
+ const longestDelay = 2 ** 31 - 1;
4402
+ /**
4403
+ * `duration` as a delay `setTimeout` keeps, or `null` for no timer: `null`
4404
+ * for `0`, as on Toast, the default when it's missing or `NaN` (or a
4405
+ * JavaScript caller's `null`), `0` for a negative one, which ends `copied` at
4406
+ * once, and the longest delay for `Infinity` or anything longer.
4407
+ */
4408
+ const delayOf = (duration) => {
4409
+ if (duration === 0)
4410
+ return null;
4411
+ return duration == null || Number.isNaN(duration)
4412
+ ? defaultDuration
4413
+ : Math.min(Math.max(duration, 0), longestDelay);
4414
+ };
4415
+ /** The error a copy reports when nothing could write and nothing said why. */
4416
+ const cannotCopy = 'The Clipboard API is missing or refused, and the copy command copied nothing.';
4417
+ /**
4418
+ * Copies `text` the way browsers did before the Clipboard API: it runs the
4419
+ * `copy` command and puts the text on the `copy` event that command fires.
4420
+ * Success is the event having fired, not the command's return value, which
4421
+ * WebKit reports as `false` for a copy that happened.
4422
+ *
4423
+ * This is the library's only call to the deprecated `document.execCommand`,
4424
+ * so it can go when browsers drop it. Until then it's what writes on a page
4425
+ * without the Clipboard API, such as one served over plain `http://`.
4426
+ *
4427
+ * @param text - The text to copy.
4428
+ * @returns Whether the `copy` event fired and took the text.
4429
+ */
4430
+ function copyWithCommand(text) {
4431
+ let written = false;
4432
+ // WebKit enables the copy command only when something is selected or a
4433
+ // `beforecopy` listener cancels that event, and nothing is selected here.
4434
+ const enable = (event) => event.preventDefault();
4435
+ // On `window` while capturing, so it runs before the page's listeners on
4436
+ // the document and its elements. The copy is this hook's own, so it's
4437
+ // stopped there: a page's own copy handler, such as one that adds a
4438
+ // credit line to whatever is copied, never sees it to rewrite it.
4439
+ const onCopy = (event) => {
4440
+ if (!event.clipboardData)
4441
+ return;
4442
+ event.clipboardData.setData('text/plain', text);
4443
+ event.preventDefault();
4444
+ event.stopImmediatePropagation();
4445
+ written = true;
4446
+ };
4447
+ window.addEventListener('beforecopy', enable, true);
4448
+ window.addEventListener('copy', onCopy, true);
4449
+ try {
4450
+ document.execCommand('copy');
4451
+ }
4452
+ catch {
4453
+ // An engine that refuses, or no longer has the command, can throw, and
4454
+ // the event not firing already says so.
4455
+ }
4456
+ finally {
4457
+ window.removeEventListener('beforecopy', enable, true);
4458
+ window.removeEventListener('copy', onCopy, true);
4459
+ }
4460
+ return written;
4461
+ }
4462
+ /**
4463
+ * Writes `text` through the Clipboard API, and through the copy command when
4464
+ * that's missing or refuses. Where it's missing, the copy command runs before
4465
+ * the first `await`, so it's still inside the click that called it. After a
4466
+ * refusal it runs once the refusal arrives, which writes only where the
4467
+ * browser still counts the click by then, and only if no later copy has
4468
+ * started in the meantime, which it would otherwise write over.
4469
+ *
4470
+ * @param text - The text to write.
4471
+ * @param isLatest - Whether this is still the latest copy.
4472
+ * @returns `null` once it's written, or why it couldn't be.
4473
+ */
4474
+ async function writeToClipboard(text, isLatest) {
4475
+ let refusal = null;
4476
+ // Read at the click, never while rendering, so there's always a navigator.
4477
+ const { clipboard } = navigator;
4478
+ if (clipboard?.writeText) {
4479
+ try {
4480
+ await clipboard.writeText(text);
4481
+ return null;
4482
+ }
4483
+ catch (reason) {
4484
+ refusal = reason;
4485
+ }
4486
+ }
4487
+ if (isLatest() && copyWithCommand(text))
4488
+ return null;
4489
+ return refusal instanceof Error ? refusal : new Error(cannotCopy);
4490
+ }
4491
+ /**
4492
+ * Copies text to the clipboard through the Clipboard API, falling back to the copy command where that's missing or refused, and keeps a `copied` flag for `duration` after a copy.
4493
+ *
4494
+ * It announces nothing. A trigger of your own says "Copied" in a
4495
+ * `role="status"` region that's in the page before the click; `CopyButton`
4496
+ * is this hook with the button, the label and the announcement built in.
4497
+ *
4498
+ * A failed copy sets `error`, calls `onCopyError`, and ends `copied` if a
4499
+ * copy before it had set it. When copies overlap, only the latest one's
4500
+ * outcome sets the state and fires the callbacks, whichever finishes first.
4501
+ * Nothing changes and no callback fires once the component using it has
4502
+ * unmounted, though a write already under way still finishes.
4503
+ *
4504
+ * @function useClipboard
4505
+ * @param options - How long `copied` lasts, and the callbacks.
4506
+ * @returns `copy`, which writes, and the `copied` and `error` state.
4507
+ *
4508
+ * @example
4509
+ * const { copy, copied } = useClipboard();
4510
+ * <Button onClick={() => copy(url)}>{copied ? 'Copied' : 'Copy link'}</Button>
4511
+ */
4512
+ function useClipboard(options = {}) {
4513
+ const [copied, setCopied] = React.useState(false);
4514
+ const [error, setError] = React.useState(null);
4515
+ const optionsRef = useLatestRef(options);
4516
+ const copiedRef = React.useRef(false);
4517
+ const timerRef = React.useRef(undefined);
4518
+ const mountedRef = React.useRef(false);
4519
+ // Counts the calls to `copy`, so only the latest one's outcome is kept.
4520
+ const latestCallRef = React.useRef(0);
4521
+ React.useEffect(() => {
4522
+ mountedRef.current = true;
4523
+ return () => {
4524
+ mountedRef.current = false;
4525
+ clearTimeout(timerRef.current);
4526
+ };
4527
+ }, []);
4528
+ const copy = React.useCallback(async (text) => {
4529
+ latestCallRef.current += 1;
4530
+ const call = latestCallRef.current;
4531
+ const isLatest = () => call === latestCallRef.current;
4532
+ const failure = await writeToClipboard(text, isLatest);
4533
+ // A write overtaken by a later copy, which may finish first, reports
4534
+ // through its promise only.
4535
+ if (!mountedRef.current || !isLatest()) {
4536
+ return failure === null;
4537
+ }
4538
+ clearTimeout(timerRef.current);
4539
+ const { duration, onCopiedChange, onCopyError } = optionsRef.current;
4540
+ const wasCopied = copiedRef.current;
4541
+ copiedRef.current = failure === null;
4542
+ setCopied(failure === null);
4543
+ setError(failure);
4544
+ if (failure) {
4545
+ if (wasCopied)
4546
+ onCopiedChange?.(false);
4547
+ onCopyError?.(failure);
4548
+ return false;
4549
+ }
4550
+ onCopiedChange?.(true);
4551
+ const delay = delayOf(duration);
4552
+ if (delay !== null) {
4553
+ timerRef.current = setTimeout(() => {
4554
+ copiedRef.current = false;
4555
+ setCopied(false);
4556
+ optionsRef.current.onCopiedChange?.(false);
4557
+ }, delay);
4558
+ }
4559
+ return true;
4560
+ }, [optionsRef]);
4561
+ return { copy, copied, error };
4562
+ }
4563
+
4564
+ /** Two sheets, one over the other. */
4565
+ const CopyGlyph = () => (jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinejoin: "round", width: "1em", height: "1em", focusable: "false", children: [jsxRuntime.jsx("path", { d: "M10.5 3.5v-1a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h1" }), jsxRuntime.jsx("rect", { x: "5.5", y: "5.5", width: "8", height: "9", rx: "1" })] }));
4566
+ const describeAnnouncement$1 = (announcement) => announcement.text;
4567
+ const noAnnouncements$1 = [];
4568
+ /** What marks an open modal dialog in ARIA. */
4569
+ const modalDialog = '[aria-modal="true"]';
4570
+ /**
4571
+ * The open modal dialog an element is in: one marked `aria-modal="true"`, or
4572
+ * a `<dialog>` opened with `showModal()`, which makes the rest of the page
4573
+ * inert. An engine without the `:modal` selector throws on the whole list,
4574
+ * so it then looks for the ARIA alone.
4575
+ *
4576
+ * @param element - Where to look up from.
4577
+ * @returns The nearest open modal dialog around it, or `null`.
4578
+ */
4579
+ function closestModalDialog(element) {
4580
+ try {
4581
+ return element.closest(`${modalDialog}, dialog:modal`);
4582
+ }
4583
+ catch {
4584
+ return element.closest(modalDialog);
4585
+ }
4586
+ }
4587
+ /**
4588
+ * The `CopyButton` component renders a button that copies a piece of text to the clipboard, says "Copied" for a moment, and announces the result.
4589
+ *
4590
+ * It's a `Button` with a copy icon and "Copy", and takes everything `Button`
4591
+ * takes. A click writes `value` through the Clipboard API, or through the
4592
+ * older copy command where that's missing or refused, and reports a failure
4593
+ * rather than showing "Copied". Each outcome is announced through a polite
4594
+ * status region portaled out of the button's place, so it stays the last
4595
+ * child of a `Control` or `Buttons`: into the open modal dialog the button is
4596
+ * in, otherwise where `Portal` puts portaled content (the `ConfigProvider`'s
4597
+ * `portalContainer`, or the end of the page). With `showLabel={false}` it's an
4598
+ * `IconButton`. For a trigger of your own, use `useClipboard`, which it's
4599
+ * built on.
4600
+ *
4601
+ * @function
4602
+ * @param {CopyButtonProps} props - Props for the CopyButton component.
4603
+ * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
4604
+ * @returns {JSX.Element} The rendered button.
4605
+ * @see {@link https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html | WCAG 4.1.3 Status Messages}
4606
+ *
4607
+ * @example
4608
+ * <CopyButton value="https://bestax.io" />
4609
+ *
4610
+ * @example
4611
+ * <CopyButton value={apiKey} showLabel={false} size="small" />
4612
+ */
4613
+ const CopyButton = React.forwardRef(function CopyButton({ value, duration = 2000, showLabel = true, labels, onCopiedChange, onCopyError, onClick, onFocus, ...props }, ref) {
4614
+ const copyLabel = labels?.copy || 'Copy';
4615
+ const copiedLabel = labels?.copied || 'Copied';
4616
+ const copiedStatus = labels?.copiedStatus || 'Copied to the clipboard';
4617
+ const failedStatus = labels?.failedStatus || "Couldn't copy to the clipboard";
4618
+ // The latest outcome. Each gets a new id, so the region announces a
4619
+ // repeat too.
4620
+ const [announcements, setAnnouncements] = React.useState(noAnnouncements$1);
4621
+ const countRef = React.useRef(0);
4622
+ const announce = (text) => {
4623
+ countRef.current += 1;
4624
+ setAnnouncements([{ id: String(countRef.current), text }]);
4625
+ };
4626
+ // The open modal dialog the button is in, which the region goes inside:
4627
+ // a screen reader ignores a live region behind one. It's looked up when
4628
+ // the button takes focus or a click, rather than once as it mounts,
4629
+ // since Modal and Sidebar keep their content mounted while closed, and
4630
+ // only an open one is `aria-modal`. A region that moves starts empty, so
4631
+ // it doesn't say the last outcome again. This lookup can go once the
4632
+ // overlays hand their content their own root as its portal container
4633
+ // (#1471).
4634
+ const [dialog, setDialog] = React.useState(null);
4635
+ const placeRegion = (button) => {
4636
+ const next = closestModalDialog(button);
4637
+ if (next === dialog)
4638
+ return;
4639
+ setDialog(next);
4640
+ setAnnouncements(noAnnouncements$1);
4641
+ };
4642
+ // The hook fires nothing once this has unmounted, so neither does the
4643
+ // announcement.
4644
+ const { copy, copied } = useClipboard({
4645
+ duration,
4646
+ onCopiedChange: next => {
4647
+ if (next)
4648
+ announce(copiedStatus);
4649
+ onCopiedChange?.(next);
4650
+ },
4651
+ onCopyError: error => {
4652
+ announce(failedStatus);
4653
+ onCopyError?.(error);
4654
+ },
4655
+ });
4656
+ // The caller's onClick runs first, so a `preventDefault()` there stops
4657
+ // the copy, as it stops ToggleButton's toggle.
4658
+ const handleClick = (event) => {
4659
+ onClick?.(event);
4660
+ if (event.defaultPrevented)
4661
+ return;
4662
+ placeRegion(event.currentTarget);
4663
+ void copy(value);
4664
+ };
4665
+ const handleFocus = (event) => {
4666
+ onFocus?.(event);
4667
+ placeRegion(event.currentTarget);
4668
+ };
4669
+ const glyph = jsxRuntime.jsx(Icon, { children: copied ? jsxRuntime.jsx(CheckIcon$1, {}) : jsxRuntime.jsx(CopyGlyph, {}) });
4670
+ // The props are spread first, so it stays a `<button>` that doesn't
4671
+ // submit by accident, whatever a JavaScript caller or a spread object
4672
+ // passes along.
4673
+ const button = showLabel ? (jsxRuntime.jsxs(Button, { ...props, ref: ref, as: "button", type: buttonType(props.type), onClick: handleClick, onFocus: handleFocus, children: [glyph, jsxRuntime.jsx("span", { children: copied ? copiedLabel : copyLabel })] })) : (jsxRuntime.jsx(IconButton, { ...{
4674
+ ...props,
4675
+ 'aria-label': props['aria-label'] || copyLabel,
4676
+ }, ref: ref, as: "button", onClick: handleClick, onFocus: handleFocus, children: glyph }));
4677
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [button, jsxRuntime.jsx(Portal, { container: dialog || undefined, children: jsxRuntime.jsx(StatusRegion, { items: announcements, describe: describeAnnouncement$1 }) })] }));
4678
+ });
4679
+ CopyButton.displayName = 'CopyButton';
4680
+
4681
+ /**
4682
+ * The `LinkButton` component renders a `<button>` that visually looks like text or a link.
4683
+ *
4684
+ * @function
4685
+ * @param {LinkButtonProps} props - Props for the LinkButton component.
4686
+ * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
4687
+ * @returns {JSX.Element} The rendered link-styled button element.
4688
+ *
4689
+ * @example
4690
+ * // Text variant (default)
4691
+ * <LinkButton onClick={handleClick}>Click me</LinkButton>
4692
+ *
4693
+ * @example
4694
+ * // Underline variant with color
4695
+ * <LinkButton variant="underline" color="primary">Learn more</LinkButton>
4696
+ */
4697
+ const LinkButton = React.forwardRef(function LinkButton({ variant = 'text', color, className, ...props }, ref) {
4698
+ const buttonColor = variant === 'underline' ? 'text' : variant;
4699
+ const prefixedClasses = usePrefixedClassNames('link-button', color && `link-button-${color}`, variant === 'underline' && 'link-button-underline');
4700
+ return (jsxRuntime.jsx(Button
4701
+ // LinkButton is polymorphic through to Button, which resolves the element
4702
+ // from `as`. TS cannot infer that through the spread below.
4703
+ , {
4704
+ // LinkButton is polymorphic through to Button, which resolves the element
4705
+ // from `as`. TS cannot infer that through the spread below.
4706
+ ref: ref, color: buttonColor, className: classNames(prefixedClasses, className), ...props }));
4707
+ });
4708
+ LinkButton.displayName = 'LinkButton';
4709
+
4710
+ /**
4711
+ * The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.
4712
+ *
4713
+ * It's always a `<button type="button">`, so it never submits a form and
4714
+ * carries no form value: a setting saved with a form is a `Checkbox` or a
4715
+ * `Switch`. A set of toggles that each turn on and off on their own goes in
4716
+ * `Buttons hasAddons` with `role="group"` and an `aria-label`. A set where only
4717
+ * one can be on is a radio group rather than a row of these: use
4718
+ * `SegmentedControl`, or `Radios` for a choice that's sent with a form.
4719
+ *
4720
+ * @function
4721
+ * @param {ToggleButtonProps} props - Props for the ToggleButton component.
4722
+ * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
4723
+ * @returns {JSX.Element} The rendered toggle button.
4724
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/button/ | APG button pattern}
4725
+ *
4726
+ * @example
4727
+ * <ToggleButton onPressedChange={setMuted}>Mute</ToggleButton>
4728
+ *
4729
+ * @example
4730
+ * <Buttons hasAddons role="group" aria-label="Text style">
4731
+ * <ToggleButton aria-label="Bold" pressed={bold} onPressedChange={setBold}>
4732
+ * <Icon name="bold" />
4733
+ * </ToggleButton>
4734
+ * </Buttons>
4735
+ */
4736
+ const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', isLoading, 'aria-busy': ariaBusy, 'aria-disabled': ariaDisabled, className, onClick, ...props }, ref) {
4737
+ const isControlled = controlledPressed !== undefined;
4738
+ const [internalPressed, setInternalPressed] = React.useState(defaultPressed);
4739
+ const pressed = controlledPressed ?? internalPressed;
4740
+ // Prefixed like Button's own classes, which a hand-written
4741
+ // `className="is-selected"` could not be under ConfigProvider classPrefix.
4742
+ // Bulma lifts `is-selected` above its neighbours in `.buttons.has-addons`.
4743
+ const selectedClass = usePrefixedClassNames({ 'is-selected': pressed });
4744
+ // The caller's onClick runs first, so a `preventDefault()` there vetoes
4745
+ // the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here
4746
+ // through the button's own click, and a disabled button gets no click.
4747
+ //
4748
+ // A loading one gets the click but drops it. Bulma's spinner only stops a
4749
+ // pointer, so without this a key flipped a toggle the mouse couldn't.
4750
+ // `disabled` would stop both, but it takes focus away mid-save.
4751
+ const handleClick = (event) => {
4752
+ if (isLoading)
4753
+ return;
4754
+ onClick?.(event);
4755
+ if (event.defaultPrevented)
4756
+ return;
4757
+ const next = !pressed;
4758
+ onPressedChange?.(next);
4759
+ if (!isControlled)
4760
+ setInternalPressed(next);
4761
+ };
4762
+ // The props are spread first, so what a toggle depends on wins over
4763
+ // anything a JavaScript caller or a spread object passes along: it stays
4764
+ // a button, never submits, reports the state it's in, and says it's busy
4765
+ // and won't respond while it's loading. A caller's own `aria-busy` or
4766
+ // `aria-disabled` stays when it isn't loading.
4767
+ return (jsxRuntime.jsx(Button, { ...props, ref: ref, as: "button", type: "button", isLoading: isLoading, "aria-busy": isLoading || ariaBusy, "aria-disabled": isLoading || ariaDisabled, "aria-pressed": pressed, color: pressed ? color : undefined, className: classNames(selectedClass, className), onClick: handleClick }));
4768
+ });
4769
+ ToggleButton.displayName = 'ToggleButton';
4770
+
4771
+ const validButtonsSizes = ['small', 'medium', 'large'];
4772
+ /**
4773
+ * The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.
4774
+ *
4775
+ * @function
4776
+ * @param {ButtonsProps} props - Props for the Buttons component.
4777
+ * @returns {JSX.Element} The rendered group of buttons.
4778
+ * @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}
4779
+ */
4780
+ const ButtonsComponent = ({ className, textColor, color, bgColor, isCentered, isRight, hasAddons, size, children, ...props }) => {
4781
+ const buttonsClasses = usePrefixedClassNames('buttons', {
4782
+ 'is-centered': isCentered,
4783
+ 'is-right': isRight,
4784
+ 'has-addons': hasAddons,
4785
+ [`are-${size}`]: size && validButtonsSizes.includes(size),
4786
+ });
4787
+ /**
4788
+ * Generates Bulma helper classes and separates out remaining props.
4789
+ */
4790
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
4791
+ color: textColor ?? color,
4792
+ backgroundColor: bgColor,
4793
+ ...props,
4794
+ });
4795
+ const combinedClasses = classNames(buttonsClasses, className, bulmaHelperClasses);
4796
+ return (jsxRuntime.jsx("div", { className: combinedClasses, ...rest, children: children }));
4797
+ };
4798
+ const Buttons = withSubComponents(ButtonsComponent, {
4799
+ Button,
4800
+ LinkButton,
4801
+ ToggleButton,
4802
+ IconButton,
4803
+ CopyButton,
4804
+ }, 'Buttons');
4805
+
4806
+ /**
4807
+ * The `Paragraph` component renders a styled `<p>` element with Bulma helper class integration.
4808
+ *
4809
+ * @function
4810
+ * @param {ParagraphProps} props - Props for the Paragraph component.
4811
+ * @returns {JSX.Element} The rendered paragraph element.
4812
+ */
4813
+ const Paragraph = ({ className, textColor, bgColor, children, ...props }) => {
4814
+ /**
4815
+ * Generates Bulma helper classes and separates out remaining props.
4816
+ */
4817
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
4818
+ color: textColor,
4819
+ backgroundColor: bgColor,
4820
+ ...props,
4821
+ });
4822
+ const bulmaClasses = usePrefixedClassNames();
4823
+ const paragraphClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
4824
+ return (jsxRuntime.jsx("p", { className: paragraphClasses || undefined, ...rest, children: children }));
4825
+ };
4826
+
4827
+ const validTitleSizes = ['1', '2', '3', '4', '5', '6'];
4828
+ const validTitleElements = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'];
4829
+ /**
4830
+ * The `Title` component renders a Bulma-styled title (heading), supporting sizes `1`-`6`, spacing, and rendering as any heading or paragraph element (`h1`-`h6`, `p`).
4831
+ *
4832
+ * @function
4833
+ * @param {TitleProps} props - Props for the Title component.
4834
+ * @returns {JSX.Element} The rendered title element.
4835
+ * @see {@link https://bulma.io/documentation/elements/title/ | Bulma Title documentation}
4836
+ */
4837
+ const Title = ({ className, size, isSpaced, as = 'h1', hasSkeleton, textColor, bgColor, children, ...props }) => {
4838
+ /**
4839
+ * Generates Bulma helper classes and separates out remaining props.
4840
+ */
4841
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
4842
+ color: textColor,
4843
+ backgroundColor: bgColor,
4844
+ ...props,
4845
+ });
4846
+ // Validate 'as' prop at runtime
4847
+ const element = validTitleElements.includes(as) ? as : 'h1';
4848
+ // Validate 'size' prop at runtime (numeric sizes normalize to strings)
4849
+ const normalized = size == null ? undefined : String(size);
4850
+ const validSize = normalized && validTitleSizes.includes(normalized)
4851
+ ? normalized
4852
+ : undefined;
4853
+ const bulmaClasses = usePrefixedClassNames('title', {
4854
+ [`is-${validSize}`]: validSize,
4855
+ 'is-spaced': isSpaced,
4856
+ 'has-skeleton': hasSkeleton,
4857
+ });
4858
+ const titleClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
4859
+ // Determine the tag based on 'element' and 'validSize'
4860
+ const Tag = element === 'p' ? 'p' : validSize ? `h${validSize}` : element;
4861
+ return (jsxRuntime.jsx(Tag, { className: titleClasses, ...rest, children: children }));
4862
+ };
4863
+
4864
+ /**
4865
+ * INTERNAL, deliberately not exported from `src/index.ts`. Whether a prop
4866
+ * was given something to render: `undefined`, `null`, a boolean and `''`
4867
+ * weren't, and `0` was, since React renders it.
4868
+ */
4869
+ const isGiven = (node) => node !== undefined &&
4870
+ node !== null &&
4871
+ typeof node !== 'boolean' &&
4872
+ node !== '';
4873
+
4874
+ const validEmptyStateColors = [
4875
+ 'primary',
4876
+ 'link',
4877
+ 'info',
4878
+ 'success',
4879
+ 'warning',
4880
+ 'danger',
4881
+ ];
4882
+ const validEmptyStateHeadings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
4883
+ const validEmptyStateSizes = ['small', 'medium', 'large'];
4884
+ /**
4885
+ * The circle the picture sits in, hidden from assistive tech whatever is in it.
4886
+ *
4887
+ * @function
4888
+ * @param {EmptyStateIndicatorProps} props - Props for EmptyState.Indicator.
4889
+ * @returns {JSX.Element} The rendered indicator.
4890
+ */
4891
+ const EmptyStateIndicator = ({ className, textColor, bgColor, children, ...props }) => {
4892
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
4893
+ color: textColor,
4894
+ backgroundColor: bgColor,
4895
+ ...props,
4896
+ });
4897
+ const indicatorClass = usePrefixedClassNames('empty-state-indicator');
4898
+ // Hidden after the rest of the props, so nothing passed in can expose the
4899
+ // picture: the title says what it means.
4900
+ return (jsxRuntime.jsx("div", { className: classNames(indicatorClass, bulmaHelperClasses, className), ...rest, "aria-hidden": "true", children: children }));
4901
+ };
4902
+ EmptyStateIndicator.displayName = 'EmptyState.Indicator';
4903
+ /**
4904
+ * The heading, a Bulma `Title` at the level `as` sets, `h2` by default, and at the EmptyState's size.
4905
+ *
4906
+ * @function
4907
+ * @param {EmptyStateTitleProps} props - Props for EmptyState.Title.
4908
+ * @returns {JSX.Element} The rendered heading.
4909
+ */
4910
+ const EmptyStateTitle = ({ as = 'h2', className, ...props }) => {
4911
+ // `size` isn't in the type, but on `Title` it picks the tag, so it's kept
4912
+ // off even when it's passed anyway. The level is `as` alone.
4913
+ const { size: _size, ...rest } = props;
4914
+ const titleClass = usePrefixedClassNames('empty-state-title');
4915
+ const level = validEmptyStateHeadings.includes(as) ? as : 'h2';
4916
+ return (jsxRuntime.jsx(Title, { as: level, className: classNames(titleClass, className), ...rest }));
4917
+ };
4918
+ EmptyStateTitle.displayName = 'EmptyState.Title';
4919
+ /**
4920
+ * A sentence or two under the title, in the color of the text around it.
4921
+ *
4922
+ * @function
4923
+ * @param {EmptyStateDescriptionProps} props - Props for EmptyState.Description.
4924
+ * @returns {JSX.Element} The rendered description.
4925
+ */
4926
+ const EmptyStateDescription = ({ className, ...props }) => {
4927
+ const descriptionClass = usePrefixedClassNames('empty-state-description');
4928
+ return (jsxRuntime.jsx(Paragraph, { className: classNames(descriptionClass, className), ...props }));
4929
+ };
4930
+ EmptyStateDescription.displayName = 'EmptyState.Description';
4931
+ /**
4932
+ * A row of buttons, centered under the text or at the start of the line beside the indicator.
4933
+ *
4934
+ * @function
4935
+ * @param {EmptyStateActionsProps} props - Props for EmptyState.Actions.
4936
+ * @returns {JSX.Element} The rendered row of buttons.
4937
+ */
4938
+ const EmptyStateActions = ({ className, ...props }) => {
4939
+ const actionsClass = usePrefixedClassNames('empty-state-actions');
4940
+ return jsxRuntime.jsx(Buttons, { className: classNames(actionsClass, className), ...props });
4941
+ };
4942
+ EmptyStateActions.displayName = 'EmptyState.Actions';
4943
+ /**
4944
+ * The `EmptyState` component renders the block an app shows where content would be: an empty list, a search with no results, or a first-run screen.
4945
+ *
4946
+ * It's a column of a picture in a circle, a heading, a line of text and the
4947
+ * actions, with one layout whether it's written with the `icon`, `title` and
4948
+ * `description` props or with the parts. The children always render as
4949
+ * written, after the parts the props render.
4950
+ *
4951
+ * It has no role and no live region. It arrives already filled and goes away
4952
+ * when results come back, so it can't announce a search that matched
4953
+ * nothing: keep a `role="status"` region beside the search field for that,
4954
+ * in the page the whole time. While the data loads, show a `Skeleton` or a
4955
+ * `Loader` instead, so "No projects yet" doesn't flash first, and show a
4956
+ * load that failed in a `Message` or a `Notification`.
4957
+ *
4958
+ * @function
4959
+ * @param {EmptyStateProps} props - Props for the EmptyState component.
4960
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the root `<div>`.
4961
+ * @returns {JSX.Element} The rendered empty state.
4962
+ *
4963
+ * @example
4964
+ * <EmptyState
4965
+ * icon="folder-open"
4966
+ * title="No projects yet"
4967
+ * description="Projects you create or join show up here."
4968
+ * >
4969
+ * <Button color="primary">New project</Button>
4970
+ * </EmptyState>
4971
+ */
4972
+ const EmptyStateRoot = React.forwardRef(({ className, icon, title, titleAs = 'h2', description, children, color, variant = 'light', size = 'normal', align = 'center', textColor, bgColor, ...props }, ref) => {
4973
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
4974
+ color: textColor,
4975
+ backgroundColor: bgColor,
4976
+ ...props,
4977
+ });
4978
+ const rootClass = usePrefixedClassNames('empty-state', {
4979
+ [`is-${size}`]: validEmptyStateSizes.includes(size),
4980
+ 'is-left': align === 'left',
4981
+ [`is-${color}`]: !!color && validEmptyStateColors.includes(color),
4982
+ 'is-filled': variant === 'filled',
4983
+ });
4984
+ // The props' parts first, each only when it's given, then the children
4985
+ // exactly as they were written: nothing here looks at them.
4986
+ return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: [isGiven(icon) && (jsxRuntime.jsx(EmptyStateIndicator, { children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), isGiven(title) && (jsxRuntime.jsx(EmptyStateTitle, { as: titleAs, children: title })), isGiven(description) && (jsxRuntime.jsx(EmptyStateDescription, { children: description })), children] }));
4987
+ });
4988
+ const EmptyState = withSubComponents(EmptyStateRoot, {
4989
+ Indicator: EmptyStateIndicator,
4990
+ Title: EmptyStateTitle,
4991
+ Description: EmptyStateDescription,
4992
+ Actions: EmptyStateActions,
4993
+ }, 'EmptyState');
4994
+
4995
+ // INTERNAL: deliberately not exported from src/index.ts.
4996
+ /** The major version of the React this bundle is running against. */
4997
+ const runningMajor = () => Number(React.version.split('.')[0]);
4998
+ /**
4999
+ * The prop that makes an element inert, spelled for the React in use, or
5000
+ * nothing when `inert` is false.
5001
+ *
5002
+ * Both majors must render `inert=""`, and each warns on the other's spelling
5003
+ * and then drops the attribute. React 19 treats `inert` as a boolean and
5004
+ * rejects the empty string. React 18 does not know the attribute, so it
5005
+ * passes a string through and rejects `true`.
5006
+ */
5007
+ const inertProps = (inert, major = runningMajor()) => {
5008
+ if (!inert)
5009
+ return {};
5010
+ // The cast is for React 18's spelling: this package's types are React 19's,
5011
+ // where `inert` is a boolean.
5012
+ return (major >= 19
5013
+ ? { inert: true }
5014
+ : { inert: '' });
5015
+ };
5016
+
5017
+ // Context to track MenuList nesting level
5018
+ const MenuListLevelContext = React.createContext(0);
5019
+ /**
5020
+ * The `Menu` component provides Bulma's vertical navigation menu: a simple, accessible sidebar or section menu for your Bulma React UI.
5021
+ *
5022
+ * @function
5023
+ * @param {MenuProps} props - Props for the Menu component.
5024
+ * @returns {JSX.Element} The rendered menu.
5025
+ * @see {@link https://bulma.io/documentation/components/menu/ | Bulma Menu documentation}
5026
+ */
5027
+ const MenuComponent = ({ className, children, ...props }) => {
5028
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
5029
+ // Generate Bulma classes with prefix
5030
+ const bulmaClasses = usePrefixedClassNames('menu');
5031
+ return (jsxRuntime.jsx("aside", { className: classNames(bulmaClasses, bulmaHelperClasses, className), ...rest, children: children }));
5032
+ };
5033
+ /**
5034
+ * Bulma Menu label component.
5035
+ *
5036
+ * @function
5037
+ * @param {MenuLabelProps} props - Props for the MenuLabel component.
5038
+ * @returns {JSX.Element} The rendered menu label.
5039
+ */
5040
+ const MenuLabel = ({ className, children, ...props }) => {
5041
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
5042
+ return (jsxRuntime.jsx("p", { className: classNames(usePrefixedClassNames('menu-label'), className, bulmaHelperClasses), ...rest, children: children }));
5043
+ };
5044
+ /**
5045
+ * MenuList applies `menu-list` class only at the top level (not for nested lists).
5046
+ *
5047
+ * Every level renders `role="list"`, which Safari needs: WebKit stops
5048
+ * exposing a `<ul>` as a list once its markers are hidden, and Bulma's reset
5049
+ * hides them.
5050
+ *
5051
+ * @function
5052
+ * @param {MenuListProps} props - Props for the MenuList component.
5053
+ * @returns {JSX.Element} The rendered menu list.
5054
+ */
5055
+ const MenuList = ({ className, children, ...props }) => {
5056
+ const level = React.useContext(MenuListLevelContext);
5057
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
5058
+ const ulClass = classNames(className, bulmaHelperClasses, {
4312
5059
  [usePrefixedClassNames('menu-list')]: level === 0,
4313
5060
  });
4314
5061
  // Increment level for nested MenuLists. role="list" restates the <ul>'s
@@ -4502,195 +5249,37 @@ const Message = withSubComponents(MessageComponent, {
4502
5249
  Body: MessageBody,
4503
5250
  }, 'Message');
4504
5251
 
4505
- // Ref-counted across every caller (e.g. a Dialog rendering its own Modal)
4506
- // so overlapping/nested locks don't fight over which one restores `overflow`.
4507
- // Every overlay that locks body scroll — Modal, Dialog, Sidebar, Loading —
4508
- // goes through here; a component setting `document.body.style.overflow`
4509
- // itself would clear a lock another overlay still needs.
4510
- let lockCount = 0;
4511
- let originalOverflow = '';
4512
- /**
4513
- * Locks (and ref-counted-ly unlocks) `document.body` scrolling while `active`
4514
- * is true. Safe to call from multiple components at once — the body scroll
4515
- * is only restored once every active caller has released its lock.
4516
- *
4517
- * @function useScrollLock
4518
- * @param active - Whether this caller wants the body scroll locked.
4519
- */
4520
- function useScrollLock(active) {
4521
- React.useEffect(() => {
4522
- if (!active)
4523
- return undefined;
4524
- lockCount++;
4525
- if (lockCount === 1) {
4526
- originalOverflow = document.body.style.overflow;
4527
- document.body.style.overflow = 'hidden';
4528
- }
4529
- return () => {
4530
- lockCount--;
4531
- if (lockCount === 0) {
4532
- document.body.style.overflow = originalOverflow;
4533
- }
4534
- };
4535
- }, [active]);
4536
- }
4537
-
4538
- // "Are we past hydration?" as a store that never changes: React reads the
4539
- // server snapshot both while server-rendering and while hydrating, and the
4540
- // client snapshot from the first post-hydration render onward.
4541
- const subscribeToNothing = () => () => { };
4542
- const getClientSnapshot = () => true;
4543
- const getServerSnapshot = () => false;
4544
- /**
4545
- * Reports whether the component has passed hydration.
4546
- *
4547
- * Returns `false` during server rendering and during the hydrating client
4548
- * render, then `true` from the commit that follows. Use it to defer
4549
- * DOM-only behaviour (such as portalling) past hydration so the first client
4550
- * render matches the server markup instead of tripping hydration recovery.
4551
- *
4552
- * Only a component that hydrates ever sees `false` on the client. One that
4553
- * mounts later (after a route change, or in an app with no server rendering)
4554
- * gets `true` on its first render, so it pays for no extra render. It
4555
- * subscribes to nothing, so no later render flips it back.
4556
- *
4557
- * @function useIsHydrated
4558
- * @returns `true` once the hydrating render has completed, otherwise `false`.
4559
- */
4560
- function useIsHydrated() {
4561
- return React.useSyncExternalStore(subscribeToNothing, getClientSnapshot, getServerSnapshot);
4562
- }
4563
-
4564
- /**
4565
- * Resolves a portal target: an `HTMLElement` is used directly, a non-empty
4566
- * `string` is treated as a `document.querySelector` selector (falling back to
4567
- * `document.body` when it matches nothing), and any falsy value — `undefined`
4568
- * or the empty string — resolves to `document.body`.
4569
- *
4570
- * The empty string is deliberately treated as "no target" rather than passed
4571
- * through: `document.querySelector('')` throws a `SyntaxError`, and callers
4572
- * building a selector from state can easily hand us `''`.
4573
- *
4574
- * @function resolvePortalContainer
4575
- * @param container - The requested portal target, if any.
4576
- * @returns The resolved DOM node to portal into.
4577
- */
4578
- function resolvePortalContainer(container) {
4579
- if (!container) {
4580
- return document.body;
4581
- }
4582
- if (typeof container === 'string') {
4583
- return (document.querySelector(container) ?? document.body);
4584
- }
4585
- return container;
4586
- }
4587
- /**
4588
- * Where a component's portal goes, as a function to call when the portal is
4589
- * created: the target the component was given, else the `portalContainer` a
4590
- * `ConfigProvider` sets, else `document.body`, each read as
4591
- * `resolvePortalContainer` reads it. A component that portals resolves its
4592
- * target through this, so the provider's default reaches it.
4593
- *
4594
- * The hook only reads context, so it is safe to call on the server, ahead of
4595
- * any early return. The function it returns reads the DOM, so call it only
4596
- * where the portal is created, which the server never reaches.
4597
- *
4598
- * @function usePortalContainer
4599
- * @returns A function that takes the component's own target, if any, and
4600
- * returns the DOM node to portal into.
4601
- */
4602
- function usePortalContainer() {
4603
- const { portalContainer } = useConfig();
4604
- return container => resolvePortalContainer(container || portalContainer);
4605
- }
4606
- /**
4607
- * Reads Modal's `portal` prop (`boolean | string | HTMLElement`) for a
4608
- * component that takes it: whether the content is in its portal yet, and
4609
- * where it goes.
4610
- *
4611
- * A portal has no server-rendered counterpart, so such a component renders in
4612
- * place on the server and while hydrating, and moves into its portal once the
4613
- * client takes over. Render the content in `Portal` with
4614
- * `disabled={!isPortaled}` and this `container`. Moving remounts the content,
4615
- * so an effect that holds one of its nodes depends on `isPortaled`, `portal`
4616
- * and `defaultTarget`.
4617
- *
4618
- * @function usePortalProp
4619
- * @param portal - The component's `portal` prop.
4620
- * @returns Whether it's portaled now, its `container`, and the provider's
4621
- * target while that places it.
4622
- */
4623
- function usePortalProp(portal) {
4624
- const hydrated = useIsHydrated();
4625
- const { portalContainer } = useConfig();
4626
- const isPortaled = Boolean(portal) && hydrated;
4627
- return {
4628
- isPortaled,
4629
- container: typeof portal === 'boolean' ? undefined : portal,
4630
- defaultTarget: isPortaled && portal === true ? portalContainer : undefined,
4631
- };
4632
- }
4633
- /**
4634
- * Renders its children into another part of the page, `document.body` unless `container` or a `ConfigProvider` says otherwise, so floating content escapes an ancestor's `overflow`, `transform` or stacking context.
4635
- *
4636
- * React's server renderer can't render a portal, so `Portal` renders nothing
4637
- * on the server and during hydration, and mounts its children in the commit
4638
- * that follows. The first client render then matches the server markup. In an app
4639
- * without server rendering it portals on the first render.
4640
- *
4641
- * React context, such as `ConfigProvider`'s class prefix, reaches the
4642
- * portaled children, and their React events bubble through the React tree.
4643
- * CSS inheritance follows the DOM instead: a scoped (non-`isRoot`) `Theme`
4644
- * sets its variables on a wrapper element the portaled content has left, so
4645
- * that content sees the page-level theme, and styles scoped to a shadow root
4646
- * don't reach `document.body` at all. To keep them, portal into an element
4647
- * inside the themed area or the shadow root: pass it as `container`, or as
4648
- * the `portalContainer` of a `ConfigProvider` around that area. The second
4649
- * also reaches what the library's components portal, such as Popover's
4650
- * `appendToBody` panel or a `Modal` with `portal`.
4651
- *
4652
- * Inside a `LocaleProvider`, the children keep its `dir` and `lang`. A
4653
- * container inside the provider's own wrapper takes the children as they
4654
- * are, and they follow the provider through the page. Any other,
4655
- * `document.body` included, gets them inside one `<div>` carrying the
4656
- * provider's values, with `display: contents` so it adds no box. It is
4657
- * still an element, so content that must be the container's direct child,
4658
- * a `<tr>` in a `<tbody>`, or that a direct-child selector styles, belongs
4659
- * in a container inside the provider. Which of the two is settled when the
4660
- * children mount, so a change of locale updates them in place rather than
4661
- * remounting them.
4662
- *
4663
- * Tab order and focus follow the DOM too. Content portaled from inside a
4664
- * focus trap is no longer inside the trapped element, so the trap neither
4665
- * reaches it nor holds focus there: render nested overlays inside the
4666
- * trapped element, or give them a trap of their own. To trap portaled
4667
- * content, put `useFocusTrap`'s ref on the element inside the `Portal`.
4668
- *
4669
- * @function
4670
- * @param {PortalProps} props - Props for the Portal component.
4671
- * @returns The portal, the children in place when `disabled`, or nothing
4672
- * before hydration.
4673
- *
4674
- * @example
4675
- * <Portal>
4676
- * <div className="box">Rendered at the end of document.body</div>
4677
- * </Portal>
5252
+ // Ref-counted across every caller (e.g. a Dialog rendering its own Modal)
5253
+ // so overlapping/nested locks don't fight over which one restores `overflow`.
5254
+ // Every overlay that locks body scroll — Modal, Dialog, Sidebar, Loading —
5255
+ // goes through here; a component setting `document.body.style.overflow`
5256
+ // itself would clear a lock another overlay still needs.
5257
+ let lockCount = 0;
5258
+ let originalOverflow = '';
5259
+ /**
5260
+ * Locks (and ref-counted-ly unlocks) `document.body` scrolling while `active`
5261
+ * is true. Safe to call from multiple components at once — the body scroll
5262
+ * is only restored once every active caller has released its lock.
4678
5263
  *
4679
- * @example
4680
- * // Into a dedicated overlay root
4681
- * <Portal container="#overlays">
4682
- * <CommandPalette />
4683
- * </Portal>
5264
+ * @function useScrollLock
5265
+ * @param active - Whether this caller wants the body scroll locked.
4684
5266
  */
4685
- function Portal({ children, container, disabled = false }) {
4686
- const hydrated = useIsHydrated();
4687
- const resolveContainer = usePortalContainer();
4688
- if (disabled)
4689
- return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
4690
- if (!hydrated)
4691
- return null;
4692
- const target = resolveContainer(container);
4693
- return reactDom.createPortal(jsxRuntime.jsx(LocalePortalScope, { target: target, children: children }), target);
5267
+ function useScrollLock(active) {
5268
+ React.useEffect(() => {
5269
+ if (!active)
5270
+ return undefined;
5271
+ lockCount++;
5272
+ if (lockCount === 1) {
5273
+ originalOverflow = document.body.style.overflow;
5274
+ document.body.style.overflow = 'hidden';
5275
+ }
5276
+ return () => {
5277
+ lockCount--;
5278
+ if (lockCount === 0) {
5279
+ document.body.style.overflow = originalOverflow;
5280
+ }
5281
+ };
5282
+ }, [active]);
4694
5283
  }
4695
5284
 
4696
5285
  // INTERNAL: deliberately not exported from src/index.ts.
@@ -8182,1040 +8771,757 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
8182
8771
  const handleTriggerKeyDown = (e) => {
8183
8772
  triggerProps.onKeyDown?.(e);
8184
8773
  // A key on something inside the trigger is that element's to answer.
8185
- if (e.defaultPrevented || e.target !== e.currentTarget)
8186
- return;
8187
- if (e.key === 'Escape') {
8188
- if (isOpen && closeOnEscape) {
8189
- e.preventDefault();
8190
- close();
8191
- }
8192
- return;
8193
- }
8194
- if (e.key !== 'Enter' && e.key !== ' ')
8195
- return;
8196
- if (handlesKeyItself(e.currentTarget, e.key))
8197
- return;
8198
- // Space would scroll the page; a held key repeats.
8199
- e.preventDefault();
8200
- if (!e.repeat)
8201
- requestOpen(!isOpen);
8202
- };
8203
- const needsButtonRole = typeof trigger.type === 'string' &&
8204
- !NATIVE_CONTROLS.has(trigger.type) &&
8205
- !(trigger.type === 'a' && triggerProps.href != null);
8206
- const clonedTrigger = React.cloneElement(trigger, {
8207
- ...(needsButtonRole
8208
- ? {
8209
- role: triggerProps.role ?? 'button',
8210
- tabIndex: triggerProps.tabIndex ?? 0,
8211
- }
8212
- : {}),
8213
- 'aria-haspopup': 'dialog',
8214
- 'aria-expanded': isOpen,
8215
- 'aria-controls': panelMounted ? panelId : undefined,
8216
- onClick: handleTriggerClick,
8217
- onKeyDown: handleTriggerKeyDown,
8218
- });
8219
- const rootClasses = classNames(usePrefixedClassNames('popover', { 'is-active': isOpen }), bulmaHelperClasses, className);
8220
- const panelClasses = classNames(usePrefixedClassNames('popover-content', {
8221
- [`is-${resolved.position}`]: true,
8222
- 'is-portal': appendToBody,
8223
- }), contentClassName);
8224
- return (jsxRuntime.jsxs("div", { className: rootClasses, ...rest, ref: rootRef, children: [clonedTrigger, isOpen && (jsxRuntime.jsx(Portal, { disabled: !appendToBody, children: jsxRuntime.jsx(PopoverPanel, { panelRef: panelRef, titleId: titleId, ariaLabel: ariaLabel, close: close, id: panelId, className: panelClasses, style: appendToBody
8225
- ? {
8226
- position: 'fixed',
8227
- top: resolved.top,
8228
- left: resolved.left,
8229
- }
8230
- : undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture: markInsidePress, children: children }) }))] }));
8231
- };
8232
- /**
8233
- * The open panel. It mounts with each opening, so it counts the
8234
- * `Popover.Header`s inside it afresh every time.
8235
- */
8236
- function PopoverPanel({ panelRef, titleId, ariaLabel, close, children, ...attrs }) {
8237
- // Headers register in a layout effect, after this first render, and this
8238
- // panel's own layout effect runs after theirs. Until then the panel assumes
8239
- // it has one, so a panel that does is named from its first commit, before
8240
- // focus moves in, and one that doesn't drops the reference before it paints.
8241
- const [titlesCounted, setTitlesCounted] = React.useState(false);
8242
- const [titleCount, setTitleCount] = React.useState(0);
8243
- const registerTitle = React.useCallback(() => {
8244
- setTitleCount(n => n + 1);
8245
- return () => setTitleCount(n => n - 1);
8246
- }, []);
8247
- useClientLayoutEffect(() => {
8248
- setTitlesCounted(true);
8249
- }, []);
8250
- const named = Boolean(ariaLabel) || titleCount > 0;
8251
- React.useEffect(() => {
8252
- if (titlesCounted && !named) {
8253
- warnOnce('Popover:unnamed', '[bestax-bulma] <Popover> opened a panel with no accessible name. ' +
8254
- 'Pass ariaLabel, or render a Popover.Header inside it.');
8255
- }
8256
- }, [titlesCounted, named]);
8257
- const context = React.useMemo(() => ({ titleId, registerTitle, close }), [titleId, registerTitle, close]);
8258
- return (jsxRuntime.jsx(PopoverContext.Provider, { value: context, children: jsxRuntime.jsx("div", { ...attrs, ref: panelRef, role: "dialog", "aria-label": ariaLabel, "aria-labelledby": !ariaLabel && (!titlesCounted || titleCount > 0) ? titleId : undefined, tabIndex: -1, children: children }) }));
8259
- }
8260
- /**
8261
- * The popover's title, which names the panel for assistive technology.
8262
- *
8263
- * @function
8264
- * @param {PopoverHeaderProps} props - Props for the Popover.Header component.
8265
- * @returns {JSX.Element} The header element.
8266
- */
8267
- const PopoverHeader = ({ className, ...props }) => {
8268
- const context = React.useContext(PopoverContext);
8269
- const registerTitle = context?.registerTitle;
8270
- // Before paint, so the panel is named by the time it shows.
8271
- useClientLayoutEffect(() => registerTitle?.(), [registerTitle]);
8272
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8273
- return (jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('popover-header'), bulmaHelperClasses, className), ...rest, id: context?.titleId }));
8274
- };
8275
- /**
8276
- * The popover's main content area.
8277
- *
8278
- * @function
8279
- * @param {PopoverBodyProps} props - Props for the Popover.Body component.
8280
- * @returns {JSX.Element} The body element.
8281
- */
8282
- const PopoverBody = ({ className, ...props }) => {
8283
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8284
- return (jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('popover-body'), bulmaHelperClasses, className), ...rest }));
8285
- };
8286
- /**
8287
- * A row of actions along the bottom of the popover, aligned to the end.
8288
- *
8289
- * @function
8290
- * @param {PopoverFooterProps} props - Props for the Popover.Footer component.
8291
- * @returns {JSX.Element} The footer element.
8292
- */
8293
- const PopoverFooter = ({ className, ...props }) => {
8294
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8295
- return (jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('popover-footer'), bulmaHelperClasses, className), ...rest }));
8296
- };
8297
- /**
8298
- * A `Button` that closes the popover, handing focus back to the trigger.
8299
- *
8300
- * @function
8301
- * @param {PopoverCloseProps} props - Props for the Popover.Close component.
8302
- * @returns {JSX.Element} The close button.
8303
- */
8304
- const PopoverClose = ({ onClick, ...props }) => {
8305
- const context = React.useContext(PopoverContext);
8306
- return (jsxRuntime.jsx(Button, { ...props, type: buttonType(props.type), onClick: (e) => {
8307
- onClick?.(e);
8308
- if (!e.defaultPrevented)
8309
- context?.close();
8310
- } }));
8311
- };
8312
- /** Popover with Header, Body, Footer and Close sub-components. */
8313
- const Popover = withSubComponents(PopoverComponent, {
8314
- Header: PopoverHeader,
8315
- Body: PopoverBody,
8316
- Footer: PopoverFooter,
8317
- Close: PopoverClose,
8318
- }, 'Popover');
8319
-
8320
- /**
8321
- * The `HoverCard` component shows a card previewing what's behind a link or a button while the pointer rests on it or keyboard focus is on it.
8322
- *
8323
- * Nothing moves focus when the card opens, and nothing traps it. In place,
8324
- * the card comes right after the trigger, so Tab goes from the trigger into
8325
- * its links and tabbing past them closes it. The pointer can move onto the
8326
- * card, Escape closes it from anywhere, and by default touch doesn't open it,
8327
- * so tapping the trigger just does what the trigger does. `openOnTap` opens
8328
- * it with a tap, for a trigger with no tap action of its own. For hints with nothing to
8329
- * click use `Tooltip`, and for content opened on purpose, such as a form,
8330
- * use `Popover`.
8331
- *
8332
- * @function
8333
- * @param {HoverCardProps} props - Props for the HoverCard component.
8334
- * @returns {JSX.Element} The trigger in its wrapper, and the card while open.
8335
- *
8336
- * @example
8337
- * <HoverCard trigger={<a href="/people/ada">Ada Lovelace</a>}>
8338
- * <Title as="p" size="6">Ada Lovelace</Title>
8339
- * <SubTitle as="p" size="6">Analytical Engine team</SubTitle>
8340
- * </HoverCard>
8341
- */
8342
- const HoverCard = ({ trigger, children, open: controlledOpen, defaultOpen = false, onOpenChange, openDelay = 600, closeDelay = 300, position = 'bottom-left', appendToBody = false, openOnTap = false, className, contentClassName, ...props }) => {
8343
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8344
- const { onPointerEnter, onPointerOver, onPointerLeave, onPointerDown, onPointerUp, onFocus, onBlur, ...attrs } = rest;
8345
- const rootRef = React.useRef(null);
8346
- const cardRef = React.useRef(null);
8347
- const cardId = `${React.useId()}-card`;
8348
- const isControlled = controlledOpen !== undefined;
8349
- const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
8350
- const isOpen = controlledOpen ?? internalOpen;
8351
- const requestOpen = React.useCallback((next) => {
8352
- onOpenChange?.(next);
8353
- if (!isControlled)
8354
- setInternalOpen(next);
8355
- }, [isControlled, onOpenChange]);
8356
- // A portaled card has no server markup, so it only exists from the commit
8357
- // after hydration, and nothing measures it before then.
8358
- const hydrated = useIsHydrated();
8359
- const cardMounted = isOpen && (hydrated || !appendToBody);
8360
- const resolved = useAnchoredPosition(rootRef, cardRef, {
8361
- active: cardMounted,
8362
- position,
8363
- fixed: appendToBody,
8364
- // The gap comes from `--bulma-hover-card-offset` in the stylesheet, so
8365
- // the coordinates leave it out and `auto` reads it off the card.
8366
- offset: 0,
8367
- styledGap: marginGap,
8368
- });
8369
- // The trigger renders first inside the wrapper. Read when needed rather
8370
- // than held, so a trigger that remounted is still the one found.
8371
- const triggerRef = React.useMemo(() => ({
8372
- get current() {
8373
- const first = rootRef.current.firstElementChild;
8374
- return first && first !== cardRef.current
8375
- ? first
8376
- : null;
8377
- },
8378
- }), []);
8379
- const hover = useHoverOpen({
8380
- open: isOpen,
8381
- onOpenChange: requestOpen,
8382
- openDelay,
8383
- closeDelay,
8384
- triggerRef,
8385
- floatingRef: cardRef,
8386
- openOnTap,
8387
- });
8388
- // A trigger that can't take focus leaves keyboard users no way to open the
8389
- // card. It has to be the trigger element itself, not something inside it,
8390
- // since focus comes back to it and its ARIA is on it. Judged by what the
8391
- // element is rather than by its styles, which can hide it for now. After
8392
- // every commit, since the trigger can change, and only in development.
8393
- React.useEffect(() => {
8394
- if (!isDev())
8395
- return;
8396
- const triggerNode = triggerRef.current;
8397
- if (!triggerNode)
8774
+ if (e.defaultPrevented || e.target !== e.currentTarget)
8398
8775
  return;
8399
- if (!triggerNode.matches(':disabled') && isTabbableKind(triggerNode)) {
8776
+ if (e.key === 'Escape') {
8777
+ if (isOpen && closeOnEscape) {
8778
+ e.preventDefault();
8779
+ close();
8780
+ }
8400
8781
  return;
8401
8782
  }
8402
- warnOnce('HoverCard:unfocusable-trigger', "[bestax-bulma] <HoverCard> has a trigger that can't take focus, so " +
8403
- "keyboard users can't open the card. Use a link or a button as the " +
8404
- 'trigger, or give it tabIndex={0}.');
8405
- });
8406
- // Expanded only while the card is on the page, so a portaled card that
8407
- // waits for hydration isn't announced before it exists.
8783
+ if (e.key !== 'Enter' && e.key !== ' ')
8784
+ return;
8785
+ if (handlesKeyItself(e.currentTarget, e.key))
8786
+ return;
8787
+ // Space would scroll the page; a held key repeats.
8788
+ e.preventDefault();
8789
+ if (!e.repeat)
8790
+ requestOpen(!isOpen);
8791
+ };
8792
+ const needsButtonRole = typeof trigger.type === 'string' &&
8793
+ !NATIVE_CONTROLS.has(trigger.type) &&
8794
+ !(trigger.type === 'a' && triggerProps.href != null);
8408
8795
  const clonedTrigger = React.cloneElement(trigger, {
8409
- 'aria-expanded': cardMounted,
8410
- 'aria-controls': cardMounted ? cardId : undefined,
8796
+ ...(needsButtonRole
8797
+ ? {
8798
+ role: triggerProps.role ?? 'button',
8799
+ tabIndex: triggerProps.tabIndex ?? 0,
8800
+ }
8801
+ : {}),
8802
+ 'aria-haspopup': 'dialog',
8803
+ 'aria-expanded': isOpen,
8804
+ 'aria-controls': panelMounted ? panelId : undefined,
8805
+ onClick: handleTriggerClick,
8806
+ onKeyDown: handleTriggerKeyDown,
8411
8807
  });
8412
- const rootClasses = classNames(usePrefixedClassNames('hover-card', { 'is-active': cardMounted }), bulmaHelperClasses, className);
8413
- const cardClasses = classNames(usePrefixedClassNames('hover-card-content', {
8808
+ const rootClasses = classNames(usePrefixedClassNames('popover', { 'is-active': isOpen }), bulmaHelperClasses, className);
8809
+ const panelClasses = classNames(usePrefixedClassNames('popover-content', {
8414
8810
  [`is-${resolved.position}`]: true,
8415
8811
  'is-portal': appendToBody,
8416
8812
  }), contentClassName);
8417
- return (jsxRuntime.jsxs("span", { className: rootClasses, ...attrs, onPointerEnter: chain$1(onPointerEnter, hover.onPointerEnter), onPointerOver: chain$1(onPointerOver, hover.onPointerOver), onPointerLeave: chain$1(onPointerLeave, hover.onPointerLeave), onFocus: chain$1(onFocus, hover.onFocus), onBlur: chain$1(onBlur, hover.onBlur), onPointerDown: chain$1(onPointerDown, hover.onPointerDown), onPointerUp: chain$1(onPointerUp, hover.onPointerUp), ref: rootRef, children: [clonedTrigger, isOpen && (jsxRuntime.jsx(Portal, { disabled: !appendToBody, children: jsxRuntime.jsx("div", { ref: cardRef, id: cardId, className: cardClasses, style: appendToBody
8813
+ return (jsxRuntime.jsxs("div", { className: rootClasses, ...rest, ref: rootRef, children: [clonedTrigger, isOpen && (jsxRuntime.jsx(Portal, { disabled: !appendToBody, children: jsxRuntime.jsx(PopoverPanel, { panelRef: panelRef, titleId: titleId, ariaLabel: ariaLabel, close: close, id: panelId, className: panelClasses, style: appendToBody
8418
8814
  ? {
8419
8815
  position: 'fixed',
8420
8816
  top: resolved.top,
8421
8817
  left: resolved.left,
8422
8818
  }
8423
- : undefined, children: children }) }))] }));
8819
+ : undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture: markInsidePress, children: children }) }))] }));
8424
8820
  };
8425
-
8426
- // INTERNAL: deliberately not exported from src/index.ts.
8427
- /** How long a touch or a pen has to stay down to open the menu, in milliseconds. */
8428
- const LONG_PRESS_DELAY = 500;
8429
- /** How far a long press can move before it counts as something else, in CSS pixels. */
8430
- const LONG_PRESS_TOLERANCE = 10;
8431
- /**
8432
- * The last press no trigger is to start a long press for: one a trigger
8433
- * took, or another pointer coming down while a press is held. A press
8434
- * reaches every trigger it bubbles through, the innermost first and all
8435
- * before the next press, so this is how a trigger around another one knows
8436
- * the inner one took it, and how a trigger a second finger lands on knows to
8437
- * leave it.
8438
- */
8439
- let claimed = null;
8440
- /** Whether the event happened in the element its handler is on, rather than in content portaled out of it. */
8441
- const inTrigger = (event) => event.currentTarget.contains(event.target);
8442
8821
  /**
8443
- * Where a `contextmenu` asks to open: at its point when that is on the
8444
- * element it was fired at, which it is for the pointer, and at that
8445
- * element's bottom-left corner otherwise, which is where the browser puts
8446
- * its own menu for a key.
8822
+ * The open panel. It mounts with each opening, so it counts the
8823
+ * `Popover.Header`s inside it afresh every time.
8447
8824
  */
8448
- function contextMenuPoint(event) {
8449
- const { clientX: x, clientY: y } = event;
8450
- const rect = event.target.getBoundingClientRect();
8451
- return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
8452
- ? { x, y }
8453
- : { x: rect.left, y: rect.bottom };
8825
+ function PopoverPanel({ panelRef, titleId, ariaLabel, close, children, ...attrs }) {
8826
+ // Headers register in a layout effect, after this first render, and this
8827
+ // panel's own layout effect runs after theirs. Until then the panel assumes
8828
+ // it has one, so a panel that does is named from its first commit, before
8829
+ // focus moves in, and one that doesn't drops the reference before it paints.
8830
+ const [titlesCounted, setTitlesCounted] = React.useState(false);
8831
+ const [titleCount, setTitleCount] = React.useState(0);
8832
+ const registerTitle = React.useCallback(() => {
8833
+ setTitleCount(n => n + 1);
8834
+ return () => setTitleCount(n => n - 1);
8835
+ }, []);
8836
+ useClientLayoutEffect(() => {
8837
+ setTitlesCounted(true);
8838
+ }, []);
8839
+ const named = Boolean(ariaLabel) || titleCount > 0;
8840
+ React.useEffect(() => {
8841
+ if (titlesCounted && !named) {
8842
+ warnOnce('Popover:unnamed', '[bestax-bulma] <Popover> opened a panel with no accessible name. ' +
8843
+ 'Pass ariaLabel, or render a Popover.Header inside it.');
8844
+ }
8845
+ }, [titlesCounted, named]);
8846
+ const context = React.useMemo(() => ({ titleId, registerTitle, close }), [titleId, registerTitle, close]);
8847
+ return (jsxRuntime.jsx(PopoverContext.Provider, { value: context, children: jsxRuntime.jsx("div", { ...attrs, ref: panelRef, role: "dialog", "aria-label": ariaLabel, "aria-labelledby": !ariaLabel && (!titlesCounted || titleCount > 0) ? titleId : undefined, tabIndex: -1, children: children }) }));
8454
8848
  }
8455
8849
  /**
8456
- * Decides when a context menu asks to open, and where. It renders nothing,
8457
- * and knows nothing about ARIA or the menu.
8458
- *
8459
- * - **`contextmenu`:** a right-click, and Shift+F10 or the context menu key
8460
- * where the browser fires the event for them, asks to open at the point
8461
- * `contextMenuPoint` works out, and prevents the browser's own menu. Each
8462
- * one asks again, so a right-click elsewhere on the trigger moves it.
8463
- * - **Long press:** a touch or a pen held still on the trigger for
8464
- * `LONG_PRESS_DELAY` fires a `contextmenu` at what it landed on, so it
8465
- * takes the same path, handlers on the way included. Moving more than
8466
- * `LONG_PRESS_TOLERANCE`, lifting, another pointer coming down anywhere in
8467
- * the document, or `pointercancel`, which is the browser taking the
8468
- * gesture for a scroll or a zoom, cancels it, and a mouse never starts
8469
- * one. A pointer that comes down while another is held starts no long
8470
- * press of its own, on this trigger or another, so two fingers on two rows
8471
- * open nothing. While the finger is down, and only then, a listener on the
8472
- * document hears other pointers coming down. Where the
8473
- * browser fires `contextmenu` for a long press itself, the first of the two
8474
- * opens it, and the end of a touch that opened it is prevented, so lifting
8475
- * doesn't click what is under the finger.
8476
- * - **Who opens:** a handler that calls `preventDefault()` on the
8477
- * `contextmenu` before this one keeps it from asking, which is how a
8478
- * trigger's own handler opts out and how the innermost of nested triggers
8479
- * wins. A long press goes to the innermost trigger that takes it. Events
8480
- * from content portaled out of the trigger, which React brings here
8481
- * anyway, are ignored.
8482
- * - **`disabled`:** nothing asks, and the browser's menu shows.
8850
+ * The popover's title, which names the panel for assistive technology.
8483
8851
  *
8484
- * Nothing it scheduled runs after the component unmounts.
8852
+ * @function
8853
+ * @param {PopoverHeaderProps} props - Props for the Popover.Header component.
8854
+ * @returns {JSX.Element} The header element.
8855
+ */
8856
+ const PopoverHeader = ({ className, ...props }) => {
8857
+ const context = React.useContext(PopoverContext);
8858
+ const registerTitle = context?.registerTitle;
8859
+ // Before paint, so the panel is named by the time it shows.
8860
+ useClientLayoutEffect(() => registerTitle?.(), [registerTitle]);
8861
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8862
+ return (jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('popover-header'), bulmaHelperClasses, className), ...rest, id: context?.titleId }));
8863
+ };
8864
+ /**
8865
+ * The popover's main content area.
8485
8866
  *
8486
- * @function useContextMenuTrigger
8487
- * @param options - How to ask to open, and whether to.
8488
- * @returns Handlers for the trigger element.
8867
+ * @function
8868
+ * @param {PopoverBodyProps} props - Props for the Popover.Body component.
8869
+ * @returns {JSX.Element} The body element.
8489
8870
  */
8490
- function useContextMenuTrigger({ onOpen, disabled, }) {
8491
- const pressRef = React.useRef(null);
8492
- /** A long press opened the menu, and the touch that made it hasn't ended. */
8493
- const heldRef = React.useRef(false);
8494
- const cancel = () => {
8495
- clearTimeout(pressRef.current?.timer);
8496
- pressRef.current?.stop();
8497
- pressRef.current = null;
8498
- };
8499
- React.useEffect(() => () => {
8500
- clearTimeout(pressRef.current?.timer);
8501
- pressRef.current?.stop();
8502
- pressRef.current = null;
8503
- }, []);
8504
- const isPress = (event) => pressRef.current?.pointerId === event.pointerId;
8505
- return {
8506
- onContextMenu: event => {
8507
- if (disabled || event.defaultPrevented || !inTrigger(event))
8508
- return;
8509
- event.preventDefault();
8510
- const press = pressRef.current;
8511
- if (press) {
8512
- if (press.opened)
8513
- return;
8514
- clearTimeout(press.timer);
8515
- press.opened = true;
8516
- heldRef.current = true;
8517
- }
8518
- onOpen(contextMenuPoint(event), event.currentTarget);
8519
- },
8520
- onPointerDown: event => {
8521
- const native = event.nativeEvent;
8522
- if (disabled ||
8523
- (event.pointerType !== 'touch' && event.pointerType !== 'pen') ||
8524
- claimed === native ||
8525
- !inTrigger(event)) {
8526
- return;
8527
- }
8528
- claimed = native;
8529
- heldRef.current = false;
8530
- const target = event.target;
8531
- const doc = target.ownerDocument;
8532
- // Another pointer coming down anywhere makes it a pinch or a second
8533
- // touch, not a long press. In the capture phase, so it's heard before
8534
- // any trigger it lands on, which then leaves it. The press's own
8535
- // pointerdown has passed the document already, so it isn't heard.
8536
- const another = (other) => {
8537
- claimed = other;
8538
- cancel();
8539
- };
8540
- doc.addEventListener('pointerdown', another, true);
8541
- const press = {
8542
- pointerId: event.pointerId,
8543
- target,
8544
- x: event.clientX,
8545
- y: event.clientY,
8546
- opened: false,
8547
- stop: () => doc.removeEventListener('pointerdown', another, true),
8548
- };
8549
- press.timer = setTimeout(() => {
8550
- const view = press.target.ownerDocument.defaultView;
8551
- press.target.dispatchEvent(new view.MouseEvent('contextmenu', {
8552
- bubbles: true,
8553
- cancelable: true,
8554
- composed: true,
8555
- clientX: press.x,
8556
- clientY: press.y,
8557
- button: 2,
8558
- }));
8559
- }, LONG_PRESS_DELAY);
8560
- pressRef.current = press;
8561
- },
8562
- onPointerMove: event => {
8563
- const press = pressRef.current;
8564
- if (!press || !isPress(event) || press.opened)
8565
- return;
8566
- const moved = Math.hypot(event.clientX - press.x, event.clientY - press.y);
8567
- if (moved > LONG_PRESS_TOLERANCE)
8568
- cancel();
8569
- },
8570
- onPointerUp: event => {
8571
- if (isPress(event))
8572
- cancel();
8573
- },
8574
- onPointerCancel: event => {
8575
- if (isPress(event))
8576
- cancel();
8577
- },
8578
- onTouchEnd: event => {
8579
- if (!heldRef.current)
8580
- return;
8581
- heldRef.current = false;
8582
- event.preventDefault();
8583
- },
8584
- };
8585
- }
8586
-
8871
+ const PopoverBody = ({ className, ...props }) => {
8872
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8873
+ return (jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('popover-body'), bulmaHelperClasses, className), ...rest }));
8874
+ };
8587
8875
  /**
8588
- * Where the menu goes along one axis, from the point at `at`: after it when
8589
- * there's room, before it when there's room there instead, and otherwise
8590
- * moved in until it fits in `room`.
8876
+ * A row of actions along the bottom of the popover, aligned to the end.
8877
+ *
8878
+ * @function
8879
+ * @param {PopoverFooterProps} props - Props for the Popover.Footer component.
8880
+ * @returns {JSX.Element} The footer element.
8591
8881
  */
8592
- function placeAlong(at, size, room) {
8593
- if (at + size <= room)
8594
- return { start: at, flip: false };
8595
- if (at - size >= 0)
8596
- return { start: at, flip: true };
8597
- return { start: Math.max(0, room - size), flip: false };
8598
- }
8599
- /** Runs the trigger's own handler, then the context menu's. */
8600
- function chain(own, ours) {
8601
- return event => {
8602
- own?.(event);
8603
- ours(event);
8604
- };
8605
- }
8882
+ const PopoverFooter = ({ className, ...props }) => {
8883
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8884
+ return (jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('popover-footer'), bulmaHelperClasses, className), ...rest }));
8885
+ };
8606
8886
  /**
8607
- * The `ContextMenu` component opens a menu of actions where you right-click an element, such as a table row or a card, or long press it on a touch screen.
8887
+ * A `Button` that closes the popover, handing focus back to the trigger.
8608
8888
  *
8609
- * Shift+F10 and the context menu key open it too, where the browser fires
8610
- * `contextmenu` for them, which Windows and Linux do. The menu is Dropdown's
8611
- * markup, portaled into the `portalContainer` a `ConfigProvider` sets, or
8612
- * the end of the trigger's own document's `<body>`, so it never has server
8613
- * markup. It opens below and to the right of the point, flips above or to
8614
- * the left where there's no room, moves in where it fits neither way, and
8615
- * scrolls inside when it's taller than the viewport. It doesn't follow the
8616
- * trigger, since scrolling closes it. Inside a `Modal` or a shadow root,
8617
- * point `portalContainer` inside it, so screen readers that honor
8618
- * `aria-modal` reach the menu and the shadow root's styles apply.
8889
+ * @function
8890
+ * @param {PopoverCloseProps} props - Props for the Popover.Close component.
8891
+ * @returns {JSX.Element} The close button.
8892
+ */
8893
+ const PopoverClose = ({ onClick, ...props }) => {
8894
+ const context = React.useContext(PopoverContext);
8895
+ return (jsxRuntime.jsx(Button, { ...props, type: buttonType(props.type), onClick: (e) => {
8896
+ onClick?.(e);
8897
+ if (!e.defaultPrevented)
8898
+ context?.close();
8899
+ } }));
8900
+ };
8901
+ /** Popover with Header, Body, Footer and Close sub-components. */
8902
+ const Popover = withSubComponents(PopoverComponent, {
8903
+ Header: PopoverHeader,
8904
+ Body: PopoverBody,
8905
+ Footer: PopoverFooter,
8906
+ Close: PopoverClose,
8907
+ }, 'Popover');
8908
+
8909
+ /**
8910
+ * The `HoverCard` component shows a card previewing what's behind a link or a button while the pointer rests on it or keyboard focus is on it.
8619
8911
  *
8620
- * Opening moves focus to the first enabled item. The arrow keys, Home, End
8621
- * and typing move between items, and Enter and Space activate one. Escape,
8622
- * Tab, an item, a press outside, scrolling, resizing and the window losing
8623
- * focus close it, handing focus back to what had it unless an item moved it
8624
- * somewhere else. For a menu opened from a button use `Dropdown`.
8912
+ * Nothing moves focus when the card opens, and nothing traps it. In place,
8913
+ * the card comes right after the trigger, so Tab goes from the trigger into
8914
+ * its links and tabbing past them closes it. The pointer can move onto the
8915
+ * card, Escape closes it from anywhere, and by default touch doesn't open it,
8916
+ * so tapping the trigger just does what the trigger does. `openOnTap` opens
8917
+ * it with a tap, for a trigger with no tap action of its own. For hints with nothing to
8918
+ * click use `Tooltip`, and for content opened on purpose, such as a form,
8919
+ * use `Popover`.
8625
8920
  *
8626
8921
  * @function
8627
- * @param {ContextMenuProps} props - Props for the ContextMenu component.
8628
- * @returns {JSX.Element} The trigger, and the menu while open.
8922
+ * @param {HoverCardProps} props - Props for the HoverCard component.
8923
+ * @returns {JSX.Element} The trigger in its wrapper, and the card while open.
8629
8924
  *
8630
8925
  * @example
8631
- * <ContextMenu
8632
- * aria-label="Actions for report.pdf"
8633
- * trigger={<Box>report.pdf</Box>}
8634
- * >
8635
- * <ContextMenu.Item as="button">Rename</ContextMenu.Item>
8636
- * <ContextMenu.Divider />
8637
- * <ContextMenu.Item as="button">Delete</ContextMenu.Item>
8638
- * </ContextMenu>
8926
+ * <HoverCard trigger={<a href="/people/ada">Ada Lovelace</a>}>
8927
+ * <Title as="p" size="6">Ada Lovelace</Title>
8928
+ * <SubTitle as="p" size="6">Analytical Engine team</SubTitle>
8929
+ * </HoverCard>
8639
8930
  */
8640
- const ContextMenuComponent = ({ trigger, children, onOpenChange, closeOnClick = true, disabled = false, className, onKeyDown, onClick, onContextMenu, ...props }) => {
8931
+ const HoverCard = ({ trigger, children, open: controlledOpen, defaultOpen = false, onOpenChange, openDelay = 600, closeDelay = 300, position = 'bottom-left', appendToBody = false, openOnTap = false, className, contentClassName, ...props }) => {
8641
8932
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8642
- const { portalContainer } = useConfig();
8643
- const [menu, setMenu] = React.useState(null);
8644
- // Turning `disabled` on closes it.
8645
- if (disabled && menu)
8646
- setMenu(null);
8647
- const open = menu !== null;
8648
- const doc = menu?.doc;
8649
- const placed = menu?.placed;
8650
- const boxRef = React.useRef(null);
8651
- const menuRef = React.useRef(null);
8652
- const openerRef = React.useRef(null);
8653
- const reportedRef = React.useRef(false);
8654
- const menuKeys = useMenuKeys();
8655
- const named = Boolean(props['aria-label'] || props['aria-labelledby']);
8656
- const handlers = useContextMenuTrigger({
8657
- disabled,
8658
- onOpen: (point, triggerNode) => {
8659
- const ownerDoc = triggerNode.ownerDocument;
8660
- openerRef.current = getDeepestActiveElement(ownerDoc);
8661
- setMenu({ point, doc: ownerDoc });
8662
- },
8933
+ const { onPointerEnter, onPointerOver, onPointerLeave, onPointerDown, onPointerUp, onFocus, onBlur, ...attrs } = rest;
8934
+ const rootRef = React.useRef(null);
8935
+ const cardRef = React.useRef(null);
8936
+ const cardId = `${React.useId()}-card`;
8937
+ const isControlled = controlledOpen !== undefined;
8938
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
8939
+ const isOpen = controlledOpen ?? internalOpen;
8940
+ const requestOpen = React.useCallback((next) => {
8941
+ onOpenChange?.(next);
8942
+ if (!isControlled)
8943
+ setInternalOpen(next);
8944
+ }, [isControlled, onOpenChange]);
8945
+ // A portaled card has no server markup, so it only exists from the commit
8946
+ // after hydration, and nothing measures it before then.
8947
+ const hydrated = useIsHydrated();
8948
+ const cardMounted = isOpen && (hydrated || !appendToBody);
8949
+ const resolved = useAnchoredPosition(rootRef, cardRef, {
8950
+ active: cardMounted,
8951
+ position,
8952
+ fixed: appendToBody,
8953
+ // The gap comes from `--bulma-hover-card-offset` in the stylesheet, so
8954
+ // the coordinates leave it out and `auto` reads it off the card.
8955
+ offset: 0,
8956
+ styledGap: marginGap,
8663
8957
  });
8664
- // Measured once per opening, before it paints, against what a fixed box
8665
- // resolves against: the root element's client area, which leaves out a
8666
- // classic scrollbar. `useAnchoredPosition` and Tooltip read innerWidth and
8667
- // innerHeight, which count it. A menu pinned to the viewport's edge would
8668
- // sit under the scrollbar by that much, so this reads the narrower box.
8669
- useClientLayoutEffect(() => {
8670
- const menuNode = menuRef.current;
8671
- if (!menu || menu.placed || !menuNode)
8672
- return;
8673
- const root = menuNode.ownerDocument.documentElement;
8674
- const { width, height } = menuNode.getBoundingClientRect();
8675
- const x = placeAlong(menu.point.x, width, root.clientWidth);
8676
- const y = placeAlong(menu.point.y, height, root.clientHeight);
8677
- setMenu({
8678
- ...menu,
8679
- placed: { left: x.start, top: y.start, right: x.flip, up: y.flip },
8680
- });
8681
- }, [menu]);
8682
- const isPlaced = Boolean(placed);
8683
- // What closes it while it's open, and focus back on closing. Nothing
8684
- // listens while it's closed.
8685
- React.useEffect(() => {
8686
- const box = boxRef.current;
8687
- const menuNode = menuRef.current;
8688
- if (!doc || !box || !menuNode)
8689
- return undefined;
8690
- const view = doc.defaultView;
8691
- // What had focus when it opened. This runs once per opening, so opening
8692
- // again at a new point while it's open keeps it.
8693
- const opener = openerRef.current;
8694
- const close = () => setMenu(null);
8695
- const closeOutside = (event) => {
8696
- if (!isEventInside(event, box))
8697
- close();
8698
- };
8699
- doc.addEventListener('pointerdown', closeOutside, true);
8700
- view.addEventListener('resize', close);
8701
- view.addEventListener('blur', close);
8702
- return () => {
8703
- doc.removeEventListener('pointerdown', closeOutside, true);
8704
- view.removeEventListener('resize', close);
8705
- view.removeEventListener('blur', close);
8706
- // Back to what had focus while the menu still has it, or it dropped to
8707
- // <body> when the menu left. An item that put it somewhere else, such
8708
- // as a Modal or a rename field, keeps it there.
8709
- const now = getDeepestActiveElement(doc);
8710
- if (!now || now === doc.body || menuNode.contains(now))
8711
- opener?.focus();
8712
- };
8713
- }, [doc]);
8714
- // Focus in, and closing on scroll, once it's placed. Placed, it's inside
8715
- // the viewport, so its first item has nothing to scroll into view, and
8716
- // `preventScroll` makes sure: a scroll the focus caused would close it as
8717
- // it opened. Opening again at a new point places it again, and leaves
8718
- // focus where it is in the menu.
8719
- React.useEffect(() => {
8720
- const box = boxRef.current;
8721
- const menuNode = menuRef.current;
8722
- if (!doc || !isPlaced || !box || !menuNode)
8723
- return undefined;
8724
- if (!menuNode.contains(getDeepestActiveElement(doc))) {
8725
- menuKeys.reset();
8726
- (enabledMenuItems(menuNode)[0] ?? menuNode).focus({
8727
- preventScroll: true,
8728
- });
8729
- }
8730
- const closeOutside = (event) => {
8731
- if (!isEventInside(event, box))
8732
- setMenu(null);
8733
- };
8734
- doc.addEventListener('scroll', closeOutside, true);
8735
- return () => doc.removeEventListener('scroll', closeOutside, true);
8736
- }, [doc, isPlaced, menuKeys]);
8737
- // Before paint, so what the caller does with it, such as highlighting the
8738
- // row, shows with the menu rather than a frame behind it.
8739
- useClientLayoutEffect(() => {
8740
- if (open === reportedRef.current)
8741
- return;
8742
- reportedRef.current = open;
8743
- onOpenChange?.(open);
8958
+ // The trigger renders first inside the wrapper. Read when needed rather
8959
+ // than held, so a trigger that remounted is still the one found.
8960
+ const triggerRef = React.useMemo(() => ({
8961
+ get current() {
8962
+ const first = rootRef.current.firstElementChild;
8963
+ return first && first !== cardRef.current
8964
+ ? first
8965
+ : null;
8966
+ },
8967
+ }), []);
8968
+ const hover = useHoverOpen({
8969
+ open: isOpen,
8970
+ onOpenChange: requestOpen,
8971
+ openDelay,
8972
+ closeDelay,
8973
+ triggerRef,
8974
+ floatingRef: cardRef,
8975
+ openOnTap,
8744
8976
  });
8977
+ // A trigger that can't take focus leaves keyboard users no way to open the
8978
+ // card. It has to be the trigger element itself, not something inside it,
8979
+ // since focus comes back to it and its ARIA is on it. Judged by what the
8980
+ // element is rather than by its styles, which can hide it for now. After
8981
+ // every commit, since the trigger can change, and only in development.
8745
8982
  React.useEffect(() => {
8746
- if (open && !named) {
8747
- warnOnce('ContextMenu:unnamed', '[bestax-bulma] <ContextMenu> opened a menu with no accessible ' +
8748
- 'name. Pass aria-label or aria-labelledby.');
8749
- }
8750
- }, [open, named]);
8751
- const handleKeyDown = (event) => {
8752
- onKeyDown?.(event);
8753
- // The menu has the keyboard while focus is in it. React brings its keys
8754
- // up the tree to wherever the ContextMenu sits, where a table that moves
8755
- // between rows on the arrow keys, or a Popover or a Modal waiting for
8756
- // Escape, would take them too.
8757
- event.stopPropagation();
8758
- if (event.key === 'Escape' || event.key === 'Tab') {
8759
- // Tab would go on from the end of <body>, past the rest of the page.
8760
- event.preventDefault();
8761
- setMenu(null);
8983
+ if (!isDev())
8762
8984
  return;
8763
- }
8764
- menuKeys.onKeyDown(event);
8765
- };
8766
- const handleClick = (event) => {
8767
- onClick?.(event);
8768
- if (!closeOnClick)
8985
+ const triggerNode = triggerRef.current;
8986
+ if (!triggerNode)
8769
8987
  return;
8770
- const item = event.target.closest(MENU_ITEM_SELECTOR);
8771
- if (item && isEnabledMenuItem(item))
8772
- setMenu(null);
8773
- };
8774
- const handleContextMenu = (event) => {
8775
- onContextMenu?.(event);
8776
- // No other menu over this one: not the browser's, and not a trigger's
8777
- // that React brings the event up to.
8778
- event.preventDefault();
8779
- event.stopPropagation();
8780
- };
8781
- const own = trigger.props;
8782
- const triggerClass = usePrefixedClassNames('context-menu');
8783
- const clonedTrigger = React.cloneElement(trigger, {
8784
- className: classNames(own.className, !disabled && triggerClass) || undefined,
8785
- onContextMenu: chain(own.onContextMenu, handlers.onContextMenu),
8786
- onPointerDown: chain(own.onPointerDown, handlers.onPointerDown),
8787
- onPointerMove: chain(own.onPointerMove, handlers.onPointerMove),
8788
- onPointerUp: chain(own.onPointerUp, handlers.onPointerUp),
8789
- onPointerCancel: chain(own.onPointerCancel, handlers.onPointerCancel),
8790
- onTouchEnd: chain(own.onTouchEnd, handlers.onTouchEnd),
8988
+ if (!triggerNode.matches(':disabled') && isTabbableKind(triggerNode)) {
8989
+ return;
8990
+ }
8991
+ warnOnce('HoverCard:unfocusable-trigger', "[bestax-bulma] <HoverCard> has a trigger that can't take focus, so " +
8992
+ "keyboard users can't open the card. Use a link or a button as the " +
8993
+ 'trigger, or give it tabIndex={0}.');
8791
8994
  });
8792
- const boxClasses = usePrefixedClassNames('context-menu-content', 'dropdown', {
8793
- 'is-active': true,
8794
- 'is-up': placed?.up,
8795
- 'is-right': placed?.right,
8995
+ // Expanded only while the card is on the page, so a portaled card that
8996
+ // waits for hydration isn't announced before it exists.
8997
+ const clonedTrigger = React.cloneElement(trigger, {
8998
+ 'aria-expanded': cardMounted,
8999
+ 'aria-controls': cardMounted ? cardId : undefined,
8796
9000
  });
8797
- const menuClasses = classNames(usePrefixedClassNames('dropdown-menu'), bulmaHelperClasses, className);
8798
- const contentClasses = usePrefixedClassNames('dropdown-content');
8799
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [clonedTrigger, menu && (jsxRuntime.jsx(Portal, { container: portalContainer || menu.doc.body, children: jsxRuntime.jsx("div", { ref: boxRef, className: boxClasses, style: {
8800
- top: placed?.top ?? menu.point.y,
8801
- left: placed?.left ?? menu.point.x,
8802
- }, children: jsxRuntime.jsx("div", { ...rest, ref: menuRef, role: "menu", tabIndex: -1, className: menuClasses, onKeyDown: handleKeyDown, onClick: handleClick, onContextMenu: handleContextMenu, children: jsxRuntime.jsx("div", { className: contentClasses, children: children }) }) }) }))] }));
9001
+ const rootClasses = classNames(usePrefixedClassNames('hover-card', { 'is-active': cardMounted }), bulmaHelperClasses, className);
9002
+ const cardClasses = classNames(usePrefixedClassNames('hover-card-content', {
9003
+ [`is-${resolved.position}`]: true,
9004
+ 'is-portal': appendToBody,
9005
+ }), contentClassName);
9006
+ return (jsxRuntime.jsxs("span", { className: rootClasses, ...attrs, onPointerEnter: chain$1(onPointerEnter, hover.onPointerEnter), onPointerOver: chain$1(onPointerOver, hover.onPointerOver), onPointerLeave: chain$1(onPointerLeave, hover.onPointerLeave), onFocus: chain$1(onFocus, hover.onFocus), onBlur: chain$1(onBlur, hover.onBlur), onPointerDown: chain$1(onPointerDown, hover.onPointerDown), onPointerUp: chain$1(onPointerUp, hover.onPointerUp), ref: rootRef, children: [clonedTrigger, isOpen && (jsxRuntime.jsx(Portal, { disabled: !appendToBody, children: jsxRuntime.jsx("div", { ref: cardRef, id: cardId, className: cardClasses, style: appendToBody
9007
+ ? {
9008
+ position: 'fixed',
9009
+ top: resolved.top,
9010
+ left: resolved.left,
9011
+ }
9012
+ : undefined, children: children }) }))] }));
8803
9013
  };
9014
+
9015
+ // INTERNAL: deliberately not exported from src/index.ts.
9016
+ /** How long a touch or a pen has to stay down to open the menu, in milliseconds. */
9017
+ const LONG_PRESS_DELAY = 500;
9018
+ /** How far a long press can move before it counts as something else, in CSS pixels. */
9019
+ const LONG_PRESS_TOLERANCE = 10;
8804
9020
  /**
8805
- * ContextMenu with Item and Divider sub-components, which are `Dropdown.Item`
8806
- * and `Dropdown.Divider`, so items written for a Dropdown work here
8807
- * unchanged.
9021
+ * The last press no trigger is to start a long press for: one a trigger
9022
+ * took, or another pointer coming down while a press is held. A press
9023
+ * reaches every trigger it bubbles through, the innermost first and all
9024
+ * before the next press, so this is how a trigger around another one knows
9025
+ * the inner one took it, and how a trigger a second finger lands on knows to
9026
+ * leave it.
8808
9027
  */
8809
- const ContextMenu = withSubComponents(ContextMenuComponent, {
8810
- Item: DropdownItem,
8811
- Divider: DropdownDivider,
8812
- }, 'ContextMenu');
8813
-
9028
+ let claimed = null;
9029
+ /** Whether the event happened in the element its handler is on, rather than in content portaled out of it. */
9030
+ const inTrigger = (event) => event.currentTarget.contains(event.target);
8814
9031
  /**
8815
- * Individual Step component for use inside Steps.
8816
- *
8817
- * @function
8818
- * @param {StepProps} props - Props for the Step component.
8819
- * @returns {JSX.Element} The rendered step segment.
9032
+ * Where a `contextmenu` asks to open: at its point when that is on the
9033
+ * element it was fired at, which it is for the pointer, and at that
9034
+ * element's bottom-left corner otherwise, which is where the browser puts
9035
+ * its own menu for a key.
8820
9036
  */
8821
- const Step = ({ isActive = false, isCompleted = false, label, icon, clickable = false, onClick, stepNumber, completedIcon, className, children, ...props }) => {
8822
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8823
- const formatNumber = useScopedNumberFormat();
8824
- const stepClasses = classNames(usePrefixedClassNames('steps-segment', {
8825
- 'is-active': isActive,
8826
- 'is-completed': isCompleted,
8827
- }), bulmaHelperClasses, className);
8828
- const stepsLinkClass = usePrefixedClassNames('steps-link');
8829
- const stepsMarkerClass = usePrefixedClassNames('steps-marker');
8830
- const stepsContentClass = usePrefixedClassNames('steps-content');
8831
- const stepsTitleClass = usePrefixedClassNames('steps-title');
8832
- const resolvedCompletedIcon = completedIcon === undefined ? '\u2713' : completedIcon;
8833
- const markerContent = icon ||
8834
- (isCompleted && resolvedCompletedIcon
8835
- ? resolvedCompletedIcon
8836
- : stepNumber == null || !formatNumber
8837
- ? (stepNumber ?? null)
8838
- : formatNumber(stepNumber));
8839
- const title = label || children;
8840
- const current = isActive ? 'step' : undefined;
8841
- // The step stays a list item, and a real button inside it does the
8842
- // clicking, so the list keeps its items and the button brings Enter, Space
8843
- // and focus with it. A button holds only phrasing content, so its parts
8844
- // are spans.
8845
- if (clickable) {
8846
- return (jsxRuntime.jsx("li", { className: stepClasses, ...rest, children: jsxRuntime.jsxs("button", { type: "button", className: stepsLinkClass, onClick: () => onClick?.(), "aria-current": current, children: [jsxRuntime.jsx("span", { className: stepsMarkerClass, children: markerContent }), title && (jsxRuntime.jsx("span", { className: stepsContentClass, children: jsxRuntime.jsx("span", { className: stepsTitleClass, children: title }) }))] }) }));
8847
- }
8848
- return (jsxRuntime.jsx("li", { className: stepClasses, "aria-current": current, ...rest, children: jsxRuntime.jsxs("div", { className: stepsLinkClass, children: [jsxRuntime.jsx("span", { className: stepsMarkerClass, children: markerContent }), title && (jsxRuntime.jsx("div", { className: stepsContentClass, children: jsxRuntime.jsx("p", { className: stepsTitleClass, children: title }) }))] }) }));
8849
- };
9037
+ function contextMenuPoint(event) {
9038
+ const { clientX: x, clientY: y } = event;
9039
+ const rect = event.target.getBoundingClientRect();
9040
+ return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
9041
+ ? { x, y }
9042
+ : { x: rect.left, y: rect.bottom };
9043
+ }
8850
9044
  /**
8851
- * The `Steps` component provides a multi-step progress indicator for wizard flows, checkout processes, or any multi-step workflow.
9045
+ * Decides when a context menu asks to open, and where. It renders nothing,
9046
+ * and knows nothing about ARIA or the menu.
8852
9047
  *
8853
- * @function
8854
- * @param {StepsProps} props - Props for the Steps component.
8855
- * @returns {JSX.Element} The rendered steps component.
9048
+ * - **`contextmenu`:** a right-click, and Shift+F10 or the context menu key
9049
+ * where the browser fires the event for them, asks to open at the point
9050
+ * `contextMenuPoint` works out, and prevents the browser's own menu. Each
9051
+ * one asks again, so a right-click elsewhere on the trigger moves it.
9052
+ * - **Long press:** a touch or a pen held still on the trigger for
9053
+ * `LONG_PRESS_DELAY` fires a `contextmenu` at what it landed on, so it
9054
+ * takes the same path, handlers on the way included. Moving more than
9055
+ * `LONG_PRESS_TOLERANCE`, lifting, another pointer coming down anywhere in
9056
+ * the document, or `pointercancel`, which is the browser taking the
9057
+ * gesture for a scroll or a zoom, cancels it, and a mouse never starts
9058
+ * one. A pointer that comes down while another is held starts no long
9059
+ * press of its own, on this trigger or another, so two fingers on two rows
9060
+ * open nothing. While the finger is down, and only then, a listener on the
9061
+ * document hears other pointers coming down. Where the
9062
+ * browser fires `contextmenu` for a long press itself, the first of the two
9063
+ * opens it, and the end of a touch that opened it is prevented, so lifting
9064
+ * doesn't click what is under the finger.
9065
+ * - **Who opens:** a handler that calls `preventDefault()` on the
9066
+ * `contextmenu` before this one keeps it from asking, which is how a
9067
+ * trigger's own handler opts out and how the innermost of nested triggers
9068
+ * wins. A long press goes to the innermost trigger that takes it. Events
9069
+ * from content portaled out of the trigger, which React brings here
9070
+ * anyway, are ignored.
9071
+ * - **`disabled`:** nothing asks, and the browser's menu shows.
8856
9072
  *
8857
- * @example
8858
- * // Basic steps with items
8859
- * <Steps
8860
- * value={1}
8861
- * items={[
8862
- * { label: 'Account' },
8863
- * { label: 'Profile' },
8864
- * { label: 'Complete' },
8865
- * ]}
8866
- * />
9073
+ * Nothing it scheduled runs after the component unmounts.
8867
9074
  *
8868
- * @example
8869
- * // Clickable steps with navigation
8870
- * <Steps
8871
- * value={currentStep}
8872
- * onStepClick={(step) => setCurrentStep(step)}
8873
- * hasNavigation
8874
- * items={[
8875
- * { label: 'Step 1', clickable: true },
8876
- * { label: 'Step 2', clickable: true },
8877
- * { label: 'Step 3', clickable: true },
8878
- * ]}
8879
- * />
9075
+ * @function useContextMenuTrigger
9076
+ * @param options - How to ask to open, and whether to.
9077
+ * @returns Handlers for the trigger element.
8880
9078
  */
8881
- const StepsComponent = ({ value = 0, items, size, color, hasMarker = true, animated = true, rounded = true, vertical = false, labelPosition = 'bottom', mobileMode, showStepNumbers = true, hasNavigation = false, prevLabel, nextLabel, onPrev, onNext, onStepClick, className, children, ...props }) => {
8882
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8883
- // Generate classes for the wrapper div
8884
- const stepsClasses = usePrefixedClassNames('steps', {
8885
- [`is-${size}`]: size,
8886
- [`is-${color}`]: color,
8887
- 'has-marker': hasMarker,
8888
- 'is-animated': animated,
8889
- 'is-rounded': rounded,
8890
- 'is-vertical': vertical,
8891
- [`has-label-${labelPosition}`]: labelPosition && (labelPosition !== 'bottom' || vertical),
8892
- [`is-${mobileMode}`]: mobileMode,
8893
- });
8894
- const combinedClasses = classNames(stepsClasses, bulmaHelperClasses, className);
8895
- // Generate classes for the ul (list)
8896
- const listClasses = usePrefixedClassNames('steps-list', {
8897
- 'is-vertical': vertical,
8898
- });
8899
- const stepsNavigationClass = usePrefixedClassNames('steps-navigation');
8900
- const prevButtonClass = usePrefixedClassNames('button');
8901
- const nextButtonClass = usePrefixedClassNames('button', {
8902
- 'is-primary': true,
8903
- });
8904
- // Count total steps for navigation
8905
- const totalSteps = items ? items.length : React.Children.count(children);
8906
- // Render items if provided
8907
- const renderSteps = () => {
8908
- if (items && items.length > 0) {
8909
- return items.map((item, index) => (jsxRuntime.jsx(Step, { isActive: index === value, isCompleted: index < value, label: item.label, icon: item.icon, completedIcon: item.completedIcon, clickable: item.clickable, stepNumber: showStepNumbers ? index + 1 : undefined, onClick: item.clickable && onStepClick ? () => onStepClick(index) : undefined, className: item.className }, index)));
8910
- }
8911
- // If children are provided, clone them with active/completed states
8912
- if (children) {
8913
- return React.Children.map(children, (child, index) => {
8914
- if (React.isValidElement(child)) {
8915
- return React.cloneElement(child, {
8916
- isActive: index === value,
8917
- isCompleted: index < value,
8918
- stepNumber: showStepNumbers ? index + 1 : undefined,
8919
- onClick: child.props.clickable && onStepClick
8920
- ? () => onStepClick(index)
8921
- : child.props.onClick,
8922
- });
8923
- }
8924
- return child;
8925
- });
8926
- }
8927
- return null;
9079
+ function useContextMenuTrigger({ onOpen, disabled, }) {
9080
+ const pressRef = React.useRef(null);
9081
+ /** A long press opened the menu, and the touch that made it hasn't ended. */
9082
+ const heldRef = React.useRef(false);
9083
+ const cancel = () => {
9084
+ clearTimeout(pressRef.current?.timer);
9085
+ pressRef.current?.stop();
9086
+ pressRef.current = null;
9087
+ };
9088
+ React.useEffect(() => () => {
9089
+ clearTimeout(pressRef.current?.timer);
9090
+ pressRef.current?.stop();
9091
+ pressRef.current = null;
9092
+ }, []);
9093
+ const isPress = (event) => pressRef.current?.pointerId === event.pointerId;
9094
+ return {
9095
+ onContextMenu: event => {
9096
+ if (disabled || event.defaultPrevented || !inTrigger(event))
9097
+ return;
9098
+ event.preventDefault();
9099
+ const press = pressRef.current;
9100
+ if (press) {
9101
+ if (press.opened)
9102
+ return;
9103
+ clearTimeout(press.timer);
9104
+ press.opened = true;
9105
+ heldRef.current = true;
9106
+ }
9107
+ onOpen(contextMenuPoint(event), event.currentTarget);
9108
+ },
9109
+ onPointerDown: event => {
9110
+ const native = event.nativeEvent;
9111
+ if (disabled ||
9112
+ (event.pointerType !== 'touch' && event.pointerType !== 'pen') ||
9113
+ claimed === native ||
9114
+ !inTrigger(event)) {
9115
+ return;
9116
+ }
9117
+ claimed = native;
9118
+ heldRef.current = false;
9119
+ const target = event.target;
9120
+ const doc = target.ownerDocument;
9121
+ // Another pointer coming down anywhere makes it a pinch or a second
9122
+ // touch, not a long press. In the capture phase, so it's heard before
9123
+ // any trigger it lands on, which then leaves it. The press's own
9124
+ // pointerdown has passed the document already, so it isn't heard.
9125
+ const another = (other) => {
9126
+ claimed = other;
9127
+ cancel();
9128
+ };
9129
+ doc.addEventListener('pointerdown', another, true);
9130
+ const press = {
9131
+ pointerId: event.pointerId,
9132
+ target,
9133
+ x: event.clientX,
9134
+ y: event.clientY,
9135
+ opened: false,
9136
+ stop: () => doc.removeEventListener('pointerdown', another, true),
9137
+ };
9138
+ press.timer = setTimeout(() => {
9139
+ const view = press.target.ownerDocument.defaultView;
9140
+ press.target.dispatchEvent(new view.MouseEvent('contextmenu', {
9141
+ bubbles: true,
9142
+ cancelable: true,
9143
+ composed: true,
9144
+ clientX: press.x,
9145
+ clientY: press.y,
9146
+ button: 2,
9147
+ }));
9148
+ }, LONG_PRESS_DELAY);
9149
+ pressRef.current = press;
9150
+ },
9151
+ onPointerMove: event => {
9152
+ const press = pressRef.current;
9153
+ if (!press || !isPress(event) || press.opened)
9154
+ return;
9155
+ const moved = Math.hypot(event.clientX - press.x, event.clientY - press.y);
9156
+ if (moved > LONG_PRESS_TOLERANCE)
9157
+ cancel();
9158
+ },
9159
+ onPointerUp: event => {
9160
+ if (isPress(event))
9161
+ cancel();
9162
+ },
9163
+ onPointerCancel: event => {
9164
+ if (isPress(event))
9165
+ cancel();
9166
+ },
9167
+ onTouchEnd: event => {
9168
+ if (!heldRef.current)
9169
+ return;
9170
+ heldRef.current = false;
9171
+ event.preventDefault();
9172
+ },
8928
9173
  };
8929
- // role="list" restates the <ul>'s own role, for Safari: WebKit stops
8930
- // exposing a list as one once its markers are hidden, as they are here,
8931
- // unless the role is set explicitly.
8932
- return (jsxRuntime.jsxs("div", { className: combinedClasses, ...rest, children: [jsxRuntime.jsx("ul", { className: listClasses, role: "list", children: renderSteps() }), hasNavigation && (jsxRuntime.jsxs("div", { className: stepsNavigationClass, children: [jsxRuntime.jsx("button", { type: "button", className: prevButtonClass, disabled: value === 0, onClick: onPrev ?? (() => onStepClick?.(value - 1)), children: prevLabel ?? 'Previous' }), jsxRuntime.jsx("button", { type: "button", className: nextButtonClass, disabled: value === totalSteps - 1, onClick: onNext ?? (() => onStepClick?.(value + 1)), children: nextLabel ?? 'Next' })] }))] }));
8933
- };
8934
- // Attach Step as static property
8935
- const Steps = withSubComponents(StepsComponent, { Step }, 'Steps');
9174
+ }
8936
9175
 
8937
9176
  /**
8938
- * How long after an item appears its announcement is written, in ms. The wait
8939
- * lets a screen reader register a region the container has only just mounted,
8940
- * and items shown in quick succession are written, and read out, together.
8941
- */
8942
- const announceDelay = 100;
8943
- /**
8944
- * How long an announcement stays in the region once written, in ms. That's
8945
- * long enough for a screen reader to pick it up. Clearing it after keeps a
8946
- * second copy of the item's text from sitting in the page, where someone
8947
- * reading through the page, or a test looking the text up, would find it
8948
- * again.
9177
+ * Where the menu goes along one axis, from the point at `at`: after it when
9178
+ * there's room, before it when there's room there instead, and otherwise
9179
+ * moved in until it fits in `room`.
8949
9180
  */
8950
- const announcementLifetime = 1000;
8951
- const noAnnouncements$1 = [];
8952
- const nothingWritten = [];
8953
- const noAnnouncementsYet = {
8954
- key: 0,
8955
- waiting: noAnnouncements$1,
8956
- written: nothingWritten,
8957
- };
8958
- // Hides the region with inline styles rather than a class, so it needs no
8959
- // stylesheet, including the helper classes a modular Bulma build can leave
8960
- // out. Clipping it, rather than using `display` or `visibility`, keeps it in
8961
- // the accessibility tree. The declarations match the `extras-sr-only` mixin,
8962
- // plus `clip-path`, which replaces the deprecated `clip`. The negative margin
8963
- // pulls the 1px box back inside the page, so it doesn't add scrollable
8964
- // overflow. Exported for other live regions that must work without a
8965
- // stylesheet, such as a range calendar's.
8966
- const visuallyHidden = {
8967
- position: 'absolute',
8968
- width: '1px',
8969
- height: '1px',
8970
- padding: 0,
8971
- margin: '-1px',
8972
- overflow: 'hidden',
8973
- clip: 'rect(0, 0, 0, 0)',
8974
- clipPath: 'inset(50%)',
8975
- whiteSpace: 'nowrap',
8976
- border: 0,
8977
- };
9181
+ function placeAlong(at, size, room) {
9182
+ if (at + size <= room)
9183
+ return { start: at, flip: false };
9184
+ if (at - size >= 0)
9185
+ return { start: at, flip: true };
9186
+ return { start: Math.max(0, room - size), flip: false };
9187
+ }
9188
+ /** Runs the trigger's own handler, then the context menu's. */
9189
+ function chain(own, ours) {
9190
+ return event => {
9191
+ own?.(event);
9192
+ ours(event);
9193
+ };
9194
+ }
8978
9195
  /**
8979
- * Works out what a status region says. Each item is announced once each time
8980
- * it appears, so one that goes and comes back is announced again. Its
8981
- * announcement is written `announceDelay` after it appears, or after the last
8982
- * item to join it when several appear in quick succession, alongside whatever
8983
- * the region still says. Once written, it stays for `announcementLifetime`,
8984
- * whatever is written after it and whether or not its item goes in the
8985
- * meantime. An item that goes before its announcement is written isn't
8986
- * announced.
9196
+ * The `ContextMenu` component opens a menu of actions where you right-click an element, such as a table row or a card, or long press it on a touch screen.
8987
9197
  *
8988
- * Nothing is written in the commit that mounts the region or the item. A
8989
- * screen reader reliably announces a polite live region whose content changes
8990
- * after the region is already in the page, and not always one that arrives
8991
- * with its content. By the time `describe` runs, an item's rendered text can
8992
- * be read.
9198
+ * Shift+F10 and the context menu key open it too, where the browser fires
9199
+ * `contextmenu` for them, which Windows and Linux do. The menu is Dropdown's
9200
+ * markup, portaled into the `portalContainer` a `ConfigProvider` sets, or
9201
+ * the end of the trigger's own document's `<body>`, so it never has server
9202
+ * markup. It opens below and to the right of the point, flips above or to
9203
+ * the left where there's no room, moves in where it fits neither way, and
9204
+ * scrolls inside when it's taller than the viewport. It doesn't follow the
9205
+ * trigger, since scrolling closes it. Inside a `Modal` or a shadow root,
9206
+ * point `portalContainer` inside it, so screen readers that honor
9207
+ * `aria-modal` reach the menu and the shadow root's styles apply.
8993
9208
  *
8994
- * @function useAnnouncements
8995
- * @param items - The items to announce, in the order they were shown.
8996
- * @param describe - The text to announce for an item, or nothing to skip it.
8997
- * It runs once the item is on screen, and it must keep its identity between
8998
- * renders (a module-level function or a `useCallback`), or every render
8999
- * re-checks the items.
9000
- * @returns The announcements to render, in the order they were written.
9209
+ * Opening moves focus to the first enabled item. The arrow keys, Home, End
9210
+ * and typing move between items, and Enter and Space activate one. Escape,
9211
+ * Tab, an item, a press outside, scrolling, resizing and the window losing
9212
+ * focus close it, handing focus back to what had it unless an item moved it
9213
+ * somewhere else. For a menu opened from a button use `Dropdown`.
9214
+ *
9215
+ * @function
9216
+ * @param {ContextMenuProps} props - Props for the ContextMenu component.
9217
+ * @returns {JSX.Element} The trigger, and the menu while open.
9218
+ *
9219
+ * @example
9220
+ * <ContextMenu
9221
+ * aria-label="Actions for report.pdf"
9222
+ * trigger={<Box>report.pdf</Box>}
9223
+ * >
9224
+ * <ContextMenu.Item as="button">Rename</ContextMenu.Item>
9225
+ * <ContextMenu.Divider />
9226
+ * <ContextMenu.Item as="button">Delete</ContextMenu.Item>
9227
+ * </ContextMenu>
9001
9228
  */
9002
- function useAnnouncements(items, describe) {
9003
- // The ids of the items on screen as of the last change, every one of which
9004
- // has been looked at for an announcement already.
9005
- const onScreenIdsRef = React.useRef(new Set());
9006
- // Clears that haven't run yet. They outlive the batch that started them, so
9007
- // they're only stopped when the region unmounts.
9008
- const clearTimersRef = React.useRef(new Set());
9009
- const [announcements, setAnnouncements] = React.useState(noAnnouncementsYet);
9010
- // Runs after the commit that puts an item on screen, which is the first
9011
- // point its rendered text can be read.
9012
- React.useEffect(() => {
9013
- const announced = onScreenIdsRef.current;
9014
- const appeared = items.filter(item => !announced.has(item.id));
9015
- // Items that have gone drop out here too, so one that comes back has
9016
- // appeared again.
9017
- onScreenIdsRef.current = new Set(items.map(item => item.id));
9018
- const next = [];
9019
- for (const item of appeared) {
9020
- const text = describe(item);
9021
- if (text)
9022
- next.push({ id: item.id, text });
9023
- }
9024
- if (next.length > 0) {
9025
- // eslint-disable-next-line react-hooks/set-state-in-effect -- an item's rendered text can only be read once it's on screen
9026
- setAnnouncements(current => ({
9027
- ...current,
9028
- key: current.key + 1,
9029
- // Announcements still waiting go out with the new ones, apart from
9030
- // one whose item came back before it was written, which the new one
9031
- // replaces. What's already written stays put.
9032
- waiting: [
9033
- ...current.waiting.filter(waiting => !next.some(({ id }) => id === waiting.id)),
9034
- ...next,
9035
- ],
9036
- }));
9037
- }
9038
- }, [items, describe]);
9039
- // The write only touches the batch it was started for: a newer batch can
9040
- // be set before the render that cancels this timer. Its clear is started
9041
- // by the write itself, so it's timed from the write without waiting on a
9042
- // render, and a newer batch arriving doesn't cut short what's written.
9043
- const { key } = announcements;
9229
+ const ContextMenuComponent = ({ trigger, children, onOpenChange, closeOnClick = true, disabled = false, className, onKeyDown, onClick, onContextMenu, ...props }) => {
9230
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
9231
+ const { portalContainer } = useConfig();
9232
+ const [menu, setMenu] = React.useState(null);
9233
+ // Turning `disabled` on closes it.
9234
+ if (disabled && menu)
9235
+ setMenu(null);
9236
+ const open = menu !== null;
9237
+ const doc = menu?.doc;
9238
+ const placed = menu?.placed;
9239
+ const boxRef = React.useRef(null);
9240
+ const menuRef = React.useRef(null);
9241
+ const openerRef = React.useRef(null);
9242
+ const reportedRef = React.useRef(false);
9243
+ const menuKeys = useMenuKeys();
9244
+ const named = Boolean(props['aria-label'] || props['aria-labelledby']);
9245
+ const handlers = useContextMenuTrigger({
9246
+ disabled,
9247
+ onOpen: (point, triggerNode) => {
9248
+ const ownerDoc = triggerNode.ownerDocument;
9249
+ openerRef.current = getDeepestActiveElement(ownerDoc);
9250
+ setMenu({ point, doc: ownerDoc });
9251
+ },
9252
+ });
9253
+ // Measured once per opening, before it paints, against what a fixed box
9254
+ // resolves against: the root element's client area, which leaves out a
9255
+ // classic scrollbar. `useAnchoredPosition` and Tooltip read innerWidth and
9256
+ // innerHeight, which count it. A menu pinned to the viewport's edge would
9257
+ // sit under the scrollbar by that much, so this reads the narrower box.
9258
+ useClientLayoutEffect(() => {
9259
+ const menuNode = menuRef.current;
9260
+ if (!menu || menu.placed || !menuNode)
9261
+ return;
9262
+ const root = menuNode.ownerDocument.documentElement;
9263
+ const { width, height } = menuNode.getBoundingClientRect();
9264
+ const x = placeAlong(menu.point.x, width, root.clientWidth);
9265
+ const y = placeAlong(menu.point.y, height, root.clientHeight);
9266
+ setMenu({
9267
+ ...menu,
9268
+ placed: { left: x.start, top: y.start, right: x.flip, up: y.flip },
9269
+ });
9270
+ }, [menu]);
9271
+ const isPlaced = Boolean(placed);
9272
+ // What closes it while it's open, and focus back on closing. Nothing
9273
+ // listens while it's closed.
9044
9274
  React.useEffect(() => {
9045
- if (key === noAnnouncementsYet.key)
9275
+ const box = boxRef.current;
9276
+ const menuNode = menuRef.current;
9277
+ if (!doc || !box || !menuNode)
9046
9278
  return undefined;
9047
- const clearTimers = clearTimersRef.current;
9048
- const write = setTimeout(() => {
9049
- // Whether an item is on screen is decided here, once. An item that
9050
- // went before its announcement was due isn't announced, and one that
9051
- // goes after keeps its announcement for the rest of its lifetime, when
9052
- // the region holds the only copy of its text rather than a second one.
9053
- const onScreen = onScreenIdsRef.current;
9054
- setAnnouncements(current => current.key === key
9055
- ? {
9056
- ...current,
9057
- waiting: noAnnouncements$1,
9058
- written: [
9059
- ...current.written,
9060
- ...current.waiting
9061
- .filter(announcement => onScreen.has(announcement.id))
9062
- .map(announcement => ({ ...announcement, batch: key })),
9063
- ],
9064
- }
9065
- : current);
9066
- // 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
9067
- const clear = setTimeout(() => {
9068
- clearTimers.delete(clear);
9069
- setAnnouncements(current => {
9070
- const written = current.written.filter(announcement => announcement.batch !== key);
9071
- return written.length === current.written.length
9072
- ? current
9073
- : { ...current, written };
9074
- });
9075
- }, announcementLifetime);
9076
- clearTimers.add(clear);
9077
- }, announceDelay);
9078
- return () => clearTimeout(write);
9079
- }, [key]);
9080
- React.useEffect(() => {
9081
- const clearTimers = clearTimersRef.current;
9279
+ const view = doc.defaultView;
9280
+ // What had focus when it opened. This runs once per opening, so opening
9281
+ // again at a new point while it's open keeps it.
9282
+ const opener = openerRef.current;
9283
+ const close = () => setMenu(null);
9284
+ const closeOutside = (event) => {
9285
+ if (!isEventInside(event, box))
9286
+ close();
9287
+ };
9288
+ doc.addEventListener('pointerdown', closeOutside, true);
9289
+ view.addEventListener('resize', close);
9290
+ view.addEventListener('blur', close);
9082
9291
  return () => {
9083
- clearTimers.forEach(clearTimeout);
9084
- clearTimers.clear();
9292
+ doc.removeEventListener('pointerdown', closeOutside, true);
9293
+ view.removeEventListener('resize', close);
9294
+ view.removeEventListener('blur', close);
9295
+ // Back to what had focus while the menu still has it, or it dropped to
9296
+ // <body> when the menu left. An item that put it somewhere else, such
9297
+ // as a Modal or a rename field, keeps it there.
9298
+ const now = getDeepestActiveElement(doc);
9299
+ if (!now || now === doc.body || menuNode.contains(now))
9300
+ opener?.focus();
9085
9301
  };
9086
- }, []);
9087
- return announcements.written;
9088
- }
9089
- /**
9090
- * Reads an element out roughly the way a screen reader would: its text, with
9091
- * an element's `aria-label`, or an image's `alt`, standing in for what's
9092
- * inside it, a break between block-level elements, and parts that are
9093
- * `aria-hidden`, `hidden` or inline-styled `display: none` left out. It isn't
9094
- * the full accessible name computation, so `aria-labelledby`, CSS-generated
9095
- * content, content hidden by a stylesheet and other ways of naming or hiding
9096
- * content aren't followed.
9097
- *
9098
- * @function spokenText
9099
- * @param element - The element to read.
9100
- * @returns Its text, with runs of whitespace collapsed and the ends trimmed.
9101
- */
9102
- function spokenText(element) {
9103
- return readAloud(element).replace(/\s+/g, ' ').trim();
9104
- }
9105
- // Elements a browser displays as blocks by default, and `br`. A screen reader
9106
- // pauses between them, so their text shouldn't run into the text beside them.
9107
- // They're known by tag rather than by computed `display`, which would depend
9108
- // on a stylesheet and on layout.
9109
- const blockLevelTags = new Set([
9110
- 'ADDRESS',
9111
- 'ARTICLE',
9112
- 'ASIDE',
9113
- 'BLOCKQUOTE',
9114
- 'BR',
9115
- 'CAPTION',
9116
- 'DD',
9117
- 'DETAILS',
9118
- 'DIALOG',
9119
- 'DIV',
9120
- 'DL',
9121
- 'DT',
9122
- 'FIELDSET',
9123
- 'FIGCAPTION',
9124
- 'FIGURE',
9125
- 'FOOTER',
9126
- 'FORM',
9127
- 'H1',
9128
- 'H2',
9129
- 'H3',
9130
- 'H4',
9131
- 'H5',
9132
- 'H6',
9133
- 'HEADER',
9134
- 'HGROUP',
9135
- 'HR',
9136
- 'LEGEND',
9137
- 'LI',
9138
- 'MAIN',
9139
- 'MENU',
9140
- 'NAV',
9141
- 'OL',
9142
- 'P',
9143
- 'PRE',
9144
- 'SEARCH',
9145
- 'SECTION',
9146
- 'SUMMARY',
9147
- 'TABLE',
9148
- 'TBODY',
9149
- 'TD',
9150
- 'TFOOT',
9151
- 'TH',
9152
- 'THEAD',
9153
- 'TR',
9154
- 'UL',
9155
- ]);
9302
+ }, [doc]);
9303
+ // Focus in, and closing on scroll, once it's placed. Placed, it's inside
9304
+ // the viewport, so its first item has nothing to scroll into view, and
9305
+ // `preventScroll` makes sure: a scroll the focus caused would close it as
9306
+ // it opened. Opening again at a new point places it again, and leaves
9307
+ // focus where it is in the menu.
9308
+ React.useEffect(() => {
9309
+ const box = boxRef.current;
9310
+ const menuNode = menuRef.current;
9311
+ if (!doc || !isPlaced || !box || !menuNode)
9312
+ return undefined;
9313
+ if (!menuNode.contains(getDeepestActiveElement(doc))) {
9314
+ menuKeys.reset();
9315
+ (enabledMenuItems(menuNode)[0] ?? menuNode).focus({
9316
+ preventScroll: true,
9317
+ });
9318
+ }
9319
+ const closeOutside = (event) => {
9320
+ if (!isEventInside(event, box))
9321
+ setMenu(null);
9322
+ };
9323
+ doc.addEventListener('scroll', closeOutside, true);
9324
+ return () => doc.removeEventListener('scroll', closeOutside, true);
9325
+ }, [doc, isPlaced, menuKeys]);
9326
+ // Before paint, so what the caller does with it, such as highlighting the
9327
+ // row, shows with the menu rather than a frame behind it.
9328
+ useClientLayoutEffect(() => {
9329
+ if (open === reportedRef.current)
9330
+ return;
9331
+ reportedRef.current = open;
9332
+ onOpenChange?.(open);
9333
+ });
9334
+ React.useEffect(() => {
9335
+ if (open && !named) {
9336
+ warnOnce('ContextMenu:unnamed', '[bestax-bulma] <ContextMenu> opened a menu with no accessible ' +
9337
+ 'name. Pass aria-label or aria-labelledby.');
9338
+ }
9339
+ }, [open, named]);
9340
+ const handleKeyDown = (event) => {
9341
+ onKeyDown?.(event);
9342
+ // The menu has the keyboard while focus is in it. React brings its keys
9343
+ // up the tree to wherever the ContextMenu sits, where a table that moves
9344
+ // between rows on the arrow keys, or a Popover or a Modal waiting for
9345
+ // Escape, would take them too.
9346
+ event.stopPropagation();
9347
+ if (event.key === 'Escape' || event.key === 'Tab') {
9348
+ // Tab would go on from the end of <body>, past the rest of the page.
9349
+ event.preventDefault();
9350
+ setMenu(null);
9351
+ return;
9352
+ }
9353
+ menuKeys.onKeyDown(event);
9354
+ };
9355
+ const handleClick = (event) => {
9356
+ onClick?.(event);
9357
+ if (!closeOnClick)
9358
+ return;
9359
+ const item = event.target.closest(MENU_ITEM_SELECTOR);
9360
+ if (item && isEnabledMenuItem(item))
9361
+ setMenu(null);
9362
+ };
9363
+ const handleContextMenu = (event) => {
9364
+ onContextMenu?.(event);
9365
+ // No other menu over this one: not the browser's, and not a trigger's
9366
+ // that React brings the event up to.
9367
+ event.preventDefault();
9368
+ event.stopPropagation();
9369
+ };
9370
+ const own = trigger.props;
9371
+ const triggerClass = usePrefixedClassNames('context-menu');
9372
+ const clonedTrigger = React.cloneElement(trigger, {
9373
+ className: classNames(own.className, !disabled && triggerClass) || undefined,
9374
+ onContextMenu: chain(own.onContextMenu, handlers.onContextMenu),
9375
+ onPointerDown: chain(own.onPointerDown, handlers.onPointerDown),
9376
+ onPointerMove: chain(own.onPointerMove, handlers.onPointerMove),
9377
+ onPointerUp: chain(own.onPointerUp, handlers.onPointerUp),
9378
+ onPointerCancel: chain(own.onPointerCancel, handlers.onPointerCancel),
9379
+ onTouchEnd: chain(own.onTouchEnd, handlers.onTouchEnd),
9380
+ });
9381
+ const boxClasses = usePrefixedClassNames('context-menu-content', 'dropdown', {
9382
+ 'is-active': true,
9383
+ 'is-up': placed?.up,
9384
+ 'is-right': placed?.right,
9385
+ });
9386
+ const menuClasses = classNames(usePrefixedClassNames('dropdown-menu'), bulmaHelperClasses, className);
9387
+ const contentClasses = usePrefixedClassNames('dropdown-content');
9388
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [clonedTrigger, menu && (jsxRuntime.jsx(Portal, { container: portalContainer || menu.doc.body, children: jsxRuntime.jsx("div", { ref: boxRef, className: boxClasses, style: {
9389
+ top: placed?.top ?? menu.point.y,
9390
+ left: placed?.left ?? menu.point.x,
9391
+ }, children: jsxRuntime.jsx("div", { ...rest, ref: menuRef, role: "menu", tabIndex: -1, className: menuClasses, onKeyDown: handleKeyDown, onClick: handleClick, onContextMenu: handleContextMenu, children: jsxRuntime.jsx("div", { className: contentClasses, children: children }) }) }) }))] }));
9392
+ };
9156
9393
  /**
9157
- * Whether an element is left out of what a screen reader reads.
9158
- *
9159
- * @function isHidden
9160
- * @param element - The element to check.
9161
- * @returns True for `aria-hidden="true"`, the `hidden` attribute, and an
9162
- * inline `display: none`, on an HTML, SVG or any other element with an
9163
- * inline style.
9394
+ * ContextMenu with Item and Divider sub-components, which are `Dropdown.Item`
9395
+ * and `Dropdown.Divider`, so items written for a Dropdown work here
9396
+ * unchanged.
9164
9397
  */
9165
- const isHidden = (element) => element.getAttribute('aria-hidden') === 'true' ||
9166
- element.hasAttribute('hidden') ||
9167
- element.style?.display === 'none';
9398
+ const ContextMenu = withSubComponents(ContextMenuComponent, {
9399
+ Item: DropdownItem,
9400
+ Divider: DropdownDivider,
9401
+ }, 'ContextMenu');
9402
+
9168
9403
  /**
9169
- * The text `spokenText` reads for an element, before its whitespace is
9170
- * tidied.
9404
+ * Individual Step component for use inside Steps.
9171
9405
  *
9172
- * @function readAloud
9173
- * @param element - The element to read.
9174
- * @returns Its text.
9406
+ * @function
9407
+ * @param {StepProps} props - Props for the Step component.
9408
+ * @returns {JSX.Element} The rendered step segment.
9175
9409
  */
9176
- function readAloud(element) {
9177
- if (isHidden(element))
9178
- return '';
9179
- const name = element.getAttribute('aria-label') ||
9180
- (element.tagName === 'IMG' ? element.getAttribute('alt') : null);
9181
- const text = name ||
9182
- Array.from(element.childNodes, child => {
9183
- if (child instanceof Element)
9184
- return readAloud(child);
9185
- return child.nodeType === Node.TEXT_NODE ? child.textContent : '';
9186
- }).join('');
9187
- // A name stands in for the element's content, and a block's content stands
9188
- // apart from what's around it, so both are padded to keep them from running
9189
- // into the neighboring text. An inline run, like `<span>Up</span>` then
9190
- // `<span>loaded</span>`, still reads as one word.
9191
- return name || blockLevelTags.has(element.tagName) ? ` ${text} ` : text;
9192
- }
9410
+ const Step = ({ isActive = false, isCompleted = false, label, icon, clickable = false, onClick, stepNumber, completedIcon, className, children, ...props }) => {
9411
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
9412
+ const formatNumber = useScopedNumberFormat();
9413
+ const stepClasses = classNames(usePrefixedClassNames('steps-segment', {
9414
+ 'is-active': isActive,
9415
+ 'is-completed': isCompleted,
9416
+ }), bulmaHelperClasses, className);
9417
+ const stepsLinkClass = usePrefixedClassNames('steps-link');
9418
+ const stepsMarkerClass = usePrefixedClassNames('steps-marker');
9419
+ const stepsContentClass = usePrefixedClassNames('steps-content');
9420
+ const stepsTitleClass = usePrefixedClassNames('steps-title');
9421
+ const resolvedCompletedIcon = completedIcon === undefined ? '\u2713' : completedIcon;
9422
+ const markerContent = icon ||
9423
+ (isCompleted && resolvedCompletedIcon
9424
+ ? resolvedCompletedIcon
9425
+ : stepNumber == null || !formatNumber
9426
+ ? (stepNumber ?? null)
9427
+ : formatNumber(stepNumber));
9428
+ const title = label || children;
9429
+ const current = isActive ? 'step' : undefined;
9430
+ // The step stays a list item, and a real button inside it does the
9431
+ // clicking, so the list keeps its items and the button brings Enter, Space
9432
+ // and focus with it. A button holds only phrasing content, so its parts
9433
+ // are spans.
9434
+ if (clickable) {
9435
+ return (jsxRuntime.jsx("li", { className: stepClasses, ...rest, children: jsxRuntime.jsxs("button", { type: "button", className: stepsLinkClass, onClick: () => onClick?.(), "aria-current": current, children: [jsxRuntime.jsx("span", { className: stepsMarkerClass, children: markerContent }), title && (jsxRuntime.jsx("span", { className: stepsContentClass, children: jsxRuntime.jsx("span", { className: stepsTitleClass, children: title }) }))] }) }));
9436
+ }
9437
+ return (jsxRuntime.jsx("li", { className: stepClasses, "aria-current": current, ...rest, children: jsxRuntime.jsxs("div", { className: stepsLinkClass, children: [jsxRuntime.jsx("span", { className: stepsMarkerClass, children: markerContent }), title && (jsxRuntime.jsx("div", { className: stepsContentClass, children: jsxRuntime.jsx("p", { className: stepsTitleClass, children: title }) }))] }) }));
9438
+ };
9193
9439
  /**
9194
- * A visually hidden, polite `status` live region that a container announces
9195
- * its items through. The container renders it from the moment it mounts,
9196
- * whether or not it has anything to show, so the region is already in the
9197
- * page when an announcement is written into it.
9440
+ * The `Steps` component provides a multi-step progress indicator for wizard flows, checkout processes, or any multi-step workflow.
9198
9441
  *
9199
- * The announcements are this component's own state, so writing and clearing
9200
- * them re-renders the region and leaves the container's items alone. Each one
9201
- * is its own keyed node, so a re-render that leaves the announcements alone
9202
- * leaves the region's content alone too, and nothing is read out twice.
9442
+ * @function
9443
+ * @param {StepsProps} props - Props for the Steps component.
9444
+ * @returns {JSX.Element} The rendered steps component.
9203
9445
  *
9204
- * @function StatusRegion
9205
- * @param props - The items to announce, and the text to announce for each.
9206
- * @returns The region.
9446
+ * @example
9447
+ * // Basic steps with items
9448
+ * <Steps
9449
+ * value={1}
9450
+ * items={[
9451
+ * { label: 'Account' },
9452
+ * { label: 'Profile' },
9453
+ * { label: 'Complete' },
9454
+ * ]}
9455
+ * />
9456
+ *
9457
+ * @example
9458
+ * // Clickable steps with navigation
9459
+ * <Steps
9460
+ * value={currentStep}
9461
+ * onStepClick={(step) => setCurrentStep(step)}
9462
+ * hasNavigation
9463
+ * items={[
9464
+ * { label: 'Step 1', clickable: true },
9465
+ * { label: 'Step 2', clickable: true },
9466
+ * { label: 'Step 3', clickable: true },
9467
+ * ]}
9468
+ * />
9207
9469
  */
9208
- function StatusRegion({ items, describe, }) {
9209
- const announcements = useAnnouncements(items, describe);
9210
- // `role="status"` makes a region atomic unless it says otherwise, which
9211
- // reads the whole region on every change. Each announcement is its own
9212
- // node, and only additions are relevant by default, so with atomic off a
9213
- // screen reader reads just what was written, and removing one announcement
9214
- // doesn't read out the ones still there. A node is keyed by its batch as
9215
- // well as its item, since an item that comes back can be written again
9216
- // while what it said before is still there.
9217
- return (jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "false", style: visuallyHidden, children: announcements.map(announcement => (jsxRuntime.jsx("div", { children: announcement.text }, `${announcement.batch}:${announcement.id}`))) }));
9218
- }
9470
+ const StepsComponent = ({ value = 0, items, size, color, hasMarker = true, animated = true, rounded = true, vertical = false, labelPosition = 'bottom', mobileMode, showStepNumbers = true, hasNavigation = false, prevLabel, nextLabel, onPrev, onNext, onStepClick, className, children, ...props }) => {
9471
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
9472
+ // Generate classes for the wrapper div
9473
+ const stepsClasses = usePrefixedClassNames('steps', {
9474
+ [`is-${size}`]: size,
9475
+ [`is-${color}`]: color,
9476
+ 'has-marker': hasMarker,
9477
+ 'is-animated': animated,
9478
+ 'is-rounded': rounded,
9479
+ 'is-vertical': vertical,
9480
+ [`has-label-${labelPosition}`]: labelPosition && (labelPosition !== 'bottom' || vertical),
9481
+ [`is-${mobileMode}`]: mobileMode,
9482
+ });
9483
+ const combinedClasses = classNames(stepsClasses, bulmaHelperClasses, className);
9484
+ // Generate classes for the ul (list)
9485
+ const listClasses = usePrefixedClassNames('steps-list', {
9486
+ 'is-vertical': vertical,
9487
+ });
9488
+ const stepsNavigationClass = usePrefixedClassNames('steps-navigation');
9489
+ const prevButtonClass = usePrefixedClassNames('button');
9490
+ const nextButtonClass = usePrefixedClassNames('button', {
9491
+ 'is-primary': true,
9492
+ });
9493
+ // Count total steps for navigation
9494
+ const totalSteps = items ? items.length : React.Children.count(children);
9495
+ // Render items if provided
9496
+ const renderSteps = () => {
9497
+ if (items && items.length > 0) {
9498
+ return items.map((item, index) => (jsxRuntime.jsx(Step, { isActive: index === value, isCompleted: index < value, label: item.label, icon: item.icon, completedIcon: item.completedIcon, clickable: item.clickable, stepNumber: showStepNumbers ? index + 1 : undefined, onClick: item.clickable && onStepClick ? () => onStepClick(index) : undefined, className: item.className }, index)));
9499
+ }
9500
+ // If children are provided, clone them with active/completed states
9501
+ if (children) {
9502
+ return React.Children.map(children, (child, index) => {
9503
+ if (React.isValidElement(child)) {
9504
+ return React.cloneElement(child, {
9505
+ isActive: index === value,
9506
+ isCompleted: index < value,
9507
+ stepNumber: showStepNumbers ? index + 1 : undefined,
9508
+ onClick: child.props.clickable && onStepClick
9509
+ ? () => onStepClick(index)
9510
+ : child.props.onClick,
9511
+ });
9512
+ }
9513
+ return child;
9514
+ });
9515
+ }
9516
+ return null;
9517
+ };
9518
+ // role="list" restates the <ul>'s own role, for Safari: WebKit stops
9519
+ // exposing a list as one once its markers are hidden, as they are here,
9520
+ // unless the role is set explicitly.
9521
+ return (jsxRuntime.jsxs("div", { className: combinedClasses, ...rest, children: [jsxRuntime.jsx("ul", { className: listClasses, role: "list", children: renderSteps() }), hasNavigation && (jsxRuntime.jsxs("div", { className: stepsNavigationClass, children: [jsxRuntime.jsx("button", { type: "button", className: prevButtonClass, disabled: value === 0, onClick: onPrev ?? (() => onStepClick?.(value - 1)), children: prevLabel ?? 'Previous' }), jsxRuntime.jsx("button", { type: "button", className: nextButtonClass, disabled: value === totalSteps - 1, onClick: onNext ?? (() => onStepClick?.(value + 1)), children: nextLabel ?? 'Next' })] }))] }));
9522
+ };
9523
+ // Attach Step as static property
9524
+ const Steps = withSubComponents(StepsComponent, { Step }, 'Steps');
9219
9525
 
9220
9526
  const validTimelineColors = [
9221
9527
  'primary',
@@ -9230,11 +9536,6 @@ const validTimelineTitleElements = ['h2', 'h3', 'h4', 'h5', 'h6', 'p'];
9230
9536
  const TimelineItemContext = React.createContext({
9231
9537
  titleAs: 'h2',
9232
9538
  });
9233
- /** Whether a prop was given something to render. */
9234
- const isGiven$2 = (node) => node !== undefined &&
9235
- node !== null &&
9236
- typeof node !== 'boolean' &&
9237
- node !== '';
9238
9539
  const isTimelineColor = (color) => !!color && validTimelineColors.includes(color);
9239
9540
  /**
9240
9541
  * One event: a marker on the rail, then its title, its time and anything else about it.
@@ -9264,7 +9565,7 @@ const TimelineItem = React.forwardRef(({ className, title, time, dateTime, icon,
9264
9565
  [`is-${color}`]: isTimelineColor(color),
9265
9566
  });
9266
9567
  const markerClass = usePrefixedClassNames('timeline-marker', {
9267
- 'is-icon': isGiven$2(icon),
9568
+ 'is-icon': isGiven(icon),
9268
9569
  });
9269
9570
  const contentClass = usePrefixedClassNames('timeline-content');
9270
9571
  const titleClass = usePrefixedClassNames('timeline-title');
@@ -9276,8 +9577,8 @@ const TimelineItem = React.forwardRef(({ className, title, time, dateTime, icon,
9276
9577
  const words = label ? (jsxRuntime.jsx("span", { style: visuallyHidden, children: ` ${label}` })) : null;
9277
9578
  return (jsxRuntime.jsxs("li", { ref: ref, className: classNames(itemClass, bulmaHelperClasses, className), ...rest, ...(state && {
9278
9579
  'aria-current': state === 'active' ? 'step' : undefined,
9279
- }), children: [jsxRuntime.jsx("div", { className: markerClass, "aria-hidden": "true", children: isGiven$2(icon) &&
9280
- (typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon) }), jsxRuntime.jsxs("div", { className: contentClass, children: [isGiven$2(title) ? (jsxRuntime.jsxs(TitleTag, { className: titleClass, children: [title, words] })) : (words), isGiven$2(time) &&
9580
+ }), children: [jsxRuntime.jsx("div", { className: markerClass, "aria-hidden": "true", children: isGiven(icon) &&
9581
+ (typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon) }), jsxRuntime.jsxs("div", { className: contentClass, children: [isGiven(title) ? (jsxRuntime.jsxs(TitleTag, { className: titleClass, children: [title, words] })) : (words), isGiven(time) &&
9281
9582
  (dateTime ? (jsxRuntime.jsx("time", { className: timeClass, dateTime: dateTime, children: time })) : (jsxRuntime.jsx("span", { className: timeClass, children: time }))), children] })] }));
9282
9583
  });
9283
9584
  TimelineItem.displayName = 'Timeline.Item';
@@ -9351,7 +9652,7 @@ const TimelineRoot = React.forwardRef(({ className, children, active, pending, a
9351
9652
  // Each child gets its state through context rather than a clone, so an
9352
9653
  // app's own component around a Timeline.Item still gets it. Fragments
9353
9654
  // and arrays are opened up first, so each item inside one counts.
9354
- return (jsxRuntime.jsxs("ol", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, role: "list", children: [flattenChildren(children).map((child, index) => (jsxRuntime.jsx(TimelineItemContext.Provider, { value: contextFor(stateAt(index)), children: child }, React.isValidElement(child) ? child.key : index))), isGiven$2(pending) && (jsxRuntime.jsx(TimelineItemContext.Provider, { value: contextFor('pending'), children: jsxRuntime.jsx(TimelineItem, { title: pending }) }))] }));
9655
+ return (jsxRuntime.jsxs("ol", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, role: "list", children: [flattenChildren(children).map((child, index) => (jsxRuntime.jsx(TimelineItemContext.Provider, { value: contextFor(stateAt(index)), children: child }, React.isValidElement(child) ? child.key : index))), isGiven(pending) && (jsxRuntime.jsx(TimelineItemContext.Provider, { value: contextFor('pending'), children: jsxRuntime.jsx(TimelineItem, { title: pending }) }))] }));
9355
9656
  });
9356
9657
  const Timeline = withSubComponents(TimelineRoot, { Item: TimelineItem }, 'Timeline');
9357
9658
 
@@ -11870,11 +12171,6 @@ const TOP = { path: [], hidden: false };
11870
12171
  const BranchContext = React.createContext(TOP);
11871
12172
  /** The box's input lets the row's click toggle it, so a change on its own does nothing. */
11872
12173
  const ignoreChange$1 = () => { };
11873
- /** Whether an `icon` was given something to render. */
11874
- const isGiven$1 = (node) => node !== undefined &&
11875
- node !== null &&
11876
- typeof node !== 'boolean' &&
11877
- node !== '';
11878
12174
  /**
11879
12175
  * The `TreeView` component renders a tree of items you browse and pick from, the way a file explorer works: one tab stop, the arrow keys to move, and Right and Left to open and close.
11880
12176
  *
@@ -11993,7 +12289,7 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
11993
12289
  ? null
11994
12290
  : isLoading
11995
12291
  ? 'loading'
11996
- : isGiven$1(loadError)
12292
+ : isGiven(loadError)
11997
12293
  ? 'error'
11998
12294
  : null;
11999
12295
  const labelRef = React.useRef(null);
@@ -12102,7 +12398,7 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
12102
12398
  if (!(event.target instanceof HTMLInputElement)) {
12103
12399
  event.preventDefault();
12104
12400
  }
12105
- }, children: jsxRuntime.jsx(Checkbox, { tabIndex: -1, checked: checked === true, indeterminate: checked === 'mixed', disabled: disabled, onChange: ignoreChange$1 }) })), isGiven$1(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), jsxRuntime.jsx("span", { className: labelClass, id: labelId, ref: labelRef, children: label })] }), open && (hasItems || status) ? (jsxRuntime.jsxs("ul", { className: groupClass, role: "group", children: [items, status && (jsxRuntime.jsx(StatusRow, { kind: status, parent: value, level: branch.path.length + 2, message: loadError, onRetry: onRetry, labelRef: statusLabelRef }))] })) : (items)] }));
12401
+ }, children: jsxRuntime.jsx(Checkbox, { tabIndex: -1, checked: checked === true, indeterminate: checked === 'mixed', disabled: disabled, onChange: ignoreChange$1 }) })), isGiven(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), jsxRuntime.jsx("span", { className: labelClass, id: labelId, ref: labelRef, children: label })] }), open && (hasItems || status) ? (jsxRuntime.jsxs("ul", { className: groupClass, role: "group", children: [items, status && (jsxRuntime.jsx(StatusRow, { kind: status, parent: value, level: branch.path.length + 2, message: loadError, onRetry: onRetry, labelRef: statusLabelRef }))] })) : (items)] }));
12106
12402
  });
12107
12403
  TreeViewItem.displayName = 'TreeView.Item';
12108
12404
  /**
@@ -12157,7 +12453,7 @@ const TreeView = withSubComponents(TreeViewRoot, { Item: TreeViewItem }, 'TreeVi
12157
12453
  const EDGES = ['top', 'bottom', 'start', 'end'];
12158
12454
  // Layout can land a fraction of a pixel short of an edge, so anything closer
12159
12455
  // than this counts as there.
12160
- const SLACK = 1;
12456
+ const SLACK$1 = 1;
12161
12457
  const sameMeasure = (a, b) => !!a &&
12162
12458
  a.overflowing === b.overflowing &&
12163
12459
  a.barInline === b.barInline &&
@@ -12238,8 +12534,8 @@ const ScrollArea = React.forwardRef(({ className, style, textColor, bgColor, hei
12238
12534
  const scrollsX = orientation !== 'vertical';
12239
12535
  const maxTop = el.scrollHeight - el.clientHeight;
12240
12536
  const maxLeft = el.scrollWidth - el.clientWidth;
12241
- const overflowY = scrollsY && maxTop > SLACK;
12242
- const overflowX = scrollsX && maxLeft > SLACK;
12537
+ const overflowY = scrollsY && maxTop > SLACK$1;
12538
+ const overflowX = scrollsX && maxLeft > SLACK$1;
12243
12539
  // Right to left, scrollLeft runs from 0 at the start to negative.
12244
12540
  const fromStart = layoutRef.current.rtl
12245
12541
  ? -el.scrollLeft
@@ -12247,10 +12543,10 @@ const ScrollArea = React.forwardRef(({ className, style, textColor, bgColor, hei
12247
12543
  const next = {
12248
12544
  overflowing: overflowY || overflowX,
12249
12545
  edges: {
12250
- top: overflowY && el.scrollTop > SLACK,
12251
- bottom: overflowY && maxTop - el.scrollTop > SLACK,
12252
- start: overflowX && fromStart > SLACK,
12253
- end: overflowX && maxLeft - fromStart > SLACK,
12546
+ top: overflowY && el.scrollTop > SLACK$1,
12547
+ bottom: overflowY && maxTop - el.scrollTop > SLACK$1,
12548
+ start: overflowX && fromStart > SLACK$1,
12549
+ end: overflowX && maxLeft - fromStart > SLACK$1,
12254
12550
  },
12255
12551
  barInline: layoutRef.current.barInline,
12256
12552
  barBlock: layoutRef.current.barBlock,
@@ -14778,50 +15074,403 @@ const DataTable = React.forwardRef(function DataTableRender({ columns, data, tab
14778
15074
  : {
14779
15075
  'aria-labelledby': ariaLabelledby || (hasCaption ? nameId : undefined),
14780
15076
  };
14781
- // Pagination names itself "pagination" unless given another name.
14782
- const pagerLabel = words.pagination || (textName ? `${textName} pages` : undefined);
14783
- const pagerName = pagerLabel ? { 'aria-label': pagerLabel } : {};
14784
- const toolbarContent = toolbar?.(table);
14785
- const hasToolbar = hasSearch || isShown(toolbarContent);
14786
- return (jsxRuntime.jsxs("div", { ref: ref, className: rootClass, ...rest, children: [hasToolbar && (jsxRuntime.jsxs("div", { className: toolbarClass, children: [hasSearch && (jsxRuntime.jsx(Input, { label: words.search, ...table.getSearchProps() })), isShown(toolbarContent) && (jsxRuntime.jsx("div", { className: actionsClass, children: toolbarContent }))] })), isResponsive ? (jsxRuntime.jsx(ScrollArea, { orientation: "horizontal", ...scrollName, children: tableElement })) : (tableElement), jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", style: visuallyHidden, children: said }), pageCount > 1 && (jsxRuntime.jsx(Pagination, { ...table.getPaginationProps(), ...pagerName }))] }));
14787
- });
14788
- DataTable.displayName = 'DataTable';
15077
+ // Pagination names itself "pagination" unless given another name.
15078
+ const pagerLabel = words.pagination || (textName ? `${textName} pages` : undefined);
15079
+ const pagerName = pagerLabel ? { 'aria-label': pagerLabel } : {};
15080
+ const toolbarContent = toolbar?.(table);
15081
+ const hasToolbar = hasSearch || isShown(toolbarContent);
15082
+ return (jsxRuntime.jsxs("div", { ref: ref, className: rootClass, ...rest, children: [hasToolbar && (jsxRuntime.jsxs("div", { className: toolbarClass, children: [hasSearch && (jsxRuntime.jsx(Input, { label: words.search, ...table.getSearchProps() })), isShown(toolbarContent) && (jsxRuntime.jsx("div", { className: actionsClass, children: toolbarContent }))] })), isResponsive ? (jsxRuntime.jsx(ScrollArea, { orientation: "horizontal", ...scrollName, children: tableElement })) : (tableElement), jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", style: visuallyHidden, children: said }), pageCount > 1 && (jsxRuntime.jsx(Pagination, { ...table.getPaginationProps(), ...pagerName }))] }));
15083
+ });
15084
+ DataTable.displayName = 'DataTable';
15085
+
15086
+ /**
15087
+ * The `Affix` component keeps its content fixed to a corner of the viewport while the page scrolls under it.
15088
+ *
15089
+ * It's a `<div>` with no role, so what's inside keeps its own. It sits
15090
+ * `--bulma-affix-offset-vertical` and `--bulma-affix-offset-horizontal`
15091
+ * from its corner's edges, plus the device's safe-area inset, and stacks at
15092
+ * `--bulma-affix-z-index`, over the page's content and under its navbar,
15093
+ * static or fixed, its menus, popovers, the Sidebar, the modals, Tooltip and
15094
+ * Toast. It's as wide as its
15095
+ * content and never wider than the viewport less its offsets. It doesn't
15096
+ * scroll, so nothing inside it is clipped, and content that can be taller
15097
+ * than the screen scrolls itself. Its place in the reading and Tab order is
15098
+ * its place in the DOM, whatever corner it shows in.
15099
+ *
15100
+ * @function
15101
+ * @param {AffixProps} props - Props for the Affix component.
15102
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `<div>`.
15103
+ * @returns The `<div>`, in place or in its portal.
15104
+ *
15105
+ * @example
15106
+ * <Affix position="bottom-left">
15107
+ * <Button color="primary">Feedback</Button>
15108
+ * </Affix>
15109
+ */
15110
+ const Affix = React.forwardRef(({ position = 'bottom-right', portal = false, className, children, ...props }, ref) => {
15111
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
15112
+ const affixClasses = usePrefixedClassNames('affix', {
15113
+ [`is-${position}`]: true,
15114
+ });
15115
+ // In place on the server and while hydrating, then into the portal, as
15116
+ // Modal does. The move remounts the `<div>`.
15117
+ const { isPortaled, container } = usePortalProp(portal);
15118
+ return (jsxRuntime.jsx(Portal, { disabled: !isPortaled, container: container, children: jsxRuntime.jsx("div", { ref: ref, className: classNames(affixClasses, bulmaHelperClasses, className), ...rest, children: children }) }));
15119
+ });
15120
+ Affix.displayName = 'Affix';
15121
+
15122
+ /**
15123
+ * How long the page has to stay still before a jump counts as over: long
15124
+ * enough to span the gap between two frames of a smooth scroll.
15125
+ */
15126
+ const SETTLE_MS = 100;
15127
+ /**
15128
+ * A pixel of slack at the line, for a jump that lands a fraction of a pixel
15129
+ * away from it.
15130
+ */
15131
+ const SLACK = 1;
15132
+ /** The page's `scroll-padding-top` in pixels, or 0 for `auto`. */
15133
+ function scrollPaddingTop() {
15134
+ const root = document.documentElement;
15135
+ const value = getComputedStyle(root).scrollPaddingTop;
15136
+ const number = parseFloat(value);
15137
+ if (!Number.isFinite(number))
15138
+ return 0;
15139
+ // A percentage is of the page's scrollport: the viewport less a classic
15140
+ // scrollbar.
15141
+ return value.endsWith('%') ? (number * root.clientHeight) / 100 : number;
15142
+ }
15143
+ /**
15144
+ * The height of the viewport the reader sees the page through, less a
15145
+ * classic scrollbar: the larger of the root's `clientHeight` and the visual
15146
+ * viewport's height. The visual viewport grows when a phone hides its
15147
+ * toolbar, where the root keeps the height it had with the toolbar shown;
15148
+ * under a pinch zoom it's the smaller, and the root's stands. Its `pageTop`
15149
+ * isn't read, since Firefox can leave it behind `scrollY` for a frame after
15150
+ * it puts back the scroll position on a reload.
15151
+ */
15152
+ function viewportHeight() {
15153
+ return Math.max(document.documentElement.clientHeight, window.visualViewport?.height ?? 0);
15154
+ }
15155
+ /** Whether the page is scrolled as far down as it goes. */
15156
+ const atEnd = () => window.scrollY > 0 &&
15157
+ window.scrollY + viewportHeight() >=
15158
+ document.documentElement.scrollHeight - 1;
15159
+ /** Whether the browser draws an element: `display: none` draws no box. */
15160
+ const isDrawn = (element) => element.getClientRects().length > 0;
15161
+ /** The id a URL fragment names, such as `#caf%C3%A9`. */
15162
+ function fragmentId(hash) {
15163
+ const raw = hash.slice(1);
15164
+ try {
15165
+ return decodeURIComponent(raw);
15166
+ }
15167
+ catch {
15168
+ return raw;
15169
+ }
15170
+ }
15171
+ /**
15172
+ * Whether the page was opened afresh, as by a link, rather than reloaded or
15173
+ * reached by Back or Forward, where the browser puts it back where it was.
15174
+ * Where the navigation isn't recorded (jsdom), it counts as opened afresh.
15175
+ */
15176
+ function openedAfresh() {
15177
+ const [entry] = performance.getEntriesByType?.('navigation') ?? [];
15178
+ const type = entry?.type;
15179
+ return type !== 'reload' && type !== 'back_forward';
15180
+ }
15181
+ /**
15182
+ * The link a click was on, if it's a plain click that follows it in this
15183
+ * page: the main button, no modifier key, and no `target` sending it
15184
+ * elsewhere. The path is read rather than the target, so a link inside a
15185
+ * shadow root counts.
15186
+ */
15187
+ function followedLink(event) {
15188
+ if (event.button !== 0 ||
15189
+ event.metaKey ||
15190
+ event.ctrlKey ||
15191
+ event.shiftKey ||
15192
+ event.altKey) {
15193
+ return undefined;
15194
+ }
15195
+ const link = event
15196
+ .composedPath()
15197
+ .find((node) => node instanceof HTMLAnchorElement);
15198
+ return link && (!link.target || link.target === '_self') ? link : undefined;
15199
+ }
15200
+ /**
15201
+ * The `useScrollSpy` hook returns the id of the section being read: the last
15202
+ * of `ids` whose top is at or above the offset line, or the last one once the
15203
+ * page is scrolled to its end. After a jump to one of them, it's that one
15204
+ * until the reader scrolls on. `null` above the first section, on the server
15205
+ * and until the page hydrates.
15206
+ *
15207
+ * It checks once per frame while the page scrolls, and on resize. A jump is
15208
+ * a plain click on a link to one of the ids (the main button, no modifier
15209
+ * key, opening in this page), noted and never stopped, so the browser jumps
15210
+ * as it always does; and the `#id` a page opened afresh opens at. Back,
15211
+ * Forward and a reload aren't jumps. That `#id` is read once, when `ids`
15212
+ * first has some: a section rendered after that isn't held, even if the
15213
+ * page opened at it, and the reading rule marks where the page is. Once the
15214
+ * page has been still for a
15215
+ * moment, the section stays current only if it's where a jump puts it: its
15216
+ * top at the line, give or take a shift of the layout smaller than its own
15217
+ * height, or, at the end of the page, on screen below the line. Otherwise,
15218
+ * as after a flick right after the click, the reading rule takes over. The
15219
+ * next scroll after that lets it go, the browser's own included, so when
15220
+ * content loading above it makes the browser scroll to keep the reader's
15221
+ * place, the rule takes over too.
15222
+ *
15223
+ * A section that isn't drawn (`display: none`) is never current. A section
15224
+ * that moves while nothing scrolls is seen at the next scroll or resize. It
15225
+ * watches the document only: not a panel that scrolls inside it, and not
15226
+ * sections inside a shadow root, which an `#id` link can't reach either.
15227
+ * Whether a page was opened afresh is read from how the document loaded, so
15228
+ * in an app that changes routes on the client, a route change can briefly
15229
+ * mark the address bar's section until the page settles, when the reading
15230
+ * rule takes over, and after a reload a route change to an `#id` holds
15231
+ * nothing.
15232
+ *
15233
+ * @function
15234
+ * @param {UseScrollSpyOptions} options - The ids to watch, the offset, and a change callback.
15235
+ * @returns {string | null} The current section's id, or `null`.
15236
+ *
15237
+ * @example
15238
+ * const current = useScrollSpy({ ids: ['profile', 'security', 'billing'] });
15239
+ */
15240
+ function useScrollSpy({ ids, offset, onCurrentChange, }) {
15241
+ const [active, setActive] = React.useState(null);
15242
+ // What was last reported, kept across a re-subscribe so the same id isn't
15243
+ // reported twice.
15244
+ const reportedRef = React.useRef(null);
15245
+ // The section a jump went to. It belongs to the hook rather than to one run
15246
+ // of the effect, so new ids, a new offset or React's development double run
15247
+ // don't lose it, and the `#id` the page opened at is read once, when the
15248
+ // ids first include some.
15249
+ const heldRef = React.useRef(null);
15250
+ const openedRef = React.useRef(false);
15251
+ const onCurrentChangeRef = useLatestRef(onCurrentChange);
15252
+ // The ids as a value, so a new array with the same ids each render doesn't
15253
+ // re-subscribe.
15254
+ const key = JSON.stringify(ids);
15255
+ const fixedOffset = typeof offset === 'number' && Number.isFinite(offset) ? offset : undefined;
15256
+ React.useEffect(() => {
15257
+ const watched = JSON.parse(key);
15258
+ let line = fixedOffset ?? scrollPaddingTop();
15259
+ let settled = true;
15260
+ let settleTimer;
15261
+ let frame = 0;
15262
+ const report = (id) => {
15263
+ if (id === reportedRef.current)
15264
+ return;
15265
+ reportedRef.current = id;
15266
+ setActive(id);
15267
+ onCurrentChangeRef.current?.(id);
15268
+ };
15269
+ /** The element for one of the ids, if it's on the page and drawn. */
15270
+ const sectionOf = (id) => {
15271
+ const element = watched.includes(id) ? document.getElementById(id) : null;
15272
+ return element && isDrawn(element) ? element : null;
15273
+ };
15274
+ /**
15275
+ * Whether a section is where a jump puts it: its top at the line, give or
15276
+ * take a shift of the layout smaller than the section's own height, or,
15277
+ * at the end of the page, as near as the page lets it come, on screen
15278
+ * below the line.
15279
+ */
15280
+ const atJump = (id) => {
15281
+ const element = sectionOf(id);
15282
+ if (!element)
15283
+ return false;
15284
+ const { top, bottom, height } = element.getBoundingClientRect();
15285
+ if (top <= line + height && bottom >= line)
15286
+ return true;
15287
+ return top > line && top < viewportHeight() && atEnd();
15288
+ };
15289
+ const check = () => {
15290
+ if (heldRef.current !== null)
15291
+ return report(heldRef.current);
15292
+ const drawn = [];
15293
+ for (const id of watched) {
15294
+ const element = document.getElementById(id);
15295
+ if (element && isDrawn(element))
15296
+ drawn.push([id, element]);
15297
+ }
15298
+ let current = null;
15299
+ if (atEnd()) {
15300
+ // A short last section can't reach the line, so the end of the page
15301
+ // stands in for it.
15302
+ if (drawn.length)
15303
+ current = drawn[drawn.length - 1][0];
15304
+ }
15305
+ else {
15306
+ for (const [id, element] of drawn) {
15307
+ if (element.getBoundingClientRect().top <= line + SLACK)
15308
+ current = id;
15309
+ }
15310
+ }
15311
+ report(current);
15312
+ };
15313
+ /** Runs the check in the next frame, once however many events ask. */
15314
+ const schedule = () => {
15315
+ if (frame)
15316
+ return;
15317
+ frame = requestAnimationFrame(() => {
15318
+ frame = 0;
15319
+ check();
15320
+ });
15321
+ };
15322
+ /**
15323
+ * Starts, or starts again, the wait for the page to be still, when a
15324
+ * held section that isn't where a jump puts it is let go.
15325
+ */
15326
+ const waitToSettle = () => {
15327
+ settled = false;
15328
+ clearTimeout(settleTimer);
15329
+ settleTimer = setTimeout(() => {
15330
+ settled = true;
15331
+ if (heldRef.current !== null && !atJump(heldRef.current)) {
15332
+ heldRef.current = null;
15333
+ }
15334
+ check();
15335
+ }, SETTLE_MS);
15336
+ };
15337
+ /** Holds the section a jump goes to, if it's one of these on the page. */
15338
+ const hold = (id) => {
15339
+ if (!sectionOf(id))
15340
+ return;
15341
+ heldRef.current = id;
15342
+ waitToSettle();
15343
+ };
15344
+ const onScroll = () => {
15345
+ if (heldRef.current !== null) {
15346
+ // Once the page has been still, the next scroll lets the jump go;
15347
+ // until then, each scroll pushes the end of the jump back.
15348
+ if (settled)
15349
+ heldRef.current = null;
15350
+ else
15351
+ waitToSettle();
15352
+ }
15353
+ schedule();
15354
+ };
15355
+ const onResize = () => {
15356
+ line = fixedOffset ?? scrollPaddingTop();
15357
+ schedule();
15358
+ };
15359
+ const onClick = (event) => {
15360
+ const link = followedLink(event);
15361
+ if (link?.hash &&
15362
+ link.href.split('#')[0] === window.location.href.split('#')[0]) {
15363
+ hold(fragmentId(link.hash));
15364
+ check();
15365
+ }
15366
+ };
15367
+ if (!openedRef.current) {
15368
+ // Ids built from content fetched after mount arrive later, so the read
15369
+ // waits until there are some.
15370
+ if (watched.length) {
15371
+ openedRef.current = true;
15372
+ if (openedAfresh())
15373
+ hold(fragmentId(window.location.hash));
15374
+ }
15375
+ }
15376
+ else if (heldRef.current !== null) {
15377
+ // A jump held across new ids carries on if its section still counts.
15378
+ if (sectionOf(heldRef.current))
15379
+ waitToSettle();
15380
+ else
15381
+ heldRef.current = null;
15382
+ }
15383
+ check();
15384
+ window.addEventListener('scroll', onScroll, { passive: true });
15385
+ window.addEventListener('resize', onResize);
15386
+ document.addEventListener('click', onClick, {
15387
+ capture: true,
15388
+ passive: true,
15389
+ });
15390
+ return () => {
15391
+ cancelAnimationFrame(frame);
15392
+ clearTimeout(settleTimer);
15393
+ window.removeEventListener('scroll', onScroll);
15394
+ window.removeEventListener('resize', onResize);
15395
+ document.removeEventListener('click', onClick, { capture: true });
15396
+ };
15397
+ }, [key, fixedOffset, onCurrentChangeRef]);
15398
+ return active;
15399
+ }
14789
15400
 
15401
+ /** Every id in the tree, in page order. */
15402
+ const idsOf = (items) => items.flatMap(item => [item.id, ...idsOf(item.children ?? [])]);
14790
15403
  /**
14791
- * The `Affix` component keeps its content fixed to a corner of the viewport while the page scrolls under it.
15404
+ * The entries as `MenuItem` links, each entry's own entries in a nested
15405
+ * `MenuList`, and the current one marked.
15406
+ */
15407
+ const entries = (items, active) => items.map(item => {
15408
+ const current = item.id === active;
15409
+ return (jsxRuntime.jsxs(MenuItem, { href: `#${encodeURIComponent(item.id)}`, active: current, "aria-current": current ? 'location' : undefined, children: [item.label, item.children?.length ? (jsxRuntime.jsx(MenuList, { children: entries(item.children, active) })) : null] }, item.id));
15410
+ });
15411
+ /**
15412
+ * The `TableOfContents` component lists a long page's sections as links in Bulma's menu look, and marks the one being read.
14792
15413
  *
14793
- * It's a `<div>` with no role, so what's inside keeps its own. It sits
14794
- * `--bulma-affix-offset-vertical` and `--bulma-affix-offset-horizontal`
14795
- * from its corner's edges, plus the device's safe-area inset, and stacks at
14796
- * `--bulma-affix-z-index`, over the page's content and under its navbar,
14797
- * static or fixed, its menus, popovers, the Sidebar, the modals, Tooltip and
14798
- * Toast. It's as wide as its
14799
- * content and never wider than the viewport less its offsets. It doesn't
14800
- * scroll, so nothing inside it is clipped, and content that can be taller
14801
- * than the screen scrolls itself. Its place in the reading and Tab order is
14802
- * its place in the DOM, whatever corner it shows in.
15414
+ * It's a `<nav>` named by its title, holding `MenuList` and `MenuItem` links
15415
+ * to each `#id`, rendered in full on the server. `useScrollSpy` picks the
15416
+ * current section, and its link alone gets `is-active` and
15417
+ * `aria-current="location"`. Use the hook on its own to mark the section in
15418
+ * view somewhere else, such as an app's nav `Menu`.
14803
15419
  *
14804
15420
  * @function
14805
- * @param {AffixProps} props - Props for the Affix component.
14806
- * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `<div>`.
14807
- * @returns The `<div>`, in place or in its portal.
15421
+ * @param {TableOfContentsProps} props - Props for the TableOfContents component.
15422
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<nav>`.
15423
+ * @returns {JSX.Element} The rendered table of contents.
14808
15424
  *
14809
15425
  * @example
14810
- * <Affix position="bottom-left">
14811
- * <Button color="primary">Feedback</Button>
14812
- * </Affix>
15426
+ * <TableOfContents
15427
+ * isSticky
15428
+ * items={[
15429
+ * { id: 'install', label: 'Install' },
15430
+ * { id: 'usage', label: 'Usage', children: [{ id: 'props', label: 'Props' }] },
15431
+ * ]}
15432
+ * />
14813
15433
  */
14814
- const Affix = React.forwardRef(({ position = 'bottom-right', portal = false, className, children, ...props }, ref) => {
14815
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
14816
- const affixClasses = usePrefixedClassNames('affix', {
14817
- [`is-${position}`]: true,
15434
+ const TableOfContents = React.forwardRef(({ className, textColor, bgColor, items, onCurrentChange, offset, isSticky, labels, ...props }, ref) => {
15435
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
15436
+ color: textColor,
15437
+ backgroundColor: bgColor,
15438
+ ...props,
14818
15439
  });
14819
- // In place on the server and while hydrating, then into the portal, as
14820
- // Modal does. The move remounts the `<div>`.
14821
- const { isPortaled, container } = usePortalProp(portal);
14822
- return (jsxRuntime.jsx(Portal, { disabled: !isPortaled, container: container, children: jsxRuntime.jsx("div", { ref: ref, className: classNames(affixClasses, bulmaHelperClasses, className), ...rest, children: children }) }));
15440
+ const titleId = React.useId();
15441
+ const active = useScrollSpy({ ids: idsOf(items), offset, onCurrentChange });
15442
+ const navRef = React.useRef(null);
15443
+ const setRefs = React.useCallback((node) => {
15444
+ navRef.current = node;
15445
+ if (typeof ref === 'function')
15446
+ return ref(node);
15447
+ if (ref)
15448
+ ref.current = node;
15449
+ }, [ref]);
15450
+ // A list taller than the room it has scrolls itself to keep the current
15451
+ // link in view. Only the list moves: scrollIntoView would move the page
15452
+ // too whenever the link is off screen.
15453
+ React.useEffect(() => {
15454
+ const nav = navRef.current;
15455
+ const link = nav.querySelector('a[aria-current="location"]');
15456
+ if (!link || nav.scrollHeight <= nav.clientHeight)
15457
+ return;
15458
+ const top = nav.getBoundingClientRect().top + nav.clientTop;
15459
+ const bottom = top + nav.clientHeight;
15460
+ const at = link.getBoundingClientRect();
15461
+ if (at.top < top)
15462
+ nav.scrollTop -= top - at.top;
15463
+ else if (at.bottom > bottom)
15464
+ nav.scrollTop += at.bottom - bottom;
15465
+ }, [active]);
15466
+ // Bulma's menu, which draws it, and its own class beside it.
15467
+ const menuClass = usePrefixedClassNames('menu');
15468
+ const rootClass = usePrefixedClassNames('table-of-contents', {
15469
+ 'is-sticky': !!isSticky,
15470
+ });
15471
+ return (jsxRuntime.jsxs("nav", { ref: setRefs, className: classNames(menuClass, rootClass, bulmaHelperClasses, className), "aria-labelledby": titleId, ...rest, children: [jsxRuntime.jsx(MenuLabel, { id: titleId, children: labels?.title || 'On this page' }), jsxRuntime.jsx(MenuList, { children: entries(items, active) })] }));
14823
15472
  });
14824
- Affix.displayName = 'Affix';
15473
+ TableOfContents.displayName = 'TableOfContents';
14825
15474
 
14826
15475
  /**
14827
15476
  * The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
@@ -14983,11 +15632,6 @@ const Delete = ({ className, textColor, color, bgColor, onClick, size, ariaLabel
14983
15632
  const validDescriptionListOrientations = ['vertical', 'horizontal'];
14984
15633
  const isOrientation = (orientation) => !!orientation &&
14985
15634
  validDescriptionListOrientations.includes(orientation);
14986
- /** Whether a prop was given something to render. */
14987
- const isGiven = (node) => node !== undefined &&
14988
- node !== null &&
14989
- typeof node !== 'boolean' &&
14990
- node !== '';
14991
15635
  /**
14992
15636
  * A label, the name of a fact: a `<dt>`, in the weaker text color.
14993
15637
  *
@@ -15934,6 +16578,225 @@ const Span = ({ className, textColor, bgColor, children, ...props }) => {
15934
16578
  return (jsxRuntime.jsx("span", { className: spanClasses || undefined, ...rest, children: children }));
15935
16579
  };
15936
16580
 
16581
+ // INTERNAL, deliberately not exported from src/index.ts: what a Stat and its
16582
+ // parts read from around them.
16583
+ const StatScopeContext = React.createContext({ row: false });
16584
+
16585
+ /** How a change is written when no format is given: a signed percentage. */
16586
+ const DEFAULT_CHANGE_FORMAT = {
16587
+ style: 'percent',
16588
+ signDisplay: 'exceptZero',
16589
+ maximumFractionDigits: 1,
16590
+ };
16591
+ /**
16592
+ * What the figure is: a `<dt>`, small and in the weaker text color.
16593
+ *
16594
+ * @function
16595
+ * @param {StatLabelProps} props - Props for Stat.Label.
16596
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dt>`.
16597
+ * @returns {JSX.Element} The rendered label.
16598
+ *
16599
+ * @example
16600
+ * <Stat.Label>Revenue</Stat.Label>
16601
+ */
16602
+ const StatLabel = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
16603
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
16604
+ color: textColor,
16605
+ backgroundColor: bgColor,
16606
+ ...props,
16607
+ });
16608
+ const labelClass = usePrefixedClassNames('stat-label');
16609
+ return (jsxRuntime.jsx("dt", { ref: ref, className: classNames(labelClass, bulmaHelperClasses, className), ...rest, children: children }));
16610
+ });
16611
+ StatLabel.displayName = 'Stat.Label';
16612
+ /**
16613
+ * The figure: a `<dd>`, large, in the Stat's color, with digits of one width so a figure that updates doesn't shift.
16614
+ *
16615
+ * @function
16616
+ * @param {StatValueProps} props - Props for Stat.Value.
16617
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dd>`.
16618
+ * @returns {JSX.Element} The rendered figure.
16619
+ *
16620
+ * @example
16621
+ * <Stat.Value>
16622
+ * <NumberFormatter value={182} format={{ style: 'unit', unit: 'millisecond' }} />
16623
+ * </Stat.Value>
16624
+ */
16625
+ const StatValue = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
16626
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
16627
+ color: textColor,
16628
+ backgroundColor: bgColor,
16629
+ ...props,
16630
+ });
16631
+ const valueClass = usePrefixedClassNames('stat-value');
16632
+ return (jsxRuntime.jsx("dd", { ref: ref, className: classNames(valueClass, bulmaHelperClasses, className), ...rest, children: children }));
16633
+ });
16634
+ StatValue.displayName = 'Stat.Value';
16635
+ /**
16636
+ * A note under the figure: a `<dd>`, small and in the weaker text color.
16637
+ *
16638
+ * @function
16639
+ * @param {StatDescriptionProps} props - Props for Stat.Description.
16640
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dd>`.
16641
+ * @returns {JSX.Element} The rendered note.
16642
+ *
16643
+ * @example
16644
+ * <Stat.Description>since last month</Stat.Description>
16645
+ */
16646
+ const StatDescription = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
16647
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
16648
+ color: textColor,
16649
+ backgroundColor: bgColor,
16650
+ ...props,
16651
+ });
16652
+ const descriptionClass = usePrefixedClassNames('stat-description');
16653
+ return (jsxRuntime.jsx("dd", { ref: ref, className: classNames(descriptionClass, bulmaHelperClasses, className), ...rest, children: children }));
16654
+ });
16655
+ StatDescription.displayName = 'Stat.Description';
16656
+ /**
16657
+ * A change written with its sign, such as "+12%", after an arrow that repeats the direction: a `<dd>`, green when it's up and red when it's down unless `color` says otherwise.
16658
+ *
16659
+ * The sign is in the text, written by `Intl` for the locale, so a screen
16660
+ * reader hears the direction; the arrow is hidden from assistive tech, and
16661
+ * the color repeats the direction for sighted readers rather than carry it.
16662
+ *
16663
+ * @function
16664
+ * @param {StatChangeProps} props - Props for Stat.Change.
16665
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dd>`.
16666
+ * @returns {JSX.Element} The rendered change.
16667
+ *
16668
+ * @example
16669
+ * <Stat.Change value={-0.08} color="success" />
16670
+ */
16671
+ const StatChange = React.forwardRef(({ className, value, format, locale, color, ...props }, ref) => {
16672
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
16673
+ const scope = React.useContext(StatScopeContext);
16674
+ const ownLocale = locale || scope.locale;
16675
+ const options = format
16676
+ ? { ...format, signDisplay: format.signDisplay || 'exceptZero' }
16677
+ : DEFAULT_CHANGE_FORMAT;
16678
+ // A change that rounds to zero gets no arrow and no tone, which would
16679
+ // contradict a figure of zero. `Intl` writes no sign with 'exceptZero'
16680
+ // only when the value rounds to zero, by whatever rounding the options
16681
+ // ask for, so the change written that way is zero's text exactly then.
16682
+ const formatNumber = useNumberFormat(ownLocale);
16683
+ const signed = { ...options, signDisplay: 'exceptZero' };
16684
+ const direction = formatNumber(value, signed) === formatNumber(0, signed)
16685
+ ? 0
16686
+ : Math.sign(value);
16687
+ const tone = color ||
16688
+ (direction > 0 ? 'success' : direction < 0 ? 'danger' : undefined);
16689
+ const changeClass = usePrefixedClassNames('stat-change', {
16690
+ [`is-${tone}`]: !!tone,
16691
+ });
16692
+ return (jsxRuntime.jsxs("dd", { ref: ref, className: classNames(changeClass, bulmaHelperClasses, className), ...rest, children: [(direction > 0 || direction < 0) && (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "1em", height: "1em", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: direction > 0
16693
+ ? 'M8 13V3M3.5 7.5 8 3l4.5 4.5'
16694
+ : 'M8 3v10M3.5 8.5 8 13l4.5-4.5', fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })), jsxRuntime.jsx(NumberFormatter, { value: value, format: options, locale: ownLocale })] }));
16695
+ });
16696
+ StatChange.displayName = 'Stat.Change';
16697
+ /**
16698
+ * The `Stat` component shows a figure with its name, and often a change and a note, such as a dashboard's revenue or an account's storage used.
16699
+ *
16700
+ * It's a label and its values in a description list: a `<dt>` for the
16701
+ * label, and a `<dd>` each for the figure, the change and the note. On its
16702
+ * own it renders its own `<dl>`; inside `Stats` it's a `<div>` in the row's.
16703
+ * A number `value` is written by `NumberFormatter`, so it follows a
16704
+ * `LocaleProvider`. A change is written with its sign ("+12%"), so the
16705
+ * direction is in the text, beside a decorative arrow and a color.
16706
+ *
16707
+ * @function
16708
+ * @param {StatProps} props - Props for the Stat component.
16709
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the root.
16710
+ * @returns {JSX.Element} The rendered stat.
16711
+ *
16712
+ * @example
16713
+ * <Stat
16714
+ * label="Revenue"
16715
+ * value={8456.4}
16716
+ * format={{ style: 'currency', currency: 'USD' }}
16717
+ * change={0.12}
16718
+ * description="since last month"
16719
+ * />
16720
+ */
16721
+ const StatRoot = React.forwardRef(({ className, label, value, format, locale, change, changeFormat, changeColor, description, size, loading, textColor, bgColor, children, ...props }, ref) => {
16722
+ const scope = React.useContext(StatScopeContext);
16723
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
16724
+ color: textColor || scope.textColor,
16725
+ backgroundColor: bgColor,
16726
+ ...props,
16727
+ });
16728
+ const statClass = usePrefixedClassNames('stat', {
16729
+ [`is-${size}`]: !!size,
16730
+ });
16731
+ // The parts given as children, by type, so the figure's `<dd>` and the
16732
+ // warning below know what's there. A part rendered from inside another
16733
+ // component isn't seen.
16734
+ const parts = flattenChildren(children);
16735
+ const hasPart = (type) => parts.some(part => React.isValidElement(part) && part.type === type);
16736
+ const hasValuePart = hasPart(StatValue);
16737
+ const labelled = isGiven(label) || hasPart(StatLabel);
16738
+ // Without a label, the figure has no name and the values no term.
16739
+ React.useEffect(() => {
16740
+ if (!labelled) {
16741
+ warnOnce('Stat:unlabelled', '[bestax-bulma] <Stat> has no label. Pass label, or a Stat.Label ' +
16742
+ 'child, so the figure has a name for assistive tech and its ' +
16743
+ 'values have a term in the list.');
16744
+ }
16745
+ }, [labelled]);
16746
+ // Its parts take its locale and leave the row, so a Stat inside one of
16747
+ // them, in a `<dd>`, renders a `<dl>` of its own.
16748
+ const inner = React.useMemo(() => ({ row: false, locale }), [locale]);
16749
+ const Root = scope.row ? 'div' : 'dl';
16750
+ const figure = loading ? (jsxRuntime.jsx(Skeleton, {})) : typeof value === 'number' ? (jsxRuntime.jsx(NumberFormatter, { value: value, format: format, locale: locale })) : isGiven(value) ? (value) : null;
16751
+ // A `Stat.Value` child is the figure. Without one, the figure's `<dd>`
16752
+ // renders for a figure, or for a label, empty while the figure is
16753
+ // missing, so the label keeps its definition.
16754
+ const showsFigure = !hasValuePart && (figure !== null || labelled);
16755
+ return (jsxRuntime.jsx(Root, { ref: ref, className: classNames(statClass, bulmaHelperClasses, className), ...rest, "aria-busy": loading || rest['aria-busy'], children: jsxRuntime.jsxs(StatScopeContext.Provider, { value: inner, children: [isGiven(label) && jsxRuntime.jsx(StatLabel, { children: label }), children, showsFigure && jsxRuntime.jsx(StatValue, { children: figure }), typeof change === 'number' && (jsxRuntime.jsx(StatChange, { value: change, format: changeFormat, color: changeColor })), isGiven(description) && (jsxRuntime.jsx(StatDescription, { children: description }))] }) }));
16756
+ });
16757
+ const Stat = withSubComponents(StatRoot, {
16758
+ Label: StatLabel,
16759
+ Value: StatValue,
16760
+ Change: StatChange,
16761
+ Description: StatDescription,
16762
+ }, 'Stat');
16763
+
16764
+ /**
16765
+ * The `Stats` component lays out a row of `Stat`s, such as the key figures at the top of a dashboard, as one description list on Bulma's smart grid.
16766
+ *
16767
+ * It's a `<dl>` with a `<div>` for each Stat, so a screen reader announces
16768
+ * the row once, then each label with its values. The tiles fill the grid's
16769
+ * columns, each at least `minCol` wide, and wrap without media queries;
16770
+ * the `gap` helper prop sets the space between them. Name the row with
16771
+ * `aria-labelledby` pointing at the heading above it. `Stat` is also
16772
+ * `Stats.Stat`.
16773
+ *
16774
+ * @function
16775
+ * @param {StatsProps} props - Props for the Stats component.
16776
+ * @param {React.Ref<HTMLDListElement>} ref - Forwarded ref to the `<dl>`.
16777
+ * @returns {JSX.Element} The rendered row.
16778
+ *
16779
+ * @example
16780
+ * <Stats aria-labelledby="kpis" minCol={8} gap="4">
16781
+ * <Stat label="Revenue" value={8456.4} format={{ style: 'currency', currency: 'USD' }} />
16782
+ * <Stat label="New customers" value={1234} change={-0.019} />
16783
+ * </Stats>
16784
+ */
16785
+ const StatsRoot = React.forwardRef(({ className, minCol, textColor, bgColor, children, ...props }, ref) => {
16786
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
16787
+ color: textColor,
16788
+ backgroundColor: bgColor,
16789
+ ...props,
16790
+ });
16791
+ const statsClass = usePrefixedClassNames('stats grid', {
16792
+ [`is-col-min-${minCol}`]: minCol !== undefined && minCol !== null,
16793
+ });
16794
+ const scope = React.useMemo(() => ({ row: true, textColor }), [textColor]);
16795
+ return (jsxRuntime.jsx("dl", { ref: ref, className: classNames(statsClass, bulmaHelperClasses, className), ...rest, children: jsxRuntime.jsx(StatScopeContext.Provider, { value: scope, children: children }) }));
16796
+ });
16797
+ StatsRoot.displayName = 'Stats';
16798
+ const Stats = withSubComponents(StatsRoot, { Stat }, 'Stats');
16799
+
15937
16800
  /**
15938
16801
  * The `Strong` component renders a styled `<strong>` element with Bulma helper class integration.
15939
16802
  *
@@ -19261,7 +20124,7 @@ const ComboboxItem = ({ value, disabled = false, checked = false, children, clas
19261
20124
  onPointerMove?.(event);
19262
20125
  if (!disabled && !highlighted)
19263
20126
  engine.hover(value);
19264
- }, children: [checked && (jsxRuntime.jsx("span", { className: checkClass, "aria-hidden": "true", children: jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", width: "1em", height: "1em", focusable: "false", children: jsxRuntime.jsx("polyline", { points: "3 8.5 6.5 12 13 4.5" }) }) })), children] }));
20127
+ }, children: [checked && (jsxRuntime.jsx("span", { className: checkClass, "aria-hidden": "true", children: jsxRuntime.jsx(CheckIcon$1, {}) })), children] }));
19265
20128
  };
19266
20129
  ComboboxItem.displayName = 'ComboboxItem';
19267
20130
  /**
@@ -21876,6 +22739,167 @@ const TextAreaBase = React.forwardRef(({ color, size, isRounded, isStatic, isHov
21876
22739
  });
21877
22740
  TextAreaBase.displayName = 'TextAreaBase';
21878
22741
 
22742
+ /** An eye, drawn while the password is hidden. */
22743
+ const EyeIcon = () => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "1.25em", height: "1.25em", "aria-hidden": "true", children: jsxRuntime.jsxs("g", { fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "M1.5 8s2.5-4.5 6.5-4.5 6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8z" }), jsxRuntime.jsx("circle", { cx: "8", cy: "8", r: "2" })] }) }));
22744
+ /** The eye crossed out, drawn while the password shows. */
22745
+ const EyeOffIcon = () => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "1.25em", height: "1.25em", "aria-hidden": "true", children: jsxRuntime.jsxs("g", { fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "M1.5 8s2.5-4.5 6.5-4.5 6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8z" }), jsxRuntime.jsx("circle", { cx: "8", cy: "8", r: "2" }), jsxRuntime.jsx("path", { d: "M2.5 2.5l11 11" })] }) }));
22746
+ /**
22747
+ * Warns in development when the `text` toggle's name doesn't contain the
22748
+ * word on it, which a voice control user says to press it.
22749
+ */
22750
+ const checkLabelInName = (name, word) => {
22751
+ if (name.toLocaleLowerCase().includes(word.toLocaleLowerCase()))
22752
+ return;
22753
+ warnOnce(`PasswordInput:label-in-name:${name}:${word}`, `[bestax-bulma] A PasswordInput with variant="text" is named ` +
22754
+ `"${name}" while its button says "${word}". A voice control user ` +
22755
+ `says the word they see, so the name has to contain it (WCAG 2.5.3, ` +
22756
+ `Label in Name). Set labels so each name contains its word.`);
22757
+ };
22758
+ /**
22759
+ * The raw PasswordInput: a password field with a button inside it that shows what was typed, without Field/Control wrapping.
22760
+ *
22761
+ * Use `PasswordInput` for the convenience wrapper.
22762
+ *
22763
+ * @function
22764
+ * @param {PasswordInputBaseProps} props - Props for PasswordInputBase.
22765
+ * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.
22766
+ * @returns {JSX.Element} The input and its toggle, in a container.
22767
+ */
22768
+ const PasswordInputBase = React.forwardRef((props, ref) => {
22769
+ const { visible: visibleProp, defaultVisible = false, onVisibleChange, variant = 'icon', labels, autoComplete = 'current-password', spellCheck = false, autoCorrect = 'off', autoCapitalize = 'none', disabled: disabledProp, color, className, style, ...others } = props;
22770
+ // Helper props, `className` and `style` go on the container, so a margin,
22771
+ // a width or a display moves the field and its toggle together. `color`
22772
+ // stays with the input, whose border it colors.
22773
+ const { bulmaHelperClasses, rest } = useBulmaClasses(others);
22774
+ const disabled = useFieldsetDisabled(disabledProp);
22775
+ // The toggle gives way to the loading spinner of a Control this sits in,
22776
+ // which takes its slot, and the field hides the password meanwhile, as
22777
+ // there's no button to hide it with.
22778
+ const controlLoading = useControlLoading();
22779
+ const isText = variant === 'text';
22780
+ const showPassword = labels?.showPassword || 'Show password';
22781
+ const hidePassword = labels?.hidePassword || 'Hide password';
22782
+ const show = labels?.show || 'Show';
22783
+ const hide = labels?.hide || 'Hide';
22784
+ if (isText) {
22785
+ checkLabelInName(showPassword, show);
22786
+ checkLabelInName(hidePassword, hide);
22787
+ }
22788
+ const isControlled = visibleProp !== undefined;
22789
+ const [internalVisible, setInternalVisible] = React.useState(defaultVisible);
22790
+ const visible = isControlled ? visibleProp : internalVisible;
22791
+ // Whether the field shows now: as last reported, until `visible` changes.
22792
+ // Two hide events before React renders, as from an effect, or a caller
22793
+ // holding `visible` that keeps it `true`, report `false` once.
22794
+ const shownRef = React.useRef(visible);
22795
+ useClientLayoutEffect(() => {
22796
+ shownRef.current = visible;
22797
+ }, [visible]);
22798
+ const setVisible = (next) => {
22799
+ shownRef.current = next;
22800
+ if (!isControlled)
22801
+ setInternalVisible(next);
22802
+ onVisibleChange?.(next);
22803
+ };
22804
+ const inputRef = React.useRef(null);
22805
+ // A caller's callback ref may return a cleanup, as React 19 allows. It's
22806
+ // held here and run on detach in place of a call with `null`, on React 18
22807
+ // as well, since this ref is the one React calls.
22808
+ const refCleanup = React.useRef(null);
22809
+ const combinedRef = React.useCallback((node) => {
22810
+ inputRef.current = node;
22811
+ if (typeof ref === 'function') {
22812
+ if (node === null) {
22813
+ const cleanup = refCleanup.current;
22814
+ refCleanup.current = null;
22815
+ if (cleanup)
22816
+ cleanup();
22817
+ else
22818
+ ref(null);
22819
+ return;
22820
+ }
22821
+ const cleanup = ref(node);
22822
+ refCleanup.current =
22823
+ typeof cleanup === 'function' ? cleanup : null;
22824
+ }
22825
+ else if (ref) {
22826
+ ref.current = node;
22827
+ }
22828
+ }, [ref]);
22829
+ // A toggle with focus that leaves while the input stays, as it does when a
22830
+ // Control starts loading, hands focus to the input rather than dropping it
22831
+ // to the page. React lets go of the toggle before it takes it out of the
22832
+ // page, so the toggle still has focus here; when the whole field unmounts,
22833
+ // React has let go of the input first.
22834
+ const toggleRef = React.useRef(null);
22835
+ const setToggle = React.useCallback((node) => {
22836
+ // React lets go only of a toggle it handed over, so there is one.
22837
+ const leaving = toggleRef.current;
22838
+ toggleRef.current = node;
22839
+ if (node)
22840
+ return;
22841
+ const root = leaving.getRootNode();
22842
+ if (root.activeElement === leaving)
22843
+ inputRef.current?.focus();
22844
+ }, []);
22845
+ // Hides the password again: reports `false`, and sets its own state when it
22846
+ // holds it. Inside `flushSync`, React renders that, a caller's own state
22847
+ // update included, before the listener returns, so the browser reads the
22848
+ // form, or saves the page's form state, from a password field. The
22849
+ // component never sets the field's type itself, so a caller holding
22850
+ // `visible` that keeps it `true` keeps the field shown. The one catch: an
22851
+ // event fired while React is rendering or committing, which includes
22852
+ // `useEffect` and `useLayoutEffect` callbacks (a `requestSubmit()` from
22853
+ // an effect), can't flush there, so React warns in development and the
22854
+ // hide lands after the form is read. Only the listeners below call it,
22855
+ // while the input is mounted.
22856
+ const hideRef = useLatestRef(() => {
22857
+ if (!shownRef.current)
22858
+ return;
22859
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the form is read, or the page saved, before React would render this on its own
22860
+ reactDom.flushSync(() => setVisible(false));
22861
+ });
22862
+ // When the form it belongs to submits or resets: whichever form that is
22863
+ // when it happens, so a form mounted after the field, or mounted again,
22864
+ // counts. Neither event leaves a shadow root, so it listens on the field's
22865
+ // own root, the document or the shadow root it renders in, and in the
22866
+ // capture phase, before any listener on the form.
22867
+ React.useEffect(() => {
22868
+ const input = inputRef.current;
22869
+ const root = input.getRootNode();
22870
+ const onFormEvent = (event) => {
22871
+ if (event.target === input.form)
22872
+ hideRef.current();
22873
+ };
22874
+ root.addEventListener('submit', onFormEvent, true);
22875
+ root.addEventListener('reset', onFormEvent, true);
22876
+ return () => {
22877
+ root.removeEventListener('submit', onFormEvent, true);
22878
+ root.removeEventListener('reset', onFormEvent, true);
22879
+ };
22880
+ }, [hideRef]);
22881
+ // When the page is left, so the history the browser keeps of the form
22882
+ // holds a password field rather than text. It fires on the way into the
22883
+ // back-forward cache too, so a page coming back from there is hidden.
22884
+ React.useEffect(() => {
22885
+ const onPageHide = () => hideRef.current();
22886
+ window.addEventListener('pagehide', onPageHide);
22887
+ return () => window.removeEventListener('pagehide', onPageHide);
22888
+ }, [hideRef]);
22889
+ const containerClass = usePrefixedClassNames('password-input-container', {
22890
+ 'is-text': isText,
22891
+ });
22892
+ const toggleClass = usePrefixedClassNames('password-input-toggle');
22893
+ // The icon keeps one name and reports its state; the words change, so
22894
+ // the name follows them and there's no state to report.
22895
+ const toggleName = isText && visible ? hidePassword : showPassword;
22896
+ const glyph = visible ? jsxRuntime.jsx(EyeOffIcon, {}) : jsxRuntime.jsx(EyeIcon, {});
22897
+ return (jsxRuntime.jsxs("div", { className: classNames(containerClass, bulmaHelperClasses, className), style: style, children: [jsxRuntime.jsx(InputBase, { ref: combinedRef, autoComplete: autoComplete, spellCheck: spellCheck, autoCorrect: autoCorrect, autoCapitalize: autoCapitalize, disabled: disabled, color: color, ...rest, type: visible && !controlLoading ? 'text' : 'password' }), !controlLoading && (jsxRuntime.jsx("button", { ref: setToggle, type: "button", className: toggleClass, "aria-label": toggleName, "aria-pressed": isText ? undefined : visible, disabled: disabled,
22898
+ // A pointer press leaves focus, and the caret, in the input.
22899
+ onMouseDown: event => event.preventDefault(), onClick: () => setVisible(!visible), children: isText ? (visible ? hide : show) : glyph }))] }));
22900
+ });
22901
+ PasswordInputBase.displayName = 'PasswordInputBase';
22902
+
21879
22903
  /**
21880
22904
  * The `Select` component provides a Bulma-styled dropdown for selecting one or more options.
21881
22905
  *
@@ -22010,6 +23034,97 @@ fieldClassName, controlClassName,
22010
23034
  });
22011
23035
  TextArea.displayName = 'TextArea';
22012
23036
 
23037
+ /** The right-icon props, which the toggle's place leaves no room for. */
23038
+ const RIGHT_ICON_PROPS = [
23039
+ 'iconRight',
23040
+ 'iconRightName',
23041
+ 'iconRightSize',
23042
+ 'hasIconsRight',
23043
+ ];
23044
+ /**
23045
+ * The `PasswordInput` component is a password field with a button inside it that shows what was typed.
23046
+ *
23047
+ * By default the button is an eye named "Show password" that reports
23048
+ * whether the password shows with `aria-pressed`; `variant="text"` makes it
23049
+ * the words "Show" and "Hide". The field asks for
23050
+ * `autocomplete="current-password"` and no spellcheck, and hides the
23051
+ * password again when its form submits.
23052
+ *
23053
+ * @function
23054
+ * @param {PasswordInputProps} props - Props for PasswordInput.
23055
+ * @returns {JSX.Element} The composed field element.
23056
+ *
23057
+ * @example
23058
+ * <PasswordInput label="Password" />
23059
+ *
23060
+ * @example
23061
+ * <PasswordInput label="New password" autoComplete="new-password" variant="text" />
23062
+ */
23063
+ const PasswordInput = React.forwardRef((props, ref) => {
23064
+ const {
23065
+ // Field props
23066
+ label, labelSize, labelProps, horizontal,
23067
+ // Control props
23068
+ iconLeft, iconLeftName, iconLeftSize, hasIconsLeft, isLoading, isExpanded, controlSize,
23069
+ // Message props
23070
+ message, messageColor,
23071
+ // Container class overrides
23072
+ fieldClassName, controlClassName,
23073
+ // Not offered, and not passed on
23074
+ iconRight: _iconRight, iconRightName: _iconRightName, iconRightSize: _iconRightSize, hasIconsRight: _hasIconsRight,
23075
+ // Everything else goes to the base
23076
+ ...baseProps } = props;
23077
+ const rightIcon = RIGHT_ICON_PROPS.filter(prop => props[prop]);
23078
+ if (rightIcon.length > 0) {
23079
+ const names = rightIcon.join(' ');
23080
+ warnOnce(`PasswordInput:right-icon:${names}`, `[bestax-bulma] <PasswordInput ${names}> draws no right icon: the ` +
23081
+ `visibility toggle sits at that edge. Leave ` +
23082
+ `${rightIcon.length > 1 ? 'them' : 'it'} out, or use a left icon.`);
23083
+ }
23084
+ const insideField = useInsideField();
23085
+ const insideControl = useInsideControl();
23086
+ const ownField = rendersOwnField('PasswordInput', {
23087
+ insideField,
23088
+ insideControl,
23089
+ label,
23090
+ message,
23091
+ horizontal,
23092
+ fieldClassName,
23093
+ });
23094
+ const ownControl = rendersOwnControl('PasswordInput', {
23095
+ insideControl,
23096
+ isLoading,
23097
+ iconLeft,
23098
+ iconLeftName,
23099
+ iconLeftSize,
23100
+ hasIconsLeft,
23101
+ isExpanded,
23102
+ controlSize,
23103
+ controlClassName,
23104
+ });
23105
+ const { controlId, fieldLabelProps } = useAutoLabelId({
23106
+ label,
23107
+ id: baseProps.id,
23108
+ labelProps,
23109
+ rendersLabel: ownField,
23110
+ });
23111
+ const helpClass = usePrefixedClassNames('help', {
23112
+ [`is-${messageColor}`]: !!messageColor,
23113
+ });
23114
+ // The base hides its toggle while the Control it sits in is loading,
23115
+ // whether that is the one rendered below or an enclosing one.
23116
+ let content = (jsxRuntime.jsx(PasswordInputBase, { ref: ref, ...baseProps, id: controlId }));
23117
+ if (ownControl) {
23118
+ content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
23119
+ }
23120
+ const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
23121
+ if (ownField) {
23122
+ return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
23123
+ }
23124
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
23125
+ });
23126
+ PasswordInput.displayName = 'PasswordInput';
23127
+
22013
23128
  function isSameDay(a, b) {
22014
23129
  return (a.getFullYear() === b.getFullYear() &&
22015
23130
  a.getMonth() === b.getMonth() &&
@@ -29318,6 +30433,7 @@ exports.Container = Container;
29318
30433
  exports.Content = Content;
29319
30434
  exports.ContextMenu = ContextMenu;
29320
30435
  exports.Control = Control;
30436
+ exports.CopyButton = CopyButton;
29321
30437
  exports.DataTable = DataTable;
29322
30438
  exports.DateInput = DateInput;
29323
30439
  exports.DateInputBase = DateInputBase;
@@ -29419,6 +30535,8 @@ exports.PanelIcon = PanelIcon;
29419
30535
  exports.PanelInputBlock = PanelInputBlock;
29420
30536
  exports.PanelTabs = PanelTabs;
29421
30537
  exports.Paragraph = Paragraph;
30538
+ exports.PasswordInput = PasswordInput;
30539
+ exports.PasswordInputBase = PasswordInputBase;
29422
30540
  exports.PinInput = PinInput;
29423
30541
  exports.Popover = Popover;
29424
30542
  exports.PopoverBody = PopoverBody;
@@ -29446,6 +30564,12 @@ exports.Span = Span;
29446
30564
  exports.Splitter = Splitter;
29447
30565
  exports.SplitterPane = SplitterPane;
29448
30566
  exports.Stack = Stack;
30567
+ exports.Stat = Stat;
30568
+ exports.StatChange = StatChange;
30569
+ exports.StatDescription = StatDescription;
30570
+ exports.StatLabel = StatLabel;
30571
+ exports.StatValue = StatValue;
30572
+ exports.Stats = Stats;
29449
30573
  exports.Step = Step;
29450
30574
  exports.Steps = Steps;
29451
30575
  exports.Strong = Strong;
@@ -29456,6 +30580,7 @@ exports.TabContentItem = TabContentItem;
29456
30580
  exports.TabItem = TabItem;
29457
30581
  exports.TabList = TabList;
29458
30582
  exports.Table = Table;
30583
+ exports.TableOfContents = TableOfContents;
29459
30584
  exports.Tabs = Tabs;
29460
30585
  exports.TabsContent = TabsContent;
29461
30586
  exports.Tag = Tag;
@@ -29501,6 +30626,7 @@ exports.toast = toast;
29501
30626
  exports.useAppFrame = useAppFrame;
29502
30627
  exports.useBulmaClasses = useBulmaClasses;
29503
30628
  exports.useClassPrefix = useClassPrefix;
30629
+ exports.useClipboard = useClipboard;
29504
30630
  exports.useColorClasses = useColorClasses;
29505
30631
  exports.useColorStyles = useColorStyles;
29506
30632
  exports.useConfig = useConfig;
@@ -29516,6 +30642,7 @@ exports.useNumberFormat = useNumberFormat;
29516
30642
  exports.useOtherClasses = useOtherClasses;
29517
30643
  exports.usePrefixedClass = usePrefixedClass;
29518
30644
  exports.usePrefixedClassNames = usePrefixedClassNames;
30645
+ exports.useScrollSpy = useScrollSpy;
29519
30646
  exports.useSpacingClasses = useSpacingClasses;
29520
30647
  exports.useTypographyClasses = useTypographyClasses;
29521
30648
  exports.useVisibilityClasses = useVisibilityClasses;