@allxsmith/bestax-bulma 5.27.8 → 5.29.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/Dialog.js +8 -4
- package/dist/esm/components/Dialog.js.map +1 -1
- package/dist/esm/components/HoverCard.js +127 -0
- package/dist/esm/components/HoverCard.js.map +1 -0
- package/dist/esm/components/Modal.js +19 -11
- package/dist/esm/components/Modal.js.map +1 -1
- package/dist/esm/components/Popover.js.map +1 -1
- package/dist/esm/components/Sidebar.js +12 -3
- package/dist/esm/components/Sidebar.js.map +1 -1
- package/dist/esm/components/Toast.js +5 -3
- package/dist/esm/components/Toast.js.map +1 -1
- package/dist/esm/elements/Notification.js +3 -1
- package/dist/esm/elements/Notification.js.map +1 -1
- package/dist/esm/form/DateInput.js.map +1 -1
- package/dist/esm/form/DateInputBase.js.map +1 -1
- package/dist/esm/form/DateRangeInputBase.js.map +1 -1
- package/dist/esm/form/DateTimeInput.js.map +1 -1
- package/dist/esm/form/DateTimeInputBase.js.map +1 -1
- package/dist/esm/form/Field.js.map +1 -1
- package/dist/esm/form/File.js.map +1 -1
- package/dist/esm/form/Input.js.map +1 -1
- package/dist/esm/form/Numberinput.js.map +1 -1
- package/dist/esm/form/Select.js.map +1 -1
- package/dist/esm/form/Slider.js.map +1 -1
- package/dist/esm/form/Taginput.js.map +1 -1
- package/dist/esm/form/TextArea.js.map +1 -1
- package/dist/esm/form/TimeInput.js.map +1 -1
- package/dist/esm/form/TimeInputBase.js.map +1 -1
- package/dist/esm/form/_pickerInternals/PickerPopover.js +3 -1
- package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
- package/dist/esm/helpers/Config.js +5 -2
- package/dist/esm/helpers/Config.js.map +1 -1
- package/dist/esm/helpers/portal.js +30 -5
- package/dist/esm/helpers/portal.js.map +1 -1
- package/dist/esm/helpers/useFocusTrap.js +2 -1
- package/dist/esm/helpers/useFocusTrap.js.map +1 -1
- package/dist/esm/helpers/useHoverOpen.js +390 -0
- package/dist/esm/helpers/useHoverOpen.js.map +1 -0
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +574 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +1 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/HoverCard.d.ts +138 -0
- package/dist/types/components/Modal.d.ts +2 -1
- package/dist/types/components/Popover.d.ts +8 -7
- package/dist/types/form/DateInput.d.ts +1 -1
- package/dist/types/form/DateInputBase.d.ts +4 -1
- package/dist/types/form/DateRangeInputBase.d.ts +4 -1
- package/dist/types/form/DateTimeInput.d.ts +1 -1
- package/dist/types/form/DateTimeInputBase.d.ts +4 -1
- package/dist/types/form/Field.d.ts +1 -1
- package/dist/types/form/File.d.ts +1 -1
- package/dist/types/form/Input.d.ts +1 -1
- package/dist/types/form/Numberinput.d.ts +1 -1
- package/dist/types/form/Select.d.ts +1 -1
- package/dist/types/form/Slider.d.ts +1 -1
- package/dist/types/form/Taginput.d.ts +1 -1
- package/dist/types/form/TextArea.d.ts +1 -1
- package/dist/types/form/TimeInput.d.ts +1 -1
- package/dist/types/form/TimeInputBase.d.ts +2 -1
- package/dist/types/helpers/Config.d.ts +34 -3
- package/dist/types/helpers/portal.d.ts +27 -5
- package/dist/types/helpers/useFocusTrap.d.ts +9 -0
- package/dist/types/helpers/useHoverOpen.d.ts +98 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types-cjs/components/HoverCard.d.ts +138 -0
- package/dist/types-cjs/components/Modal.d.ts +2 -1
- package/dist/types-cjs/components/Popover.d.ts +8 -7
- package/dist/types-cjs/form/DateInput.d.ts +1 -1
- package/dist/types-cjs/form/DateInputBase.d.ts +4 -1
- package/dist/types-cjs/form/DateRangeInputBase.d.ts +4 -1
- package/dist/types-cjs/form/DateTimeInput.d.ts +1 -1
- package/dist/types-cjs/form/DateTimeInputBase.d.ts +4 -1
- package/dist/types-cjs/form/Field.d.ts +1 -1
- package/dist/types-cjs/form/File.d.ts +1 -1
- package/dist/types-cjs/form/Input.d.ts +1 -1
- package/dist/types-cjs/form/Numberinput.d.ts +1 -1
- package/dist/types-cjs/form/Select.d.ts +1 -1
- package/dist/types-cjs/form/Slider.d.ts +1 -1
- package/dist/types-cjs/form/Taginput.d.ts +1 -1
- package/dist/types-cjs/form/TextArea.d.ts +1 -1
- package/dist/types-cjs/form/TimeInput.d.ts +1 -1
- package/dist/types-cjs/form/TimeInputBase.d.ts +2 -1
- package/dist/types-cjs/helpers/Config.d.ts +34 -3
- package/dist/types-cjs/helpers/portal.d.ts +27 -5
- package/dist/types-cjs/helpers/useFocusTrap.d.ts +9 -0
- package/dist/types-cjs/helpers/useHoverOpen.d.ts +98 -0
- package/dist/types-cjs/index.d.ts +1 -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 +2 -2
- package/src/scss/components/_hovercard.scss +118 -0
- package/src/scss/components/_index.scss +1 -0
package/dist/index.cjs
CHANGED
|
@@ -16,6 +16,9 @@ const useConfig = () => React.useContext(ConfigContext);
|
|
|
16
16
|
/**
|
|
17
17
|
* ConfigProvider injects configuration into all child components via React context.
|
|
18
18
|
*
|
|
19
|
+
* A nested provider replaces the outer one's settings rather than adding to
|
|
20
|
+
* them, so inside it any setting it leaves out is back to its default.
|
|
21
|
+
*
|
|
19
22
|
* @function
|
|
20
23
|
* @param {ConfigProviderProps} props - Provider props.
|
|
21
24
|
* @returns {JSX.Element} The context provider wrapping children.
|
|
@@ -25,8 +28,8 @@ const useConfig = () => React.useContext(ConfigContext);
|
|
|
25
28
|
* <App />
|
|
26
29
|
* </ConfigProvider>
|
|
27
30
|
*/
|
|
28
|
-
const ConfigProvider = ({ classPrefix, iconLibrary, children, }) => {
|
|
29
|
-
return (jsxRuntime.jsx(ConfigContext.Provider, { value: { classPrefix, iconLibrary }, children: children }));
|
|
31
|
+
const ConfigProvider = ({ classPrefix, iconLibrary, portalContainer, children, }) => {
|
|
32
|
+
return (jsxRuntime.jsx(ConfigContext.Provider, { value: { classPrefix, iconLibrary, portalContainer }, children: children }));
|
|
30
33
|
};
|
|
31
34
|
/**
|
|
32
35
|
* Utility hook to get the configured class prefix string.
|
|
@@ -2934,7 +2937,26 @@ function resolvePortalContainer(container) {
|
|
|
2934
2937
|
return container;
|
|
2935
2938
|
}
|
|
2936
2939
|
/**
|
|
2937
|
-
*
|
|
2940
|
+
* Where a component's portal goes, as a function to call when the portal is
|
|
2941
|
+
* created: the target the component was given, else the `portalContainer` a
|
|
2942
|
+
* `ConfigProvider` sets, else `document.body`, each read as
|
|
2943
|
+
* `resolvePortalContainer` reads it. A component that portals resolves its
|
|
2944
|
+
* target through this, so the provider's default reaches it.
|
|
2945
|
+
*
|
|
2946
|
+
* The hook only reads context, so it is safe to call on the server, ahead of
|
|
2947
|
+
* any early return. The function it returns reads the DOM, so call it only
|
|
2948
|
+
* where the portal is created, which the server never reaches.
|
|
2949
|
+
*
|
|
2950
|
+
* @function usePortalContainer
|
|
2951
|
+
* @returns A function that takes the component's own target, if any, and
|
|
2952
|
+
* returns the DOM node to portal into.
|
|
2953
|
+
*/
|
|
2954
|
+
function usePortalContainer() {
|
|
2955
|
+
const { portalContainer } = useConfig();
|
|
2956
|
+
return container => resolvePortalContainer(container || portalContainer);
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* 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.
|
|
2938
2960
|
*
|
|
2939
2961
|
* React's server renderer can't render a portal, so `Portal` renders nothing
|
|
2940
2962
|
* on the server and during hydration, and mounts its children in the commit
|
|
@@ -2945,8 +2967,12 @@ function resolvePortalContainer(container) {
|
|
|
2945
2967
|
* portaled children, and their React events bubble through the React tree.
|
|
2946
2968
|
* CSS inheritance follows the DOM instead: a scoped (non-`isRoot`) `Theme`
|
|
2947
2969
|
* sets its variables on a wrapper element the portaled content has left, so
|
|
2948
|
-
* that content sees the page-level theme
|
|
2949
|
-
*
|
|
2970
|
+
* that content sees the page-level theme, and styles scoped to a shadow root
|
|
2971
|
+
* don't reach `document.body` at all. To keep them, portal into an element
|
|
2972
|
+
* inside the themed area or the shadow root: pass it as `container`, or as
|
|
2973
|
+
* the `portalContainer` of a `ConfigProvider` around that area. The second
|
|
2974
|
+
* also reaches what the library's components portal, such as Popover's
|
|
2975
|
+
* `appendToBody` panel or a `Modal` with `portal`.
|
|
2950
2976
|
*
|
|
2951
2977
|
* Tab order and focus follow the DOM too. Content portaled from inside a
|
|
2952
2978
|
* focus trap is no longer inside the trapped element, so the trap neither
|
|
@@ -2972,11 +2998,12 @@ function resolvePortalContainer(container) {
|
|
|
2972
2998
|
*/
|
|
2973
2999
|
function Portal({ children, container, disabled = false }) {
|
|
2974
3000
|
const hydrated = useIsHydrated();
|
|
3001
|
+
const resolveContainer = usePortalContainer();
|
|
2975
3002
|
if (disabled)
|
|
2976
3003
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
2977
3004
|
if (!hydrated)
|
|
2978
3005
|
return null;
|
|
2979
|
-
return reactDom.createPortal(children,
|
|
3006
|
+
return reactDom.createPortal(children, resolveContainer(container));
|
|
2980
3007
|
}
|
|
2981
3008
|
|
|
2982
3009
|
/**
|
|
@@ -3131,7 +3158,7 @@ const ModalClose = ({ className, size = 'large', variant = 'delete', ...props })
|
|
|
3131
3158
|
* @see {@link https://bulma.io/documentation/components/modal/ | Bulma Modal documentation}
|
|
3132
3159
|
*/
|
|
3133
3160
|
const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClose, className, textColor, bgColor, modalCardTitle, modalCardFoot, type, children, closeOnEscape = true, lockScroll = true, portal = false, role, 'aria-modal': ariaModalProp, ...props }, ref) {
|
|
3134
|
-
const { classPrefix } = useConfig();
|
|
3161
|
+
const { classPrefix, portalContainer } = useConfig();
|
|
3135
3162
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
3136
3163
|
color: textColor,
|
|
3137
3164
|
backgroundColor: bgColor,
|
|
@@ -3181,10 +3208,17 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
|
|
|
3181
3208
|
// hydrating render have to stay inline and only move into the portal once
|
|
3182
3209
|
// we're past hydration.
|
|
3183
3210
|
const onClient = useIsHydrated();
|
|
3184
|
-
|
|
3185
|
-
//
|
|
3186
|
-
//
|
|
3211
|
+
const resolveContainer = usePortalContainer();
|
|
3212
|
+
// Moving into the portal, or from one portal target to another, remounts
|
|
3213
|
+
// the modal's subtree, so any effect holding a DOM node from before the move
|
|
3214
|
+
// is holding a detached one. Effects that touch the modal's nodes key on
|
|
3215
|
+
// this, on `portal` and on `defaultTarget`, so they re-run against the new
|
|
3216
|
+
// tree.
|
|
3187
3217
|
const isPortaled = Boolean(portal) && onClient;
|
|
3218
|
+
// The configured `portalContainer` only places a modal portaled with
|
|
3219
|
+
// `portal` set to `true`. In place, or with a target of its own, a change
|
|
3220
|
+
// to it moves nothing, so it mustn't re-run the effects either.
|
|
3221
|
+
const defaultTarget = isPortaled && portal === true ? portalContainer : undefined;
|
|
3188
3222
|
useScrollLock(isModalActive && lockScroll);
|
|
3189
3223
|
// Record open order so keyboard handling only applies to the topmost modal.
|
|
3190
3224
|
React.useEffect(() => {
|
|
@@ -3264,11 +3298,12 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
|
|
|
3264
3298
|
previouslyFocusedRef.current?.focus?.();
|
|
3265
3299
|
};
|
|
3266
3300
|
// `isPortaled` flips once when a hydrated portal modal moves out of the
|
|
3267
|
-
// inline tree
|
|
3268
|
-
//
|
|
3269
|
-
//
|
|
3270
|
-
//
|
|
3271
|
-
|
|
3301
|
+
// inline tree, and `portal` or `defaultTarget` changing moves a portaled
|
|
3302
|
+
// one to another target; re-running rebinds focus onto the remounted
|
|
3303
|
+
// nodes. The cleanup above restores focus to the pre-open element first
|
|
3304
|
+
// (the detached subtree has left focus on <body>), so the re-run re-records
|
|
3305
|
+
// the same element and restore-on-close still lands in the right place.
|
|
3306
|
+
}, [isModalActive, isPortaled, portal, defaultTarget]);
|
|
3272
3307
|
// Check if children contain compound components
|
|
3273
3308
|
const hasCompoundComponents = React.Children.toArray(children).some(child => React.isValidElement(child) &&
|
|
3274
3309
|
(child.type === ModalBackground ||
|
|
@@ -3305,7 +3340,7 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
|
|
|
3305
3340
|
modalElement = (jsxRuntime.jsxs("div", { className: modalClasses, ref: combinedRef, role: resolvedRole, "aria-modal": resolvedAriaModal, "aria-labelledby": titleId, tabIndex: -1, ...rest, "data-testid": "modal", children: [jsxRuntime.jsx("div", { className: prefixedClassNames(classPrefix, 'modal-background'), onClick: onClose, "data-testid": "modal-background" }), isModalCard ? (jsxRuntime.jsxs("div", { className: prefixedClassNames(classPrefix, 'modal-card'), children: [modalCardTitle && (jsxRuntime.jsxs("header", { className: prefixedClassNames(classPrefix, 'modal-card-head'), children: [jsxRuntime.jsx("p", { id: titleId, className: prefixedClassNames(classPrefix, 'modal-card-title'), children: modalCardTitle }), onClose && (jsxRuntime.jsx("button", { className: deleteClass, "aria-label": "close", onClick: onClose, type: "button", "data-testid": "modal-close" }))] })), jsxRuntime.jsx("section", { className: prefixedClassNames(classPrefix, 'modal-card-body'), "data-testid": "modal-body", children: children }), modalCardFoot && (jsxRuntime.jsx("footer", { className: prefixedClassNames(classPrefix, 'modal-card-foot'), children: modalCardFoot }))] })) : (jsxRuntime.jsx("div", { className: prefixedClassNames(classPrefix, 'modal-content'), "data-testid": "modal-content", children: children })), (!isModalCard || (!modalCardTitle && onClose)) && onClose && (jsxRuntime.jsx("button", { className: prefixedClassNames(classPrefix, 'modal-close', 'is-large'), "aria-label": "close", onClick: onClose, type: "button", "data-testid": "modal-close-float" }))] }));
|
|
3306
3341
|
}
|
|
3307
3342
|
if (isPortaled) {
|
|
3308
|
-
const target =
|
|
3343
|
+
const target = resolveContainer(typeof portal === 'boolean' ? undefined : portal);
|
|
3309
3344
|
return reactDom.createPortal(modalElement, target);
|
|
3310
3345
|
}
|
|
3311
3346
|
return modalElement;
|
|
@@ -5087,6 +5122,7 @@ const isDetailsSummary = (el) => el.parentElement?.localName === 'details' &&
|
|
|
5087
5122
|
* can read `-1` for an editable region or an `<embed>` that Tab still visits,
|
|
5088
5123
|
* and `0` for a `<video>` without controls that it doesn't. So a valid
|
|
5089
5124
|
* `tabindex` attribute decides, and otherwise the element's kind does.
|
|
5125
|
+
* Internal like `firstTabStop`: `src/index.ts` names what it takes from here.
|
|
5090
5126
|
*/
|
|
5091
5127
|
function isTabbableKind(el) {
|
|
5092
5128
|
const tabindex = el.getAttribute('tabindex');
|
|
@@ -6014,6 +6050,502 @@ const Popover = withSubComponents(PopoverComponent, {
|
|
|
6014
6050
|
Close: PopoverClose,
|
|
6015
6051
|
}, 'Popover');
|
|
6016
6052
|
|
|
6053
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
6054
|
+
/**
|
|
6055
|
+
* Whether focus that just arrived on `el` is keyboard focus, as the browser
|
|
6056
|
+
* decides it for `:focus-visible`: focus from a key or from the browser's
|
|
6057
|
+
* own controls is, focus a press brings isn't, and focus a script moves
|
|
6058
|
+
* follows whichever came last. A browser that can't match the selector
|
|
6059
|
+
* throws, and then every focus counts, so keyboard users can still open it.
|
|
6060
|
+
*/
|
|
6061
|
+
function isKeyboardFocus(el) {
|
|
6062
|
+
try {
|
|
6063
|
+
return el.matches(':focus-visible');
|
|
6064
|
+
}
|
|
6065
|
+
catch {
|
|
6066
|
+
return true;
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
/** Whether `node` is `container` or inside it, when there are both. */
|
|
6070
|
+
const within = (container, node) => Boolean(container && node && container.contains(node));
|
|
6071
|
+
/**
|
|
6072
|
+
* Per document, the hooks that want Escape and the one listener they share
|
|
6073
|
+
* while there are any, so a card inside another card doesn't add a second
|
|
6074
|
+
* listener that closes both on one keypress.
|
|
6075
|
+
*/
|
|
6076
|
+
const escapeListeners = new Map();
|
|
6077
|
+
/** Counts openings across the page, so the most recent one is known. */
|
|
6078
|
+
let openings = 0;
|
|
6079
|
+
/**
|
|
6080
|
+
* Adds `entry` to the hooks that want Escape on `doc`, starting the shared
|
|
6081
|
+
* listener when it is the first.
|
|
6082
|
+
*
|
|
6083
|
+
* Escape takes back every open that is on its way, so nothing appears right
|
|
6084
|
+
* after it, and closes one open floating element, chosen by these rules in
|
|
6085
|
+
* turn:
|
|
6086
|
+
*
|
|
6087
|
+
* 1. One that focus is in, on its trigger or inside it, when there is one.
|
|
6088
|
+
* 2. The innermost: one whose floating element holds no other's trigger.
|
|
6089
|
+
* That follows nesting whether either one is portaled, since an inner
|
|
6090
|
+
* trigger sits in the outer floating element either way.
|
|
6091
|
+
* 3. The one that opened last.
|
|
6092
|
+
*
|
|
6093
|
+
* The keypress belongs to whatever has focus. When that is one of the open
|
|
6094
|
+
* floating elements or their triggers, or nothing, the listener stops it in
|
|
6095
|
+
* the capture phase, before anything around (a Popover, a Modal) sees it.
|
|
6096
|
+
* When focus is anywhere else, such as a combobox or a dialog of its own,
|
|
6097
|
+
* the floating element still closes, since hover put it there without
|
|
6098
|
+
* focus, and the key goes on to that owner.
|
|
6099
|
+
*
|
|
6100
|
+
* @returns What to call when the hook stops wanting Escape.
|
|
6101
|
+
*/
|
|
6102
|
+
function wantEscape(doc, entry) {
|
|
6103
|
+
let listener = escapeListeners.get(doc);
|
|
6104
|
+
if (!listener) {
|
|
6105
|
+
const entries = new Set();
|
|
6106
|
+
const handleKeyDown = (event) => {
|
|
6107
|
+
if (event.key !== 'Escape')
|
|
6108
|
+
return;
|
|
6109
|
+
const open = [];
|
|
6110
|
+
// A copy, since taking an open back can take its hook out of the set.
|
|
6111
|
+
for (const each of [...entries]) {
|
|
6112
|
+
if (each.openedAt)
|
|
6113
|
+
open.push(each);
|
|
6114
|
+
else
|
|
6115
|
+
each.cancel();
|
|
6116
|
+
}
|
|
6117
|
+
const focused = getDeepestActiveElement(doc);
|
|
6118
|
+
const holding = open.filter(each => within(each.trigger(), focused) || within(each.floating(), focused));
|
|
6119
|
+
const pool = holding.length > 0 ? holding : open;
|
|
6120
|
+
let top;
|
|
6121
|
+
for (const each of pool) {
|
|
6122
|
+
const holdsAnother = pool.some(other => other !== each && within(each.floating(), other.trigger()));
|
|
6123
|
+
if (!holdsAnother && (!top || each.openedAt > top.openedAt)) {
|
|
6124
|
+
top = each;
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
if (!top)
|
|
6128
|
+
return;
|
|
6129
|
+
if (holding.length > 0 || !focused || focused === doc.body) {
|
|
6130
|
+
event.preventDefault();
|
|
6131
|
+
event.stopPropagation();
|
|
6132
|
+
}
|
|
6133
|
+
top.close();
|
|
6134
|
+
};
|
|
6135
|
+
doc.addEventListener('keydown', handleKeyDown, true);
|
|
6136
|
+
listener = {
|
|
6137
|
+
entries,
|
|
6138
|
+
stop: () => {
|
|
6139
|
+
doc.removeEventListener('keydown', handleKeyDown, true);
|
|
6140
|
+
escapeListeners.delete(doc);
|
|
6141
|
+
},
|
|
6142
|
+
};
|
|
6143
|
+
escapeListeners.set(doc, listener);
|
|
6144
|
+
}
|
|
6145
|
+
const shared = listener;
|
|
6146
|
+
shared.entries.add(entry);
|
|
6147
|
+
return () => {
|
|
6148
|
+
shared.entries.delete(entry);
|
|
6149
|
+
if (shared.entries.size === 0)
|
|
6150
|
+
shared.stop();
|
|
6151
|
+
};
|
|
6152
|
+
}
|
|
6153
|
+
/**
|
|
6154
|
+
* Decides when a floating element that opens on hover or keyboard focus asks
|
|
6155
|
+
* to open and to close. It renders nothing, and leaves roles, ARIA and
|
|
6156
|
+
* placement to the component that calls it.
|
|
6157
|
+
*
|
|
6158
|
+
* - The pointer entering, or keyboard focus arriving on the trigger, asks to
|
|
6159
|
+
* open after `openDelay`. A touch pointer doesn't count. Neither does focus
|
|
6160
|
+
* that a press brings to the trigger: it counts when the browser shows it
|
|
6161
|
+
* as `:focus-visible`.
|
|
6162
|
+
* - Focus in the floating element holds it open however it got there,
|
|
6163
|
+
* including focus in content it portals elsewhere, such as a nested card.
|
|
6164
|
+
* Whether a click focuses what it lands on is up to the browser: Safari
|
|
6165
|
+
* and Firefox on macOS don't focus a button on click, so there a clicked
|
|
6166
|
+
* button doesn't hold it once the pointer leaves.
|
|
6167
|
+
* - Once the pointer and focus have both left, it asks to close after
|
|
6168
|
+
* `closeDelay`, and either coming back before then keeps it open. Nothing
|
|
6169
|
+
* else closes it on a timer.
|
|
6170
|
+
* - Escape closes it at once, wherever focus is, and stops there when focus
|
|
6171
|
+
* is in it, on its trigger or nowhere, so a `Popover` or `Modal` around it
|
|
6172
|
+
* keeps its own. With focus somewhere else, the key goes on after closing
|
|
6173
|
+
* it. Inside another one, the inner one closes first. Escape before it
|
|
6174
|
+
* opens takes the opening back and goes on its way. One key listener per
|
|
6175
|
+
* document serves every hook on it, there only while one of them is open
|
|
6176
|
+
* or on its way to open.
|
|
6177
|
+
* - With `openOnTap`, a tap on the trigger opens it at once and the next tap
|
|
6178
|
+
* on the trigger closes it. While a tap holds it open, a press outside the
|
|
6179
|
+
* trigger and the floating element closes it, from a listener on the
|
|
6180
|
+
* document that is there only then. Taps never count as hover, and mouse
|
|
6181
|
+
* and pen work the same with it or without.
|
|
6182
|
+
* - Once it closes, for any reason, whatever was holding it open has to
|
|
6183
|
+
* leave and come back to open it again.
|
|
6184
|
+
* - When the floating element closes with focus inside it, focus goes back
|
|
6185
|
+
* to the trigger, unless the same update put it somewhere else. Handing it
|
|
6186
|
+
* back doesn't count as focus arriving.
|
|
6187
|
+
*
|
|
6188
|
+
* Nothing it scheduled runs after the component unmounts.
|
|
6189
|
+
*
|
|
6190
|
+
* @function useHoverOpen
|
|
6191
|
+
* @param options - The open state, how to ask for another, the delays, and the two elements.
|
|
6192
|
+
* @returns Handlers for the element that holds the trigger.
|
|
6193
|
+
*/
|
|
6194
|
+
function useHoverOpen(options) {
|
|
6195
|
+
const latestRef = React.useRef(options);
|
|
6196
|
+
const stateRef = React.useRef({
|
|
6197
|
+
pointerIn: false,
|
|
6198
|
+
focusIn: false,
|
|
6199
|
+
focusInFloating: false,
|
|
6200
|
+
held: false,
|
|
6201
|
+
opening: false,
|
|
6202
|
+
asked: false,
|
|
6203
|
+
restoring: false,
|
|
6204
|
+
});
|
|
6205
|
+
// In the hooks that want Escape from the moment an open is on its way until
|
|
6206
|
+
// the floating element closes, and out otherwise. Stamped with the order it
|
|
6207
|
+
// opened in.
|
|
6208
|
+
const listenForEscape = React.useCallback(function listen() {
|
|
6209
|
+
const s = stateRef.current;
|
|
6210
|
+
const entry = entryRef.current;
|
|
6211
|
+
const { open, triggerRef } = latestRef.current;
|
|
6212
|
+
if (!open)
|
|
6213
|
+
entry.openedAt = 0;
|
|
6214
|
+
else if (!entry.openedAt) {
|
|
6215
|
+
openings += 1;
|
|
6216
|
+
entry.openedAt = openings;
|
|
6217
|
+
}
|
|
6218
|
+
const wanted = open || s.opening || s.asked;
|
|
6219
|
+
if (wanted === Boolean(s.releaseEscape))
|
|
6220
|
+
return;
|
|
6221
|
+
if (s.releaseEscape) {
|
|
6222
|
+
s.releaseEscape();
|
|
6223
|
+
s.releaseEscape = undefined;
|
|
6224
|
+
return;
|
|
6225
|
+
}
|
|
6226
|
+
const doc = triggerRef.current?.ownerDocument ?? document;
|
|
6227
|
+
s.releaseEscape = wantEscape(doc, entry);
|
|
6228
|
+
}, []);
|
|
6229
|
+
const entryRef = React.useRef({
|
|
6230
|
+
openedAt: 0,
|
|
6231
|
+
trigger: () => latestRef.current.triggerRef.current,
|
|
6232
|
+
floating: () => latestRef.current.floatingRef.current,
|
|
6233
|
+
// Only ever called while an open is on its way, so it is one or the
|
|
6234
|
+
// other: still waiting on its delay, or asked for already.
|
|
6235
|
+
cancel: () => {
|
|
6236
|
+
const s = stateRef.current;
|
|
6237
|
+
clearTimeout(s.timer);
|
|
6238
|
+
// An ask already made is taken back by asking the other way.
|
|
6239
|
+
if (s.asked)
|
|
6240
|
+
latestRef.current.onOpenChange(false);
|
|
6241
|
+
s.opening = false;
|
|
6242
|
+
s.asked = false;
|
|
6243
|
+
listenForEscape();
|
|
6244
|
+
},
|
|
6245
|
+
close: () => {
|
|
6246
|
+
clearTimeout(stateRef.current.timer);
|
|
6247
|
+
latestRef.current.onOpenChange(false);
|
|
6248
|
+
},
|
|
6249
|
+
});
|
|
6250
|
+
// Asks to open or to close. Escape can take an ask to open back until it is
|
|
6251
|
+
// answered: by the commit that follows it, or, when the caller keeps it
|
|
6252
|
+
// closed without rendering at all, once the event that asked is over, by
|
|
6253
|
+
// which time React has rendered any answer the caller set.
|
|
6254
|
+
const ask = React.useCallback((next) => {
|
|
6255
|
+
const s = stateRef.current;
|
|
6256
|
+
s.asked = next;
|
|
6257
|
+
latestRef.current.onOpenChange(next);
|
|
6258
|
+
listenForEscape();
|
|
6259
|
+
if (!next)
|
|
6260
|
+
return;
|
|
6261
|
+
clearTimeout(s.askTimer);
|
|
6262
|
+
s.askTimer = setTimeout(() => {
|
|
6263
|
+
// Still closed: the caller turned it down.
|
|
6264
|
+
if (latestRef.current.open)
|
|
6265
|
+
return;
|
|
6266
|
+
s.asked = false;
|
|
6267
|
+
s.tappedIn = undefined;
|
|
6268
|
+
listenForEscape();
|
|
6269
|
+
});
|
|
6270
|
+
}, [listenForEscape]);
|
|
6271
|
+
// Asks for what the pointer and focus call for, after its delay, when that
|
|
6272
|
+
// changed since the last time and isn't the state already.
|
|
6273
|
+
const settle = React.useCallback(() => {
|
|
6274
|
+
const s = stateRef.current;
|
|
6275
|
+
const held = s.pointerIn || s.focusIn;
|
|
6276
|
+
if (held !== s.held) {
|
|
6277
|
+
s.held = held;
|
|
6278
|
+
clearTimeout(s.timer);
|
|
6279
|
+
const { open, openDelay, closeDelay } = latestRef.current;
|
|
6280
|
+
s.opening = held && !open;
|
|
6281
|
+
if (held !== open) {
|
|
6282
|
+
s.timer = setTimeout(() => {
|
|
6283
|
+
s.opening = false;
|
|
6284
|
+
if (latestRef.current.open !== held)
|
|
6285
|
+
ask(held);
|
|
6286
|
+
else
|
|
6287
|
+
listenForEscape();
|
|
6288
|
+
}, held ? openDelay : closeDelay);
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
listenForEscape();
|
|
6292
|
+
}, [listenForEscape, ask]);
|
|
6293
|
+
// While a tap holds the floating element open, a press outside the trigger
|
|
6294
|
+
// and the floating element closes it. Set up again each time it opens or
|
|
6295
|
+
// closes.
|
|
6296
|
+
const listenForPress = React.useCallback(() => {
|
|
6297
|
+
const s = stateRef.current;
|
|
6298
|
+
s.stopPress?.();
|
|
6299
|
+
s.stopPress = undefined;
|
|
6300
|
+
const doc = latestRef.current.open ? s.tappedIn : undefined;
|
|
6301
|
+
if (!doc)
|
|
6302
|
+
return;
|
|
6303
|
+
const handlePointerDown = (event) => {
|
|
6304
|
+
// Inside is a press React brought to the holder, which covers content
|
|
6305
|
+
// the floating element portals elsewhere, or one whose path runs
|
|
6306
|
+
// through the trigger or the floating element, which covers a press
|
|
6307
|
+
// that something inside stopped before React got it to the holder.
|
|
6308
|
+
const { triggerRef, floatingRef } = latestRef.current;
|
|
6309
|
+
if (event === s.pressedInside ||
|
|
6310
|
+
isEventInside(event, triggerRef.current) ||
|
|
6311
|
+
isEventInside(event, floatingRef.current)) {
|
|
6312
|
+
return;
|
|
6313
|
+
}
|
|
6314
|
+
latestRef.current.onOpenChange(false);
|
|
6315
|
+
};
|
|
6316
|
+
doc.addEventListener('pointerdown', handlePointerDown);
|
|
6317
|
+
s.stopPress = () => doc.removeEventListener('pointerdown', handlePointerDown);
|
|
6318
|
+
}, []);
|
|
6319
|
+
useClientLayoutEffect(() => {
|
|
6320
|
+
latestRef.current = options;
|
|
6321
|
+
// The commit after an ask to open answers it, open or still closed. A
|
|
6322
|
+
// tap that asked and still finds it closed was turned down.
|
|
6323
|
+
const s = stateRef.current;
|
|
6324
|
+
if (s.asked) {
|
|
6325
|
+
s.asked = false;
|
|
6326
|
+
if (!options.open)
|
|
6327
|
+
s.tappedIn = undefined;
|
|
6328
|
+
listenForEscape();
|
|
6329
|
+
}
|
|
6330
|
+
});
|
|
6331
|
+
useClientLayoutEffect(() => {
|
|
6332
|
+
const s = stateRef.current;
|
|
6333
|
+
return () => {
|
|
6334
|
+
clearTimeout(s.timer);
|
|
6335
|
+
clearTimeout(s.askTimer);
|
|
6336
|
+
s.opening = false;
|
|
6337
|
+
s.asked = false;
|
|
6338
|
+
s.releaseEscape?.();
|
|
6339
|
+
s.releaseEscape = undefined;
|
|
6340
|
+
s.stopPress?.();
|
|
6341
|
+
s.stopPress = undefined;
|
|
6342
|
+
};
|
|
6343
|
+
}, []);
|
|
6344
|
+
const { open } = options;
|
|
6345
|
+
// Before paint, so a pointer or a key arriving next finds this settled.
|
|
6346
|
+
useClientLayoutEffect(() => {
|
|
6347
|
+
const s = stateRef.current;
|
|
6348
|
+
if (!open) {
|
|
6349
|
+
// The floating element left from under the pointer, if it was there,
|
|
6350
|
+
// and no leave event says so. Focus resting on the trigger has done
|
|
6351
|
+
// its job too, so neither opens it again until it comes back.
|
|
6352
|
+
s.pointerIn = false;
|
|
6353
|
+
s.focusIn = false;
|
|
6354
|
+
s.tappedIn = undefined;
|
|
6355
|
+
if (s.focusInFloating) {
|
|
6356
|
+
s.focusInFloating = false;
|
|
6357
|
+
const trigger = latestRef.current.triggerRef.current;
|
|
6358
|
+
// Focus that went down with the floating element sits on <body>.
|
|
6359
|
+
// Focus somewhere else was put there by the same update, so it stays.
|
|
6360
|
+
if (trigger &&
|
|
6361
|
+
trigger.ownerDocument.activeElement === trigger.ownerDocument.body) {
|
|
6362
|
+
s.restoring = true;
|
|
6363
|
+
trigger.focus();
|
|
6364
|
+
s.restoring = false;
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6367
|
+
}
|
|
6368
|
+
settle();
|
|
6369
|
+
listenForPress();
|
|
6370
|
+
}, [open, settle, listenForPress]);
|
|
6371
|
+
return React.useMemo(() => {
|
|
6372
|
+
const inTrigger = (node) => within(latestRef.current.triggerRef.current, node);
|
|
6373
|
+
return {
|
|
6374
|
+
onPointerEnter: event => {
|
|
6375
|
+
if (event.pointerType === 'touch')
|
|
6376
|
+
return;
|
|
6377
|
+
stateRef.current.pointerIn = true;
|
|
6378
|
+
settle();
|
|
6379
|
+
},
|
|
6380
|
+
onPointerLeave: event => {
|
|
6381
|
+
// A touch pointer never set pointerIn, so its leave has nothing to
|
|
6382
|
+
// clear, and on a device with a mouse too it would close a card the
|
|
6383
|
+
// mouse is still holding open.
|
|
6384
|
+
if (event.pointerType === 'touch')
|
|
6385
|
+
return;
|
|
6386
|
+
stateRef.current.pointerIn = false;
|
|
6387
|
+
settle();
|
|
6388
|
+
},
|
|
6389
|
+
onFocus: event => {
|
|
6390
|
+
const s = stateRef.current;
|
|
6391
|
+
// React brings focus here from the trigger and from the floating
|
|
6392
|
+
// element's whole tree, portals included, so focus not in the
|
|
6393
|
+
// trigger is in the floating element, wherever its DOM is.
|
|
6394
|
+
s.focusInFloating = !inTrigger(event.target);
|
|
6395
|
+
if (s.restoring)
|
|
6396
|
+
return;
|
|
6397
|
+
// Any focus in the floating element holds it. On the trigger, only
|
|
6398
|
+
// keyboard focus does, so a click there opens nothing.
|
|
6399
|
+
s.focusIn = s.focusInFloating || isKeyboardFocus(event.target);
|
|
6400
|
+
settle();
|
|
6401
|
+
},
|
|
6402
|
+
onBlur: event => {
|
|
6403
|
+
const s = stateRef.current;
|
|
6404
|
+
const next = event.relatedTarget;
|
|
6405
|
+
// Where focus goes next, as far as the DOM can say. Focus going into
|
|
6406
|
+
// content the floating element portals elsewhere reads as leaving,
|
|
6407
|
+
// and the focus event that follows straight after brings it back.
|
|
6408
|
+
s.focusInFloating = within(latestRef.current.floatingRef.current, next);
|
|
6409
|
+
// Moving between the trigger and the floating element isn't leaving.
|
|
6410
|
+
if (s.focusInFloating || within(event.currentTarget, next))
|
|
6411
|
+
return;
|
|
6412
|
+
s.focusIn = false;
|
|
6413
|
+
settle();
|
|
6414
|
+
},
|
|
6415
|
+
onPointerDown: event => {
|
|
6416
|
+
stateRef.current.pressedInside = event.nativeEvent;
|
|
6417
|
+
},
|
|
6418
|
+
onPointerUp: event => {
|
|
6419
|
+
const now = latestRef.current;
|
|
6420
|
+
// With openOnTap, a tap on the trigger opens it at once, and the
|
|
6421
|
+
// next one closes it. A tap anywhere else does nothing here.
|
|
6422
|
+
if (!now.openOnTap ||
|
|
6423
|
+
event.pointerType !== 'touch' ||
|
|
6424
|
+
!inTrigger(event.target)) {
|
|
6425
|
+
return;
|
|
6426
|
+
}
|
|
6427
|
+
stateRef.current.tappedIn = now.open
|
|
6428
|
+
? undefined
|
|
6429
|
+
: event.target.ownerDocument;
|
|
6430
|
+
ask(!now.open);
|
|
6431
|
+
},
|
|
6432
|
+
};
|
|
6433
|
+
}, [settle, ask]);
|
|
6434
|
+
}
|
|
6435
|
+
|
|
6436
|
+
/** Runs the caller's handler on the wrapper, then the hover card's. */
|
|
6437
|
+
function chain(own, ours) {
|
|
6438
|
+
return event => {
|
|
6439
|
+
own?.(event);
|
|
6440
|
+
ours(event);
|
|
6441
|
+
};
|
|
6442
|
+
}
|
|
6443
|
+
/**
|
|
6444
|
+
* 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.
|
|
6445
|
+
*
|
|
6446
|
+
* Nothing moves focus when the card opens, and nothing traps it. In place,
|
|
6447
|
+
* the card comes right after the trigger, so Tab goes from the trigger into
|
|
6448
|
+
* its links and tabbing past them closes it. The pointer can move onto the
|
|
6449
|
+
* card, Escape closes it from anywhere, and by default touch doesn't open it,
|
|
6450
|
+
* so tapping the trigger just does what the trigger does. `openOnTap` opens
|
|
6451
|
+
* it with a tap, for a trigger with no tap action of its own. For hints with nothing to
|
|
6452
|
+
* click use `Tooltip`, and for content opened on purpose, such as a form,
|
|
6453
|
+
* use `Popover`.
|
|
6454
|
+
*
|
|
6455
|
+
* @function
|
|
6456
|
+
* @param {HoverCardProps} props - Props for the HoverCard component.
|
|
6457
|
+
* @returns {JSX.Element} The trigger in its wrapper, and the card while open.
|
|
6458
|
+
*
|
|
6459
|
+
* @example
|
|
6460
|
+
* <HoverCard trigger={<a href="/people/ada">Ada Lovelace</a>}>
|
|
6461
|
+
* <Title as="p" size="6">Ada Lovelace</Title>
|
|
6462
|
+
* <SubTitle as="p" size="6">Analytical Engine team</SubTitle>
|
|
6463
|
+
* </HoverCard>
|
|
6464
|
+
*/
|
|
6465
|
+
const HoverCard = ({ trigger, children, open: controlledOpen, defaultOpen = false, onOpenChange, openDelay = 600, closeDelay = 300, position = 'bottom-left', appendToBody = false, openOnTap = false, className, contentClassName, ...props }) => {
|
|
6466
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
6467
|
+
const { onPointerEnter, onPointerLeave, onPointerDown, onPointerUp, onFocus, onBlur, ...attrs } = rest;
|
|
6468
|
+
const rootRef = React.useRef(null);
|
|
6469
|
+
const cardRef = React.useRef(null);
|
|
6470
|
+
const cardId = `${React.useId()}-card`;
|
|
6471
|
+
const isControlled = controlledOpen !== undefined;
|
|
6472
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
|
|
6473
|
+
const isOpen = controlledOpen ?? internalOpen;
|
|
6474
|
+
const requestOpen = React.useCallback((next) => {
|
|
6475
|
+
onOpenChange?.(next);
|
|
6476
|
+
if (!isControlled)
|
|
6477
|
+
setInternalOpen(next);
|
|
6478
|
+
}, [isControlled, onOpenChange]);
|
|
6479
|
+
// A portaled card has no server markup, so it only exists from the commit
|
|
6480
|
+
// after hydration, and nothing measures it before then.
|
|
6481
|
+
const hydrated = useIsHydrated();
|
|
6482
|
+
const cardMounted = isOpen && (hydrated || !appendToBody);
|
|
6483
|
+
const resolved = useAnchoredPosition(rootRef, cardRef, {
|
|
6484
|
+
active: cardMounted,
|
|
6485
|
+
position,
|
|
6486
|
+
fixed: appendToBody,
|
|
6487
|
+
// The gap comes from `--bulma-hover-card-offset` in the stylesheet, so
|
|
6488
|
+
// the coordinates leave it out and `auto` reads it off the card.
|
|
6489
|
+
offset: 0,
|
|
6490
|
+
styledGap: marginGap,
|
|
6491
|
+
});
|
|
6492
|
+
// The trigger renders first inside the wrapper. Read when needed rather
|
|
6493
|
+
// than held, so a trigger that remounted is still the one found.
|
|
6494
|
+
const triggerRef = React.useMemo(() => ({
|
|
6495
|
+
get current() {
|
|
6496
|
+
const first = rootRef.current.firstElementChild;
|
|
6497
|
+
return first && first !== cardRef.current
|
|
6498
|
+
? first
|
|
6499
|
+
: null;
|
|
6500
|
+
},
|
|
6501
|
+
}), []);
|
|
6502
|
+
const hover = useHoverOpen({
|
|
6503
|
+
open: isOpen,
|
|
6504
|
+
onOpenChange: requestOpen,
|
|
6505
|
+
openDelay,
|
|
6506
|
+
closeDelay,
|
|
6507
|
+
triggerRef,
|
|
6508
|
+
floatingRef: cardRef,
|
|
6509
|
+
openOnTap,
|
|
6510
|
+
});
|
|
6511
|
+
// A trigger that can't take focus leaves keyboard users no way to open the
|
|
6512
|
+
// card. It has to be the trigger element itself, not something inside it,
|
|
6513
|
+
// since focus comes back to it and its ARIA is on it. Judged by what the
|
|
6514
|
+
// element is rather than by its styles, which can hide it for now. After
|
|
6515
|
+
// every commit, since the trigger can change, and only in development.
|
|
6516
|
+
React.useEffect(() => {
|
|
6517
|
+
if (!isDev())
|
|
6518
|
+
return;
|
|
6519
|
+
const triggerNode = triggerRef.current;
|
|
6520
|
+
if (!triggerNode)
|
|
6521
|
+
return;
|
|
6522
|
+
if (!triggerNode.matches(':disabled') && isTabbableKind(triggerNode)) {
|
|
6523
|
+
return;
|
|
6524
|
+
}
|
|
6525
|
+
warnOnce('HoverCard:unfocusable-trigger', "[bestax-bulma] <HoverCard> has a trigger that can't take focus, so " +
|
|
6526
|
+
"keyboard users can't open the card. Use a link or a button as the " +
|
|
6527
|
+
'trigger, or give it tabIndex={0}.');
|
|
6528
|
+
});
|
|
6529
|
+
// Expanded only while the card is on the page, so a portaled card that
|
|
6530
|
+
// waits for hydration isn't announced before it exists.
|
|
6531
|
+
const clonedTrigger = React.cloneElement(trigger, {
|
|
6532
|
+
'aria-expanded': cardMounted,
|
|
6533
|
+
'aria-controls': cardMounted ? cardId : undefined,
|
|
6534
|
+
});
|
|
6535
|
+
const rootClasses = classNames(usePrefixedClassNames('hover-card', { 'is-active': cardMounted }), bulmaHelperClasses, className);
|
|
6536
|
+
const cardClasses = classNames(usePrefixedClassNames('hover-card-content', {
|
|
6537
|
+
[`is-${resolved.position}`]: true,
|
|
6538
|
+
'is-portal': appendToBody,
|
|
6539
|
+
}), contentClassName);
|
|
6540
|
+
return (jsxRuntime.jsxs("span", { className: rootClasses, ...attrs, onPointerEnter: chain(onPointerEnter, hover.onPointerEnter), onPointerLeave: chain(onPointerLeave, hover.onPointerLeave), onFocus: chain(onFocus, hover.onFocus), onBlur: chain(onBlur, hover.onBlur), onPointerDown: chain(onPointerDown, hover.onPointerDown), onPointerUp: chain(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
|
|
6541
|
+
? {
|
|
6542
|
+
position: 'fixed',
|
|
6543
|
+
top: resolved.top,
|
|
6544
|
+
left: resolved.left,
|
|
6545
|
+
}
|
|
6546
|
+
: undefined, children: children }) }))] }));
|
|
6547
|
+
};
|
|
6548
|
+
|
|
6017
6549
|
/**
|
|
6018
6550
|
* Individual Step component for use inside Steps.
|
|
6019
6551
|
*
|
|
@@ -6161,6 +6693,11 @@ const Steps = withSubComponents(StepsComponent, { Step }, 'Steps');
|
|
|
6161
6693
|
*/
|
|
6162
6694
|
const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left', width = '260px', isFullwidth, fullWidth, overlay = true, overlayClose = true, escapeClose = true, canCancel = true, children, inline = false, className, style, ...props }, ref) => {
|
|
6163
6695
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
6696
|
+
const resolveContainer = usePortalContainer();
|
|
6697
|
+
const { portalContainer } = useConfig();
|
|
6698
|
+
// Where the configured default puts the sidebar. An `inline` one doesn't
|
|
6699
|
+
// portal, so a change to the default moves nothing.
|
|
6700
|
+
const defaultTarget = inline ? undefined : portalContainer;
|
|
6164
6701
|
const sidebarRef = React.useRef(null);
|
|
6165
6702
|
// Cleanup returned by a consumer's callback ref on attach, held until detach.
|
|
6166
6703
|
const consumerCleanupRef = React.useRef(null);
|
|
@@ -6192,7 +6729,9 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
6192
6729
|
// Prevent body scroll when sidebar is open. Ref-counted through the shared
|
|
6193
6730
|
// helper so an overlapping Modal/Dialog/Loading doesn't unlock underneath.
|
|
6194
6731
|
useScrollLock(isOpen && overlay);
|
|
6195
|
-
// Focus trap (basic - focus sidebar when opened)
|
|
6732
|
+
// Focus trap (basic - focus sidebar when opened). A different
|
|
6733
|
+
// `defaultTarget` remounts the portaled sidebar, so it focuses the new
|
|
6734
|
+
// nodes then too.
|
|
6196
6735
|
React.useEffect(() => {
|
|
6197
6736
|
if (isOpen) {
|
|
6198
6737
|
const element = sidebarRef.current;
|
|
@@ -6207,7 +6746,7 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
6207
6746
|
}
|
|
6208
6747
|
}
|
|
6209
6748
|
}
|
|
6210
|
-
}, [isOpen]);
|
|
6749
|
+
}, [isOpen, defaultTarget]);
|
|
6211
6750
|
// Use combined ref
|
|
6212
6751
|
const combinedRef = React.useCallback((node) => {
|
|
6213
6752
|
sidebarRef.current =
|
|
@@ -6262,7 +6801,7 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
6262
6801
|
}
|
|
6263
6802
|
// Render to portal for proper stacking
|
|
6264
6803
|
if (typeof document !== 'undefined') {
|
|
6265
|
-
return reactDom.createPortal(sidebarContent,
|
|
6804
|
+
return reactDom.createPortal(sidebarContent, resolveContainer());
|
|
6266
6805
|
}
|
|
6267
6806
|
return null;
|
|
6268
6807
|
});
|
|
@@ -6390,6 +6929,7 @@ function groupIntoPositionStacks(items, positionOf, containerPosition, order) {
|
|
|
6390
6929
|
*/
|
|
6391
6930
|
const Toast = React.forwardRef(({ message, type = 'default', actionType, position = 'top-right', duration = 2000, indefinite = false, dismissible = true, closable = false, rounded = false, pauseOnHover = false, cancelable = true, actionText, cancelText, onAction, onClose, container, inline = false, className, ...props }, ref) => {
|
|
6392
6931
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
6932
|
+
const resolveContainer = usePortalContainer();
|
|
6393
6933
|
const [isVisible, setIsVisible] = React.useState(true);
|
|
6394
6934
|
const [isPaused, setIsPaused] = React.useState(false);
|
|
6395
6935
|
const toastRef = React.useRef(null);
|
|
@@ -6515,7 +7055,7 @@ const Toast = React.forwardRef(({ message, type = 'default', actionType, positio
|
|
|
6515
7055
|
}
|
|
6516
7056
|
const toastContent = jsxRuntime.jsx("div", { className: containerClasses, children: toastElement });
|
|
6517
7057
|
if (typeof document !== 'undefined') {
|
|
6518
|
-
return reactDom.createPortal(toastContent,
|
|
7058
|
+
return reactDom.createPortal(toastContent, resolveContainer(container));
|
|
6519
7059
|
}
|
|
6520
7060
|
return null;
|
|
6521
7061
|
});
|
|
@@ -6672,6 +7212,7 @@ const ToastContainer = ({ position = 'top-right' }) => {
|
|
|
6672
7212
|
// Starts from the toasts already showing instead of an empty list, then
|
|
6673
7213
|
// follows changes.
|
|
6674
7214
|
const toastList = React.useSyncExternalStore(toast.subscribe, getVisibleToasts, getServerToasts);
|
|
7215
|
+
const resolveContainer = usePortalContainer();
|
|
6675
7216
|
if (typeof document === 'undefined' || toastList.length === 0) {
|
|
6676
7217
|
return null;
|
|
6677
7218
|
}
|
|
@@ -6679,7 +7220,7 @@ const ToastContainer = ({ position = 'top-right' }) => {
|
|
|
6679
7220
|
return reactDom.createPortal(stacks.map(stack => (jsxRuntime.jsx("div", { className: `toast-container is-${stack.position}`, children: stack.items.map(t => {
|
|
6680
7221
|
const { queue: _queue, ...toastProps } = t.props;
|
|
6681
7222
|
return (jsxRuntime.jsx(Toast, { ...toastProps, position: stack.position, onClose: () => toast.close(t.id) }, t.id));
|
|
6682
|
-
}) }, stack.key))),
|
|
7223
|
+
}) }, stack.key))), resolveContainer());
|
|
6683
7224
|
};
|
|
6684
7225
|
|
|
6685
7226
|
/**
|
|
@@ -6736,11 +7277,14 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
|
|
|
6736
7277
|
handleCancel();
|
|
6737
7278
|
}
|
|
6738
7279
|
}, [canCancel, handleCancel]);
|
|
6739
|
-
// Moving into the portal
|
|
6740
|
-
// included
|
|
6741
|
-
// nodes, exactly as the Modal's own focus
|
|
7280
|
+
// Moving into the portal, or from one portal target to another, remounts
|
|
7281
|
+
// the Modal's subtree, these buttons included, so the focus effect below
|
|
7282
|
+
// has to re-run against the new nodes, exactly as the Modal's own focus
|
|
7283
|
+
// effect does, on the same inputs.
|
|
6742
7284
|
const isHydrated = useIsHydrated();
|
|
6743
7285
|
const isPortaled = Boolean(portal) && isHydrated;
|
|
7286
|
+
const { portalContainer } = useConfig();
|
|
7287
|
+
const defaultTarget = isPortaled && portal === true ? portalContainer : undefined;
|
|
6744
7288
|
// Focus management — Dialog picks a specific button rather than the
|
|
6745
7289
|
// Modal's default (first focusable/root); this effect runs after the
|
|
6746
7290
|
// inner Modal's own focus-on-open effect, so it wins.
|
|
@@ -6749,7 +7293,7 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
|
|
|
6749
7293
|
const buttonToFocus = focusCancel && showCancel ? cancelRef.current : confirmRef.current;
|
|
6750
7294
|
buttonToFocus?.focus();
|
|
6751
7295
|
}
|
|
6752
|
-
}, [isOpen, focusCancel, showCancel, isPortaled]);
|
|
7296
|
+
}, [isOpen, focusCancel, showCancel, isPortaled, portal, defaultTarget]);
|
|
6753
7297
|
// Use combined ref
|
|
6754
7298
|
const combinedRef = React.useCallback((node) => {
|
|
6755
7299
|
dialogRef.current =
|
|
@@ -8384,6 +8928,7 @@ const NotificationContainer = ({ position = 'top-right' }) => {
|
|
|
8384
8928
|
// the server render and the hydrating one render nothing, and the container
|
|
8385
8929
|
// portals in from the render after hydration.
|
|
8386
8930
|
const isHydrated = useIsHydrated();
|
|
8931
|
+
const resolveContainer = usePortalContainer();
|
|
8387
8932
|
// The notifications the status region announces. Danger and warning
|
|
8388
8933
|
// announce themselves.
|
|
8389
8934
|
const politeItems = React.useMemo(() => items.filter(item => !isUrgentColor(item.options.color)), [items]);
|
|
@@ -8416,7 +8961,7 @@ const NotificationContainer = ({ position = 'top-right' }) => {
|
|
|
8416
8961
|
return null;
|
|
8417
8962
|
}
|
|
8418
8963
|
const stacks = groupIntoPositionStacks(items, item => item.options.position, position, notificationStackOrder);
|
|
8419
|
-
return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [stacks.map(stack => (jsxRuntime.jsx("div", { style: notificationStackStyle(stack.position), children: stack.items.map(item => (jsxRuntime.jsx(NotificationItem, { instance: item, onClose: notification.close, registerMessage: registerMessage }, item.id))) }, stack.key))), jsxRuntime.jsx(StatusRegion, { items: politeItems, describe: describe })] }),
|
|
8964
|
+
return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [stacks.map(stack => (jsxRuntime.jsx("div", { style: notificationStackStyle(stack.position), children: stack.items.map(item => (jsxRuntime.jsx(NotificationItem, { instance: item, onClose: notification.close, registerMessage: registerMessage }, item.id))) }, stack.key))), jsxRuntime.jsx(StatusRegion, { items: politeItems, describe: describe })] }), resolveContainer());
|
|
8420
8965
|
};
|
|
8421
8966
|
|
|
8422
8967
|
/**
|
|
@@ -13343,6 +13888,7 @@ Calendar.displayName = 'Calendar';
|
|
|
13343
13888
|
const isBrowser = typeof window !== 'undefined';
|
|
13344
13889
|
const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'bottom-left', appendToBody = false, className, trapFocus = true, closeOnClickOutside = true, closeOnEscape = true, ariaLabel, ariaLabelledBy, role = 'dialog', id, restoreFocusRef, initialFocusSelector, }) => {
|
|
13345
13890
|
const panelRef = React.useRef(null);
|
|
13891
|
+
const resolveContainer = usePortalContainer();
|
|
13346
13892
|
// Read as the trap turns on, once the panel's content has rendered.
|
|
13347
13893
|
const initialFocusRef = React.useMemo(() => initialFocusSelector === undefined
|
|
13348
13894
|
? undefined
|
|
@@ -13419,7 +13965,7 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
|
|
|
13419
13965
|
: {};
|
|
13420
13966
|
const panel = (jsxRuntime.jsx("div", { ref: panelRef, id: id, role: role, "aria-modal": role === 'dialog' ? 'false' : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, tabIndex: -1, className: classNames(panelClass, className), style: style, children: children }));
|
|
13421
13967
|
if (appendToBody && isBrowser) {
|
|
13422
|
-
return reactDom.createPortal(panel,
|
|
13968
|
+
return reactDom.createPortal(panel, resolveContainer());
|
|
13423
13969
|
}
|
|
13424
13970
|
return panel;
|
|
13425
13971
|
};
|
|
@@ -17957,6 +18503,7 @@ exports.Hero = Hero;
|
|
|
17957
18503
|
exports.HeroBody = HeroBody;
|
|
17958
18504
|
exports.HeroFoot = HeroFoot;
|
|
17959
18505
|
exports.HeroHead = HeroHead;
|
|
18506
|
+
exports.HoverCard = HoverCard;
|
|
17960
18507
|
exports.Icon = Icon;
|
|
17961
18508
|
exports.IconText = IconText;
|
|
17962
18509
|
exports.Image = Image;
|