@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.
- package/dist/bestax.css +1 -1
- package/dist/bestax.css.map +1 -1
- package/dist/esm/components/EmptyState.js +2 -6
- package/dist/esm/components/EmptyState.js.map +1 -1
- package/dist/esm/components/ScrollArea.js +7 -7
- package/dist/esm/components/ScrollArea.js.map +1 -1
- package/dist/esm/components/TableOfContents.js +85 -0
- package/dist/esm/components/TableOfContents.js.map +1 -0
- package/dist/esm/components/Timeline.js +5 -9
- package/dist/esm/components/Timeline.js.map +1 -1
- package/dist/esm/components/TreeView.js +3 -7
- package/dist/esm/components/TreeView.js.map +1 -1
- package/dist/esm/elements/Buttons.js +2 -0
- package/dist/esm/elements/Buttons.js.map +1 -1
- package/dist/esm/elements/CopyButton.js +132 -0
- package/dist/esm/elements/CopyButton.js.map +1 -0
- package/dist/esm/elements/DescriptionList.js +1 -5
- package/dist/esm/elements/DescriptionList.js.map +1 -1
- package/dist/esm/elements/Stat.js +196 -0
- package/dist/esm/elements/Stat.js.map +1 -0
- package/dist/esm/elements/Stats.js +48 -0
- package/dist/esm/elements/Stats.js.map +1 -0
- package/dist/esm/elements/statContext.js +10 -0
- package/dist/esm/elements/statContext.js.map +1 -0
- package/dist/esm/form/Combobox.js +2 -1
- package/dist/esm/form/Combobox.js.map +1 -1
- package/dist/esm/form/PasswordInput.js +105 -0
- package/dist/esm/form/PasswordInput.js.map +1 -0
- package/dist/esm/form/PasswordInputBase.js +177 -0
- package/dist/esm/form/PasswordInputBase.js.map +1 -0
- package/dist/esm/helpers/checkIcon.js +20 -0
- package/dist/esm/helpers/checkIcon.js.map +1 -0
- package/dist/esm/helpers/isGiven.js +13 -0
- package/dist/esm/helpers/isGiven.js.map +1 -0
- package/dist/esm/helpers/statusRegion.js +3 -3
- package/dist/esm/helpers/useClipboard.js +176 -0
- package/dist/esm/helpers/useClipboard.js.map +1 -0
- package/dist/esm/helpers/useScrollSpy.js +286 -0
- package/dist/esm/helpers/useScrollSpy.js.map +1 -0
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +2671 -1544
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +8 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/TableOfContents.d.ts +87 -0
- package/dist/types/elements/Buttons.d.ts +1 -0
- package/dist/types/elements/CopyButton.d.ts +105 -0
- package/dist/types/elements/Stat.d.ts +242 -0
- package/dist/types/elements/Stats.d.ts +40 -0
- package/dist/types/elements/statContext.d.ts +18 -0
- package/dist/types/form/PasswordInput.d.ts +98 -0
- package/dist/types/form/PasswordInputBase.d.ts +91 -0
- package/dist/types/helpers/checkIcon.d.ts +10 -0
- package/dist/types/helpers/isGiven.d.ts +7 -0
- package/dist/types/helpers/useClipboard.d.ts +65 -0
- package/dist/types/helpers/useScrollSpy.d.ts +61 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types-cjs/components/TableOfContents.d.ts +87 -0
- package/dist/types-cjs/elements/Buttons.d.ts +1 -0
- package/dist/types-cjs/elements/CopyButton.d.ts +105 -0
- package/dist/types-cjs/elements/Stat.d.ts +242 -0
- package/dist/types-cjs/elements/Stats.d.ts +40 -0
- package/dist/types-cjs/elements/statContext.d.ts +18 -0
- package/dist/types-cjs/form/PasswordInput.d.ts +98 -0
- package/dist/types-cjs/form/PasswordInputBase.d.ts +91 -0
- package/dist/types-cjs/helpers/checkIcon.d.ts +10 -0
- package/dist/types-cjs/helpers/isGiven.d.ts +7 -0
- package/dist/types-cjs/helpers/useClipboard.d.ts +65 -0
- package/dist/types-cjs/helpers/useScrollSpy.d.ts +61 -0
- package/dist/types-cjs/index.d.ts +10 -0
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/CLAUDE.md +3 -0
- package/src/scss/_input-trigger.scss +76 -0
- package/src/scss/components/_index.scss +1 -0
- package/src/scss/components/_tableofcontents.scss +42 -0
- package/src/scss/elements/_index.scss +1 -0
- package/src/scss/elements/_stat.scss +153 -0
- package/src/scss/form/_index.scss +1 -0
- package/src/scss/form/_passwordinput.scss +112 -0
- 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
|
-
*
|
|
3942
|
-
*
|
|
3943
|
-
*
|
|
3944
|
-
*
|
|
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
|
-
* @
|
|
3953
|
-
*
|
|
3954
|
-
* <LinkButton variant="underline" color="primary">Learn more</LinkButton>
|
|
3947
|
+
* @function CheckIcon
|
|
3948
|
+
* @returns The check mark's `<svg>`.
|
|
3955
3949
|
*/
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
3980
|
-
*
|
|
3981
|
-
*
|
|
3982
|
-
*
|
|
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
|
-
*
|
|
3986
|
-
*
|
|
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
|
-
* @
|
|
3989
|
-
*
|
|
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
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
4035
|
-
*
|
|
4036
|
-
*
|
|
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
|
|
4069
|
-
* @returns
|
|
3990
|
+
* @function resolvePortalContainer
|
|
3991
|
+
* @param container - The requested portal target, if any.
|
|
3992
|
+
* @returns The resolved DOM node to portal into.
|
|
4070
3993
|
*/
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
4091
|
-
*
|
|
4092
|
-
*
|
|
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
|
-
* @
|
|
4137
|
-
*
|
|
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
|
-
|
|
4140
|
-
const {
|
|
4141
|
-
|
|
4142
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
4155
|
-
*
|
|
4156
|
-
*
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
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
|
|
4172
|
-
* @returns
|
|
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
|
-
|
|
4175
|
-
const
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
4183
|
-
*
|
|
4184
|
-
*
|
|
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
|
-
*
|
|
4200
|
-
*
|
|
4201
|
-
*
|
|
4202
|
-
*
|
|
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
|
-
*
|
|
4205
|
-
*
|
|
4206
|
-
*
|
|
4207
|
-
*
|
|
4208
|
-
*
|
|
4209
|
-
*
|
|
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 {
|
|
4213
|
-
* @
|
|
4214
|
-
*
|
|
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
|
-
* <
|
|
4218
|
-
*
|
|
4219
|
-
*
|
|
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
|
-
*
|
|
4256
|
-
*
|
|
4257
|
-
*
|
|
4258
|
-
*
|
|
4095
|
+
* @example
|
|
4096
|
+
* // Into a dedicated overlay root
|
|
4097
|
+
* <Portal container="#overlays">
|
|
4098
|
+
* <CommandPalette />
|
|
4099
|
+
* </Portal>
|
|
4259
4100
|
*/
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
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
|
-
*
|
|
4274
|
-
*
|
|
4275
|
-
*
|
|
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
|
|
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
|
-
*
|
|
4288
|
-
*
|
|
4289
|
-
*
|
|
4290
|
-
*
|
|
4291
|
-
*
|
|
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
|
|
4294
|
-
|
|
4295
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
4301
|
-
*
|
|
4302
|
-
*
|
|
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
|
|
4306
|
-
* @
|
|
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
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
const
|
|
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
|
-
* @
|
|
4680
|
-
*
|
|
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
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
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 (
|
|
8776
|
+
if (e.key === 'Escape') {
|
|
8777
|
+
if (isOpen && closeOnEscape) {
|
|
8778
|
+
e.preventDefault();
|
|
8779
|
+
close();
|
|
8780
|
+
}
|
|
8400
8781
|
return;
|
|
8401
8782
|
}
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
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
|
-
|
|
8410
|
-
|
|
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('
|
|
8413
|
-
const
|
|
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("
|
|
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
|
-
*
|
|
8444
|
-
*
|
|
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
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
8487
|
-
* @param
|
|
8488
|
-
* @returns
|
|
8867
|
+
* @function
|
|
8868
|
+
* @param {PopoverBodyProps} props - Props for the Popover.Body component.
|
|
8869
|
+
* @returns {JSX.Element} The body element.
|
|
8489
8870
|
*/
|
|
8490
|
-
|
|
8491
|
-
const
|
|
8492
|
-
|
|
8493
|
-
|
|
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
|
-
*
|
|
8589
|
-
*
|
|
8590
|
-
*
|
|
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
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
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
|
-
*
|
|
8887
|
+
* A `Button` that closes the popover, handing focus back to the trigger.
|
|
8608
8888
|
*
|
|
8609
|
-
*
|
|
8610
|
-
*
|
|
8611
|
-
*
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
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
|
-
*
|
|
8621
|
-
*
|
|
8622
|
-
*
|
|
8623
|
-
*
|
|
8624
|
-
*
|
|
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 {
|
|
8628
|
-
* @returns {JSX.Element} The trigger, and the
|
|
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
|
-
* <
|
|
8632
|
-
*
|
|
8633
|
-
*
|
|
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
|
|
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 {
|
|
8643
|
-
const
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
const
|
|
8648
|
-
const
|
|
8649
|
-
const
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
const
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
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
|
-
//
|
|
8665
|
-
//
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
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 (
|
|
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
|
-
|
|
8765
|
-
};
|
|
8766
|
-
const handleClick = (event) => {
|
|
8767
|
-
onClick?.(event);
|
|
8768
|
-
if (!closeOnClick)
|
|
8985
|
+
const triggerNode = triggerRef.current;
|
|
8986
|
+
if (!triggerNode)
|
|
8769
8987
|
return;
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
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
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
'
|
|
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
|
|
8798
|
-
const
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
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
|
-
*
|
|
8806
|
-
*
|
|
8807
|
-
*
|
|
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
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
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
|
-
*
|
|
8816
|
-
*
|
|
8817
|
-
*
|
|
8818
|
-
*
|
|
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
|
-
|
|
8822
|
-
const {
|
|
8823
|
-
const
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
8854
|
-
*
|
|
8855
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
8869
|
-
*
|
|
8870
|
-
*
|
|
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
|
-
|
|
8882
|
-
const
|
|
8883
|
-
|
|
8884
|
-
const
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
}
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
8939
|
-
*
|
|
8940
|
-
*
|
|
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
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
8989
|
-
*
|
|
8990
|
-
*
|
|
8991
|
-
*
|
|
8992
|
-
*
|
|
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
|
-
*
|
|
8995
|
-
*
|
|
8996
|
-
*
|
|
8997
|
-
*
|
|
8998
|
-
*
|
|
8999
|
-
*
|
|
9000
|
-
* @
|
|
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
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
const
|
|
9006
|
-
//
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
const
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
React.
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
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
|
-
|
|
9275
|
+
const box = boxRef.current;
|
|
9276
|
+
const menuNode = menuRef.current;
|
|
9277
|
+
if (!doc || !box || !menuNode)
|
|
9046
9278
|
return undefined;
|
|
9047
|
-
const
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
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
|
-
|
|
9084
|
-
|
|
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
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
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
|
-
*
|
|
9158
|
-
*
|
|
9159
|
-
*
|
|
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
|
|
9166
|
-
|
|
9167
|
-
|
|
9398
|
+
const ContextMenu = withSubComponents(ContextMenuComponent, {
|
|
9399
|
+
Item: DropdownItem,
|
|
9400
|
+
Divider: DropdownDivider,
|
|
9401
|
+
}, 'ContextMenu');
|
|
9402
|
+
|
|
9168
9403
|
/**
|
|
9169
|
-
*
|
|
9170
|
-
* tidied.
|
|
9404
|
+
* Individual Step component for use inside Steps.
|
|
9171
9405
|
*
|
|
9172
|
-
* @function
|
|
9173
|
-
* @param
|
|
9174
|
-
* @returns
|
|
9406
|
+
* @function
|
|
9407
|
+
* @param {StepProps} props - Props for the Step component.
|
|
9408
|
+
* @returns {JSX.Element} The rendered step segment.
|
|
9175
9409
|
*/
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
const
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
9200
|
-
*
|
|
9201
|
-
*
|
|
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
|
-
* @
|
|
9205
|
-
*
|
|
9206
|
-
*
|
|
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
|
-
|
|
9209
|
-
const
|
|
9210
|
-
//
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
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
|
|
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
|
|
9280
|
-
(typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon) }), jsxRuntime.jsxs("div", { className: contentClass, children: [isGiven
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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 `<
|
|
14794
|
-
*
|
|
14795
|
-
*
|
|
14796
|
-
*
|
|
14797
|
-
*
|
|
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 {
|
|
14806
|
-
* @param {React.Ref<
|
|
14807
|
-
* @returns The
|
|
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
|
-
* <
|
|
14811
|
-
*
|
|
14812
|
-
*
|
|
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
|
|
14815
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses(
|
|
14816
|
-
|
|
14817
|
-
|
|
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
|
-
|
|
14820
|
-
|
|
14821
|
-
const
|
|
14822
|
-
|
|
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
|
-
|
|
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(
|
|
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;
|