@allxsmith/bestax-bulma 5.59.0 → 5.61.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/Affix.js +46 -0
- package/dist/esm/components/Affix.js.map +1 -0
- package/dist/esm/components/Carousel.js +3 -43
- package/dist/esm/components/Carousel.js.map +1 -1
- package/dist/esm/components/Command.js +3 -3
- package/dist/esm/components/Dialog.js +2 -6
- package/dist/esm/components/Dialog.js.map +1 -1
- package/dist/esm/components/Modal.js +12 -24
- package/dist/esm/components/Modal.js.map +1 -1
- package/dist/esm/components/Navbar.js +3 -1
- package/dist/esm/components/Navbar.js.map +1 -1
- package/dist/esm/components/Popover.js +7 -27
- package/dist/esm/components/Popover.js.map +1 -1
- package/dist/esm/components/Sidebar.js +17 -58
- package/dist/esm/components/Sidebar.js.map +1 -1
- package/dist/esm/form/ColorPicker.js +4 -4
- package/dist/esm/form/ColorPicker.js.map +1 -1
- package/dist/esm/form/Combobox.js +3 -3
- package/dist/esm/helpers/overlayStack.js +33 -1
- package/dist/esm/helpers/overlayStack.js.map +1 -1
- package/dist/esm/helpers/portal.js +28 -1
- package/dist/esm/helpers/portal.js.map +1 -1
- package/dist/esm/helpers/useBulmaViewport.js +82 -0
- package/dist/esm/helpers/useBulmaViewport.js.map +1 -0
- package/dist/esm/helpers/useInertSiblings.js +7 -3
- package/dist/esm/helpers/useInertSiblings.js.map +1 -1
- package/dist/esm/helpers/useOutsidePress.js +53 -0
- package/dist/esm/helpers/useOutsidePress.js.map +1 -0
- package/dist/esm/helpers/useOverlayFocus.js +74 -0
- package/dist/esm/helpers/useOverlayFocus.js.map +1 -0
- package/dist/esm/layout/AppFrame.js +438 -0
- package/dist/esm/layout/AppFrame.js.map +1 -0
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +754 -160
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +2 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/Affix.d.ts +67 -0
- package/dist/types/components/Navbar.d.ts +4 -3
- package/dist/types/helpers/overlayStack.d.ts +17 -0
- package/dist/types/helpers/portal.d.ts +42 -0
- package/dist/types/helpers/useBulmaViewport.d.ts +34 -0
- package/dist/types/helpers/useInertSiblings.d.ts +4 -1
- package/dist/types/helpers/useOutsidePress.d.ts +24 -0
- package/dist/types/helpers/useOverlayFocus.d.ts +44 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/layout/AppFrame.d.ts +306 -0
- package/dist/types-cjs/components/Affix.d.ts +67 -0
- package/dist/types-cjs/components/Navbar.d.ts +4 -3
- package/dist/types-cjs/helpers/overlayStack.d.ts +17 -0
- package/dist/types-cjs/helpers/portal.d.ts +42 -0
- package/dist/types-cjs/helpers/useBulmaViewport.d.ts +34 -0
- package/dist/types-cjs/helpers/useInertSiblings.d.ts +4 -1
- package/dist/types-cjs/helpers/useOutsidePress.d.ts +24 -0
- package/dist/types-cjs/helpers/useOverlayFocus.d.ts +44 -0
- package/dist/types-cjs/index.d.ts +2 -0
- package/dist/types-cjs/layout/AppFrame.d.ts +306 -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/components/_affix.scss +74 -0
- package/src/scss/components/_index.scss +1 -0
- package/src/scss/layout/_appframe.scss +306 -0
- package/src/scss/layout/_index.scss +1 -0
package/dist/index.cjs
CHANGED
|
@@ -4603,6 +4603,33 @@ function usePortalContainer() {
|
|
|
4603
4603
|
const { portalContainer } = useConfig();
|
|
4604
4604
|
return container => resolvePortalContainer(container || portalContainer);
|
|
4605
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
|
+
}
|
|
4606
4633
|
/**
|
|
4607
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.
|
|
4608
4635
|
*
|
|
@@ -4728,6 +4755,37 @@ function useOverlayStack(inStack, modal = true) {
|
|
|
4728
4755
|
return !above.some(overlay => among !== 'modal' || overlay.modal);
|
|
4729
4756
|
}, []);
|
|
4730
4757
|
}
|
|
4758
|
+
/**
|
|
4759
|
+
* Calls `onEscape` for an Escape while `active` and the overlay is the
|
|
4760
|
+
* topmost one, as `isTopmost` (from `useOverlayStack`) says, and marks the key
|
|
4761
|
+
* used with `preventDefault()`, so nothing under it acts on it too.
|
|
4762
|
+
*
|
|
4763
|
+
* It listens on `window`, so everything inside the overlay has had the key
|
|
4764
|
+
* first, and an Escape something there used, such as a popup closing, is
|
|
4765
|
+
* left to it. The listener stays put while `onEscape` changes identity, as an
|
|
4766
|
+
* inline one does on every render of the parent: it reads the latest one as
|
|
4767
|
+
* the key arrives.
|
|
4768
|
+
*
|
|
4769
|
+
* @function useOverlayEscape
|
|
4770
|
+
* @param active - Whether the overlay is open and closes on Escape.
|
|
4771
|
+
* @param isTopmost - The overlay's check from `useOverlayStack`.
|
|
4772
|
+
* @param onEscape - What closing it does.
|
|
4773
|
+
*/
|
|
4774
|
+
function useOverlayEscape(active, isTopmost, onEscape) {
|
|
4775
|
+
const onEscapeRef = useLatestRef(onEscape);
|
|
4776
|
+
React.useEffect(() => {
|
|
4777
|
+
if (!active)
|
|
4778
|
+
return undefined;
|
|
4779
|
+
const handleKeyDown = (e) => {
|
|
4780
|
+
if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
|
|
4781
|
+
return;
|
|
4782
|
+
e.preventDefault();
|
|
4783
|
+
onEscapeRef.current();
|
|
4784
|
+
};
|
|
4785
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
4786
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
4787
|
+
}, [active, isTopmost, onEscapeRef]);
|
|
4788
|
+
}
|
|
4731
4789
|
|
|
4732
4790
|
/**
|
|
4733
4791
|
* Whether `el` is editable while its parent is not, which makes it an editing
|
|
@@ -5401,7 +5459,7 @@ const ModalClose = ({ className, size = 'large', variant = 'delete', onClick, ..
|
|
|
5401
5459
|
* @see {@link https://bulma.io/documentation/components/modal/ | Bulma Modal documentation}
|
|
5402
5460
|
*/
|
|
5403
5461
|
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) {
|
|
5404
|
-
const { classPrefix
|
|
5462
|
+
const { classPrefix } = useConfig();
|
|
5405
5463
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
5406
5464
|
color: textColor,
|
|
5407
5465
|
backgroundColor: bgColor,
|
|
@@ -5447,20 +5505,15 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
|
|
|
5447
5505
|
const previouslyFocusedRef = React.useRef(null);
|
|
5448
5506
|
const generatedTitleId = React.useId();
|
|
5449
5507
|
// A portal has no server-rendered counterpart, so the server render and the
|
|
5450
|
-
// hydrating render
|
|
5451
|
-
//
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
//
|
|
5455
|
-
//
|
|
5456
|
-
//
|
|
5457
|
-
//
|
|
5458
|
-
|
|
5459
|
-
const isPortaled = Boolean(portal) && onClient;
|
|
5460
|
-
// The configured `portalContainer` only places a modal portaled with
|
|
5461
|
-
// `portal` set to `true`. In place, or with a target of its own, a change
|
|
5462
|
-
// to it moves nothing, so it mustn't re-run the effects either.
|
|
5463
|
-
const defaultTarget = isPortaled && portal === true ? portalContainer : undefined;
|
|
5508
|
+
// hydrating render stay inline and only move into the portal once we're
|
|
5509
|
+
// past hydration. Moving into the portal, or from one portal target to
|
|
5510
|
+
// another, remounts the modal's subtree, so any effect holding a DOM node
|
|
5511
|
+
// from before the move is holding a detached one. Effects that touch the
|
|
5512
|
+
// modal's nodes key on `isPortaled`, on `portal` and on `defaultTarget`, so
|
|
5513
|
+
// they re-run against the new tree. `defaultTarget` is the configured
|
|
5514
|
+
// `portalContainer` only while it places the modal (portaled with `portal`
|
|
5515
|
+
// set to `true`), so a change to it that moves nothing re-runs nothing.
|
|
5516
|
+
const { isPortaled, container, defaultTarget } = usePortalProp(portal);
|
|
5464
5517
|
useScrollLock(isModalActive && lockScroll);
|
|
5465
5518
|
// Read when Escape arrives, so the key listener stays put while `onClose`
|
|
5466
5519
|
// changes identity, as an inline one does on every render of the parent.
|
|
@@ -5573,11 +5626,7 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
|
|
|
5573
5626
|
}
|
|
5574
5627
|
// In both forms, so a `Modal.Close` anywhere inside closes this modal.
|
|
5575
5628
|
modalElement = (jsxRuntime.jsx(ModalCloseContext.Provider, { value: onClose, children: modalElement }));
|
|
5576
|
-
|
|
5577
|
-
const target = resolveContainer(typeof portal === 'boolean' ? undefined : portal);
|
|
5578
|
-
return reactDom.createPortal(jsxRuntime.jsx(LocalePortalScope, { target: target, children: modalElement }), target);
|
|
5579
|
-
}
|
|
5580
|
-
return modalElement;
|
|
5629
|
+
return (jsxRuntime.jsx(Portal, { disabled: !isPortaled, container: container, children: modalElement }));
|
|
5581
5630
|
});
|
|
5582
5631
|
const Modal = withSubComponents(ModalRoot, {
|
|
5583
5632
|
Background: ModalBackground,
|
|
@@ -5662,7 +5711,9 @@ const NavbarBurger = React.forwardRef(function NavbarBurger({ className, active,
|
|
|
5662
5711
|
});
|
|
5663
5712
|
return (jsxRuntime.jsxs("button", { ref: ref, className: classNames(usePrefixedClassNames('navbar-burger', {
|
|
5664
5713
|
'is-active': active,
|
|
5665
|
-
}), bulmaHelperClasses, className), ...rest, "aria-label": rest['aria-label'] || 'menu', "aria-expanded": rest['aria-expanded']
|
|
5714
|
+
}), bulmaHelperClasses, className), ...rest, "aria-label": rest['aria-label'] || 'menu', "aria-expanded": rest['aria-expanded'] === null
|
|
5715
|
+
? undefined
|
|
5716
|
+
: (rest['aria-expanded'] ?? !!active), type: buttonType(rest.type), children: [jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), children] }));
|
|
5666
5717
|
});
|
|
5667
5718
|
NavbarBurger.displayName = 'NavbarBurger';
|
|
5668
5719
|
/**
|
|
@@ -6833,16 +6884,20 @@ const above = (el) => el.parentElement ?? el.getRootNode().host;
|
|
|
6833
6884
|
* @param active - Whether what's beside it is inert.
|
|
6834
6885
|
* @param wholePage - Whether that goes for the whole page rather than only
|
|
6835
6886
|
* the element's siblings.
|
|
6887
|
+
* @param except - Ref to an element beside it to leave reachable too, such
|
|
6888
|
+
* as the overlay's own backdrop, which has to take the presses that close
|
|
6889
|
+
* it rather than let them through to what's under it.
|
|
6836
6890
|
*/
|
|
6837
|
-
function useInertSiblings(ref, active, wholePage) {
|
|
6891
|
+
function useInertSiblings(ref, active, wholePage, except) {
|
|
6838
6892
|
React.useEffect(() => {
|
|
6839
6893
|
const el = ref.current;
|
|
6840
6894
|
if (!active || !el)
|
|
6841
6895
|
return undefined;
|
|
6842
6896
|
const doc = el.ownerDocument;
|
|
6897
|
+
const keep = except?.current;
|
|
6843
6898
|
const held = [];
|
|
6844
6899
|
for (let node = el; node && node !== doc.body; node = wholePage ? above(node) : undefined) {
|
|
6845
|
-
held.push(...siblingsOf(node).filter(hold));
|
|
6900
|
+
held.push(...siblingsOf(node).filter(sibling => sibling !== keep && hold(sibling)));
|
|
6846
6901
|
}
|
|
6847
6902
|
// Focus this takes away, to hand back later. It's looked up as `el`'s
|
|
6848
6903
|
// tree sees it, where focus inside a nested shadow root reads as its
|
|
@@ -6857,7 +6912,7 @@ function useInertSiblings(ref, active, wholePage) {
|
|
|
6857
6912
|
if (doc.activeElement === doc.body)
|
|
6858
6913
|
taken?.focus();
|
|
6859
6914
|
};
|
|
6860
|
-
}, [ref, active, wholePage]);
|
|
6915
|
+
}, [ref, active, wholePage, except]);
|
|
6861
6916
|
}
|
|
6862
6917
|
|
|
6863
6918
|
/**
|
|
@@ -7953,6 +8008,51 @@ function useAnchoredPosition(anchorRef, panelRef, { active, position, fixed, off
|
|
|
7953
8008
|
return resolved;
|
|
7954
8009
|
}
|
|
7955
8010
|
|
|
8011
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
8012
|
+
/**
|
|
8013
|
+
* Calls `onOutsidePress` for a `pointerdown` outside `ref`'s element while
|
|
8014
|
+
* `active`, as a Popover closes on one.
|
|
8015
|
+
*
|
|
8016
|
+
* A press counts as inside in two ways: on the element's own path in the DOM,
|
|
8017
|
+
* and anywhere in a React tree the returned handler is put on, as
|
|
8018
|
+
* `onPointerDownCapture`. React's tree includes content portaled out of it,
|
|
8019
|
+
* which the DOM path doesn't, and React has seen a press there by the time
|
|
8020
|
+
* the document hears it.
|
|
8021
|
+
*
|
|
8022
|
+
* The listener is on the element's own document, an iframe's when the tree
|
|
8023
|
+
* renders into one, and reads the latest `onOutsidePress` as a press
|
|
8024
|
+
* arrives, so it stays put while the callback changes identity.
|
|
8025
|
+
*
|
|
8026
|
+
* @function useOutsidePress
|
|
8027
|
+
* @param ref - Ref to the element a press inside of doesn't count.
|
|
8028
|
+
* @param active - Whether to listen.
|
|
8029
|
+
* @param onOutsidePress - Called with each press outside.
|
|
8030
|
+
* @returns The `onPointerDownCapture` handler that marks a press in its tree
|
|
8031
|
+
* as inside.
|
|
8032
|
+
*/
|
|
8033
|
+
function useOutsidePress(ref, active, onOutsidePress) {
|
|
8034
|
+
// The last pointerdown React saw inside.
|
|
8035
|
+
const insidePressRef = React.useRef(null);
|
|
8036
|
+
const onOutsidePressRef = useLatestRef(onOutsidePress);
|
|
8037
|
+
React.useEffect(() => {
|
|
8038
|
+
const element = ref.current;
|
|
8039
|
+
if (!active || !element)
|
|
8040
|
+
return undefined;
|
|
8041
|
+
const handlePointerDown = (e) => {
|
|
8042
|
+
if (e === insidePressRef.current || isEventInside(e, ref.current)) {
|
|
8043
|
+
return;
|
|
8044
|
+
}
|
|
8045
|
+
onOutsidePressRef.current(e);
|
|
8046
|
+
};
|
|
8047
|
+
const doc = element.ownerDocument;
|
|
8048
|
+
doc.addEventListener('pointerdown', handlePointerDown);
|
|
8049
|
+
return () => doc.removeEventListener('pointerdown', handlePointerDown);
|
|
8050
|
+
}, [ref, active, onOutsidePressRef]);
|
|
8051
|
+
return React.useCallback((event) => {
|
|
8052
|
+
insidePressRef.current = event.nativeEvent;
|
|
8053
|
+
}, []);
|
|
8054
|
+
}
|
|
8055
|
+
|
|
7956
8056
|
const PopoverContext = React.createContext(null);
|
|
7957
8057
|
/** Intrinsic elements that are controls already, and keep their own role. */
|
|
7958
8058
|
const NATIVE_CONTROLS = new Set([
|
|
@@ -8003,9 +8103,6 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
|
|
|
8003
8103
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
8004
8104
|
const rootRef = React.useRef(null);
|
|
8005
8105
|
const panelRef = React.useRef(null);
|
|
8006
|
-
// The last pointerdown React saw inside the panel. React's tree includes
|
|
8007
|
-
// content the panel portals elsewhere, which the DOM path doesn't.
|
|
8008
|
-
const insidePointerRef = React.useRef(null);
|
|
8009
8106
|
const baseId = React.useId();
|
|
8010
8107
|
const panelId = `${baseId}-panel`;
|
|
8011
8108
|
const titleId = `${baseId}-title`;
|
|
@@ -8061,25 +8158,10 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
|
|
|
8061
8158
|
}
|
|
8062
8159
|
};
|
|
8063
8160
|
}, [panelMounted, trapFocus, triggerNode]);
|
|
8064
|
-
//
|
|
8065
|
-
//
|
|
8066
|
-
//
|
|
8067
|
-
const
|
|
8068
|
-
React.useEffect(() => {
|
|
8069
|
-
if (!panelMounted || !closeOnClickOutside)
|
|
8070
|
-
return undefined;
|
|
8071
|
-
const handlePointerDown = (e) => {
|
|
8072
|
-
if (e === insidePointerRef.current || isEventInside(e, rootRef.current)) {
|
|
8073
|
-
return;
|
|
8074
|
-
}
|
|
8075
|
-
closeRef.current();
|
|
8076
|
-
};
|
|
8077
|
-
// The wrapper is on the page whenever the panel is. Its document, an
|
|
8078
|
-
// iframe's when the tree renders into one, is where presses land.
|
|
8079
|
-
const doc = rootRef.current.ownerDocument;
|
|
8080
|
-
doc.addEventListener('pointerdown', handlePointerDown);
|
|
8081
|
-
return () => doc.removeEventListener('pointerdown', handlePointerDown);
|
|
8082
|
-
}, [panelMounted, closeOnClickOutside, closeRef]);
|
|
8161
|
+
// A press outside the wrapper, which holds the trigger and the panel,
|
|
8162
|
+
// closes it. One inside the panel's React tree counts as inside, so content
|
|
8163
|
+
// the panel portals elsewhere does too.
|
|
8164
|
+
const markInsidePress = useOutsidePress(rootRef, panelMounted && closeOnClickOutside, close);
|
|
8083
8165
|
const handlePanelKeyDown = (e) => {
|
|
8084
8166
|
if (e.key !== 'Escape' || !closeOnEscape || e.defaultPrevented)
|
|
8085
8167
|
return;
|
|
@@ -8145,9 +8227,7 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
|
|
|
8145
8227
|
top: resolved.top,
|
|
8146
8228
|
left: resolved.left,
|
|
8147
8229
|
}
|
|
8148
|
-
: undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture:
|
|
8149
|
-
insidePointerRef.current = e.nativeEvent;
|
|
8150
|
-
}, children: children }) }))] }));
|
|
8230
|
+
: undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture: markInsidePress, children: children }) }))] }));
|
|
8151
8231
|
};
|
|
8152
8232
|
/**
|
|
8153
8233
|
* The open panel. It mounts with each opening, so it counts the
|
|
@@ -9275,6 +9355,69 @@ const TimelineRoot = React.forwardRef(({ className, children, active, pending, a
|
|
|
9275
9355
|
});
|
|
9276
9356
|
const Timeline = withSubComponents(TimelineRoot, { Item: TimelineItem }, 'Timeline');
|
|
9277
9357
|
|
|
9358
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
9359
|
+
/**
|
|
9360
|
+
* Moves focus into an overlay as it opens, to the first stop Tab wraps to
|
|
9361
|
+
* inside `ref`'s element or the element itself, and hands it back to
|
|
9362
|
+
* whatever had it as it closes, as `Modal` does. Focus goes back only while
|
|
9363
|
+
* the overlay still holds it, or it's been dropped to `<body>`: closing one
|
|
9364
|
+
* under another overlay mustn't pull focus out of the one on top, and focus
|
|
9365
|
+
* moved out to the page stays there. The opener is found wherever it sits,
|
|
9366
|
+
* inside a shadow root too, in the overlay's own document, an iframe's when
|
|
9367
|
+
* it renders into one. It's read in a layout effect as the overlay opens, so
|
|
9368
|
+
* before the commit's passive effects, such as one that makes the page
|
|
9369
|
+
* inert. Content inside the overlay with a layout effect of its own, or an
|
|
9370
|
+
* `autoFocus`, can still move focus first, and then there's no opener to go
|
|
9371
|
+
* back to: focus is never handed back into the overlay that's closing.
|
|
9372
|
+
*
|
|
9373
|
+
* @function useOverlayFocus
|
|
9374
|
+
* @param ref - Ref to the overlay's element.
|
|
9375
|
+
* @param open - Whether the overlay is open.
|
|
9376
|
+
* @param options - Whether it's modal, what remounts it, and whether a
|
|
9377
|
+
* close hands focus back.
|
|
9378
|
+
*/
|
|
9379
|
+
function useOverlayFocus(ref, open, { modal, remountKey, restore }) {
|
|
9380
|
+
const openerRef = React.useRef(null);
|
|
9381
|
+
const restoreRef = useLatestRef(restore);
|
|
9382
|
+
// The element that had focus, wherever it sits: a portaled overlay renders
|
|
9383
|
+
// under `document.body` while its opener can be inside a shadow root.
|
|
9384
|
+
useClientLayoutEffect(() => {
|
|
9385
|
+
if (!open)
|
|
9386
|
+
return;
|
|
9387
|
+
const element = ref.current;
|
|
9388
|
+
/* istanbul ignore if: as below, the ref is set while the overlay is open */
|
|
9389
|
+
if (!element)
|
|
9390
|
+
return;
|
|
9391
|
+
// Focus already inside, where an `autoFocus` in the overlay puts it, has
|
|
9392
|
+
// no opener to go back to.
|
|
9393
|
+
openerRef.current = element.contains(getActiveElementInTree(element))
|
|
9394
|
+
? null
|
|
9395
|
+
: getDeepestActiveElement(element.ownerDocument);
|
|
9396
|
+
}, [open, ref]);
|
|
9397
|
+
React.useEffect(() => {
|
|
9398
|
+
if (!open)
|
|
9399
|
+
return undefined;
|
|
9400
|
+
const element = ref.current;
|
|
9401
|
+
/* istanbul ignore if: the overlay renders whenever it's open, so its ref is set before this runs */
|
|
9402
|
+
if (!element)
|
|
9403
|
+
return undefined;
|
|
9404
|
+
if (modal || !isModalOverlayOpen()) {
|
|
9405
|
+
focusTabStop(element);
|
|
9406
|
+
}
|
|
9407
|
+
return () => {
|
|
9408
|
+
// A removed subtree leaves focus on <body>, which still counts as held.
|
|
9409
|
+
const active = getActiveElementInTree(element);
|
|
9410
|
+
const held = active === element.ownerDocument.body || element.contains(active);
|
|
9411
|
+
// Asked now rather than as it opened: it's about this close.
|
|
9412
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
9413
|
+
if (held && (restoreRef.current?.() ?? true)) {
|
|
9414
|
+
openerRef.current?.focus();
|
|
9415
|
+
}
|
|
9416
|
+
};
|
|
9417
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- `modal` is read as the overlay opens, on purpose
|
|
9418
|
+
}, [open, remountKey, ref, restoreRef]);
|
|
9419
|
+
}
|
|
9420
|
+
|
|
9278
9421
|
/**
|
|
9279
9422
|
* The `Sidebar` component provides a slide-out navigation panel that appears from the left or right side of the screen.
|
|
9280
9423
|
*
|
|
@@ -9324,31 +9467,15 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
9324
9467
|
handleClose();
|
|
9325
9468
|
}
|
|
9326
9469
|
}, [overlayClose, handleClose]);
|
|
9327
|
-
// Read when Escape arrives, so the key listener stays put while
|
|
9328
|
-
// `onClose` changes identity, as an inline one does on every render of
|
|
9329
|
-
// the parent.
|
|
9330
|
-
const handleCloseRef = useLatestRef(handleClose);
|
|
9331
9470
|
// Joins the stack of open overlays, so the sidebar answers Escape only
|
|
9332
9471
|
// while it's the topmost overlay, and keeps it from those under it. It's
|
|
9333
9472
|
// modal with an overlay: without one it leaves Tab to a modal overlay
|
|
9334
9473
|
// under it.
|
|
9335
9474
|
const isTopmost = useOverlayStack(isOpen, overlay);
|
|
9336
9475
|
const closesOnEscape = escapeClose && canCancel && Boolean(onClose);
|
|
9337
|
-
//
|
|
9338
|
-
// sidebar has had the key first, and an Escape it used is left to it.
|
|
9339
|
-
|
|
9340
|
-
React.useEffect(() => {
|
|
9341
|
-
if (!isOpen || !closesOnEscape)
|
|
9342
|
-
return;
|
|
9343
|
-
const handleKeyDown = (e) => {
|
|
9344
|
-
if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
|
|
9345
|
-
return;
|
|
9346
|
-
e.preventDefault();
|
|
9347
|
-
handleCloseRef.current();
|
|
9348
|
-
};
|
|
9349
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
9350
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
9351
|
-
}, [isOpen, closesOnEscape, handleCloseRef, isTopmost]);
|
|
9476
|
+
// Escape closes it, on `window`, as in `Modal`, so everything inside the
|
|
9477
|
+
// sidebar has had the key first, and an Escape it used is left to it.
|
|
9478
|
+
useOverlayEscape(isOpen && closesOnEscape, isTopmost, handleClose);
|
|
9352
9479
|
// Keep Tab inside while there's an overlay, as `Modal` does:
|
|
9353
9480
|
// `aria-modal` hides the rest of the page from assistive technology, so
|
|
9354
9481
|
// the keyboard order has to agree. Only while it's the topmost modal
|
|
@@ -9371,41 +9498,17 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
9371
9498
|
useScrollLock(isOpen && overlay);
|
|
9372
9499
|
// Move focus into the sidebar on open, to the first stop Tab wraps to or
|
|
9373
9500
|
// the sidebar itself, and hand it back on close, as `Modal` does. A
|
|
9374
|
-
// different `defaultTarget` remounts the portaled sidebar:
|
|
9375
|
-
//
|
|
9376
|
-
//
|
|
9377
|
-
//
|
|
9378
|
-
//
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
return undefined;
|
|
9386
|
-
// The element that had focus, wherever it sits: a portaled sidebar
|
|
9387
|
-
// renders under `document.body` while its opener can be inside a
|
|
9388
|
-
// shadow root.
|
|
9389
|
-
const opener = getDeepestActiveElement();
|
|
9390
|
-
// Without an overlay the sidebar holds no Tab, so with a modal overlay
|
|
9391
|
-
// open under it, the first Tab would take focus back there and leave
|
|
9392
|
-
// the sidebar's other controls out of reach. It leaves focus alone
|
|
9393
|
-
// then, and the cleanup below finds focus outside and leaves it too.
|
|
9394
|
-
if (overlay || !isModalOverlayOpen()) {
|
|
9395
|
-
focusTabStop(element);
|
|
9396
|
-
}
|
|
9397
|
-
return () => {
|
|
9398
|
-
// Only while the sidebar still holds focus: closing one under another
|
|
9399
|
-
// overlay mustn't pull focus out of the one on top, and focus moved
|
|
9400
|
-
// out to the page stays there. A removed subtree leaves focus on
|
|
9401
|
-
// <body>, which still counts as held.
|
|
9402
|
-
const active = getActiveElementInTree(element);
|
|
9403
|
-
if (active === document.body || element.contains(active)) {
|
|
9404
|
-
opener?.focus();
|
|
9405
|
-
}
|
|
9406
|
-
};
|
|
9407
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- `overlay` is read as the sidebar opens, on purpose
|
|
9408
|
-
}, [isOpen, defaultTarget]);
|
|
9501
|
+
// different `defaultTarget` remounts the portaled sidebar: focus is handed
|
|
9502
|
+
// back from the old nodes and moved into the new ones. Whether it takes
|
|
9503
|
+
// focus is decided as it opens, so `overlay` changing while it's open
|
|
9504
|
+
// leaves focus where the user has it. Without an overlay the sidebar holds
|
|
9505
|
+
// no Tab, so with a modal overlay open under it, it leaves focus alone:
|
|
9506
|
+
// the first Tab would take focus back there and leave the sidebar's other
|
|
9507
|
+
// controls out of reach.
|
|
9508
|
+
useOverlayFocus(sidebarRef, isOpen, {
|
|
9509
|
+
modal: overlay,
|
|
9510
|
+
remountKey: defaultTarget,
|
|
9511
|
+
});
|
|
9409
9512
|
// Use combined ref
|
|
9410
9513
|
const combinedRef = React.useCallback((node) => {
|
|
9411
9514
|
sidebarRef.current =
|
|
@@ -9967,10 +10070,7 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
|
|
|
9967
10070
|
// the Modal's subtree, these buttons included, so the focus effect below
|
|
9968
10071
|
// has to re-run against the new nodes, exactly as the Modal's own focus
|
|
9969
10072
|
// effect does, on the same inputs.
|
|
9970
|
-
const
|
|
9971
|
-
const isPortaled = Boolean(portal) && isHydrated;
|
|
9972
|
-
const { portalContainer } = useConfig();
|
|
9973
|
-
const defaultTarget = isPortaled && portal === true ? portalContainer : undefined;
|
|
10073
|
+
const { isPortaled, defaultTarget } = usePortalProp(portal);
|
|
9974
10074
|
// Focus management — Dialog picks a specific button rather than the
|
|
9975
10075
|
// Modal's default (first focusable/root); this effect runs after the
|
|
9976
10076
|
// inner Modal's own focus-on-open effect, so it wins.
|
|
@@ -10132,6 +10232,81 @@ const DialogContainer = () => {
|
|
|
10132
10232
|
return (jsxRuntime.jsx(Dialog, { isOpen: true, title: options.title, message: options.message, type: options.type, confirmText: options.confirmText, cancelText: options.cancelText, icon: options.icon, showCancel: type === 'confirm', focusCancel: options.focusCancel, onConfirm: () => dialog.close(type === 'confirm' ? true : undefined), onCancel: () => dialog.close(type === 'confirm' ? false : undefined) }));
|
|
10133
10233
|
};
|
|
10134
10234
|
|
|
10235
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
10236
|
+
/**
|
|
10237
|
+
* The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
|
|
10238
|
+
* mixins open with, widest first, at Bulma's default breakpoints. A
|
|
10239
|
+
* stylesheet lays a component out with those mixins; these tell its script
|
|
10240
|
+
* which layout that is. A theme that moves Bulma's breakpoints moves the
|
|
10241
|
+
* stylesheet's switch but not these. Narrower than the last one is `mobile`.
|
|
10242
|
+
*/
|
|
10243
|
+
const VIEWPORT_QUERIES = [
|
|
10244
|
+
'screen and (min-width: 1408px)',
|
|
10245
|
+
'screen and (min-width: 1216px)',
|
|
10246
|
+
'screen and (min-width: 1024px)',
|
|
10247
|
+
'screen and (min-width: 769px), print',
|
|
10248
|
+
];
|
|
10249
|
+
/** `useBulmaViewport`'s answer below the tablet breakpoint. */
|
|
10250
|
+
const MOBILE = VIEWPORT_QUERIES.length;
|
|
10251
|
+
/** Where Bulma's `widescreen`, `desktop` and `tablet` sit in `VIEWPORT_QUERIES`. */
|
|
10252
|
+
const VIEWPORT_INDEX = {
|
|
10253
|
+
widescreen: 1,
|
|
10254
|
+
desktop: 2,
|
|
10255
|
+
tablet: 3,
|
|
10256
|
+
};
|
|
10257
|
+
/**
|
|
10258
|
+
* Which of Bulma's viewports the window is in: the index of the widest
|
|
10259
|
+
* `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
|
|
10260
|
+
* when that isn't known (on the server, before the first layout effect, and
|
|
10261
|
+
* whenever `enabled` is off, so a caller that needs no answer subscribes to
|
|
10262
|
+
* nothing). It's read in a layout effect, so the first paint after hydration
|
|
10263
|
+
* already has it.
|
|
10264
|
+
*
|
|
10265
|
+
* @function useBulmaViewport
|
|
10266
|
+
* @param enabled - Whether the caller needs the viewport at all.
|
|
10267
|
+
* @returns The viewport, as described above.
|
|
10268
|
+
*/
|
|
10269
|
+
function useBulmaViewport(enabled) {
|
|
10270
|
+
const [viewport, setViewport] = React.useState(-1);
|
|
10271
|
+
useClientLayoutEffect(() => {
|
|
10272
|
+
if (!enabled || typeof window.matchMedia !== 'function')
|
|
10273
|
+
return undefined;
|
|
10274
|
+
const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
|
|
10275
|
+
const update = () => {
|
|
10276
|
+
const index = lists.findIndex(list => list.matches);
|
|
10277
|
+
setViewport(index < 0 ? MOBILE : index);
|
|
10278
|
+
};
|
|
10279
|
+
update();
|
|
10280
|
+
lists.forEach(list => list.addEventListener('change', update));
|
|
10281
|
+
return () => lists.forEach(list => list.removeEventListener('change', update));
|
|
10282
|
+
}, [enabled]);
|
|
10283
|
+
return enabled ? viewport : -1;
|
|
10284
|
+
}
|
|
10285
|
+
/**
|
|
10286
|
+
* Whether the window is at or past one of Bulma's breakpoints, at its default
|
|
10287
|
+
* width, through the one media query Bulma's mixin for it opens with, so a
|
|
10288
|
+
* caller re-renders only when the window crosses that breakpoint. `null`
|
|
10289
|
+
* until it's known: on the server, and before the first layout effect. Where
|
|
10290
|
+
* `matchMedia` isn't there, it's `true`, the wide layout a server renders.
|
|
10291
|
+
*
|
|
10292
|
+
* @function useBulmaBreakpoint
|
|
10293
|
+
* @param breakpoint - The breakpoint: `tablet`, `desktop` or `widescreen`.
|
|
10294
|
+
* @returns Whether the window is at or past it, or `null`.
|
|
10295
|
+
*/
|
|
10296
|
+
function useBulmaBreakpoint(breakpoint) {
|
|
10297
|
+
const [atOrPast, setAtOrPast] = React.useState(null);
|
|
10298
|
+
useClientLayoutEffect(() => {
|
|
10299
|
+
const list = typeof window.matchMedia === 'function'
|
|
10300
|
+
? window.matchMedia(VIEWPORT_QUERIES[VIEWPORT_INDEX[breakpoint]])
|
|
10301
|
+
: null;
|
|
10302
|
+
const update = () => setAtOrPast(list ? list.matches : true);
|
|
10303
|
+
update();
|
|
10304
|
+
list?.addEventListener('change', update);
|
|
10305
|
+
return () => list?.removeEventListener('change', update);
|
|
10306
|
+
}, [breakpoint]);
|
|
10307
|
+
return atOrPast;
|
|
10308
|
+
}
|
|
10309
|
+
|
|
10135
10310
|
/**
|
|
10136
10311
|
* Individual carousel item/slide.
|
|
10137
10312
|
*
|
|
@@ -10159,24 +10334,10 @@ const DefaultPrevIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
|
|
|
10159
10334
|
* @returns {JSX.Element} The rendered SVG chevron-right icon.
|
|
10160
10335
|
*/
|
|
10161
10336
|
const DefaultNextIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }));
|
|
10162
|
-
/**
|
|
10163
|
-
* The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
|
|
10164
|
-
* mixins open with, widest first, at Bulma's default breakpoints. The
|
|
10165
|
-
* stylesheet picks the count in view with those mixins; these tell the
|
|
10166
|
-
* script which count that is. Narrower than the last one is `mobile`.
|
|
10167
|
-
*/
|
|
10168
|
-
const VIEWPORT_QUERIES = [
|
|
10169
|
-
'screen and (min-width: 1408px)',
|
|
10170
|
-
'screen and (min-width: 1216px)',
|
|
10171
|
-
'screen and (min-width: 1024px)',
|
|
10172
|
-
'screen and (min-width: 769px), print',
|
|
10173
|
-
];
|
|
10174
|
-
/** `useBulmaViewport`'s answer below the tablet breakpoint. */
|
|
10175
|
-
const MOBILE = VIEWPORT_QUERIES.length;
|
|
10176
10337
|
/**
|
|
10177
10338
|
* The `--bulma-carousel-items-to-show*` variable each count prop writes, in
|
|
10178
|
-
* the order the component lists them:
|
|
10179
|
-
* last `itemsToShow` itself.
|
|
10339
|
+
* the order the component lists them: `useBulmaViewport`'s viewports, widest
|
|
10340
|
+
* first, then mobile, and last `itemsToShow` itself.
|
|
10180
10341
|
*/
|
|
10181
10342
|
const COUNT_SUFFIXES = [
|
|
10182
10343
|
'-fullhd',
|
|
@@ -10186,33 +10347,6 @@ const COUNT_SUFFIXES = [
|
|
|
10186
10347
|
'-mobile',
|
|
10187
10348
|
'',
|
|
10188
10349
|
];
|
|
10189
|
-
/**
|
|
10190
|
-
* Which of Bulma's viewports the window is in: the index of the widest
|
|
10191
|
-
* `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
|
|
10192
|
-
* when that isn't known (on the server, before the first layout effect, and
|
|
10193
|
-
* whenever `enabled` is off, so a carousel with one count everywhere
|
|
10194
|
-
* subscribes to nothing).
|
|
10195
|
-
*
|
|
10196
|
-
* @function
|
|
10197
|
-
* @param {boolean} enabled - Whether any viewport-specific count is set.
|
|
10198
|
-
* @returns {number} The viewport, as described above.
|
|
10199
|
-
*/
|
|
10200
|
-
function useBulmaViewport(enabled) {
|
|
10201
|
-
const [viewport, setViewport] = React.useState(-1);
|
|
10202
|
-
useClientLayoutEffect(() => {
|
|
10203
|
-
if (!enabled || typeof window.matchMedia !== 'function')
|
|
10204
|
-
return undefined;
|
|
10205
|
-
const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
|
|
10206
|
-
const update = () => {
|
|
10207
|
-
const index = lists.findIndex(list => list.matches);
|
|
10208
|
-
setViewport(index < 0 ? MOBILE : index);
|
|
10209
|
-
};
|
|
10210
|
-
update();
|
|
10211
|
-
lists.forEach(list => list.addEventListener('change', update));
|
|
10212
|
-
return () => lists.forEach(list => list.removeEventListener('change', update));
|
|
10213
|
-
}, [enabled]);
|
|
10214
|
-
return enabled ? viewport : -1;
|
|
10215
|
-
}
|
|
10216
10350
|
/**
|
|
10217
10351
|
* A count prop as the whole number the carousel works with: rounded down,
|
|
10218
10352
|
* and never below 1, so a zero or a stray `NaN` can't divide by nothing.
|
|
@@ -13110,7 +13244,7 @@ const standalone$2 = {
|
|
|
13110
13244
|
const CommandContext = React.createContext(null);
|
|
13111
13245
|
const useCommand = () => React.useContext(CommandContext) ?? standalone$2;
|
|
13112
13246
|
/** Hands a node to a forwarded ref and to one of the component's own. */
|
|
13113
|
-
function setRefs$
|
|
13247
|
+
function setRefs$3(node, own, forwarded) {
|
|
13114
13248
|
own.current = node;
|
|
13115
13249
|
if (typeof forwarded === 'function')
|
|
13116
13250
|
forwarded(node);
|
|
@@ -13227,7 +13361,7 @@ const CommandParts = React.forwardRef(({ children, label, query: controlledQuery
|
|
|
13227
13361
|
registerHandler,
|
|
13228
13362
|
};
|
|
13229
13363
|
const { rootRef } = engine;
|
|
13230
|
-
const rootRefs = React.useCallback((node) => setRefs$
|
|
13364
|
+
const rootRefs = React.useCallback((node) => setRefs$3(node, rootRef, ref), [rootRef, ref]);
|
|
13231
13365
|
return (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(CommandContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
|
|
13232
13366
|
});
|
|
13233
13367
|
CommandParts.displayName = 'CommandParts';
|
|
@@ -13247,7 +13381,7 @@ const CommandInput = React.forwardRef(({ isLoading, iconLeft, iconLeftName, onKe
|
|
|
13247
13381
|
return () => reportLoading(false);
|
|
13248
13382
|
}, [reportLoading, isLoading]);
|
|
13249
13383
|
const { inputRef } = engine;
|
|
13250
|
-
const inputRefs = React.useCallback((node) => setRefs$
|
|
13384
|
+
const inputRefs = React.useCallback((node) => setRefs$3(node, inputRef, ref), [inputRef, ref]);
|
|
13251
13385
|
const { expanded, highlighted } = engine;
|
|
13252
13386
|
// A name the caller gave the field wins over the Command's `label`.
|
|
13253
13387
|
const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
|
|
@@ -14624,6 +14758,42 @@ const DataTable = React.forwardRef(function DataTableRender({ columns, data, tab
|
|
|
14624
14758
|
});
|
|
14625
14759
|
DataTable.displayName = 'DataTable';
|
|
14626
14760
|
|
|
14761
|
+
/**
|
|
14762
|
+
* The `Affix` component keeps its content fixed to a corner of the viewport while the page scrolls under it.
|
|
14763
|
+
*
|
|
14764
|
+
* It's a `<div>` with no role, so what's inside keeps its own. It sits
|
|
14765
|
+
* `--bulma-affix-offset-vertical` and `--bulma-affix-offset-horizontal`
|
|
14766
|
+
* from its corner's edges, plus the device's safe-area inset, and stacks at
|
|
14767
|
+
* `--bulma-affix-z-index`, over the page's content and under its navbar,
|
|
14768
|
+
* static or fixed, its menus, popovers, the Sidebar, the modals, Tooltip and
|
|
14769
|
+
* Toast. It's as wide as its
|
|
14770
|
+
* content and never wider than the viewport less its offsets. It doesn't
|
|
14771
|
+
* scroll, so nothing inside it is clipped, and content that can be taller
|
|
14772
|
+
* than the screen scrolls itself. Its place in the reading and Tab order is
|
|
14773
|
+
* its place in the DOM, whatever corner it shows in.
|
|
14774
|
+
*
|
|
14775
|
+
* @function
|
|
14776
|
+
* @param {AffixProps} props - Props for the Affix component.
|
|
14777
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `<div>`.
|
|
14778
|
+
* @returns The `<div>`, in place or in its portal.
|
|
14779
|
+
*
|
|
14780
|
+
* @example
|
|
14781
|
+
* <Affix position="bottom-left">
|
|
14782
|
+
* <Button color="primary">Feedback</Button>
|
|
14783
|
+
* </Affix>
|
|
14784
|
+
*/
|
|
14785
|
+
const Affix = React.forwardRef(({ position = 'bottom-right', portal = false, className, children, ...props }, ref) => {
|
|
14786
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
14787
|
+
const affixClasses = usePrefixedClassNames('affix', {
|
|
14788
|
+
[`is-${position}`]: true,
|
|
14789
|
+
});
|
|
14790
|
+
// In place on the server and while hydrating, then into the portal, as
|
|
14791
|
+
// Modal does. The move remounts the `<div>`.
|
|
14792
|
+
const { isPortaled, container } = usePortalProp(portal);
|
|
14793
|
+
return (jsxRuntime.jsx(Portal, { disabled: !isPortaled, container: container, children: jsxRuntime.jsx("div", { ref: ref, className: classNames(affixClasses, bulmaHelperClasses, className), ...rest, children: children }) }));
|
|
14794
|
+
});
|
|
14795
|
+
Affix.displayName = 'Affix';
|
|
14796
|
+
|
|
14627
14797
|
/**
|
|
14628
14798
|
* The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
|
|
14629
14799
|
*
|
|
@@ -18740,7 +18910,7 @@ const standalone$1 = {
|
|
|
18740
18910
|
const ComboboxContext = React.createContext(null);
|
|
18741
18911
|
const useCombobox = () => React.useContext(ComboboxContext) ?? standalone$1;
|
|
18742
18912
|
/** Hands a node to a forwarded ref and to one of the component's own. */
|
|
18743
|
-
function setRefs$
|
|
18913
|
+
function setRefs$2(node, own, forwarded) {
|
|
18744
18914
|
own.current = node;
|
|
18745
18915
|
if (typeof forwarded === 'function')
|
|
18746
18916
|
forwarded(node);
|
|
@@ -18872,7 +19042,7 @@ const ComboboxParts = React.forwardRef(({ children, below, onSettle, open, defau
|
|
|
18872
19042
|
appendToBody,
|
|
18873
19043
|
};
|
|
18874
19044
|
const { rootRef } = engine;
|
|
18875
|
-
const rootRefs = React.useCallback((node) => setRefs$
|
|
19045
|
+
const rootRefs = React.useCallback((node) => setRefs$2(node, rootRef, ref), [rootRef, ref]);
|
|
18876
19046
|
const comboboxElement = (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(ComboboxContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
|
|
18877
19047
|
const fieldElement = below === undefined ? (comboboxElement) : (jsxRuntime.jsxs("div", { children: [comboboxElement, jsxRuntime.jsx("div", { onPointerDownCapture: engine.markInside, onMouseDown: engine.keepFocus, children: below })] }));
|
|
18878
19048
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
@@ -18905,7 +19075,7 @@ const ComboboxInput = React.forwardRef(({ id, isLoading, controlSize, onKeyDown,
|
|
|
18905
19075
|
return () => reportLoading(false);
|
|
18906
19076
|
}, [reportLoading, isLoading]);
|
|
18907
19077
|
const { inputRef } = engine;
|
|
18908
|
-
const inputRefs = React.useCallback((node) => setRefs$
|
|
19078
|
+
const inputRefs = React.useCallback((node) => setRefs$2(node, inputRef, ref), [inputRef, ref]);
|
|
18909
19079
|
const { expanded, highlighted } = engine;
|
|
18910
19080
|
// An `id` of the field's own reaches the label from a layout effect, so
|
|
18911
19081
|
// in server markup, and the first render, the label's `for` still names
|
|
@@ -20202,7 +20372,7 @@ const rgbChannels = (color) => {
|
|
|
20202
20372
|
/** Whether a press should start a drag: any touch or pen, a mouse's main button. */
|
|
20203
20373
|
const startsDrag = (event) => event.pointerType !== 'mouse' || event.button === 0;
|
|
20204
20374
|
/** Hands a node to a forwarded ref and to one of the part's own. */
|
|
20205
|
-
function setRefs(node, own, forwarded) {
|
|
20375
|
+
function setRefs$1(node, own, forwarded) {
|
|
20206
20376
|
own.current = node;
|
|
20207
20377
|
if (typeof forwarded === 'function')
|
|
20208
20378
|
forwarded(node);
|
|
@@ -20334,7 +20504,7 @@ const ColorPickerArea = React.forwardRef(({ className, style, ...props }, ref) =
|
|
|
20334
20504
|
const hiddenOf = (which) => touch || keyed || axis === which ? undefined : true;
|
|
20335
20505
|
if (!picker)
|
|
20336
20506
|
return null;
|
|
20337
|
-
return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
|
|
20507
|
+
return (jsxRuntime.jsx("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
|
|
20338
20508
|
...style,
|
|
20339
20509
|
'--color-picker-hue': Math.round(color.h),
|
|
20340
20510
|
'--color-picker-rgb': rgbChannels(color),
|
|
@@ -20408,7 +20578,7 @@ const ChannelSlider = React.forwardRef(({ channel, className, style, ...props },
|
|
|
20408
20578
|
};
|
|
20409
20579
|
if (!picker)
|
|
20410
20580
|
return null;
|
|
20411
|
-
return (jsxRuntime.jsxs("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
|
|
20581
|
+
return (jsxRuntime.jsxs("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
|
|
20412
20582
|
...style,
|
|
20413
20583
|
'--color-picker-hue': Math.round(color.h),
|
|
20414
20584
|
'--color-picker-rgb': rgbChannels(color),
|
|
@@ -20532,7 +20702,7 @@ const ColorPickerSwatches = React.forwardRef(({ className, swatches: ownSwatches
|
|
|
20532
20702
|
};
|
|
20533
20703
|
if (!picker || list.length === 0)
|
|
20534
20704
|
return null;
|
|
20535
|
-
return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(groupClass, bulmaHelperClasses, className), role: "radiogroup", "aria-label": labels.swatches, "aria-disabled": disabled || undefined, ...rest, children: list.map((entry, i) => {
|
|
20705
|
+
return (jsxRuntime.jsx("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(groupClass, bulmaHelperClasses, className), role: "radiogroup", "aria-label": labels.swatches, "aria-disabled": disabled || undefined, ...rest, children: list.map((entry, i) => {
|
|
20536
20706
|
const swatch = colorOf(i);
|
|
20537
20707
|
const isChecked = i === checked;
|
|
20538
20708
|
// The check is drawn in black or white with a ring of the other, so
|
|
@@ -26960,6 +27130,421 @@ function ClientOnly({ children, fallback = null }) {
|
|
|
26960
27130
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: typeof children === 'function' ? children() : children });
|
|
26961
27131
|
}
|
|
26962
27132
|
|
|
27133
|
+
const FrameContext = React.createContext(null);
|
|
27134
|
+
/** A width prop as CSS: a number is pixels. */
|
|
27135
|
+
const cssLength = (value) => typeof value === 'number' ? `${value}px` : value;
|
|
27136
|
+
/**
|
|
27137
|
+
* Hands `node` to a part's own ref and to the one forwarded to it. A
|
|
27138
|
+
* forwarded callback that returns a cleanup, as React 19 allows, has it held
|
|
27139
|
+
* in `cleanup` and run on detach in place of a call with `null`, on React 18
|
|
27140
|
+
* as well.
|
|
27141
|
+
*/
|
|
27142
|
+
function setRefs(node, own, forwarded, cleanup) {
|
|
27143
|
+
own.current = node;
|
|
27144
|
+
if (typeof forwarded === 'function') {
|
|
27145
|
+
if (node === null) {
|
|
27146
|
+
const detach = cleanup.current;
|
|
27147
|
+
cleanup.current = null;
|
|
27148
|
+
if (detach)
|
|
27149
|
+
detach();
|
|
27150
|
+
else
|
|
27151
|
+
forwarded(null);
|
|
27152
|
+
return;
|
|
27153
|
+
}
|
|
27154
|
+
const returned = forwarded(node);
|
|
27155
|
+
cleanup.current =
|
|
27156
|
+
typeof returned === 'function' ? returned : null;
|
|
27157
|
+
}
|
|
27158
|
+
else if (forwarded) {
|
|
27159
|
+
forwarded.current = node;
|
|
27160
|
+
}
|
|
27161
|
+
}
|
|
27162
|
+
/**
|
|
27163
|
+
* Warns when a part has an `id` the frame couldn't read, because it isn't a
|
|
27164
|
+
* direct child, so the skip link or the burger points at the wrong one.
|
|
27165
|
+
*/
|
|
27166
|
+
function warnOnUnreadId(part, id, expected) {
|
|
27167
|
+
if (id && id !== expected) {
|
|
27168
|
+
warnOnce(`AppFrame:${part}-id`, `[bestax-bulma] <AppFrame.${part}> has an id <AppFrame> can't read, ` +
|
|
27169
|
+
`so ${part === 'Main' ? 'the skip link' : "the burger's aria-controls"} ` +
|
|
27170
|
+
`misses it. Put AppFrame.${part} directly inside AppFrame.`);
|
|
27171
|
+
}
|
|
27172
|
+
}
|
|
27173
|
+
/**
|
|
27174
|
+
* The top bar of an AppFrame, which sticks while the page scrolls.
|
|
27175
|
+
*
|
|
27176
|
+
* @function
|
|
27177
|
+
* @param {AppFrameHeaderProps} props - Props for AppFrame.Header.
|
|
27178
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<header>`.
|
|
27179
|
+
* @returns {JSX.Element} The rendered header.
|
|
27180
|
+
*/
|
|
27181
|
+
const AppFrameHeader = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
27182
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
27183
|
+
const headerClass = usePrefixedClassNames('app-frame-header');
|
|
27184
|
+
return (jsxRuntime.jsx("header", { ref: ref, className: classNames(headerClass, bulmaHelperClasses, className), ...rest, children: children }));
|
|
27185
|
+
});
|
|
27186
|
+
AppFrameHeader.displayName = 'AppFrame.Header';
|
|
27187
|
+
/**
|
|
27188
|
+
* The navigation of an AppFrame: a column beside the page from the breakpoint up, and a drawer below it.
|
|
27189
|
+
*
|
|
27190
|
+
* @function
|
|
27191
|
+
* @param {AppFrameNavProps} props - Props for AppFrame.Nav.
|
|
27192
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<nav>`.
|
|
27193
|
+
* @returns {JSX.Element} The rendered nav, and the drawer's backdrop.
|
|
27194
|
+
*/
|
|
27195
|
+
const AppFrameNav = React.forwardRef(({ className, style, width, overlay = true, closeOnEscape = true, closeOnClickOutside = true, closeOnNavigate = true, onClick, onPointerDownCapture, children, ...props }, ref) => {
|
|
27196
|
+
const frame = React.useContext(FrameContext);
|
|
27197
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
27198
|
+
const navClass = usePrefixedClassNames('app-frame-nav');
|
|
27199
|
+
const closeClass = usePrefixedClassNames('app-frame-close');
|
|
27200
|
+
const backdropClass = usePrefixedClassNames('app-frame-backdrop');
|
|
27201
|
+
const activeClass = usePrefixedClassNames('is-active');
|
|
27202
|
+
const navRef = React.useRef(null);
|
|
27203
|
+
const backdropRef = React.useRef(null);
|
|
27204
|
+
const navCleanupRef = React.useRef(null);
|
|
27205
|
+
const navRefs = React.useCallback((node) => setRefs(node, navRef, ref, navCleanupRef), [ref]);
|
|
27206
|
+
const navId = rest.id || frame?.state.navId;
|
|
27207
|
+
if (frame)
|
|
27208
|
+
warnOnUnreadId('Nav', rest.id, frame.state.navId);
|
|
27209
|
+
// The drawer is open only below the breakpoint. Above it the nav is a
|
|
27210
|
+
// column, and none of what follows applies.
|
|
27211
|
+
const isDrawer = !!frame?.state.isDrawer;
|
|
27212
|
+
const open = isDrawer && !!frame?.state.navOpen;
|
|
27213
|
+
// The open drawer is Bulma's active state, on the nav as on a navbar's
|
|
27214
|
+
// menu, and on its backdrop.
|
|
27215
|
+
const shown = { [activeClass]: !!frame?.state.navOpen };
|
|
27216
|
+
const modal = open && overlay;
|
|
27217
|
+
const close = () => frame?.state.setNavOpen(false);
|
|
27218
|
+
// The rest of the frame goes inert: the header, the page, the aside and
|
|
27219
|
+
// the footer, but not the backdrop, which takes the presses outside the
|
|
27220
|
+
// drawer rather than let them through to what's under it. Not the whole
|
|
27221
|
+
// page, since a Modal or a toast renders beside the frame, often into an
|
|
27222
|
+
// element that's there before the drawer opens, and a Modal opened from
|
|
27223
|
+
// the drawer has to stay usable. First, so that as the drawer closes it
|
|
27224
|
+
// lets go before focus goes back to the burger: an inert element can't
|
|
27225
|
+
// take focus.
|
|
27226
|
+
useInertSiblings(navRef, modal, false, backdropRef);
|
|
27227
|
+
const isTopmost = useOverlayStack(open, overlay);
|
|
27228
|
+
useScrollLock(modal);
|
|
27229
|
+
// Tab stays inside while the drawer is the topmost modal overlay, as in
|
|
27230
|
+
// Sidebar, so a Modal opened from the drawer holds Tab until it closes.
|
|
27231
|
+
React.useEffect(() => {
|
|
27232
|
+
if (!modal)
|
|
27233
|
+
return undefined;
|
|
27234
|
+
return trapTab(() => navRef.current, window, () => isTopmost('modal'));
|
|
27235
|
+
}, [modal, isTopmost]);
|
|
27236
|
+
useOverlayEscape(open && closeOnEscape, isTopmost, close);
|
|
27237
|
+
// Without an overlay there's no backdrop: a press outside the drawer
|
|
27238
|
+
// closes it, as Popover's does, while it's the topmost overlay, so a
|
|
27239
|
+
// press in a Modal opened over it is the Modal's. A press on a control
|
|
27240
|
+
// that names the nav in its `aria-controls`, such as the burger, is left
|
|
27241
|
+
// to that control, which toggles it.
|
|
27242
|
+
const markInsidePress = useOutsidePress(navRef, open && !overlay && closeOnClickOutside, e => {
|
|
27243
|
+
const id = navRef.current?.id;
|
|
27244
|
+
const controlsNav = (node) => !!node
|
|
27245
|
+
.getAttribute?.('aria-controls')
|
|
27246
|
+
?.split(/\s+/)
|
|
27247
|
+
.includes(id);
|
|
27248
|
+
if (!isTopmost() || e.composedPath().some(controlsNav))
|
|
27249
|
+
return;
|
|
27250
|
+
close();
|
|
27251
|
+
});
|
|
27252
|
+
// Focus moves to the drawer's first tab stop, the close button, as it
|
|
27253
|
+
// opens, and back to whatever had it as it closes, unless it has left
|
|
27254
|
+
// the drawer. After a press outside, the press's own mousedown then takes
|
|
27255
|
+
// it wherever the press goes. When the window widened, the drawer is the
|
|
27256
|
+
// column again, and focus on its links stays; the close button hides
|
|
27257
|
+
// there, so focus on it, or dropped from it to <body>, goes back too.
|
|
27258
|
+
const isDrawerRef = useLatestRef(isDrawer);
|
|
27259
|
+
useOverlayFocus(navRef, open, {
|
|
27260
|
+
modal: overlay,
|
|
27261
|
+
restore: () => {
|
|
27262
|
+
if (isDrawerRef.current)
|
|
27263
|
+
return true;
|
|
27264
|
+
const nav = navRef.current;
|
|
27265
|
+
const active = getActiveElementInTree(nav);
|
|
27266
|
+
return (!nav.contains(active) || !!active?.classList.contains(closeClass));
|
|
27267
|
+
},
|
|
27268
|
+
});
|
|
27269
|
+
const handleClick = (e) => {
|
|
27270
|
+
onClick?.(e);
|
|
27271
|
+
if (!open ||
|
|
27272
|
+
!closeOnNavigate ||
|
|
27273
|
+
e.button !== 0 ||
|
|
27274
|
+
e.metaKey ||
|
|
27275
|
+
e.ctrlKey ||
|
|
27276
|
+
e.shiftKey ||
|
|
27277
|
+
e.altKey) {
|
|
27278
|
+
return;
|
|
27279
|
+
}
|
|
27280
|
+
// Not skipped for a click whose default was prevented: a router's
|
|
27281
|
+
// link prevents it to navigate without a page load. Only a link the nav
|
|
27282
|
+
// holds: one in a Modal portaled from it is the Modal's.
|
|
27283
|
+
const link = e.target.closest('a[href]');
|
|
27284
|
+
if (!link || !navRef.current?.contains(link))
|
|
27285
|
+
return;
|
|
27286
|
+
if (link.hasAttribute('download'))
|
|
27287
|
+
return;
|
|
27288
|
+
const target = link.getAttribute('target');
|
|
27289
|
+
if (target && target !== '_self')
|
|
27290
|
+
return;
|
|
27291
|
+
close();
|
|
27292
|
+
};
|
|
27293
|
+
const navStyle = width === undefined
|
|
27294
|
+
? style
|
|
27295
|
+
: {
|
|
27296
|
+
...style,
|
|
27297
|
+
'--bulma-app-frame-nav-width': cssLength(width),
|
|
27298
|
+
};
|
|
27299
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("nav", { ref: navRefs, className: classNames(navClass, shown, bulmaHelperClasses, className), style: navStyle, onClick: handleClick, onPointerDownCapture: e => {
|
|
27300
|
+
onPointerDownCapture?.(e);
|
|
27301
|
+
markInsidePress(e);
|
|
27302
|
+
}, ...rest, id: navId, "aria-label": rest['aria-label'] ||
|
|
27303
|
+
(rest['aria-labelledby'] ? undefined : frame?.labels.nav), children: [frame && (jsxRuntime.jsx(Delete, { className: closeClass, size: "medium", ariaLabel: frame.labels.closeNav, onClick: close })), children] }), frame && overlay && (jsxRuntime.jsx("div", { ref: backdropRef, className: classNames(backdropClass, shown), "aria-hidden": "true", onClick: closeOnClickOutside
|
|
27304
|
+
? () => {
|
|
27305
|
+
// Not under an overlay opened over the drawer.
|
|
27306
|
+
if (isTopmost())
|
|
27307
|
+
close();
|
|
27308
|
+
}
|
|
27309
|
+
: undefined }))] }));
|
|
27310
|
+
});
|
|
27311
|
+
AppFrameNav.displayName = 'AppFrame.Nav';
|
|
27312
|
+
/**
|
|
27313
|
+
* The page's main content in an AppFrame, which the skip link moves focus to.
|
|
27314
|
+
*
|
|
27315
|
+
* @function
|
|
27316
|
+
* @param {AppFrameMainProps} props - Props for AppFrame.Main.
|
|
27317
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<main>`.
|
|
27318
|
+
* @returns {JSX.Element} The rendered main.
|
|
27319
|
+
*/
|
|
27320
|
+
const AppFrameMain = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
27321
|
+
const frame = React.useContext(FrameContext);
|
|
27322
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
27323
|
+
const mainClass = usePrefixedClassNames('app-frame-main');
|
|
27324
|
+
if (frame)
|
|
27325
|
+
warnOnUnreadId('Main', rest.id, frame.mainId);
|
|
27326
|
+
return (jsxRuntime.jsx("main", { ref: ref, tabIndex: -1, className: classNames(mainClass, bulmaHelperClasses, className), ...rest, id: rest.id || frame?.mainId, children: children }));
|
|
27327
|
+
});
|
|
27328
|
+
AppFrameMain.displayName = 'AppFrame.Main';
|
|
27329
|
+
/**
|
|
27330
|
+
* Content beside the page in an AppFrame, which moves under it below the breakpoint.
|
|
27331
|
+
*
|
|
27332
|
+
* @function
|
|
27333
|
+
* @param {AppFrameAsideProps} props - Props for AppFrame.Aside.
|
|
27334
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<aside>`.
|
|
27335
|
+
* @returns {JSX.Element} The rendered aside.
|
|
27336
|
+
*/
|
|
27337
|
+
const AppFrameAside = React.forwardRef(({ className, style, width, children, ...props }, ref) => {
|
|
27338
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
27339
|
+
const asideClass = usePrefixedClassNames('app-frame-aside');
|
|
27340
|
+
const asideStyle = width === undefined
|
|
27341
|
+
? style
|
|
27342
|
+
: {
|
|
27343
|
+
...style,
|
|
27344
|
+
'--bulma-app-frame-aside-width': cssLength(width),
|
|
27345
|
+
};
|
|
27346
|
+
return (jsxRuntime.jsx("aside", { ref: ref, className: classNames(asideClass, bulmaHelperClasses, className), style: asideStyle, ...rest, children: children }));
|
|
27347
|
+
});
|
|
27348
|
+
AppFrameAside.displayName = 'AppFrame.Aside';
|
|
27349
|
+
/**
|
|
27350
|
+
* The bottom of an AppFrame, under the page.
|
|
27351
|
+
*
|
|
27352
|
+
* @function
|
|
27353
|
+
* @param {AppFrameFooterProps} props - Props for AppFrame.Footer.
|
|
27354
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<footer>`.
|
|
27355
|
+
* @returns {JSX.Element} The rendered footer.
|
|
27356
|
+
*/
|
|
27357
|
+
const AppFrameFooter = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
27358
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
27359
|
+
const footerClass = usePrefixedClassNames('app-frame-footer');
|
|
27360
|
+
return (jsxRuntime.jsx("footer", { ref: ref, className: classNames(footerClass, bulmaHelperClasses, className), ...rest, children: children }));
|
|
27361
|
+
});
|
|
27362
|
+
AppFrameFooter.displayName = 'AppFrame.Footer';
|
|
27363
|
+
/**
|
|
27364
|
+
* The button that opens the drawer, or collapses the column from the breakpoint up: Navbar.Burger, wired to the frame.
|
|
27365
|
+
*
|
|
27366
|
+
* @function
|
|
27367
|
+
* @param {AppFrameBurgerProps} props - Props for AppFrame.Burger.
|
|
27368
|
+
* @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
|
|
27369
|
+
* @returns {JSX.Element} The rendered burger.
|
|
27370
|
+
*/
|
|
27371
|
+
const AppFrameBurger = React.forwardRef(({ className, onClick, ...props }, ref) => {
|
|
27372
|
+
// Outside a frame it's a Navbar.Burger that controls nothing.
|
|
27373
|
+
const frame = React.useContext(FrameContext);
|
|
27374
|
+
const burgerClass = usePrefixedClassNames('app-frame-burger');
|
|
27375
|
+
const state = frame?.state;
|
|
27376
|
+
// Until the frame knows whether the nav is a drawer, the burger says
|
|
27377
|
+
// nothing it can't stand behind: no `aria-expanded`, and its bars. It can't
|
|
27378
|
+
// act before then anyway, since that's before hydration. Outside a frame
|
|
27379
|
+
// it's a plain Navbar.Burger.
|
|
27380
|
+
const known = !!frame?.modeKnown;
|
|
27381
|
+
const expanded = known && !!state && (state.isDrawer ? state.navOpen : !state.navCollapsed);
|
|
27382
|
+
return (jsxRuntime.jsx(NavbarBurger, { ref: ref, ...props, className: classNames(burgerClass, className), "aria-label": props['aria-label'] || frame?.labels.burger, active: expanded, "aria-expanded": frame && !known ? null : expanded, "aria-controls": state?.navId, onClick: e => {
|
|
27383
|
+
onClick?.(e);
|
|
27384
|
+
if (e.defaultPrevented || !frame)
|
|
27385
|
+
return;
|
|
27386
|
+
// Safari, and Firefox on a Mac, don't focus a button that's pressed.
|
|
27387
|
+
// Focused, it's the element the drawer hands focus back to.
|
|
27388
|
+
e.currentTarget.focus();
|
|
27389
|
+
frame.state.toggleNav();
|
|
27390
|
+
} }));
|
|
27391
|
+
});
|
|
27392
|
+
AppFrameBurger.displayName = 'AppFrame.Burger';
|
|
27393
|
+
const isMain = (child) => React.isValidElement(child) && child.type === AppFrameMain;
|
|
27394
|
+
const isNav = (child) => React.isValidElement(child) && child.type === AppFrameNav;
|
|
27395
|
+
/**
|
|
27396
|
+
* The `AppFrame` component lays out an app's pages: a header across the top, a navigation column down the side, the page, and an aside and a footer, with the nav a drawer behind a burger on narrow screens.
|
|
27397
|
+
*
|
|
27398
|
+
* The header and the column stick while the document scrolls, and a skip
|
|
27399
|
+
* link comes first. The layout is CSS, so the server renders it as the
|
|
27400
|
+
* browser does. It holds two states, each yours to control: whether the
|
|
27401
|
+
* drawer is open, and whether the column is collapsed. `AppFrame.Burger`
|
|
27402
|
+
* and `useAppFrame()` toggle whichever applies at the current width.
|
|
27403
|
+
*
|
|
27404
|
+
* @function
|
|
27405
|
+
* @param {AppFrameProps} props - Props for the AppFrame component.
|
|
27406
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the root `<div>`.
|
|
27407
|
+
* @returns {JSX.Element} The rendered frame.
|
|
27408
|
+
*
|
|
27409
|
+
* @example
|
|
27410
|
+
* <AppFrame>
|
|
27411
|
+
* <AppFrame.Header>
|
|
27412
|
+
* <Navbar aria-label="Site">
|
|
27413
|
+
* <Navbar.Brand>
|
|
27414
|
+
* <Navbar.Item href="/">Acme</Navbar.Item>
|
|
27415
|
+
* <AppFrame.Burger />
|
|
27416
|
+
* </Navbar.Brand>
|
|
27417
|
+
* </Navbar>
|
|
27418
|
+
* </AppFrame.Header>
|
|
27419
|
+
* <AppFrame.Nav>
|
|
27420
|
+
* <Menu>…</Menu>
|
|
27421
|
+
* </AppFrame.Nav>
|
|
27422
|
+
* <AppFrame.Main>…</AppFrame.Main>
|
|
27423
|
+
* </AppFrame>
|
|
27424
|
+
*/
|
|
27425
|
+
const AppFrameRoot = React.forwardRef(({ className, textColor, bgColor, breakpoint = 'desktop', navOpen: navOpenProp, defaultNavOpen = false, onNavOpenChange, navCollapsed: navCollapsedProp, defaultNavCollapsed = false, onNavCollapsedChange, skipLink = true, labels, children, ...props }, ref) => {
|
|
27426
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
27427
|
+
color: textColor,
|
|
27428
|
+
backgroundColor: bgColor,
|
|
27429
|
+
...props,
|
|
27430
|
+
});
|
|
27431
|
+
const rootRef = React.useRef(null);
|
|
27432
|
+
const rootCleanupRef = React.useRef(null);
|
|
27433
|
+
const rootRefs = React.useCallback((node) => setRefs(node, rootRef, ref, rootCleanupRef), [ref]);
|
|
27434
|
+
const [openState, setOpenState] = React.useState(defaultNavOpen);
|
|
27435
|
+
const [collapsedState, setCollapsedState] = React.useState(defaultNavCollapsed);
|
|
27436
|
+
const navOpen = navOpenProp ?? openState;
|
|
27437
|
+
const navCollapsed = navCollapsedProp ?? collapsedState;
|
|
27438
|
+
const openControlled = navOpenProp !== undefined;
|
|
27439
|
+
const collapsedControlled = navCollapsedProp !== undefined;
|
|
27440
|
+
const onNavOpenChangeRef = useLatestRef(onNavOpenChange);
|
|
27441
|
+
const onNavCollapsedChangeRef = useLatestRef(onNavCollapsedChange);
|
|
27442
|
+
const setNavOpen = React.useCallback((open) => {
|
|
27443
|
+
onNavOpenChangeRef.current?.(open);
|
|
27444
|
+
if (!openControlled)
|
|
27445
|
+
setOpenState(open);
|
|
27446
|
+
}, [openControlled, onNavOpenChangeRef]);
|
|
27447
|
+
const setNavCollapsed = React.useCallback((collapsed) => {
|
|
27448
|
+
onNavCollapsedChangeRef.current?.(collapsed);
|
|
27449
|
+
if (!collapsedControlled)
|
|
27450
|
+
setCollapsedState(collapsed);
|
|
27451
|
+
}, [collapsedControlled, onNavCollapsedChangeRef]);
|
|
27452
|
+
// The layout switches in the stylesheet. The script needs the width
|
|
27453
|
+
// only to know which state the burger toggles, and to close the drawer
|
|
27454
|
+
// once the window is at or past the breakpoint.
|
|
27455
|
+
const wide = useBulmaBreakpoint(breakpoint);
|
|
27456
|
+
const isDrawer = wide === false;
|
|
27457
|
+
const isWide = wide === true;
|
|
27458
|
+
React.useEffect(() => {
|
|
27459
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- the width is known only in the browser, and the drawer closes in answer to it
|
|
27460
|
+
if (isWide && navOpen)
|
|
27461
|
+
setNavOpen(false);
|
|
27462
|
+
}, [isWide, navOpen, setNavOpen]);
|
|
27463
|
+
const toggleNav = React.useCallback(() => {
|
|
27464
|
+
if (isDrawer)
|
|
27465
|
+
setNavOpen(!navOpen);
|
|
27466
|
+
else
|
|
27467
|
+
setNavCollapsed(!navCollapsed);
|
|
27468
|
+
}, [isDrawer, navOpen, navCollapsed, setNavOpen, setNavCollapsed]);
|
|
27469
|
+
// The ids of the parts the skip link and the burger point at, read off
|
|
27470
|
+
// the children so the server renders them too.
|
|
27471
|
+
const baseId = React.useId();
|
|
27472
|
+
const parts = flattenChildren(children);
|
|
27473
|
+
const mainId = parts.find(isMain)?.props.id || `${baseId}-main`;
|
|
27474
|
+
const navId = parts.find(isNav)?.props.id || `${baseId}-nav`;
|
|
27475
|
+
const { skipLink: skipText, nav, closeNav, burger } = labels ?? {};
|
|
27476
|
+
const value = React.useMemo(() => ({
|
|
27477
|
+
state: {
|
|
27478
|
+
navOpen,
|
|
27479
|
+
setNavOpen,
|
|
27480
|
+
navCollapsed,
|
|
27481
|
+
setNavCollapsed,
|
|
27482
|
+
isDrawer,
|
|
27483
|
+
toggleNav,
|
|
27484
|
+
navId,
|
|
27485
|
+
},
|
|
27486
|
+
labels: {
|
|
27487
|
+
skipLink: skipText || 'Skip to main content',
|
|
27488
|
+
nav: nav || 'Sections',
|
|
27489
|
+
closeNav: closeNav || 'Close navigation',
|
|
27490
|
+
burger: burger || undefined,
|
|
27491
|
+
},
|
|
27492
|
+
mainId,
|
|
27493
|
+
modeKnown: wide !== null,
|
|
27494
|
+
}), [
|
|
27495
|
+
navOpen,
|
|
27496
|
+
setNavOpen,
|
|
27497
|
+
navCollapsed,
|
|
27498
|
+
setNavCollapsed,
|
|
27499
|
+
isDrawer,
|
|
27500
|
+
toggleNav,
|
|
27501
|
+
navId,
|
|
27502
|
+
skipText,
|
|
27503
|
+
nav,
|
|
27504
|
+
closeNav,
|
|
27505
|
+
burger,
|
|
27506
|
+
mainId,
|
|
27507
|
+
wide,
|
|
27508
|
+
]);
|
|
27509
|
+
// Its header and footer are the page's banner and contentinfo only
|
|
27510
|
+
// outside `main` and sectioning content, as HTML-AAM maps them.
|
|
27511
|
+
React.useEffect(() => {
|
|
27512
|
+
const host = rootRef.current?.parentElement?.closest('main, article, aside, nav, section');
|
|
27513
|
+
if (host) {
|
|
27514
|
+
warnOnce('AppFrame:nested', `[bestax-bulma] <AppFrame> sits inside a <${host.localName}>, where ` +
|
|
27515
|
+
"its header and footer are no longer the page's banner and " +
|
|
27516
|
+
'contentinfo landmarks. Put AppFrame at the top of the page.');
|
|
27517
|
+
}
|
|
27518
|
+
}, []);
|
|
27519
|
+
const rootClass = usePrefixedClassNames('app-frame', {
|
|
27520
|
+
[`is-${breakpoint}`]: true,
|
|
27521
|
+
'is-nav-collapsed': navCollapsed,
|
|
27522
|
+
});
|
|
27523
|
+
const skipClass = usePrefixedClassNames('app-frame-skip-link');
|
|
27524
|
+
return (jsxRuntime.jsxs("div", { ref: rootRefs, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: [skipLink && (jsxRuntime.jsx("a", { className: skipClass, href: `#${mainId}`, children: value.labels.skipLink })), jsxRuntime.jsx(FrameContext.Provider, { value: value, children: children })] }));
|
|
27525
|
+
});
|
|
27526
|
+
/**
|
|
27527
|
+
* The frame's state, for a control of your own inside it: whether the
|
|
27528
|
+
* drawer is open and the column collapsed, setters that go through the
|
|
27529
|
+
* frame's callbacks, and `toggleNav()`, which does what the burger does.
|
|
27530
|
+
* Outside an AppFrame it's `null`, as the library's other context hooks
|
|
27531
|
+
* are, so a control that can sit outside one renders a fallback or nothing.
|
|
27532
|
+
*
|
|
27533
|
+
* @function useAppFrame
|
|
27534
|
+
* @returns The frame's state, or `null` outside an AppFrame.
|
|
27535
|
+
*/
|
|
27536
|
+
function useAppFrame() {
|
|
27537
|
+
return React.useContext(FrameContext)?.state ?? null;
|
|
27538
|
+
}
|
|
27539
|
+
const AppFrame = withSubComponents(AppFrameRoot, {
|
|
27540
|
+
Header: AppFrameHeader,
|
|
27541
|
+
Nav: AppFrameNav,
|
|
27542
|
+
Main: AppFrameMain,
|
|
27543
|
+
Aside: AppFrameAside,
|
|
27544
|
+
Footer: AppFrameFooter,
|
|
27545
|
+
Burger: AppFrameBurger,
|
|
27546
|
+
}, 'AppFrame');
|
|
27547
|
+
|
|
26963
27548
|
/**
|
|
26964
27549
|
* The `.stack` element behind Stack, Group, Center and Flex.
|
|
26965
27550
|
*
|
|
@@ -27968,6 +28553,14 @@ const Stack = React.forwardRef(function Stack(props, ref) {
|
|
|
27968
28553
|
});
|
|
27969
28554
|
Stack.displayName = 'Stack';
|
|
27970
28555
|
|
|
28556
|
+
exports.Affix = Affix;
|
|
28557
|
+
exports.AppFrame = AppFrame;
|
|
28558
|
+
exports.AppFrameAside = AppFrameAside;
|
|
28559
|
+
exports.AppFrameBurger = AppFrameBurger;
|
|
28560
|
+
exports.AppFrameFooter = AppFrameFooter;
|
|
28561
|
+
exports.AppFrameHeader = AppFrameHeader;
|
|
28562
|
+
exports.AppFrameMain = AppFrameMain;
|
|
28563
|
+
exports.AppFrameNav = AppFrameNav;
|
|
27971
28564
|
exports.Autocomplete = Autocomplete;
|
|
27972
28565
|
exports.Avatar = Avatar;
|
|
27973
28566
|
exports.Avatars = Avatars;
|
|
@@ -28198,6 +28791,7 @@ exports.radioSizes = radioSizes;
|
|
|
28198
28791
|
exports.switchColors = switchColors;
|
|
28199
28792
|
exports.switchSizes = switchSizes;
|
|
28200
28793
|
exports.toast = toast;
|
|
28794
|
+
exports.useAppFrame = useAppFrame;
|
|
28201
28795
|
exports.useBulmaClasses = useBulmaClasses;
|
|
28202
28796
|
exports.useClassPrefix = useClassPrefix;
|
|
28203
28797
|
exports.useColorClasses = useColorClasses;
|