@allxsmith/bestax-bulma 5.60.0 → 5.62.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/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/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/components/TreeView.js +14 -10
- package/dist/esm/components/TreeView.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 +11 -16
- package/dist/esm/form/Combobox.js.map +1 -1
- package/dist/esm/form/Field.js.map +1 -1
- package/dist/esm/form/TreeSelect.js +395 -0
- package/dist/esm/form/TreeSelect.js.map +1 -0
- package/dist/esm/form/_pickerInternals/PickerPopover.js +5 -1
- package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
- package/dist/esm/form/_selectInternals/DialogSelect.js +208 -0
- package/dist/esm/form/_selectInternals/DialogSelect.js.map +1 -0
- package/dist/esm/form/_selectInternals/ValueTags.js +28 -0
- package/dist/esm/form/_selectInternals/ValueTags.js.map +1 -0
- package/dist/esm/helpers/overlayStack.js +33 -1
- package/dist/esm/helpers/overlayStack.js.map +1 -1
- package/dist/esm/helpers/treeData.js +97 -0
- package/dist/esm/helpers/treeData.js.map +1 -0
- package/dist/esm/helpers/useBulmaViewport.js +82 -0
- package/dist/esm/helpers/useBulmaViewport.js.map +1 -0
- package/dist/esm/helpers/useComboboxEngine.js +2 -2
- package/dist/esm/helpers/useComboboxEngine.js.map +1 -1
- package/dist/esm/helpers/useControlled.js +30 -0
- package/dist/esm/helpers/useControlled.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/helpers/useTreeView.js +15 -13
- package/dist/esm/helpers/useTreeView.js.map +1 -1
- 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 +1767 -518
- 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/Navbar.d.ts +4 -3
- package/dist/types/form/Field.d.ts +1 -1
- package/dist/types/form/TreeSelect.d.ts +308 -0
- package/dist/types/form/_pickerInternals/PickerPopover.d.ts +8 -0
- package/dist/types/form/_selectInternals/DialogSelect.d.ts +86 -0
- package/dist/types/form/_selectInternals/ValueTags.d.ts +40 -0
- package/dist/types/helpers/overlayStack.d.ts +17 -0
- package/dist/types/helpers/treeData.d.ts +48 -0
- package/dist/types/helpers/useBulmaViewport.d.ts +34 -0
- package/dist/types/helpers/useControlled.d.ts +14 -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/helpers/useTreeView.d.ts +30 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/layout/AppFrame.d.ts +306 -0
- package/dist/types-cjs/components/Navbar.d.ts +4 -3
- package/dist/types-cjs/form/Field.d.ts +1 -1
- package/dist/types-cjs/form/TreeSelect.d.ts +308 -0
- package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +8 -0
- package/dist/types-cjs/form/_selectInternals/DialogSelect.d.ts +86 -0
- package/dist/types-cjs/form/_selectInternals/ValueTags.d.ts +40 -0
- package/dist/types-cjs/helpers/overlayStack.d.ts +17 -0
- package/dist/types-cjs/helpers/treeData.d.ts +48 -0
- package/dist/types-cjs/helpers/useBulmaViewport.d.ts +34 -0
- package/dist/types-cjs/helpers/useControlled.d.ts +14 -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/helpers/useTreeView.d.ts +30 -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/form/_index.scss +1 -0
- package/src/scss/form/_picker-popover.scss +6 -0
- package/src/scss/form/_treeselect.scss +146 -0
- package/src/scss/layout/_appframe.scss +306 -0
- package/src/scss/layout/_index.scss +1 -0
package/dist/index.cjs
CHANGED
|
@@ -4755,6 +4755,37 @@ function useOverlayStack(inStack, modal = true) {
|
|
|
4755
4755
|
return !above.some(overlay => among !== 'modal' || overlay.modal);
|
|
4756
4756
|
}, []);
|
|
4757
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
|
+
}
|
|
4758
4789
|
|
|
4759
4790
|
/**
|
|
4760
4791
|
* Whether `el` is editable while its parent is not, which makes it an editing
|
|
@@ -5680,7 +5711,9 @@ const NavbarBurger = React.forwardRef(function NavbarBurger({ className, active,
|
|
|
5680
5711
|
});
|
|
5681
5712
|
return (jsxRuntime.jsxs("button", { ref: ref, className: classNames(usePrefixedClassNames('navbar-burger', {
|
|
5682
5713
|
'is-active': active,
|
|
5683
|
-
}), 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] }));
|
|
5684
5717
|
});
|
|
5685
5718
|
NavbarBurger.displayName = 'NavbarBurger';
|
|
5686
5719
|
/**
|
|
@@ -6851,16 +6884,20 @@ const above = (el) => el.parentElement ?? el.getRootNode().host;
|
|
|
6851
6884
|
* @param active - Whether what's beside it is inert.
|
|
6852
6885
|
* @param wholePage - Whether that goes for the whole page rather than only
|
|
6853
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.
|
|
6854
6890
|
*/
|
|
6855
|
-
function useInertSiblings(ref, active, wholePage) {
|
|
6891
|
+
function useInertSiblings(ref, active, wholePage, except) {
|
|
6856
6892
|
React.useEffect(() => {
|
|
6857
6893
|
const el = ref.current;
|
|
6858
6894
|
if (!active || !el)
|
|
6859
6895
|
return undefined;
|
|
6860
6896
|
const doc = el.ownerDocument;
|
|
6897
|
+
const keep = except?.current;
|
|
6861
6898
|
const held = [];
|
|
6862
6899
|
for (let node = el; node && node !== doc.body; node = wholePage ? above(node) : undefined) {
|
|
6863
|
-
held.push(...siblingsOf(node).filter(hold));
|
|
6900
|
+
held.push(...siblingsOf(node).filter(sibling => sibling !== keep && hold(sibling)));
|
|
6864
6901
|
}
|
|
6865
6902
|
// Focus this takes away, to hand back later. It's looked up as `el`'s
|
|
6866
6903
|
// tree sees it, where focus inside a nested shadow root reads as its
|
|
@@ -6875,7 +6912,7 @@ function useInertSiblings(ref, active, wholePage) {
|
|
|
6875
6912
|
if (doc.activeElement === doc.body)
|
|
6876
6913
|
taken?.focus();
|
|
6877
6914
|
};
|
|
6878
|
-
}, [ref, active, wholePage]);
|
|
6915
|
+
}, [ref, active, wholePage, except]);
|
|
6879
6916
|
}
|
|
6880
6917
|
|
|
6881
6918
|
/**
|
|
@@ -7971,6 +8008,51 @@ function useAnchoredPosition(anchorRef, panelRef, { active, position, fixed, off
|
|
|
7971
8008
|
return resolved;
|
|
7972
8009
|
}
|
|
7973
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
|
+
|
|
7974
8056
|
const PopoverContext = React.createContext(null);
|
|
7975
8057
|
/** Intrinsic elements that are controls already, and keep their own role. */
|
|
7976
8058
|
const NATIVE_CONTROLS = new Set([
|
|
@@ -8021,9 +8103,6 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
|
|
|
8021
8103
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
8022
8104
|
const rootRef = React.useRef(null);
|
|
8023
8105
|
const panelRef = React.useRef(null);
|
|
8024
|
-
// The last pointerdown React saw inside the panel. React's tree includes
|
|
8025
|
-
// content the panel portals elsewhere, which the DOM path doesn't.
|
|
8026
|
-
const insidePointerRef = React.useRef(null);
|
|
8027
8106
|
const baseId = React.useId();
|
|
8028
8107
|
const panelId = `${baseId}-panel`;
|
|
8029
8108
|
const titleId = `${baseId}-title`;
|
|
@@ -8079,25 +8158,10 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
|
|
|
8079
8158
|
}
|
|
8080
8159
|
};
|
|
8081
8160
|
}, [panelMounted, trapFocus, triggerNode]);
|
|
8082
|
-
//
|
|
8083
|
-
//
|
|
8084
|
-
//
|
|
8085
|
-
const
|
|
8086
|
-
React.useEffect(() => {
|
|
8087
|
-
if (!panelMounted || !closeOnClickOutside)
|
|
8088
|
-
return undefined;
|
|
8089
|
-
const handlePointerDown = (e) => {
|
|
8090
|
-
if (e === insidePointerRef.current || isEventInside(e, rootRef.current)) {
|
|
8091
|
-
return;
|
|
8092
|
-
}
|
|
8093
|
-
closeRef.current();
|
|
8094
|
-
};
|
|
8095
|
-
// The wrapper is on the page whenever the panel is. Its document, an
|
|
8096
|
-
// iframe's when the tree renders into one, is where presses land.
|
|
8097
|
-
const doc = rootRef.current.ownerDocument;
|
|
8098
|
-
doc.addEventListener('pointerdown', handlePointerDown);
|
|
8099
|
-
return () => doc.removeEventListener('pointerdown', handlePointerDown);
|
|
8100
|
-
}, [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);
|
|
8101
8165
|
const handlePanelKeyDown = (e) => {
|
|
8102
8166
|
if (e.key !== 'Escape' || !closeOnEscape || e.defaultPrevented)
|
|
8103
8167
|
return;
|
|
@@ -8163,9 +8227,7 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
|
|
|
8163
8227
|
top: resolved.top,
|
|
8164
8228
|
left: resolved.left,
|
|
8165
8229
|
}
|
|
8166
|
-
: undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture:
|
|
8167
|
-
insidePointerRef.current = e.nativeEvent;
|
|
8168
|
-
}, children: children }) }))] }));
|
|
8230
|
+
: undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture: markInsidePress, children: children }) }))] }));
|
|
8169
8231
|
};
|
|
8170
8232
|
/**
|
|
8171
8233
|
* The open panel. It mounts with each opening, so it counts the
|
|
@@ -9293,6 +9355,69 @@ const TimelineRoot = React.forwardRef(({ className, children, active, pending, a
|
|
|
9293
9355
|
});
|
|
9294
9356
|
const Timeline = withSubComponents(TimelineRoot, { Item: TimelineItem }, 'Timeline');
|
|
9295
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
|
+
|
|
9296
9421
|
/**
|
|
9297
9422
|
* The `Sidebar` component provides a slide-out navigation panel that appears from the left or right side of the screen.
|
|
9298
9423
|
*
|
|
@@ -9342,31 +9467,15 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
9342
9467
|
handleClose();
|
|
9343
9468
|
}
|
|
9344
9469
|
}, [overlayClose, handleClose]);
|
|
9345
|
-
// Read when Escape arrives, so the key listener stays put while
|
|
9346
|
-
// `onClose` changes identity, as an inline one does on every render of
|
|
9347
|
-
// the parent.
|
|
9348
|
-
const handleCloseRef = useLatestRef(handleClose);
|
|
9349
9470
|
// Joins the stack of open overlays, so the sidebar answers Escape only
|
|
9350
9471
|
// while it's the topmost overlay, and keeps it from those under it. It's
|
|
9351
9472
|
// modal with an overlay: without one it leaves Tab to a modal overlay
|
|
9352
9473
|
// under it.
|
|
9353
9474
|
const isTopmost = useOverlayStack(isOpen, overlay);
|
|
9354
9475
|
const closesOnEscape = escapeClose && canCancel && Boolean(onClose);
|
|
9355
|
-
//
|
|
9356
|
-
// sidebar has had the key first, and an Escape it used is left to it.
|
|
9357
|
-
|
|
9358
|
-
React.useEffect(() => {
|
|
9359
|
-
if (!isOpen || !closesOnEscape)
|
|
9360
|
-
return;
|
|
9361
|
-
const handleKeyDown = (e) => {
|
|
9362
|
-
if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
|
|
9363
|
-
return;
|
|
9364
|
-
e.preventDefault();
|
|
9365
|
-
handleCloseRef.current();
|
|
9366
|
-
};
|
|
9367
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
9368
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
9369
|
-
}, [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);
|
|
9370
9479
|
// Keep Tab inside while there's an overlay, as `Modal` does:
|
|
9371
9480
|
// `aria-modal` hides the rest of the page from assistive technology, so
|
|
9372
9481
|
// the keyboard order has to agree. Only while it's the topmost modal
|
|
@@ -9389,41 +9498,17 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
9389
9498
|
useScrollLock(isOpen && overlay);
|
|
9390
9499
|
// Move focus into the sidebar on open, to the first stop Tab wraps to or
|
|
9391
9500
|
// the sidebar itself, and hand it back on close, as `Modal` does. A
|
|
9392
|
-
// different `defaultTarget` remounts the portaled sidebar:
|
|
9393
|
-
//
|
|
9394
|
-
//
|
|
9395
|
-
//
|
|
9396
|
-
//
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
return undefined;
|
|
9404
|
-
// The element that had focus, wherever it sits: a portaled sidebar
|
|
9405
|
-
// renders under `document.body` while its opener can be inside a
|
|
9406
|
-
// shadow root.
|
|
9407
|
-
const opener = getDeepestActiveElement();
|
|
9408
|
-
// Without an overlay the sidebar holds no Tab, so with a modal overlay
|
|
9409
|
-
// open under it, the first Tab would take focus back there and leave
|
|
9410
|
-
// the sidebar's other controls out of reach. It leaves focus alone
|
|
9411
|
-
// then, and the cleanup below finds focus outside and leaves it too.
|
|
9412
|
-
if (overlay || !isModalOverlayOpen()) {
|
|
9413
|
-
focusTabStop(element);
|
|
9414
|
-
}
|
|
9415
|
-
return () => {
|
|
9416
|
-
// Only while the sidebar still holds focus: closing one under another
|
|
9417
|
-
// overlay mustn't pull focus out of the one on top, and focus moved
|
|
9418
|
-
// out to the page stays there. A removed subtree leaves focus on
|
|
9419
|
-
// <body>, which still counts as held.
|
|
9420
|
-
const active = getActiveElementInTree(element);
|
|
9421
|
-
if (active === document.body || element.contains(active)) {
|
|
9422
|
-
opener?.focus();
|
|
9423
|
-
}
|
|
9424
|
-
};
|
|
9425
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- `overlay` is read as the sidebar opens, on purpose
|
|
9426
|
-
}, [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
|
+
});
|
|
9427
9512
|
// Use combined ref
|
|
9428
9513
|
const combinedRef = React.useCallback((node) => {
|
|
9429
9514
|
sidebarRef.current =
|
|
@@ -10147,6 +10232,81 @@ const DialogContainer = () => {
|
|
|
10147
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) }));
|
|
10148
10233
|
};
|
|
10149
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
|
+
|
|
10150
10310
|
/**
|
|
10151
10311
|
* Individual carousel item/slide.
|
|
10152
10312
|
*
|
|
@@ -10174,24 +10334,10 @@ const DefaultPrevIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
|
|
|
10174
10334
|
* @returns {JSX.Element} The rendered SVG chevron-right icon.
|
|
10175
10335
|
*/
|
|
10176
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" }) }));
|
|
10177
|
-
/**
|
|
10178
|
-
* The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
|
|
10179
|
-
* mixins open with, widest first, at Bulma's default breakpoints. The
|
|
10180
|
-
* stylesheet picks the count in view with those mixins; these tell the
|
|
10181
|
-
* script which count that is. Narrower than the last one is `mobile`.
|
|
10182
|
-
*/
|
|
10183
|
-
const VIEWPORT_QUERIES = [
|
|
10184
|
-
'screen and (min-width: 1408px)',
|
|
10185
|
-
'screen and (min-width: 1216px)',
|
|
10186
|
-
'screen and (min-width: 1024px)',
|
|
10187
|
-
'screen and (min-width: 769px), print',
|
|
10188
|
-
];
|
|
10189
|
-
/** `useBulmaViewport`'s answer below the tablet breakpoint. */
|
|
10190
|
-
const MOBILE = VIEWPORT_QUERIES.length;
|
|
10191
10337
|
/**
|
|
10192
10338
|
* The `--bulma-carousel-items-to-show*` variable each count prop writes, in
|
|
10193
|
-
* the order the component lists them:
|
|
10194
|
-
* last `itemsToShow` itself.
|
|
10339
|
+
* the order the component lists them: `useBulmaViewport`'s viewports, widest
|
|
10340
|
+
* first, then mobile, and last `itemsToShow` itself.
|
|
10195
10341
|
*/
|
|
10196
10342
|
const COUNT_SUFFIXES = [
|
|
10197
10343
|
'-fullhd',
|
|
@@ -10201,33 +10347,6 @@ const COUNT_SUFFIXES = [
|
|
|
10201
10347
|
'-mobile',
|
|
10202
10348
|
'',
|
|
10203
10349
|
];
|
|
10204
|
-
/**
|
|
10205
|
-
* Which of Bulma's viewports the window is in: the index of the widest
|
|
10206
|
-
* `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
|
|
10207
|
-
* when that isn't known (on the server, before the first layout effect, and
|
|
10208
|
-
* whenever `enabled` is off, so a carousel with one count everywhere
|
|
10209
|
-
* subscribes to nothing).
|
|
10210
|
-
*
|
|
10211
|
-
* @function
|
|
10212
|
-
* @param {boolean} enabled - Whether any viewport-specific count is set.
|
|
10213
|
-
* @returns {number} The viewport, as described above.
|
|
10214
|
-
*/
|
|
10215
|
-
function useBulmaViewport(enabled) {
|
|
10216
|
-
const [viewport, setViewport] = React.useState(-1);
|
|
10217
|
-
useClientLayoutEffect(() => {
|
|
10218
|
-
if (!enabled || typeof window.matchMedia !== 'function')
|
|
10219
|
-
return undefined;
|
|
10220
|
-
const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
|
|
10221
|
-
const update = () => {
|
|
10222
|
-
const index = lists.findIndex(list => list.matches);
|
|
10223
|
-
setViewport(index < 0 ? MOBILE : index);
|
|
10224
|
-
};
|
|
10225
|
-
update();
|
|
10226
|
-
lists.forEach(list => list.addEventListener('change', update));
|
|
10227
|
-
return () => lists.forEach(list => list.removeEventListener('change', update));
|
|
10228
|
-
}, [enabled]);
|
|
10229
|
-
return enabled ? viewport : -1;
|
|
10230
|
-
}
|
|
10231
10350
|
/**
|
|
10232
10351
|
* A count prop as the whole number the carousel works with: rounded down,
|
|
10233
10352
|
* and never below 1, so a zero or a stray `NaN` can't divide by nothing.
|
|
@@ -10818,6 +10937,30 @@ function RevealImpl(revealProps) {
|
|
|
10818
10937
|
const Reveal = RevealImpl;
|
|
10819
10938
|
Reveal.displayName = 'Reveal';
|
|
10820
10939
|
|
|
10940
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
10941
|
+
/**
|
|
10942
|
+
* A value the caller holds, or one this hook holds when the caller doesn't:
|
|
10943
|
+
* the `x` / `defaultX` pair of a component's props. The caller holds it
|
|
10944
|
+
* whenever `controlled` isn't `undefined`, so `null` is a held value. The
|
|
10945
|
+
* setter changes only the value held here, so a held one changes when the
|
|
10946
|
+
* caller passes the new value, and it reports nothing: the component calls
|
|
10947
|
+
* its own `onXChange`.
|
|
10948
|
+
*
|
|
10949
|
+
* @function useControlled
|
|
10950
|
+
* @param controlled - The caller's value, or `undefined` to hold one here.
|
|
10951
|
+
* @param initial - The value held here to begin with.
|
|
10952
|
+
* @returns The current value, and the setter for the one held here.
|
|
10953
|
+
*/
|
|
10954
|
+
function useControlled(controlled, initial) {
|
|
10955
|
+
const [own, setOwn] = React.useState(initial);
|
|
10956
|
+
const isControlled = controlled !== undefined;
|
|
10957
|
+
const set = (next) => {
|
|
10958
|
+
if (!isControlled)
|
|
10959
|
+
setOwn(next);
|
|
10960
|
+
};
|
|
10961
|
+
return [isControlled ? controlled : own, set];
|
|
10962
|
+
}
|
|
10963
|
+
|
|
10821
10964
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
10822
10965
|
/** The keys and clicks of both checkbox modes, which differ only in what a check reaches. */
|
|
10823
10966
|
const CHECKBOX_ACTIONS = {
|
|
@@ -10891,6 +11034,14 @@ function pathOf(element, { values }) {
|
|
|
10891
11034
|
}
|
|
10892
11035
|
return path;
|
|
10893
11036
|
}
|
|
11037
|
+
/**
|
|
11038
|
+
* The values of the items a tree hides while they stay in it, as TreeSelect's
|
|
11039
|
+
* search hides the items that don't match. A hidden item renders as one under
|
|
11040
|
+
* a closed parent does: nothing on the page, and in a `checkbox` tree it and
|
|
11041
|
+
* its children still register, so a check still reaches them. Internal, for
|
|
11042
|
+
* the components that put a TreeView inside them.
|
|
11043
|
+
*/
|
|
11044
|
+
const TreeViewHiddenContext = React.createContext(new Set());
|
|
10894
11045
|
/**
|
|
10895
11046
|
* Every known item's check state, by the rules in #1096:
|
|
10896
11047
|
*
|
|
@@ -10966,16 +11117,6 @@ function toggledList(records, list, value) {
|
|
|
10966
11117
|
...list.filter(item => !records.has(item)),
|
|
10967
11118
|
];
|
|
10968
11119
|
}
|
|
10969
|
-
/** A value the caller holds, or one this hook holds when the caller doesn't. */
|
|
10970
|
-
function useControlled(controlled, initial) {
|
|
10971
|
-
const [own, setOwn] = React.useState(initial);
|
|
10972
|
-
const isControlled = controlled !== undefined;
|
|
10973
|
-
const set = (next) => {
|
|
10974
|
-
if (!isControlled)
|
|
10975
|
-
setOwn(next);
|
|
10976
|
-
};
|
|
10977
|
-
return [isControlled ? controlled : own, set];
|
|
10978
|
-
}
|
|
10979
11120
|
/**
|
|
10980
11121
|
* The list with `value` added at the end, or, when it's `present`, with
|
|
10981
11122
|
* every copy of it taken out. Everything else stays as it was, in its order.
|
|
@@ -11243,12 +11384,15 @@ function useTreeView(options) {
|
|
|
11243
11384
|
press(item, actions.space);
|
|
11244
11385
|
break;
|
|
11245
11386
|
case '*': {
|
|
11387
|
+
// The siblings on the page: not the ones a component around the
|
|
11388
|
+
// tree hides, which stay registered.
|
|
11246
11389
|
const closed = [];
|
|
11247
11390
|
registry.records.forEach((record, value) => {
|
|
11248
11391
|
if (record.parent === item.parent &&
|
|
11249
11392
|
record.isParent &&
|
|
11250
11393
|
!record.disabled &&
|
|
11251
|
-
!expanded.includes(value)
|
|
11394
|
+
!expanded.includes(value) &&
|
|
11395
|
+
registry.elements.has(value)) {
|
|
11252
11396
|
closed.push(value);
|
|
11253
11397
|
}
|
|
11254
11398
|
});
|
|
@@ -11725,7 +11869,7 @@ const sayLoaded = (name) => `${name} loaded`;
|
|
|
11725
11869
|
const TOP = { path: [], hidden: false };
|
|
11726
11870
|
const BranchContext = React.createContext(TOP);
|
|
11727
11871
|
/** The box's input lets the row's click toggle it, so a change on its own does nothing. */
|
|
11728
|
-
const ignoreChange = () => { };
|
|
11872
|
+
const ignoreChange$1 = () => { };
|
|
11729
11873
|
/** Whether an `icon` was given something to render. */
|
|
11730
11874
|
const isGiven$1 = (node) => node !== undefined &&
|
|
11731
11875
|
node !== null &&
|
|
@@ -11833,6 +11977,10 @@ const TreeViewRoot = React.forwardRef(function TreeView({ children, selectionMod
|
|
|
11833
11977
|
const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon, children, hasChildren = false, isLoading = false, loadError, onRetry, disabled = false, className, onClick, onKeyDown, onFocus, onBlur, ...props }, ref) {
|
|
11834
11978
|
const tree = React.useContext(TreeViewContext);
|
|
11835
11979
|
const branch = React.useContext(BranchContext);
|
|
11980
|
+
// Hidden by a component around the tree, such as TreeSelect's search,
|
|
11981
|
+
// it renders as an item under a closed parent does.
|
|
11982
|
+
const hiddenItems = React.useContext(TreeViewHiddenContext);
|
|
11983
|
+
const hiddenHere = branch.hidden || hiddenItems.has(value);
|
|
11836
11984
|
// Also the item's key in the live region.
|
|
11837
11985
|
const labelId = React.useId();
|
|
11838
11986
|
const hasItems = React.Children.toArray(children).length > 0;
|
|
@@ -11840,7 +11988,7 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
|
|
|
11840
11988
|
const open = isParent && !!tree?.isOpen(value);
|
|
11841
11989
|
const parent = branch.path[branch.path.length - 1] ?? null;
|
|
11842
11990
|
// The row at the end of its group, while it's open and on the page.
|
|
11843
|
-
const shown = open && !
|
|
11991
|
+
const shown = open && !hiddenHere;
|
|
11844
11992
|
const status = !shown
|
|
11845
11993
|
? null
|
|
11846
11994
|
: isLoading
|
|
@@ -11905,8 +12053,8 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
|
|
|
11905
12053
|
const toggleRef = React.useRef(null);
|
|
11906
12054
|
const childBranch = React.useMemo(() => ({
|
|
11907
12055
|
path: [...branch.path, value],
|
|
11908
|
-
hidden:
|
|
11909
|
-
}), [branch, value, open]);
|
|
12056
|
+
hidden: hiddenHere || !open,
|
|
12057
|
+
}), [branch, value, hiddenHere, open]);
|
|
11910
12058
|
const { selected, checked } = tree?.stateOf(value, branch.path, disabled) ?? {};
|
|
11911
12059
|
const rowClass = usePrefixedClassNames('tree-view-row', {
|
|
11912
12060
|
'is-selected': selected,
|
|
@@ -11915,10 +12063,10 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
|
|
|
11915
12063
|
if (!tree)
|
|
11916
12064
|
return null;
|
|
11917
12065
|
// While it's open the children render in its group. In a `checkbox` tree
|
|
11918
|
-
// they render while it's closed too, drawing nothing, so a
|
|
11919
|
-
// what's under it.
|
|
11920
|
-
const items = (
|
|
11921
|
-
if (
|
|
12066
|
+
// they render while it's closed or hidden too, drawing nothing, so a
|
|
12067
|
+
// check knows what's under it.
|
|
12068
|
+
const items = (shown || tree.rendersClosed) && (jsxRuntime.jsx(BranchContext.Provider, { value: childBranch, children: children }));
|
|
12069
|
+
if (hiddenHere)
|
|
11922
12070
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: items });
|
|
11923
12071
|
const info = { value, parent, disabled, isParent, open };
|
|
11924
12072
|
// A position the caller gives wins, for a set the page holds only part of.
|
|
@@ -11954,7 +12102,7 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
|
|
|
11954
12102
|
if (!(event.target instanceof HTMLInputElement)) {
|
|
11955
12103
|
event.preventDefault();
|
|
11956
12104
|
}
|
|
11957
|
-
}, children: jsxRuntime.jsx(Checkbox, { tabIndex: -1, checked: checked === true, indeterminate: checked === 'mixed', disabled: disabled, onChange: ignoreChange }) })), isGiven$1(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), jsxRuntime.jsx("span", { className: labelClass, id: labelId, ref: labelRef, children: label })] }), open && (hasItems || status) ? (jsxRuntime.jsxs("ul", { className: groupClass, role: "group", children: [items, status && (jsxRuntime.jsx(StatusRow, { kind: status, parent: value, level: branch.path.length + 2, message: loadError, onRetry: onRetry, labelRef: statusLabelRef }))] })) : (items)] }));
|
|
12105
|
+
}, children: jsxRuntime.jsx(Checkbox, { tabIndex: -1, checked: checked === true, indeterminate: checked === 'mixed', disabled: disabled, onChange: ignoreChange$1 }) })), isGiven$1(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), jsxRuntime.jsx("span", { className: labelClass, id: labelId, ref: labelRef, children: label })] }), open && (hasItems || status) ? (jsxRuntime.jsxs("ul", { className: groupClass, role: "group", children: [items, status && (jsxRuntime.jsx(StatusRow, { kind: status, parent: value, level: branch.path.length + 2, message: loadError, onRetry: onRetry, labelRef: statusLabelRef }))] })) : (items)] }));
|
|
11958
12106
|
});
|
|
11959
12107
|
TreeViewItem.displayName = 'TreeView.Item';
|
|
11960
12108
|
/**
|
|
@@ -12223,7 +12371,7 @@ ScrollArea.displayName = 'ScrollArea';
|
|
|
12223
12371
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
12224
12372
|
/** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */
|
|
12225
12373
|
const isComposing$2 = (event) => event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
|
|
12226
|
-
const sameValues = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
|
|
12374
|
+
const sameValues$1 = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
|
|
12227
12375
|
/**
|
|
12228
12376
|
* The keys, the open state, the highlight and the ARIA of an editable
|
|
12229
12377
|
* combobox whose list the app renders and filters (#1083). DOM focus stays
|
|
@@ -12325,7 +12473,7 @@ function useComboboxEngine({ open: controlledOpen, defaultOpen, onOpenChange, on
|
|
|
12325
12473
|
}
|
|
12326
12474
|
const options = shownOptions();
|
|
12327
12475
|
const values = options.map(option => option.value);
|
|
12328
|
-
const changed = !sameValues(values, shownValuesRef.current);
|
|
12476
|
+
const changed = !sameValues$1(values, shownValuesRef.current);
|
|
12329
12477
|
shownValuesRef.current = values;
|
|
12330
12478
|
setListShown(values.length > 0);
|
|
12331
12479
|
const landing = landingRef.current ?? (opened ? 'open' : null);
|
|
@@ -13125,7 +13273,7 @@ const standalone$2 = {
|
|
|
13125
13273
|
const CommandContext = React.createContext(null);
|
|
13126
13274
|
const useCommand = () => React.useContext(CommandContext) ?? standalone$2;
|
|
13127
13275
|
/** Hands a node to a forwarded ref and to one of the component's own. */
|
|
13128
|
-
function setRefs$
|
|
13276
|
+
function setRefs$3(node, own, forwarded) {
|
|
13129
13277
|
own.current = node;
|
|
13130
13278
|
if (typeof forwarded === 'function')
|
|
13131
13279
|
forwarded(node);
|
|
@@ -13242,7 +13390,7 @@ const CommandParts = React.forwardRef(({ children, label, query: controlledQuery
|
|
|
13242
13390
|
registerHandler,
|
|
13243
13391
|
};
|
|
13244
13392
|
const { rootRef } = engine;
|
|
13245
|
-
const rootRefs = React.useCallback((node) => setRefs$
|
|
13393
|
+
const rootRefs = React.useCallback((node) => setRefs$3(node, rootRef, ref), [rootRef, ref]);
|
|
13246
13394
|
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 })] }));
|
|
13247
13395
|
});
|
|
13248
13396
|
CommandParts.displayName = 'CommandParts';
|
|
@@ -13262,7 +13410,7 @@ const CommandInput = React.forwardRef(({ isLoading, iconLeft, iconLeftName, onKe
|
|
|
13262
13410
|
return () => reportLoading(false);
|
|
13263
13411
|
}, [reportLoading, isLoading]);
|
|
13264
13412
|
const { inputRef } = engine;
|
|
13265
|
-
const inputRefs = React.useCallback((node) => setRefs$
|
|
13413
|
+
const inputRefs = React.useCallback((node) => setRefs$3(node, inputRef, ref), [inputRef, ref]);
|
|
13266
13414
|
const { expanded, highlighted } = engine;
|
|
13267
13415
|
// A name the caller gave the field wins over the Command's `label`.
|
|
13268
13416
|
const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
|
|
@@ -18743,6 +18891,25 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
18743
18891
|
});
|
|
18744
18892
|
Autocomplete.displayName = 'Autocomplete';
|
|
18745
18893
|
|
|
18894
|
+
const everyValue = () => true;
|
|
18895
|
+
/**
|
|
18896
|
+
* The values of a field holding several, as Bulma tags under it, each with a
|
|
18897
|
+
* `Delete` that takes its value off. Nothing while there are none.
|
|
18898
|
+
*/
|
|
18899
|
+
function ValueTags({ values, ids, labelFor, disabled, ariaCloseLabel, onRemove, removable = everyValue, }) {
|
|
18900
|
+
if (values.length === 0)
|
|
18901
|
+
return null;
|
|
18902
|
+
return (jsxRuntime.jsx(Tags, { mt: "2", children: values.map((value, index) => (jsxRuntime.jsxs(Tag, { children: [jsxRuntime.jsx("span", { id: ids[index], children: labelFor(value) }), !disabled && removable(value) && (jsxRuntime.jsx(Delete, { size: "small", ariaLabel: ariaCloseLabel || `Remove ${labelFor(value)}`, "aria-describedby": ariaCloseLabel ? ids[index] : undefined, onClick: () => onRemove(value) }))] }, value))) }));
|
|
18903
|
+
}
|
|
18904
|
+
/** The ids `ValueTags` gives its labels, built from one id of the field's. */
|
|
18905
|
+
const tagIds = (base, values) => values.map((_, index) => `${base}-${index}`);
|
|
18906
|
+
/** A hidden input for each value, so a form posts them under `name`. */
|
|
18907
|
+
function HiddenValues({ name, values, form, disabled, }) {
|
|
18908
|
+
if (!name)
|
|
18909
|
+
return null;
|
|
18910
|
+
return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: values.map(value => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: value, form: form, disabled: disabled }, value))) }));
|
|
18911
|
+
}
|
|
18912
|
+
|
|
18746
18913
|
const validComboboxSizes = ['small', 'medium', 'large'];
|
|
18747
18914
|
// What the status region announces: the `Combobox.Empty` content, as the one
|
|
18748
18915
|
// item it's handed while that content is on screen because of the user's
|
|
@@ -18791,7 +18958,7 @@ const standalone$1 = {
|
|
|
18791
18958
|
const ComboboxContext = React.createContext(null);
|
|
18792
18959
|
const useCombobox = () => React.useContext(ComboboxContext) ?? standalone$1;
|
|
18793
18960
|
/** Hands a node to a forwarded ref and to one of the component's own. */
|
|
18794
|
-
function setRefs$
|
|
18961
|
+
function setRefs$2(node, own, forwarded) {
|
|
18795
18962
|
own.current = node;
|
|
18796
18963
|
if (typeof forwarded === 'function')
|
|
18797
18964
|
forwarded(node);
|
|
@@ -18923,7 +19090,7 @@ const ComboboxParts = React.forwardRef(({ children, below, onSettle, open, defau
|
|
|
18923
19090
|
appendToBody,
|
|
18924
19091
|
};
|
|
18925
19092
|
const { rootRef } = engine;
|
|
18926
|
-
const rootRefs = React.useCallback((node) => setRefs$
|
|
19093
|
+
const rootRefs = React.useCallback((node) => setRefs$2(node, rootRef, ref), [rootRef, ref]);
|
|
18927
19094
|
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 })] }));
|
|
18928
19095
|
const fieldElement = below === undefined ? (comboboxElement) : (jsxRuntime.jsxs("div", { children: [comboboxElement, jsxRuntime.jsx("div", { onPointerDownCapture: engine.markInside, onMouseDown: engine.keepFocus, children: below })] }));
|
|
18929
19096
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
@@ -18956,7 +19123,7 @@ const ComboboxInput = React.forwardRef(({ id, isLoading, controlSize, onKeyDown,
|
|
|
18956
19123
|
return () => reportLoading(false);
|
|
18957
19124
|
}, [reportLoading, isLoading]);
|
|
18958
19125
|
const { inputRef } = engine;
|
|
18959
|
-
const inputRefs = React.useCallback((node) => setRefs$
|
|
19126
|
+
const inputRefs = React.useCallback((node) => setRefs$2(node, inputRef, ref), [inputRef, ref]);
|
|
18960
19127
|
const { expanded, highlighted } = engine;
|
|
18961
19128
|
// An `id` of the field's own reaches the label from a layout effect, so
|
|
18962
19129
|
// in server markup, and the first render, the label's `for` still names
|
|
@@ -19135,7 +19302,7 @@ ComboboxEmpty.displayName = 'ComboboxEmpty';
|
|
|
19135
19302
|
const toItem = (entry) => typeof entry === 'string' ? { value: entry, label: entry } : entry;
|
|
19136
19303
|
const labelOf = (item) => item.label ?? item.value;
|
|
19137
19304
|
/** One value or several, as a list: one value is a list of none or one. */
|
|
19138
|
-
const asList = (value) => value == null ? [] : Array.isArray(value) ? value : [value];
|
|
19305
|
+
const asList$1 = (value) => value == null ? [] : Array.isArray(value) ? value : [value];
|
|
19139
19306
|
/**
|
|
19140
19307
|
* The Combobox given `data`. It renders the parts itself, filters the items
|
|
19141
19308
|
* by label as the user types, and keeps the value, so a pick goes through
|
|
@@ -19167,9 +19334,9 @@ const ComboboxFromData = React.forwardRef((props, ref) => {
|
|
|
19167
19334
|
return item ? labelOf(item) : picked;
|
|
19168
19335
|
};
|
|
19169
19336
|
const controlled = value !== undefined;
|
|
19170
|
-
const [ownValue, setOwnValue] = React.useState(() => asList(defaultValue));
|
|
19337
|
+
const [ownValue, setOwnValue] = React.useState(() => asList$1(defaultValue));
|
|
19171
19338
|
const matchText = useLocaleMatcher();
|
|
19172
|
-
const picked = controlled ? asList(value) : ownValue;
|
|
19339
|
+
const picked = controlled ? asList$1(value) : ownValue;
|
|
19173
19340
|
const change = (next) => {
|
|
19174
19341
|
if (!controlled)
|
|
19175
19342
|
setOwnValue(next);
|
|
@@ -19221,7 +19388,7 @@ const ComboboxFromData = React.forwardRef((props, ref) => {
|
|
|
19221
19388
|
};
|
|
19222
19389
|
// The picks, for the field's description, since its text is the search.
|
|
19223
19390
|
const tagsId = React.useId();
|
|
19224
|
-
const
|
|
19391
|
+
const ids = tagIds(tagsId, picked);
|
|
19225
19392
|
// Each group where `data` first names it, and the items with none
|
|
19226
19393
|
// together where the first of them is.
|
|
19227
19394
|
const sections = new Map();
|
|
@@ -19235,14 +19402,11 @@ const ComboboxFromData = React.forwardRef((props, ref) => {
|
|
|
19235
19402
|
if (!next)
|
|
19236
19403
|
settle();
|
|
19237
19404
|
onOpenChange?.(next);
|
|
19238
|
-
}, below: multiple
|
|
19239
|
-
? picked.length > 0 && (jsxRuntime.jsx(Tags, { mt: "2", children: picked.map((v, index) => (jsxRuntime.jsxs(Tag, { children: [jsxRuntime.jsx("span", { id: tagIds[index], children: labelFor(v) }), !disabled && (jsxRuntime.jsx(Delete, { size: "small", ariaLabel: ariaCloseLabel || `Remove ${labelFor(v)}`, "aria-describedby": ariaCloseLabel ? tagIds[index] : undefined, onClick: () => remove(v) }))] }, v))) }))
|
|
19240
|
-
: undefined, children: [jsxRuntime.jsx(ComboboxInput, { ...named, ref: inputRef, id: id, disabled: disabled,
|
|
19405
|
+
}, below: multiple ? (jsxRuntime.jsx(ValueTags, { values: picked, ids: ids, labelFor: labelFor, disabled: disabled, ariaCloseLabel: ariaCloseLabel, onRemove: remove })) : undefined, children: [jsxRuntime.jsx(ComboboxInput, { ...named, ref: inputRef, id: id, disabled: disabled,
|
|
19241
19406
|
// The browser's check reads the field's text, and only a pick
|
|
19242
19407
|
// fills it for good, so it's required until there is one.
|
|
19243
|
-
required: required && picked.length === 0, "aria-required": required || undefined, form: form, "aria-describedby": multiple && picked.length > 0 ?
|
|
19244
|
-
(multiple || picked.length === 0 ? '' : labelFor(picked[0])), onChange: event => setQuery(event.target.value), onBlur: settle }), inline ? list : jsxRuntime.jsx(ComboboxMenu, { children: list }), name
|
|
19245
|
-
(multiple ? picked : [picked[0] ?? '']).map(v => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: v, form: form, disabled: disabled }, v)))] }));
|
|
19408
|
+
required: required && picked.length === 0, "aria-required": required || undefined, form: form, "aria-describedby": multiple && picked.length > 0 ? ids.join(' ') : undefined, placeholder: placeholder, value: query ??
|
|
19409
|
+
(multiple || picked.length === 0 ? '' : labelFor(picked[0])), onChange: event => setQuery(event.target.value), onBlur: settle }), inline ? list : jsxRuntime.jsx(ComboboxMenu, { children: list }), jsxRuntime.jsx(HiddenValues, { name: name, values: multiple ? picked : [picked[0] ?? ''], form: form, disabled: disabled })] }));
|
|
19246
19410
|
});
|
|
19247
19411
|
ComboboxFromData.displayName = 'ComboboxFromData';
|
|
19248
19412
|
/**
|
|
@@ -19315,51 +19479,946 @@ const Combobox = withSubComponents(ComboboxRoot, {
|
|
|
19315
19479
|
Empty: ComboboxEmpty,
|
|
19316
19480
|
}, 'Combobox');
|
|
19317
19481
|
|
|
19318
|
-
//
|
|
19319
|
-
//
|
|
19320
|
-
//
|
|
19321
|
-
|
|
19482
|
+
// INTERNAL: deliberately not exported from src/index.ts. Walking the nodes a
|
|
19483
|
+
// TreeSelect takes as `data`, so a picker can name, check and search a tree
|
|
19484
|
+
// while it isn't on the page.
|
|
19485
|
+
/**
|
|
19486
|
+
* Every node by its value, in the order the tree reads from top to bottom.
|
|
19487
|
+
* Values are meant to be unique, and the first node with a value wins.
|
|
19488
|
+
*/
|
|
19489
|
+
function indexTree(data) {
|
|
19490
|
+
const index = new Map();
|
|
19491
|
+
const walk = (nodes, path) => {
|
|
19492
|
+
for (const node of nodes) {
|
|
19493
|
+
if (!index.has(node.value))
|
|
19494
|
+
index.set(node.value, { node, path });
|
|
19495
|
+
if (node.children)
|
|
19496
|
+
walk(node.children, [...path, node.value]);
|
|
19497
|
+
}
|
|
19498
|
+
};
|
|
19499
|
+
walk(data, []);
|
|
19500
|
+
return index;
|
|
19501
|
+
}
|
|
19502
|
+
/** What a node is called: its `label`, or its `value` when that's empty. */
|
|
19503
|
+
const nodeLabel = (node) => node.label || node.value;
|
|
19504
|
+
/** The nodes under a node, none when it has no `children`. */
|
|
19505
|
+
const childNodes = (node) => node.children ?? [];
|
|
19506
|
+
/** Whether a node is a parent: it has children, or `hasChildren` says it will. */
|
|
19507
|
+
const isParentNode = (node) => childNodes(node).length > 0 || !!node.hasChildren;
|
|
19508
|
+
/**
|
|
19509
|
+
* The records TreeView's check rules read, built from the nodes rather than
|
|
19510
|
+
* from the items on the page, so the checked list can be worked out while
|
|
19511
|
+
* the tree isn't there.
|
|
19512
|
+
*/
|
|
19513
|
+
function checkRecords(index) {
|
|
19514
|
+
const records = new Map();
|
|
19515
|
+
index.forEach(({ node, path }, value) => records.set(value, {
|
|
19516
|
+
parent: path[path.length - 1] ?? null,
|
|
19517
|
+
disabled: !!node.disabled,
|
|
19518
|
+
isParent: isParentNode(node),
|
|
19519
|
+
}));
|
|
19520
|
+
return records;
|
|
19521
|
+
}
|
|
19322
19522
|
/**
|
|
19323
|
-
* The
|
|
19324
|
-
*
|
|
19325
|
-
*
|
|
19326
|
-
*
|
|
19327
|
-
*
|
|
19328
|
-
*
|
|
19329
|
-
|
|
19330
|
-
|
|
19331
|
-
|
|
19332
|
-
|
|
19333
|
-
|
|
19334
|
-
|
|
19335
|
-
|
|
19336
|
-
|
|
19337
|
-
|
|
19338
|
-
|
|
19339
|
-
|
|
19340
|
-
|
|
19341
|
-
|
|
19342
|
-
|
|
19343
|
-
* />
|
|
19344
|
-
*/
|
|
19345
|
-
const Taginput = React.forwardRef(({
|
|
19346
|
-
// Field props
|
|
19347
|
-
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = [], data: dataProp, placeholder, field = 'label', allowNew = true, allowDuplicates = false, openOnFocus = false, removeOnKeys = true, confirmKeys = ['Enter', ','], closable = true, attached = false, maxTags, maxlength, disabled: disabledProp, readonly = false, rounded = false, ellipsis = false, hasCounter = true, onPasteSeparators = [','], beforeAdding, createTag, keepFirst = false, keepOpen = true, loading = false, ariaCloseLabel, icon, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, tagColor, size, onChange, onAdd, onRemove, onTyping, tagTemplate, name, form, id, className, ...props }, ref) => {
|
|
19348
|
-
const disabled = useFieldsetDisabled(disabledProp);
|
|
19349
|
-
const insideField = useInsideField();
|
|
19350
|
-
const insideControl = useInsideControl();
|
|
19351
|
-
const ownField = rendersOwnField('Taginput', {
|
|
19352
|
-
insideField,
|
|
19353
|
-
insideControl,
|
|
19354
|
-
label,
|
|
19355
|
-
message,
|
|
19356
|
-
horizontal,
|
|
19357
|
-
fieldClassName,
|
|
19523
|
+
* The checked list a `checkedStrategy` reports for the items `listed` checks
|
|
19524
|
+
* by TreeView's rules, in the tree's order. `all` is every checked item;
|
|
19525
|
+
* `child` the checked items with no enabled item under them; `parent` the
|
|
19526
|
+
* checked items whose parent isn't. A disabled item is checked by its own
|
|
19527
|
+
* value only, so a checked one is in every list. Values no node has are
|
|
19528
|
+
* kept, at the end. Passed back in, each list checks the same items.
|
|
19529
|
+
*/
|
|
19530
|
+
function checkedByStrategy(records, listed, strategy) {
|
|
19531
|
+
const { states, ends } = readChecks(records, new Set(listed));
|
|
19532
|
+
const isEnd = new Set(ends);
|
|
19533
|
+
const on = (value) => value !== null && states.get(value) === true;
|
|
19534
|
+
const kept = [];
|
|
19535
|
+
records.forEach(({ parent, disabled }, value) => {
|
|
19536
|
+
if (!on(value))
|
|
19537
|
+
return;
|
|
19538
|
+
if (strategy === 'all' ||
|
|
19539
|
+
disabled ||
|
|
19540
|
+
(strategy === 'child' ? isEnd.has(value) : !on(parent))) {
|
|
19541
|
+
kept.push(value);
|
|
19542
|
+
}
|
|
19358
19543
|
});
|
|
19359
|
-
const
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
|
|
19544
|
+
const unknown = new Set(listed.filter(value => !records.has(value)));
|
|
19545
|
+
return [...kept, ...unknown];
|
|
19546
|
+
}
|
|
19547
|
+
/**
|
|
19548
|
+
* What a search shows of a tree: the nodes that match, the nodes above them,
|
|
19549
|
+
* so the results stay a tree, and the nodes under them, so a matching branch
|
|
19550
|
+
* can still be opened.
|
|
19551
|
+
*/
|
|
19552
|
+
function searchTree(data, matches) {
|
|
19553
|
+
const hidden = new Set();
|
|
19554
|
+
const open = [];
|
|
19555
|
+
// Whether the nodes, or any node under them, match. `above` is the path
|
|
19556
|
+
// down to them.
|
|
19557
|
+
const walk = (nodes, underMatch, above) => {
|
|
19558
|
+
let found = false;
|
|
19559
|
+
for (const node of nodes) {
|
|
19560
|
+
const path = [...above, node];
|
|
19561
|
+
const self = matches(node, path);
|
|
19562
|
+
const below = walk(childNodes(node), underMatch || self, path);
|
|
19563
|
+
if (below)
|
|
19564
|
+
open.push(node.value);
|
|
19565
|
+
if (!underMatch && !self && !below)
|
|
19566
|
+
hidden.add(node.value);
|
|
19567
|
+
found = found || self || below;
|
|
19568
|
+
}
|
|
19569
|
+
return found;
|
|
19570
|
+
};
|
|
19571
|
+
return { hidden, open, found: walk(data, false, []) };
|
|
19572
|
+
}
|
|
19573
|
+
|
|
19574
|
+
const isBrowser = typeof window !== 'undefined';
|
|
19575
|
+
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, localeScope, matchWidth = false, followContent = false, }) => {
|
|
19576
|
+
const panelRef = React.useRef(null);
|
|
19577
|
+
const resolveContainer = usePortalContainer();
|
|
19578
|
+
// Read as the trap turns on, once the panel's content has rendered.
|
|
19579
|
+
const initialFocusRef = React.useMemo(() => initialFocusSelector === undefined
|
|
19580
|
+
? undefined
|
|
19581
|
+
: {
|
|
19582
|
+
get current() {
|
|
19583
|
+
return (panelRef.current?.querySelector(initialFocusSelector) ?? null);
|
|
19584
|
+
},
|
|
19585
|
+
}, [initialFocusSelector]);
|
|
19586
|
+
const resolved = useAnchoredPosition(anchorRef, panelRef, {
|
|
19587
|
+
active: isOpen,
|
|
19588
|
+
position,
|
|
19589
|
+
fixed: appendToBody,
|
|
19590
|
+
// The gap comes from `--bulma-picker-popover-offset` as a margin, in place
|
|
19591
|
+
// or portaled, so the coordinates leave it out and `auto` reads it off
|
|
19592
|
+
// the panel.
|
|
19593
|
+
offset: 0,
|
|
19594
|
+
styledGap: marginGap,
|
|
19595
|
+
matchWidth,
|
|
19596
|
+
followContent,
|
|
19597
|
+
});
|
|
19598
|
+
// Read when a press lands outside or a key arrives, so both listeners stay
|
|
19599
|
+
// put while `onClose` changes identity, as an inline one does on every
|
|
19600
|
+
// render of the field.
|
|
19601
|
+
const onCloseRef = useLatestRef(onClose);
|
|
19602
|
+
React.useEffect(() => {
|
|
19603
|
+
if (!isOpen || !closeOnClickOutside || !isBrowser)
|
|
19604
|
+
return undefined;
|
|
19605
|
+
// Listen on `pointerdown` (rather than `mousedown`) so touch starts
|
|
19606
|
+
// outside the popover dismiss it on mobile — including swipe-from-outside
|
|
19607
|
+
// gestures, where the synthetic mouse event would otherwise lag a few
|
|
19608
|
+
// hundred ms behind the actual touch.
|
|
19609
|
+
const handler = (e) => {
|
|
19610
|
+
const path = e.composedPath();
|
|
19611
|
+
if (panelRef.current && path.includes(panelRef.current))
|
|
19612
|
+
return;
|
|
19613
|
+
if (anchorRef.current && path.includes(anchorRef.current))
|
|
19614
|
+
return;
|
|
19615
|
+
onCloseRef.current();
|
|
19616
|
+
};
|
|
19617
|
+
document.addEventListener('pointerdown', handler);
|
|
19618
|
+
return () => document.removeEventListener('pointerdown', handler);
|
|
19619
|
+
}, [isOpen, closeOnClickOutside, anchorRef, onCloseRef]);
|
|
19620
|
+
// Escape closes it, and so does Alt+ArrowUp, the combobox's key for it,
|
|
19621
|
+
// which the calendar and the time wheels inside leave to it. Escape closes
|
|
19622
|
+
// it from anywhere, but the key is only the picker's when it was pressed in
|
|
19623
|
+
// the panel or the field: then it's marked used, so a Modal or Sidebar
|
|
19624
|
+
// around the picker stays open. From anywhere else, such as a dialog opened
|
|
19625
|
+
// over the picker, the key goes on to whatever has focus.
|
|
19626
|
+
React.useEffect(() => {
|
|
19627
|
+
if (!isOpen || !closeOnEscape || !isBrowser)
|
|
19628
|
+
return undefined;
|
|
19629
|
+
const handler = (e) => {
|
|
19630
|
+
if (e.key === 'Escape') {
|
|
19631
|
+
if (isEventInside(e, panelRef.current) ||
|
|
19632
|
+
isEventInside(e, anchorRef.current)) {
|
|
19633
|
+
e.preventDefault();
|
|
19634
|
+
}
|
|
19635
|
+
onCloseRef.current();
|
|
19636
|
+
}
|
|
19637
|
+
else if (e.altKey && e.key === 'ArrowUp') {
|
|
19638
|
+
e.preventDefault();
|
|
19639
|
+
e.stopPropagation();
|
|
19640
|
+
onCloseRef.current();
|
|
19641
|
+
}
|
|
19642
|
+
};
|
|
19643
|
+
document.addEventListener('keydown', handler);
|
|
19644
|
+
return () => document.removeEventListener('keydown', handler);
|
|
19645
|
+
}, [isOpen, closeOnEscape, anchorRef, onCloseRef]);
|
|
19646
|
+
useFocusTrap(panelRef, {
|
|
19647
|
+
active: isOpen && trapFocus,
|
|
19648
|
+
initialFocusRef,
|
|
19649
|
+
restoreFocus: restoreFocusRef ?? true,
|
|
19650
|
+
});
|
|
19651
|
+
const panelClass = usePrefixedClassNames('picker-popover', {
|
|
19652
|
+
'is-active': isOpen,
|
|
19653
|
+
[`is-${resolved.position}`]: true,
|
|
19654
|
+
'is-portal': appendToBody,
|
|
19655
|
+
'has-anchor-width': matchWidth,
|
|
19656
|
+
});
|
|
19657
|
+
if (!isOpen)
|
|
19658
|
+
return null;
|
|
19659
|
+
const style = appendToBody
|
|
19660
|
+
? {
|
|
19661
|
+
position: 'fixed',
|
|
19662
|
+
top: resolved.top,
|
|
19663
|
+
left: resolved.left,
|
|
19664
|
+
minWidth: resolved.minWidth,
|
|
19665
|
+
}
|
|
19666
|
+
: {};
|
|
19667
|
+
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,
|
|
19668
|
+
// An Escape pressed in the panel is the picker's, and the listener
|
|
19669
|
+
// above claims it. A Dropdown menu around the picker hears Escape in a
|
|
19670
|
+
// React handler, which runs before that listener, so the panel claims
|
|
19671
|
+
// it here too, where React gets to it first.
|
|
19672
|
+
onKeyDown: e => {
|
|
19673
|
+
if (closeOnEscape && e.key === 'Escape')
|
|
19674
|
+
e.preventDefault();
|
|
19675
|
+
}, children: children }));
|
|
19676
|
+
if (appendToBody && isBrowser) {
|
|
19677
|
+
const target = resolveContainer();
|
|
19678
|
+
return reactDom.createPortal(jsxRuntime.jsx(LocalePortalScope, { target: target, scope: localeScope, children: panel }), target);
|
|
19679
|
+
}
|
|
19680
|
+
return panel;
|
|
19681
|
+
};
|
|
19682
|
+
PickerPopover.displayName = 'PickerPopover';
|
|
19683
|
+
|
|
19684
|
+
/**
|
|
19685
|
+
* Whether a key is Alt+ArrowDown or Alt+ArrowUp, which open and close a
|
|
19686
|
+
* picker's popover. The calendar and the time wheels leave both to it rather
|
|
19687
|
+
* than reading them as plain arrows.
|
|
19688
|
+
*/
|
|
19689
|
+
const isPopoverToggleKey = (e) => e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp');
|
|
19690
|
+
const DEFAULT_PICKER_LABELS = {
|
|
19691
|
+
prevMonth: 'Previous month',
|
|
19692
|
+
nextMonth: 'Next month',
|
|
19693
|
+
prevYear: 'Previous year',
|
|
19694
|
+
nextYear: 'Next year',
|
|
19695
|
+
chooseDate: 'Choose date',
|
|
19696
|
+
chooseMonth: 'Choose month',
|
|
19697
|
+
chooseYear: 'Choose year',
|
|
19698
|
+
rangeStart: 'Start date',
|
|
19699
|
+
rangeEnd: 'End date',
|
|
19700
|
+
rangePreviewEnd: 'Choose as end date',
|
|
19701
|
+
rangeSeparator: '–',
|
|
19702
|
+
chooseDateRange: 'Choose date range',
|
|
19703
|
+
hours: 'hours',
|
|
19704
|
+
minutes: 'minutes',
|
|
19705
|
+
seconds: 'seconds',
|
|
19706
|
+
ampm: 'AM/PM',
|
|
19707
|
+
increaseHours: 'Increase hours',
|
|
19708
|
+
decreaseHours: 'Decrease hours',
|
|
19709
|
+
increaseMinutes: 'Increase minutes',
|
|
19710
|
+
decreaseMinutes: 'Decrease minutes',
|
|
19711
|
+
increaseSeconds: 'Increase seconds',
|
|
19712
|
+
decreaseSeconds: 'Decrease seconds',
|
|
19713
|
+
toggleAmPm: 'Toggle AM/PM',
|
|
19714
|
+
chooseTime: 'Choose time',
|
|
19715
|
+
chooseDateTime: 'Choose date and time',
|
|
19716
|
+
now: 'Now',
|
|
19717
|
+
today: 'Today',
|
|
19718
|
+
clear: 'Clear',
|
|
19719
|
+
cancel: 'Cancel',
|
|
19720
|
+
ok: 'OK',
|
|
19721
|
+
reset: 'Reset',
|
|
19722
|
+
done: 'Done',
|
|
19723
|
+
time: 'Time',
|
|
19724
|
+
};
|
|
19725
|
+
/**
|
|
19726
|
+
* Merge user-supplied label overrides with the defaults. A key set to
|
|
19727
|
+
* `undefined` keeps its default, so a locale table that is missing a string
|
|
19728
|
+
* still names its control instead of leaving it with no accessible name.
|
|
19729
|
+
*/
|
|
19730
|
+
const mergeLabels = (overrides) => {
|
|
19731
|
+
const merged = { ...DEFAULT_PICKER_LABELS };
|
|
19732
|
+
for (const [key, value] of Object.entries(overrides ?? {})) {
|
|
19733
|
+
if (value !== undefined)
|
|
19734
|
+
merged[key] = value;
|
|
19735
|
+
}
|
|
19736
|
+
return merged;
|
|
19737
|
+
};
|
|
19738
|
+
|
|
19739
|
+
/**
|
|
19740
|
+
* A picker's locale: its own `locale`, else the one a `LocaleProvider` around
|
|
19741
|
+
* it sets, else `undefined`, which leaves `Intl` to use the runtime's.
|
|
19742
|
+
*/
|
|
19743
|
+
function usePickerLocale(own) {
|
|
19744
|
+
const scope = useLocaleScope();
|
|
19745
|
+
return own || scope?.locale;
|
|
19746
|
+
}
|
|
19747
|
+
const NO_ROOT_ATTRS = {};
|
|
19748
|
+
/**
|
|
19749
|
+
* A picker's language and direction inside a `LocaleProvider`, by the rule a
|
|
19750
|
+
* nested provider follows: its `lang` is its own `locale`, else the
|
|
19751
|
+
* provider's, and its `dir` is the `dir` passed to it, else its own locale's
|
|
19752
|
+
* direction when it has one, else the provider's. Outside a provider it
|
|
19753
|
+
* changes nothing. A `locale` `Intl` refuses counts as none here, since the
|
|
19754
|
+
* picker's own formatting reports it.
|
|
19755
|
+
*/
|
|
19756
|
+
function usePickerLocaleScope(ownLocale, ownDir) {
|
|
19757
|
+
const scope = useLocaleScope();
|
|
19758
|
+
return React.useMemo(() => {
|
|
19759
|
+
if (!scope)
|
|
19760
|
+
return { rootAttrs: NO_ROOT_ATTRS };
|
|
19761
|
+
let own;
|
|
19762
|
+
try {
|
|
19763
|
+
own = canonicalLocale(ownLocale);
|
|
19764
|
+
}
|
|
19765
|
+
catch {
|
|
19766
|
+
own = undefined;
|
|
19767
|
+
}
|
|
19768
|
+
const dir = ownDir || (own ? localeDirection(own) : scope.dir);
|
|
19769
|
+
return {
|
|
19770
|
+
rootAttrs: own ? { lang: own, dir } : NO_ROOT_ATTRS,
|
|
19771
|
+
panel: { locale: own || scope.locale, dir },
|
|
19772
|
+
};
|
|
19773
|
+
}, [scope, ownLocale, ownDir]);
|
|
19774
|
+
}
|
|
19775
|
+
/**
|
|
19776
|
+
* Whether a picker's `dayNames` replace the calendar's own, which `Calendar`
|
|
19777
|
+
* does only for a full week of seven.
|
|
19778
|
+
*/
|
|
19779
|
+
function hasOwnDayNames(dayNames) {
|
|
19780
|
+
return dayNames !== undefined && dayNames.length === 7;
|
|
19781
|
+
}
|
|
19782
|
+
/**
|
|
19783
|
+
* The day a picker's week starts on: its own `firstDayOfWeek`, else, inside a
|
|
19784
|
+
* `LocaleProvider`, the first day of the locale the picker uses, else Sunday.
|
|
19785
|
+
* Outside a provider it stays Sunday whatever the locale, as it always has,
|
|
19786
|
+
* and so does a picker inside one with no locale from either, since the
|
|
19787
|
+
* runtime's own differs between the server and the browser.
|
|
19788
|
+
*
|
|
19789
|
+
* It also stays Sunday for a picker that brings its own `dayNames`. The
|
|
19790
|
+
* calendar shows those exactly as given, in calendar order, and before the
|
|
19791
|
+
* provider an unset `firstDayOfWeek` was always Sunday, so names written
|
|
19792
|
+
* Sunday first would sit a day or more off their columns if the week moved
|
|
19793
|
+
* under them.
|
|
19794
|
+
*/
|
|
19795
|
+
function usePickerFirstDayOfWeek(own, locale, hasDayNames) {
|
|
19796
|
+
const inProvider = useLocaleScope() !== null;
|
|
19797
|
+
return React.useMemo(() => {
|
|
19798
|
+
if (own !== undefined)
|
|
19799
|
+
return own;
|
|
19800
|
+
if (!inProvider || !locale || hasDayNames)
|
|
19801
|
+
return 0;
|
|
19802
|
+
try {
|
|
19803
|
+
return localeFirstDay(canonicalLocale(locale));
|
|
19804
|
+
}
|
|
19805
|
+
catch {
|
|
19806
|
+
// A tag Intl refuses: the picker's own formatting says so.
|
|
19807
|
+
return 0;
|
|
19808
|
+
}
|
|
19809
|
+
}, [own, inProvider, locale, hasDayNames]);
|
|
19810
|
+
}
|
|
19811
|
+
|
|
19812
|
+
/**
|
|
19813
|
+
* A field laid over the real one, unseen and letting presses through, so the
|
|
19814
|
+
* browser's `required` check reads it and its message points at the field.
|
|
19815
|
+
*/
|
|
19816
|
+
const requiredProxyStyle = {
|
|
19817
|
+
position: 'absolute',
|
|
19818
|
+
inset: 0,
|
|
19819
|
+
width: '100%',
|
|
19820
|
+
height: '100%',
|
|
19821
|
+
margin: 0,
|
|
19822
|
+
padding: 0,
|
|
19823
|
+
border: 0,
|
|
19824
|
+
opacity: 0,
|
|
19825
|
+
pointerEvents: 'none',
|
|
19826
|
+
// Focused, it opens no on-screen keyboard (`inputMode`), and iOS zooms
|
|
19827
|
+
// in on a field set under 16px. It's unseen, so the size shows nowhere.
|
|
19828
|
+
fontSize: '16px',
|
|
19829
|
+
};
|
|
19830
|
+
/** Keys pressed on their own that don't act, such as Ctrl to stop speech. */
|
|
19831
|
+
const MODIFIER_KEYS = new Set([
|
|
19832
|
+
'Alt',
|
|
19833
|
+
'AltGraph',
|
|
19834
|
+
'CapsLock',
|
|
19835
|
+
'Control',
|
|
19836
|
+
'Fn',
|
|
19837
|
+
'Meta',
|
|
19838
|
+
'NumLock',
|
|
19839
|
+
'ScrollLock',
|
|
19840
|
+
'Shift',
|
|
19841
|
+
]);
|
|
19842
|
+
const ignoreChange = () => { };
|
|
19843
|
+
/**
|
|
19844
|
+
* A read-only `role="combobox"` field, wired as the date pickers' are, that
|
|
19845
|
+
* opens a `PickerPopover` dialog panel: in a `Field` of its own unless one is
|
|
19846
|
+
* around it, with a chevron at its inline end, a clear button, the hidden
|
|
19847
|
+
* inputs for `name` and what the browser's `required` check reads. A click,
|
|
19848
|
+
* Enter, Space, Down or Alt+Down opens the panel, which holds focus, as the
|
|
19849
|
+
* date pickers' does: Tab and Shift+Tab wrap inside it. Escape and Alt+Up
|
|
19850
|
+
* close it and put focus back on the field, and a press outside closes it
|
|
19851
|
+
* where the press put focus.
|
|
19852
|
+
*/
|
|
19853
|
+
function DialogSelect({ component, rootClass, panelClass, text, values, hasValue, open, onOpenChange, onFormReset, onPrintableKey, onClear, clearable, clearLabel, chooseLabel, placeholder, name, form, required = false, disabled, color, size, isRounded, position = 'bottom-left', appendToBody = false, inputRef, fieldRef, describedBy, below, initialFocusSelector, children, label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, className, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, ...props }) {
|
|
19854
|
+
const insideField = useInsideField();
|
|
19855
|
+
const insideControl = useInsideControl();
|
|
19856
|
+
const ownField = rendersOwnField(component, {
|
|
19857
|
+
insideField,
|
|
19858
|
+
insideControl,
|
|
19859
|
+
label,
|
|
19860
|
+
message,
|
|
19861
|
+
horizontal,
|
|
19862
|
+
fieldClassName,
|
|
19863
|
+
});
|
|
19864
|
+
const { controlId, fieldLabelProps, labelId } = useAutoLabelId({
|
|
19865
|
+
label,
|
|
19866
|
+
id,
|
|
19867
|
+
labelProps,
|
|
19868
|
+
rendersLabel: ownField,
|
|
19869
|
+
needsLabelId: true,
|
|
19870
|
+
});
|
|
19871
|
+
const reactId = React.useId();
|
|
19872
|
+
const panelId = `${controlId ?? reactId}-panel`;
|
|
19873
|
+
const localeScope = usePickerLocaleScope(undefined, props.dir);
|
|
19874
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
19875
|
+
const rootRef = React.useRef(null);
|
|
19876
|
+
// The field's name, which the panel takes too unless it's given its own.
|
|
19877
|
+
const fieldName = ariaLabel
|
|
19878
|
+
? { 'aria-label': ariaLabel }
|
|
19879
|
+
: ariaLabelledBy
|
|
19880
|
+
? { 'aria-labelledby': ariaLabelledBy }
|
|
19881
|
+
: labelId
|
|
19882
|
+
? { 'aria-labelledby': labelId }
|
|
19883
|
+
: {};
|
|
19884
|
+
const panelName = chooseLabel
|
|
19885
|
+
? { 'aria-label': chooseLabel }
|
|
19886
|
+
: fieldName;
|
|
19887
|
+
// Whether the field `required` hangs on has focus, which the browser gives
|
|
19888
|
+
// it when it stops a submit. Taking it away or disabling it while it has
|
|
19889
|
+
// focus fires no blur, so that clears it too.
|
|
19890
|
+
const [proxyFocused, setProxyFocused] = React.useState(false);
|
|
19891
|
+
if (proxyFocused && (!required || disabled))
|
|
19892
|
+
setProxyFocused(false);
|
|
19893
|
+
const showClear = !!clearable && hasValue && !disabled;
|
|
19894
|
+
const rootClasses = usePrefixedClassNames(rootClass, {
|
|
19895
|
+
'is-disabled': disabled,
|
|
19896
|
+
'has-clear': showClear,
|
|
19897
|
+
});
|
|
19898
|
+
const inputClass = usePrefixedClassNames('input', {
|
|
19899
|
+
[`is-${color}`]: !!color,
|
|
19900
|
+
[`is-${size}`]: !!size,
|
|
19901
|
+
'is-rounded': !!isRounded,
|
|
19902
|
+
'is-focused': proxyFocused,
|
|
19903
|
+
});
|
|
19904
|
+
const chevronClass = usePrefixedClassNames(`${rootClass}-chevron`);
|
|
19905
|
+
const clearClass = usePrefixedClassNames(`${rootClass}-clear`);
|
|
19906
|
+
const panelClasses = usePrefixedClassNames(panelClass, {
|
|
19907
|
+
[`is-${size}`]: !!size,
|
|
19908
|
+
});
|
|
19909
|
+
const helpClass = usePrefixedClassNames('help', {
|
|
19910
|
+
[`is-${messageColor}`]: !!messageColor,
|
|
19911
|
+
});
|
|
19912
|
+
const isOpen = open && !disabled;
|
|
19913
|
+
// A reset of the field's form puts the value back, as it does a native
|
|
19914
|
+
// field's. The reset event fires before the form resets, and a listener
|
|
19915
|
+
// after this one can still cancel it, so the value goes back a task later,
|
|
19916
|
+
// and only if the reset went ahead, as File's and Dropzone's do. It's
|
|
19917
|
+
// heard on the field's document, or the shadow root it's in, and matched
|
|
19918
|
+
// to the field's form as it is then, so a form its `form` names that
|
|
19919
|
+
// mounts later, or a `form` that changes, is followed, and a reset
|
|
19920
|
+
// already under way still lands.
|
|
19921
|
+
const onFormResetRef = useLatestRef(onFormReset);
|
|
19922
|
+
React.useEffect(() => {
|
|
19923
|
+
const root = inputRef.current.getRootNode();
|
|
19924
|
+
let timer;
|
|
19925
|
+
const onReset = (event) => {
|
|
19926
|
+
if (event.target !== inputRef.current?.form)
|
|
19927
|
+
return;
|
|
19928
|
+
timer = setTimeout(() => {
|
|
19929
|
+
if (!event.defaultPrevented)
|
|
19930
|
+
onFormResetRef.current();
|
|
19931
|
+
});
|
|
19932
|
+
};
|
|
19933
|
+
root.addEventListener('reset', onReset, true);
|
|
19934
|
+
return () => {
|
|
19935
|
+
root.removeEventListener('reset', onReset, true);
|
|
19936
|
+
clearTimeout(timer);
|
|
19937
|
+
};
|
|
19938
|
+
}, [inputRef, onFormResetRef]);
|
|
19939
|
+
const handleKeyDown = (event) => {
|
|
19940
|
+
if (disabled)
|
|
19941
|
+
return;
|
|
19942
|
+
if (isPopoverToggleKey(event)) {
|
|
19943
|
+
event.preventDefault();
|
|
19944
|
+
if (event.key === 'ArrowDown')
|
|
19945
|
+
onOpenChange(true);
|
|
19946
|
+
return;
|
|
19947
|
+
}
|
|
19948
|
+
if (event.altKey || event.ctrlKey || event.metaKey)
|
|
19949
|
+
return;
|
|
19950
|
+
if (event.key === 'Enter' ||
|
|
19951
|
+
event.key === ' ' ||
|
|
19952
|
+
event.key === 'ArrowDown') {
|
|
19953
|
+
event.preventDefault();
|
|
19954
|
+
onOpenChange(true);
|
|
19955
|
+
}
|
|
19956
|
+
else if (event.key.length === 1 &&
|
|
19957
|
+
!event.nativeEvent.isComposing &&
|
|
19958
|
+
onPrintableKey?.(event.key)) {
|
|
19959
|
+
event.preventDefault();
|
|
19960
|
+
}
|
|
19961
|
+
};
|
|
19962
|
+
const fieldDescription = [ariaDescribedBy, describedBy].filter(Boolean).join(' ') || undefined;
|
|
19963
|
+
const field = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("input", { ref: fieldRef, type: "text", className: inputClass, id: controlId, readOnly: true, role: "combobox", "aria-haspopup": "dialog", "aria-expanded": isOpen, "aria-controls": isOpen ? panelId : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": fieldDescription, "aria-required": required || undefined, autoComplete: "off", value: text, placeholder: placeholder, disabled: disabled, form: form,
|
|
19964
|
+
// A disabled field takes no clicks.
|
|
19965
|
+
onClick: () => onOpenChange(!isOpen), onKeyDown: handleKeyDown }), showClear && (jsxRuntime.jsx(Delete, { className: clearClass, size: size, ariaLabel: clearLabel, onClick: () => {
|
|
19966
|
+
onClear();
|
|
19967
|
+
onOpenChange(false);
|
|
19968
|
+
inputRef.current?.focus();
|
|
19969
|
+
} })), jsxRuntime.jsx("span", { className: chevronClass, "aria-hidden": "true", children: jsxRuntime.jsx(Icon, { name: "chevron-down", size: size }) })] }));
|
|
19970
|
+
const root = (jsxRuntime.jsxs("div", { ...localeScope.rootAttrs, ...rest, ref: rootRef, className: classNames(rootClasses, bulmaHelperClasses, className), children: [insideControl ? field : jsxRuntime.jsx(Control, { size: size, children: field }), required && (jsxRuntime.jsx("input", { ...(proxyFocused
|
|
19971
|
+
? {
|
|
19972
|
+
...fieldName,
|
|
19973
|
+
role: 'combobox',
|
|
19974
|
+
'aria-haspopup': 'dialog',
|
|
19975
|
+
'aria-expanded': false,
|
|
19976
|
+
'aria-describedby': fieldDescription,
|
|
19977
|
+
}
|
|
19978
|
+
: { 'aria-hidden': true }), tabIndex: -1, inputMode: "none", required: true, value: hasValue ? 'picked' : '', onChange: ignoreChange, form: form, disabled: disabled,
|
|
19979
|
+
// When the browser stops a submit, it focuses this and shows its
|
|
19980
|
+
// message beside it, and every browser takes the message down as
|
|
19981
|
+
// this loses focus. So focus stays here, over the field, which
|
|
19982
|
+
// looks focused, and a press goes where it lands, as from any
|
|
19983
|
+
// field. The next key hands focus to the field and does there
|
|
19984
|
+
// what it would have. Firefox also stops taking clicks if focus
|
|
19985
|
+
// leaves while it's still focusing this.
|
|
19986
|
+
onFocus: () => setProxyFocused(true), onBlur: () => setProxyFocused(false), onKeyDown: event => {
|
|
19987
|
+
if (MODIFIER_KEYS.has(event.key))
|
|
19988
|
+
return;
|
|
19989
|
+
inputRef.current?.focus();
|
|
19990
|
+
handleKeyDown(event);
|
|
19991
|
+
}, style: requiredProxyStyle })), jsxRuntime.jsx(HiddenValues, { name: name, values: values, form: form, disabled: disabled }), jsxRuntime.jsx(PickerPopover, { localeScope: localeScope.panel, isOpen: isOpen, onClose: () => onOpenChange(false), anchorRef: rootRef, position: position, appendToBody: appendToBody, className: panelClasses, ariaLabel: panelName['aria-label'], ariaLabelledBy: panelName['aria-labelledby'], id: panelId, restoreFocusRef: inputRef, initialFocusSelector: initialFocusSelector, matchWidth: true, followContent: true, children: children(panelName) })] }));
|
|
19992
|
+
const fieldElement = below === undefined ? (root) : (jsxRuntime.jsxs("div", { children: [root, below] }));
|
|
19993
|
+
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
19994
|
+
if (ownField) {
|
|
19995
|
+
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fieldElement, messageEl] }));
|
|
19996
|
+
}
|
|
19997
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [fieldElement, messageEl] }));
|
|
19998
|
+
}
|
|
19999
|
+
|
|
20000
|
+
const NOTHING_HIDDEN = new Set();
|
|
20001
|
+
const noMatches = [{ id: 'empty' }];
|
|
20002
|
+
const nothingToSay = [];
|
|
20003
|
+
/** What takes focus as the panel opens. */
|
|
20004
|
+
const INITIAL_FOCUS = '[data-initial-focus]';
|
|
20005
|
+
/** One value or several, as a list. */
|
|
20006
|
+
const asList = (value) => value == null ? [] : Array.isArray(value) ? value : [value];
|
|
20007
|
+
/** Whether two lists hold the same values, whatever their order and repeats. */
|
|
20008
|
+
const sameValues = (a, b) => {
|
|
20009
|
+
const inA = new Set(a);
|
|
20010
|
+
const inB = new Set(b);
|
|
20011
|
+
return inA.size === inB.size && [...inA].every(value => inB.has(value));
|
|
20012
|
+
};
|
|
20013
|
+
/** The list with every value of `more` it doesn't have added at the end. */
|
|
20014
|
+
const withAll = (list, more) => [
|
|
20015
|
+
...list,
|
|
20016
|
+
...more.filter(value => !list.includes(value)),
|
|
20017
|
+
];
|
|
20018
|
+
/**
|
|
20019
|
+
* The `TreeSelect` component is a form field whose options are a tree, such as categories, places or teams: it opens a panel holding a real `TreeView`, picks one item or checks a branch at a time, and searches labels.
|
|
20020
|
+
*
|
|
20021
|
+
* The field is a read-only `role="combobox"` that opens a dialog, wired as
|
|
20022
|
+
* DateInput's is, so focus moves into the tree for real and the tree keeps
|
|
20023
|
+
* TreeView's keys, type-ahead and spoken output. The tree is given as `data`,
|
|
20024
|
+
* so the field and the tags can name what's picked while the panel is shut,
|
|
20025
|
+
* and on the server.
|
|
20026
|
+
*
|
|
20027
|
+
* `selectionMode` picks the kind. `'single'` picks one item and closes. With
|
|
20028
|
+
* `'checkbox'`, a check reaches every enabled item under it, closed or hidden
|
|
20029
|
+
* by a search, and `checkedStrategy` decides what the value holds: the
|
|
20030
|
+
* leaves, the topmost checked items or every one. With
|
|
20031
|
+
* `'checkbox-independent'`, each box is its own item's, so a parent means
|
|
20032
|
+
* itself alone (Mantine's and Ant Design's `checkStrictly`). The value shows
|
|
20033
|
+
* as tags under the field.
|
|
20034
|
+
*
|
|
20035
|
+
* A value no node has is kept and shown as it is, with a warning in
|
|
20036
|
+
* development, so a value under a branch that hasn't loaded survives until
|
|
20037
|
+
* it does.
|
|
20038
|
+
*
|
|
20039
|
+
* @function
|
|
20040
|
+
* @param {TreeSelectProps} props - Props for the TreeSelect component.
|
|
20041
|
+
* @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the field.
|
|
20042
|
+
* @returns {JSX.Element} The field, its tags and its panel, in a `Field` with the label unless one is around it.
|
|
20043
|
+
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ | APG Combobox Pattern}
|
|
20044
|
+
*
|
|
20045
|
+
* @example
|
|
20046
|
+
* <TreeSelect
|
|
20047
|
+
* label="Save to"
|
|
20048
|
+
* data={[
|
|
20049
|
+
* { value: 'docs', label: 'Documents', children: [{ value: 'invoices', label: 'Invoices' }] },
|
|
20050
|
+
* { value: 'photos', label: 'Photos' },
|
|
20051
|
+
* ]}
|
|
20052
|
+
* searchable
|
|
20053
|
+
* />
|
|
20054
|
+
*
|
|
20055
|
+
* @example
|
|
20056
|
+
* <TreeSelect label="Regions" selectionMode="checkbox" checkedStrategy="parent" data={regions} />
|
|
20057
|
+
*/
|
|
20058
|
+
const TreeSelect = React.forwardRef((props, ref) => {
|
|
20059
|
+
const { data, expanded: expandedProp, defaultExpanded = [], onExpandedChange, open: openProp, defaultOpen = false, onOpenChange, searchable = false, query: queryProp, defaultQuery = '', onQueryChange, filter, empty, itemTemplate, disabled: disabledProp, clearable = false, size, labels, selectionMode = 'single', value: valueProp, defaultValue,
|
|
20060
|
+
// Called below through `props`, which knows which value it takes.
|
|
20061
|
+
onChange: _onChange, parentsSelectable = true, closeOnSelect = true, checkedStrategy = 'child', ariaCloseLabel, ...rest } = props;
|
|
20062
|
+
const disabled = useFieldsetDisabled(disabledProp);
|
|
20063
|
+
const single = selectionMode === 'single';
|
|
20064
|
+
const cascade = selectionMode === 'checkbox';
|
|
20065
|
+
const index = React.useMemo(() => indexTree(data), [data]);
|
|
20066
|
+
const records = React.useMemo(() => checkRecords(index), [index]);
|
|
20067
|
+
const labelFor = (value) => {
|
|
20068
|
+
const entry = index.get(value);
|
|
20069
|
+
return entry ? nodeLabel(entry.node) : value;
|
|
20070
|
+
};
|
|
20071
|
+
// The value, as a list: none or one in `single`.
|
|
20072
|
+
const [held, setHeld] = useControlled(valueProp, defaultValue ?? null);
|
|
20073
|
+
const listed = asList(held);
|
|
20074
|
+
const change = (next) => {
|
|
20075
|
+
const value = single ? (next[0] ?? null) : next;
|
|
20076
|
+
setHeld(value);
|
|
20077
|
+
// The mode decides which of the two `onChange` takes.
|
|
20078
|
+
const report = props.onChange;
|
|
20079
|
+
report?.(value);
|
|
20080
|
+
};
|
|
20081
|
+
// What the field, the tags and the hidden inputs show: the value as it's
|
|
20082
|
+
// held. `checkedStrategy` shapes a new value as a check makes it, so a
|
|
20083
|
+
// branch that loads later changes nothing the form posts.
|
|
20084
|
+
const picked = single ? listed.slice(0, 1) : Array.from(new Set(listed));
|
|
20085
|
+
const unknown = picked.find(value => !index.has(value));
|
|
20086
|
+
React.useEffect(() => {
|
|
20087
|
+
if (unknown !== undefined) {
|
|
20088
|
+
warnOnce('TreeSelect:unknown-value', `[bestax-bulma] <TreeSelect> was given the value "${unknown}", ` +
|
|
20089
|
+
'which no node in its data has. It is kept and shown as it is, ' +
|
|
20090
|
+
'as it should be for a node under a branch that has not loaded ' +
|
|
20091
|
+
'yet. Otherwise, check the value against the data.');
|
|
20092
|
+
}
|
|
20093
|
+
}, [unknown]);
|
|
20094
|
+
const [expanded, setExpanded] = useControlled(expandedProp, defaultExpanded);
|
|
20095
|
+
const changeExpanded = (next) => {
|
|
20096
|
+
setExpanded(next);
|
|
20097
|
+
onExpandedChange?.(next);
|
|
20098
|
+
};
|
|
20099
|
+
// The picked item focus goes to: the first in the tree's order.
|
|
20100
|
+
const pickedSet = new Set(picked);
|
|
20101
|
+
const target = Array.from(index.keys()).find(value => pickedSet.has(value));
|
|
20102
|
+
const [query, setQueryState] = useControlled(queryProp, defaultQuery);
|
|
20103
|
+
const setQuery = (next) => {
|
|
20104
|
+
if (next === query)
|
|
20105
|
+
return;
|
|
20106
|
+
setQueryState(next);
|
|
20107
|
+
onQueryChange?.(next);
|
|
20108
|
+
};
|
|
20109
|
+
const [isOpen, setOpenState] = useControlled(openProp, defaultOpen);
|
|
20110
|
+
const setOpen = (next) => {
|
|
20111
|
+
if (next === isOpen)
|
|
20112
|
+
return;
|
|
20113
|
+
if (next) {
|
|
20114
|
+
// Opening shows the picked item, its branch open.
|
|
20115
|
+
const path = (target !== undefined && index.get(target)?.path) || [];
|
|
20116
|
+
if (path.some(value => !expanded.includes(value))) {
|
|
20117
|
+
changeExpanded(withAll(expanded, path));
|
|
20118
|
+
}
|
|
20119
|
+
}
|
|
20120
|
+
else {
|
|
20121
|
+
setQuery('');
|
|
20122
|
+
}
|
|
20123
|
+
setOpenState(next);
|
|
20124
|
+
onOpenChange?.(next);
|
|
20125
|
+
};
|
|
20126
|
+
const matchText = useLocaleMatcher();
|
|
20127
|
+
const typed = searchable ? query.trim() : '';
|
|
20128
|
+
// Every item reads the hidden set, so it's only built again when the
|
|
20129
|
+
// search text or the data changes.
|
|
20130
|
+
const search = React.useMemo(() => typed
|
|
20131
|
+
? searchTree(data, (node, path) => {
|
|
20132
|
+
if (filter)
|
|
20133
|
+
return filter(typed, node, path);
|
|
20134
|
+
const text = nodeLabel(node);
|
|
20135
|
+
return matchText
|
|
20136
|
+
? matchText(text, typed)
|
|
20137
|
+
: text.toLowerCase().includes(typed.toLowerCase());
|
|
20138
|
+
})
|
|
20139
|
+
: null,
|
|
20140
|
+
// `filter` is read as it is when the text or the data changes: one
|
|
20141
|
+
// written inline is a new function every render, and listing it would
|
|
20142
|
+
// search again on each.
|
|
20143
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
20144
|
+
[data, typed, matchText]);
|
|
20145
|
+
// The branches a search opened that were closed during it, for the
|
|
20146
|
+
// search text they were closed under: any new text, from the box or
|
|
20147
|
+
// from a held `query`, starts over.
|
|
20148
|
+
const [searchClosed, setSearchClosed] = React.useState({
|
|
20149
|
+
query: typed,
|
|
20150
|
+
values: [],
|
|
20151
|
+
});
|
|
20152
|
+
if (searchClosed.query !== typed) {
|
|
20153
|
+
setSearchClosed({ query: typed, values: [] });
|
|
20154
|
+
}
|
|
20155
|
+
const closedHere = searchClosed.query === typed ? searchClosed.values : [];
|
|
20156
|
+
// The branches a search opens show open over the ones you opened, and
|
|
20157
|
+
// close again once it's cleared.
|
|
20158
|
+
const forced = search
|
|
20159
|
+
? search.open.filter(value => !closedHere.includes(value))
|
|
20160
|
+
: [];
|
|
20161
|
+
const shownExpanded = withAll(expanded, forced);
|
|
20162
|
+
// TreeView's click on a row selects it and opens it. A pick by click, as
|
|
20163
|
+
// a pick by Enter, only picks, so the open the tree asks for right after
|
|
20164
|
+
// one on a parent is dropped, and a lazy parent isn't fetched to be
|
|
20165
|
+
// picked.
|
|
20166
|
+
const clickPickedRef = React.useRef(false);
|
|
20167
|
+
const onTreeExpandedChange = (next) => {
|
|
20168
|
+
if (clickPickedRef.current) {
|
|
20169
|
+
clickPickedRef.current = false;
|
|
20170
|
+
return;
|
|
20171
|
+
}
|
|
20172
|
+
const closed = shownExpanded.filter(value => !next.includes(value));
|
|
20173
|
+
const opened = next.filter(value => !shownExpanded.includes(value));
|
|
20174
|
+
const closedForced = closed.filter(value => forced.includes(value));
|
|
20175
|
+
if (closedForced.length > 0) {
|
|
20176
|
+
setSearchClosed({
|
|
20177
|
+
query: typed,
|
|
20178
|
+
values: [...closedHere, ...closedForced],
|
|
20179
|
+
});
|
|
20180
|
+
}
|
|
20181
|
+
if (opened.length > 0 || closed.some(value => expanded.includes(value))) {
|
|
20182
|
+
changeExpanded([
|
|
20183
|
+
...expanded.filter(value => !closed.includes(value)),
|
|
20184
|
+
...opened,
|
|
20185
|
+
]);
|
|
20186
|
+
}
|
|
20187
|
+
};
|
|
20188
|
+
// Whether an item is on the page with the panel open: every branch
|
|
20189
|
+
// above it open, and the search not hiding it.
|
|
20190
|
+
const visible = (value) => !search?.hidden.has(value) &&
|
|
20191
|
+
index.get(value).path.every(above => shownExpanded.includes(above));
|
|
20192
|
+
const focusTarget = target !== undefined && visible(target)
|
|
20193
|
+
? target
|
|
20194
|
+
: data.find(node => visible(node.value))?.value;
|
|
20195
|
+
// Picking in `single`. TreeView shows `selected`, held here, and the
|
|
20196
|
+
// picks are made from the rows' own keys and clicks, so a parent can
|
|
20197
|
+
// open instead and a pick of the picked item still closes.
|
|
20198
|
+
const pickable = (node) => !node.disabled && (parentsSelectable || !isParentNode(node));
|
|
20199
|
+
const pick = (value) => {
|
|
20200
|
+
if (listed.length !== 1 || listed[0] !== value)
|
|
20201
|
+
change([value]);
|
|
20202
|
+
if (closeOnSelect)
|
|
20203
|
+
setOpen(false);
|
|
20204
|
+
};
|
|
20205
|
+
const toggleClass = usePrefixedClassNames('tree-view-toggle');
|
|
20206
|
+
const rowHandlers = (node) => single
|
|
20207
|
+
? {
|
|
20208
|
+
onKeyDown: (event) => {
|
|
20209
|
+
if (event.target !== event.currentTarget ||
|
|
20210
|
+
event.repeat ||
|
|
20211
|
+
event.altKey ||
|
|
20212
|
+
event.ctrlKey ||
|
|
20213
|
+
event.metaKey ||
|
|
20214
|
+
(event.key !== 'Enter' && event.key !== ' ')) {
|
|
20215
|
+
return;
|
|
20216
|
+
}
|
|
20217
|
+
if (pickable(node)) {
|
|
20218
|
+
pick(node.value);
|
|
20219
|
+
}
|
|
20220
|
+
else if (!node.disabled && isParentNode(node)) {
|
|
20221
|
+
// TreeView leaves a key that's been used alone.
|
|
20222
|
+
event.preventDefault();
|
|
20223
|
+
onTreeExpandedChange(shownExpanded.includes(node.value)
|
|
20224
|
+
? shownExpanded.filter(value => value !== node.value)
|
|
20225
|
+
: [...shownExpanded, node.value]);
|
|
20226
|
+
}
|
|
20227
|
+
},
|
|
20228
|
+
onClick: (event) => {
|
|
20229
|
+
const item = event.currentTarget;
|
|
20230
|
+
const target = event.target;
|
|
20231
|
+
// A click in this item's own row, where TreeView reads one,
|
|
20232
|
+
// not one in the group of items inside it, and not on its
|
|
20233
|
+
// chevron, which only opens it.
|
|
20234
|
+
const inRow = target === item || item.firstElementChild.contains(target);
|
|
20235
|
+
const onChevron = isParentNode(node) && !!target.closest(`.${toggleClass}`);
|
|
20236
|
+
if (inRow && !onChevron && pickable(node)) {
|
|
20237
|
+
// TreeView opens a parent it's handed this click on, in
|
|
20238
|
+
// this same click: the flag goes once the click is over.
|
|
20239
|
+
clickPickedRef.current = isParentNode(node);
|
|
20240
|
+
queueMicrotask(() => {
|
|
20241
|
+
clickPickedRef.current = false;
|
|
20242
|
+
});
|
|
20243
|
+
pick(node.value);
|
|
20244
|
+
}
|
|
20245
|
+
},
|
|
20246
|
+
}
|
|
20247
|
+
: undefined;
|
|
20248
|
+
const renderNode = (node) => (jsxRuntime.jsx(TreeView.Item, { value: node.value, label: itemTemplate ? itemTemplate(node) : nodeLabel(node), icon: node.icon, hasChildren: node.hasChildren, isLoading: node.isLoading, loadError: node.loadError, onRetry: node.onRetry, disabled: node.disabled, "data-initial-focus": !searchable && node.value === focusTarget ? '' : undefined, ...rowHandlers(node), children: childNodes(node).map(renderNode) }, node.value));
|
|
20249
|
+
// In `checkbox`, TreeView reports every checked item, read here into
|
|
20250
|
+
// the list `checkedStrategy` gives.
|
|
20251
|
+
const shaped = (next) => cascade ? checkedByStrategy(records, next, checkedStrategy) : next;
|
|
20252
|
+
const onCheckedChange = (next) => change(shaped(next));
|
|
20253
|
+
// Every item's check state, read once a render, which the clear button
|
|
20254
|
+
// and the tags' buttons decide from. Only `checkbox` reads TreeView's
|
|
20255
|
+
// rules: in `checkbox-independent` a listed item is a checked one.
|
|
20256
|
+
const listedKey = JSON.stringify(listed);
|
|
20257
|
+
const checks = React.useMemo(() => (cascade ? readChecks(records, new Set(listed)) : null),
|
|
20258
|
+
// `listed` by what it holds, since a held list is a new array each
|
|
20259
|
+
// render.
|
|
20260
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
20261
|
+
[cascade, records, listedKey]);
|
|
20262
|
+
const isDisabled = (value) => !!index.get(value)?.node.disabled;
|
|
20263
|
+
// Read in `checkbox` only, where there are states to read.
|
|
20264
|
+
const isChecked = (value) => checks.states.get(value) === true;
|
|
20265
|
+
// What a control can uncheck. In `checkbox`, that's each checked end no
|
|
20266
|
+
// checked disabled item above it keeps checked, and every item above it,
|
|
20267
|
+
// which takes it along; elsewhere it's each listed item that isn't
|
|
20268
|
+
// disabled. A disabled item is checked only by its own value, so nothing
|
|
20269
|
+
// unchecks it, or what it keeps checked.
|
|
20270
|
+
const uncheckable = new Set();
|
|
20271
|
+
if (checks) {
|
|
20272
|
+
for (const end of checks.ends) {
|
|
20273
|
+
const { path } = index.get(end);
|
|
20274
|
+
if (isChecked(end) &&
|
|
20275
|
+
!path.some(above => isDisabled(above) && isChecked(above))) {
|
|
20276
|
+
uncheckable.add(end);
|
|
20277
|
+
path.forEach(above => uncheckable.add(above));
|
|
20278
|
+
}
|
|
20279
|
+
}
|
|
20280
|
+
}
|
|
20281
|
+
else if (!single) {
|
|
20282
|
+
picked.forEach(value => !isDisabled(value) && uncheckable.add(value));
|
|
20283
|
+
}
|
|
20284
|
+
// A tag's button takes its value off where that changes what's checked,
|
|
20285
|
+
// the way its box would. A value no node has can always go.
|
|
20286
|
+
const removable = (value) => !index.has(value) || (!isDisabled(value) && uncheckable.has(value));
|
|
20287
|
+
const without = (value) => cascade && records.has(value)
|
|
20288
|
+
? shaped(toggledList(records, listed, value))
|
|
20289
|
+
: listed.filter(item => item !== value);
|
|
20290
|
+
// What the clear button leaves: the checked disabled items, and in
|
|
20291
|
+
// `checkbox` what they keep checked, shaped as a check's value is.
|
|
20292
|
+
const cleared = single
|
|
20293
|
+
? []
|
|
20294
|
+
: shaped(checks
|
|
20295
|
+
? Array.from(records.keys()).filter(value => isDisabled(value) && isChecked(value))
|
|
20296
|
+
: picked.filter(isDisabled));
|
|
20297
|
+
const canClear = single
|
|
20298
|
+
? picked.length > 0
|
|
20299
|
+
: uncheckable.size > 0 || picked.some(value => !index.has(value));
|
|
20300
|
+
const inputRef = React.useRef(null);
|
|
20301
|
+
// A forwarded callback's cleanup, which React 19 lets it return, run on
|
|
20302
|
+
// detach in place of a call with `null`, on React 18 as well.
|
|
20303
|
+
const refCleanupRef = React.useRef(null);
|
|
20304
|
+
const fieldRef = React.useCallback((node) => {
|
|
20305
|
+
inputRef.current = node;
|
|
20306
|
+
if (typeof ref === 'function') {
|
|
20307
|
+
if (node === null) {
|
|
20308
|
+
const cleanup = refCleanupRef.current;
|
|
20309
|
+
refCleanupRef.current = null;
|
|
20310
|
+
if (cleanup)
|
|
20311
|
+
cleanup();
|
|
20312
|
+
else
|
|
20313
|
+
ref(null);
|
|
20314
|
+
return;
|
|
20315
|
+
}
|
|
20316
|
+
const cleanup = ref(node);
|
|
20317
|
+
refCleanupRef.current =
|
|
20318
|
+
typeof cleanup === 'function' ? cleanup : null;
|
|
20319
|
+
}
|
|
20320
|
+
else if (ref) {
|
|
20321
|
+
ref.current = node;
|
|
20322
|
+
}
|
|
20323
|
+
}, [ref]);
|
|
20324
|
+
const tagsId = React.useId();
|
|
20325
|
+
const ids = tagIds(tagsId, picked);
|
|
20326
|
+
const searchClass = usePrefixedClassNames('input', 'tree-select-search', {
|
|
20327
|
+
[`is-${size}`]: !!size,
|
|
20328
|
+
});
|
|
20329
|
+
const emptyClass = usePrefixedClassNames('tree-select-empty');
|
|
20330
|
+
const treeRef = React.useRef(null);
|
|
20331
|
+
const emptyRef = React.useRef(null);
|
|
20332
|
+
const nothingFound = !!search && !search.found;
|
|
20333
|
+
// The region reads the text on screen, whatever `empty` renders.
|
|
20334
|
+
const describeEmpty = React.useCallback(() => {
|
|
20335
|
+
const node = emptyRef.current;
|
|
20336
|
+
/* istanbul ignore if: it's only announced while it's on screen */
|
|
20337
|
+
if (!node)
|
|
20338
|
+
return null;
|
|
20339
|
+
return spokenText(node);
|
|
20340
|
+
}, []);
|
|
20341
|
+
return (jsxRuntime.jsx(DialogSelect, { ...rest, component: "TreeSelect", rootClass: "tree-select", panelClass: "tree-select-panel", text: single && picked.length > 0 ? labelFor(picked[0]) : '', values: single ? [picked[0] ?? ''] : picked, hasValue: picked.length > 0, open: isOpen, onOpenChange: setOpen, onPrintableKey: searchable
|
|
20342
|
+
? key => {
|
|
20343
|
+
setQuery(key);
|
|
20344
|
+
setOpen(true);
|
|
20345
|
+
return true;
|
|
20346
|
+
}
|
|
20347
|
+
: undefined, onClear: () => change(cleared), clearable: clearable && canClear, onFormReset: () => {
|
|
20348
|
+
// Back to `defaultValue`, or to nothing for a held value, which
|
|
20349
|
+
// its owner sets from `onChange`.
|
|
20350
|
+
const back = valueProp === undefined ? asList(defaultValue) : [];
|
|
20351
|
+
if (!sameValues(back, listed))
|
|
20352
|
+
change(back);
|
|
20353
|
+
}, clearLabel: labels?.clear || 'Clear', chooseLabel: labels?.choose, disabled: disabled, size: size, inputRef: inputRef, fieldRef: fieldRef, describedBy: single ? undefined : ids.join(' '), below: single ? undefined : (jsxRuntime.jsx(ValueTags, { values: picked, ids: ids, labelFor: labelFor, disabled: disabled, ariaCloseLabel: ariaCloseLabel, removable: removable, onRemove: value => {
|
|
20354
|
+
change(without(value));
|
|
20355
|
+
inputRef.current?.focus();
|
|
20356
|
+
} })), initialFocusSelector: INITIAL_FOCUS, children: panelName => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [searchable && (jsxRuntime.jsx("input", { className: searchClass, type: "search", "aria-label": labels?.search || 'Search', autoComplete: "off", value: query, "data-initial-focus": "", onChange: event => setQuery(event.target.value), onFocus: event => {
|
|
20357
|
+
// A key that opened the panel is in the box: type after it.
|
|
20358
|
+
const { length } = event.currentTarget.value;
|
|
20359
|
+
event.currentTarget.setSelectionRange(length, length);
|
|
20360
|
+
}, onKeyDown: event => {
|
|
20361
|
+
if (event.key === 'Enter') {
|
|
20362
|
+
// The search box's Enter would submit the form.
|
|
20363
|
+
event.preventDefault();
|
|
20364
|
+
}
|
|
20365
|
+
else if (event.key === 'ArrowDown' && !event.altKey) {
|
|
20366
|
+
event.preventDefault();
|
|
20367
|
+
const tree = treeRef.current;
|
|
20368
|
+
(tree?.querySelector('[role="treeitem"][tabindex="0"]') ?? tree?.querySelector('[role="treeitem"]'))?.focus();
|
|
20369
|
+
}
|
|
20370
|
+
} })), nothingFound ? (empty && (jsxRuntime.jsx("div", { className: emptyClass, ref: emptyRef, children: empty }))) : (jsxRuntime.jsx(TreeViewHiddenContext.Provider, { value: search?.hidden ?? NOTHING_HIDDEN, children: jsxRuntime.jsx(TreeView, { ...panelName, ref: treeRef,
|
|
20371
|
+
// It scrolls, and Firefox makes a scroller a Tab stop of
|
|
20372
|
+
// its own: the roving one on its items is the tree's.
|
|
20373
|
+
tabIndex: -1, selectionMode: selectionMode, size: size, selected: single ? (picked[0] ?? null) : undefined, checked: single ? undefined : listed, onCheckedChange: onCheckedChange, expanded: shownExpanded, onExpandedChange: onTreeExpandedChange, loadingLabel: labels?.loading, retryLabel: labels?.retry, loadingAnnouncement: labels?.loadingAnnouncement, loadedAnnouncement: labels?.loadedAnnouncement, children: data.map(renderNode) }) })), jsxRuntime.jsx(StatusRegion, { items: nothingFound && empty ? noMatches : nothingToSay, describe: describeEmpty })] })) }));
|
|
20374
|
+
});
|
|
20375
|
+
TreeSelect.displayName = 'TreeSelect';
|
|
20376
|
+
|
|
20377
|
+
// An IME that's composing owns every key: Enter confirms what it composed,
|
|
20378
|
+
// the arrows move through its candidates, and Escape cancels it. Safari can
|
|
20379
|
+
// say so with `keyCode` 229 rather than `isComposing` (#1102).
|
|
20380
|
+
const isComposing = (e) => e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
|
|
20381
|
+
/**
|
|
20382
|
+
* The `Taginput` component provides a tag/chip input field for managing multiple tags.
|
|
20383
|
+
*
|
|
20384
|
+
* @function
|
|
20385
|
+
* @param {TaginputProps} props - Props for the Taginput component.
|
|
20386
|
+
* @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the input element.
|
|
20387
|
+
* @returns {JSX.Element} The rendered taginput component.
|
|
20388
|
+
*
|
|
20389
|
+
* @example
|
|
20390
|
+
* // Basic usage
|
|
20391
|
+
* <Taginput
|
|
20392
|
+
* placeholder="Add a tag..."
|
|
20393
|
+
* onChange={(tags) => console.log(tags)}
|
|
20394
|
+
* />
|
|
20395
|
+
*
|
|
20396
|
+
* @example
|
|
20397
|
+
* // With autocomplete
|
|
20398
|
+
* <Taginput
|
|
20399
|
+
* data={['React', 'Vue', 'Angular', 'Svelte']}
|
|
20400
|
+
* allowNew={false}
|
|
20401
|
+
* placeholder="Select frameworks..."
|
|
20402
|
+
* />
|
|
20403
|
+
*/
|
|
20404
|
+
const Taginput = React.forwardRef(({
|
|
20405
|
+
// Field props
|
|
20406
|
+
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = [], data: dataProp, placeholder, field = 'label', allowNew = true, allowDuplicates = false, openOnFocus = false, removeOnKeys = true, confirmKeys = ['Enter', ','], closable = true, attached = false, maxTags, maxlength, disabled: disabledProp, readonly = false, rounded = false, ellipsis = false, hasCounter = true, onPasteSeparators = [','], beforeAdding, createTag, keepFirst = false, keepOpen = true, loading = false, ariaCloseLabel, icon, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, tagColor, size, onChange, onAdd, onRemove, onTyping, tagTemplate, name, form, id, className, ...props }, ref) => {
|
|
20407
|
+
const disabled = useFieldsetDisabled(disabledProp);
|
|
20408
|
+
const insideField = useInsideField();
|
|
20409
|
+
const insideControl = useInsideControl();
|
|
20410
|
+
const ownField = rendersOwnField('Taginput', {
|
|
20411
|
+
insideField,
|
|
20412
|
+
insideControl,
|
|
20413
|
+
label,
|
|
20414
|
+
message,
|
|
20415
|
+
horizontal,
|
|
20416
|
+
fieldClassName,
|
|
20417
|
+
});
|
|
20418
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
20419
|
+
const { classPrefix } = useConfig();
|
|
20420
|
+
const defaultIconLibrary = useIconLibrary();
|
|
20421
|
+
const formatNumber = useScopedNumberFormat();
|
|
19363
20422
|
const matchText = useLocaleMatcher();
|
|
19364
20423
|
const resolvedLibrary = iconLibraryProp || defaultIconLibrary || 'fa';
|
|
19365
20424
|
const pcn = (...args) => prefixedClassNames(classPrefix, ...args);
|
|
@@ -20253,7 +21312,7 @@ const rgbChannels = (color) => {
|
|
|
20253
21312
|
/** Whether a press should start a drag: any touch or pen, a mouse's main button. */
|
|
20254
21313
|
const startsDrag = (event) => event.pointerType !== 'mouse' || event.button === 0;
|
|
20255
21314
|
/** Hands a node to a forwarded ref and to one of the part's own. */
|
|
20256
|
-
function setRefs(node, own, forwarded) {
|
|
21315
|
+
function setRefs$1(node, own, forwarded) {
|
|
20257
21316
|
own.current = node;
|
|
20258
21317
|
if (typeof forwarded === 'function')
|
|
20259
21318
|
forwarded(node);
|
|
@@ -20385,7 +21444,7 @@ const ColorPickerArea = React.forwardRef(({ className, style, ...props }, ref) =
|
|
|
20385
21444
|
const hiddenOf = (which) => touch || keyed || axis === which ? undefined : true;
|
|
20386
21445
|
if (!picker)
|
|
20387
21446
|
return null;
|
|
20388
|
-
return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
|
|
21447
|
+
return (jsxRuntime.jsx("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
|
|
20389
21448
|
...style,
|
|
20390
21449
|
'--color-picker-hue': Math.round(color.h),
|
|
20391
21450
|
'--color-picker-rgb': rgbChannels(color),
|
|
@@ -20459,7 +21518,7 @@ const ChannelSlider = React.forwardRef(({ channel, className, style, ...props },
|
|
|
20459
21518
|
};
|
|
20460
21519
|
if (!picker)
|
|
20461
21520
|
return null;
|
|
20462
|
-
return (jsxRuntime.jsxs("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
|
|
21521
|
+
return (jsxRuntime.jsxs("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
|
|
20463
21522
|
...style,
|
|
20464
21523
|
'--color-picker-hue': Math.round(color.h),
|
|
20465
21524
|
'--color-picker-rgb': rgbChannels(color),
|
|
@@ -20583,7 +21642,7 @@ const ColorPickerSwatches = React.forwardRef(({ className, swatches: ownSwatches
|
|
|
20583
21642
|
};
|
|
20584
21643
|
if (!picker || list.length === 0)
|
|
20585
21644
|
return null;
|
|
20586
|
-
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) => {
|
|
21645
|
+
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) => {
|
|
20587
21646
|
const swatch = colorOf(i);
|
|
20588
21647
|
const isChecked = i === checked;
|
|
20589
21648
|
// The check is drawn in black or white with a ring of the other, so
|
|
@@ -20951,61 +22010,6 @@ fieldClassName, controlClassName,
|
|
|
20951
22010
|
});
|
|
20952
22011
|
TextArea.displayName = 'TextArea';
|
|
20953
22012
|
|
|
20954
|
-
/**
|
|
20955
|
-
* Whether a key is Alt+ArrowDown or Alt+ArrowUp, which open and close a
|
|
20956
|
-
* picker's popover. The calendar and the time wheels leave both to it rather
|
|
20957
|
-
* than reading them as plain arrows.
|
|
20958
|
-
*/
|
|
20959
|
-
const isPopoverToggleKey = (e) => e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp');
|
|
20960
|
-
const DEFAULT_PICKER_LABELS = {
|
|
20961
|
-
prevMonth: 'Previous month',
|
|
20962
|
-
nextMonth: 'Next month',
|
|
20963
|
-
prevYear: 'Previous year',
|
|
20964
|
-
nextYear: 'Next year',
|
|
20965
|
-
chooseDate: 'Choose date',
|
|
20966
|
-
chooseMonth: 'Choose month',
|
|
20967
|
-
chooseYear: 'Choose year',
|
|
20968
|
-
rangeStart: 'Start date',
|
|
20969
|
-
rangeEnd: 'End date',
|
|
20970
|
-
rangePreviewEnd: 'Choose as end date',
|
|
20971
|
-
rangeSeparator: '–',
|
|
20972
|
-
chooseDateRange: 'Choose date range',
|
|
20973
|
-
hours: 'hours',
|
|
20974
|
-
minutes: 'minutes',
|
|
20975
|
-
seconds: 'seconds',
|
|
20976
|
-
ampm: 'AM/PM',
|
|
20977
|
-
increaseHours: 'Increase hours',
|
|
20978
|
-
decreaseHours: 'Decrease hours',
|
|
20979
|
-
increaseMinutes: 'Increase minutes',
|
|
20980
|
-
decreaseMinutes: 'Decrease minutes',
|
|
20981
|
-
increaseSeconds: 'Increase seconds',
|
|
20982
|
-
decreaseSeconds: 'Decrease seconds',
|
|
20983
|
-
toggleAmPm: 'Toggle AM/PM',
|
|
20984
|
-
chooseTime: 'Choose time',
|
|
20985
|
-
chooseDateTime: 'Choose date and time',
|
|
20986
|
-
now: 'Now',
|
|
20987
|
-
today: 'Today',
|
|
20988
|
-
clear: 'Clear',
|
|
20989
|
-
cancel: 'Cancel',
|
|
20990
|
-
ok: 'OK',
|
|
20991
|
-
reset: 'Reset',
|
|
20992
|
-
done: 'Done',
|
|
20993
|
-
time: 'Time',
|
|
20994
|
-
};
|
|
20995
|
-
/**
|
|
20996
|
-
* Merge user-supplied label overrides with the defaults. A key set to
|
|
20997
|
-
* `undefined` keeps its default, so a locale table that is missing a string
|
|
20998
|
-
* still names its control instead of leaving it with no accessible name.
|
|
20999
|
-
*/
|
|
21000
|
-
const mergeLabels = (overrides) => {
|
|
21001
|
-
const merged = { ...DEFAULT_PICKER_LABELS };
|
|
21002
|
-
for (const [key, value] of Object.entries(overrides ?? {})) {
|
|
21003
|
-
if (value !== undefined)
|
|
21004
|
-
merged[key] = value;
|
|
21005
|
-
}
|
|
21006
|
-
return merged;
|
|
21007
|
-
};
|
|
21008
|
-
|
|
21009
22013
|
function isSameDay(a, b) {
|
|
21010
22014
|
return (a.getFullYear() === b.getFullYear() &&
|
|
21011
22015
|
a.getMonth() === b.getMonth() &&
|
|
@@ -22259,186 +23263,80 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
|
|
|
22259
23263
|
'is-disabled': disabled,
|
|
22260
23264
|
'is-other-month': otherMonth,
|
|
22261
23265
|
});
|
|
22262
|
-
const display = !otherMonth || nearbyMonthDays;
|
|
22263
|
-
return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "aria-describedby": describedBy, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
|
|
22264
|
-
// Reached by Tab while the focused day is disabled, or
|
|
22265
|
-
// by pointer: keys move on from here. A nearby month's
|
|
22266
|
-
// day waits for its click, which turns the grid.
|
|
22267
|
-
if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
|
|
22268
|
-
onFocusedDateChange(cell.date);
|
|
22269
|
-
}
|
|
22270
|
-
},
|
|
22271
|
-
// Mouseover rather than mouseenter: browsers send it to a
|
|
22272
|
-
// disabled day as well, where React holds mouseenter back,
|
|
22273
|
-
// so the preview leaves the last enabled day behind.
|
|
22274
|
-
onMouseOver: anchor ? () => setHoverDate(cell.date) : undefined, onClick: () => {
|
|
22275
|
-
if (disabled)
|
|
22276
|
-
return;
|
|
22277
|
-
onFocusedDateChange(cell.date);
|
|
22278
|
-
if (rangeMode)
|
|
22279
|
-
pickRangeDay(cell.date);
|
|
22280
|
-
else
|
|
22281
|
-
onSelect?.(cell.date);
|
|
22282
|
-
}, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
|
|
22283
|
-
}), weekClass) })] })), view === 'months' && (jsxRuntime.jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsxRuntime.jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
|
|
22284
|
-
const date = makeDate(focusedYear, month);
|
|
22285
|
-
const disabled = disabledMonths[month];
|
|
22286
|
-
const isSelected = !!value && isSameMonth(value, date);
|
|
22287
|
-
const isFocused = month === tabStopMonth;
|
|
22288
|
-
const isCurrent = isSameMonth(date, today);
|
|
22289
|
-
const cellClass = prefixedClassNames(classPrefix, 'dateinput-month-cell', {
|
|
22290
|
-
'is-selected': isSelected,
|
|
22291
|
-
'is-today': isCurrent,
|
|
22292
|
-
'is-disabled': disabled,
|
|
22293
|
-
});
|
|
22294
|
-
return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": isCurrent ? 'date' : undefined, "aria-label": computedMonthNames[month], "data-focused": isFocused ? 'true' : undefined, disabled: disabled, className: cellClass, onFocus: () => {
|
|
22295
|
-
// Reached by Tab while the focused month is disabled,
|
|
22296
|
-
// or by pointer: keys move on from here.
|
|
22297
|
-
if (month !== focusedDate.getMonth()) {
|
|
22298
|
-
onFocusedDateChange(clampDate(date, min, max));
|
|
22299
|
-
}
|
|
22300
|
-
}, onClick: () => {
|
|
22301
|
-
onFocusedDateChange(clampDate(date, min, max));
|
|
22302
|
-
onSelect?.(date);
|
|
22303
|
-
}, children: monthCellNames[month] }, month));
|
|
22304
|
-
}) }, row[0]))) })), view === 'years' && (jsxRuntime.jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
|
|
22305
|
-
const isFocused = year === tabStopYear;
|
|
22306
|
-
const isToday = year === todayYear;
|
|
22307
|
-
// As navigation the focused year is the current one. As the
|
|
22308
|
-
// selection surface the value's year is selected and focus roams.
|
|
22309
|
-
const isSelected = isYearGranularity
|
|
22310
|
-
? !!value && value.getFullYear() === year
|
|
22311
|
-
: isFocused;
|
|
22312
|
-
const disabled = disabledYears.has(year);
|
|
22313
|
-
const cellCls = prefixedClassNames(classPrefix, 'dateinput-year-cell', {
|
|
22314
|
-
'is-selected': isSelected,
|
|
22315
|
-
'is-today': isToday,
|
|
22316
|
-
'is-disabled': disabled,
|
|
22317
|
-
});
|
|
22318
|
-
return (jsxRuntime.jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: year === listTabStop ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
|
|
22319
|
-
// As navigation, focusing a year by key or pointer only
|
|
22320
|
-
// moves the tab stop: nothing is committed until a year is
|
|
22321
|
-
// picked. As the selection surface it is reached by Tab
|
|
22322
|
-
// while the focused year is disabled, or by pointer, and
|
|
22323
|
-
// keys move on from here.
|
|
22324
|
-
if (!isYearGranularity) {
|
|
22325
|
-
setNavYear(year);
|
|
22326
|
-
}
|
|
22327
|
-
else if (year !== focusedYear) {
|
|
22328
|
-
onFocusedDateChange(clampDate(makeDate(year), min, max));
|
|
22329
|
-
}
|
|
22330
|
-
}, onClick: () => isYearGranularity
|
|
22331
|
-
? handleYearPick(year)
|
|
22332
|
-
: handleYearSelect(year), children: year }, year));
|
|
22333
|
-
}) })), rangeMode && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { id: rangeStartId, hidden: true, children: t.rangeStart }), jsxRuntime.jsx("span", { id: rangeEndId, hidden: true, children: t.rangeEnd }), jsxRuntime.jsx("span", { id: rangePreviewEndId, hidden: true, children: t.rangePreviewEnd }), jsxRuntime.jsx("div", { id: rangeSummaryId, "aria-live": "polite", style: visuallyHidden, children: rangeSummary })] }))] }));
|
|
22334
|
-
};
|
|
22335
|
-
Calendar.displayName = 'Calendar';
|
|
22336
|
-
|
|
22337
|
-
const isBrowser = typeof window !== 'undefined';
|
|
22338
|
-
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, localeScope, }) => {
|
|
22339
|
-
const panelRef = React.useRef(null);
|
|
22340
|
-
const resolveContainer = usePortalContainer();
|
|
22341
|
-
// Read as the trap turns on, once the panel's content has rendered.
|
|
22342
|
-
const initialFocusRef = React.useMemo(() => initialFocusSelector === undefined
|
|
22343
|
-
? undefined
|
|
22344
|
-
: {
|
|
22345
|
-
get current() {
|
|
22346
|
-
return (panelRef.current?.querySelector(initialFocusSelector) ?? null);
|
|
22347
|
-
},
|
|
22348
|
-
}, [initialFocusSelector]);
|
|
22349
|
-
const resolved = useAnchoredPosition(anchorRef, panelRef, {
|
|
22350
|
-
active: isOpen,
|
|
22351
|
-
position,
|
|
22352
|
-
fixed: appendToBody,
|
|
22353
|
-
// The gap comes from `--bulma-picker-popover-offset` as a margin, in place
|
|
22354
|
-
// or portaled, so the coordinates leave it out and `auto` reads it off
|
|
22355
|
-
// the panel.
|
|
22356
|
-
offset: 0,
|
|
22357
|
-
styledGap: marginGap,
|
|
22358
|
-
});
|
|
22359
|
-
// Read when a press lands outside or a key arrives, so both listeners stay
|
|
22360
|
-
// put while `onClose` changes identity, as an inline one does on every
|
|
22361
|
-
// render of the field.
|
|
22362
|
-
const onCloseRef = useLatestRef(onClose);
|
|
22363
|
-
React.useEffect(() => {
|
|
22364
|
-
if (!isOpen || !closeOnClickOutside || !isBrowser)
|
|
22365
|
-
return undefined;
|
|
22366
|
-
// Listen on `pointerdown` (rather than `mousedown`) so touch starts
|
|
22367
|
-
// outside the popover dismiss it on mobile — including swipe-from-outside
|
|
22368
|
-
// gestures, where the synthetic mouse event would otherwise lag a few
|
|
22369
|
-
// hundred ms behind the actual touch.
|
|
22370
|
-
const handler = (e) => {
|
|
22371
|
-
const path = e.composedPath();
|
|
22372
|
-
if (panelRef.current && path.includes(panelRef.current))
|
|
22373
|
-
return;
|
|
22374
|
-
if (anchorRef.current && path.includes(anchorRef.current))
|
|
22375
|
-
return;
|
|
22376
|
-
onCloseRef.current();
|
|
22377
|
-
};
|
|
22378
|
-
document.addEventListener('pointerdown', handler);
|
|
22379
|
-
return () => document.removeEventListener('pointerdown', handler);
|
|
22380
|
-
}, [isOpen, closeOnClickOutside, anchorRef, onCloseRef]);
|
|
22381
|
-
// Escape closes it, and so does Alt+ArrowUp, the combobox's key for it,
|
|
22382
|
-
// which the calendar and the time wheels inside leave to it. Escape closes
|
|
22383
|
-
// it from anywhere, but the key is only the picker's when it was pressed in
|
|
22384
|
-
// the panel or the field: then it's marked used, so a Modal or Sidebar
|
|
22385
|
-
// around the picker stays open. From anywhere else, such as a dialog opened
|
|
22386
|
-
// over the picker, the key goes on to whatever has focus.
|
|
22387
|
-
React.useEffect(() => {
|
|
22388
|
-
if (!isOpen || !closeOnEscape || !isBrowser)
|
|
22389
|
-
return undefined;
|
|
22390
|
-
const handler = (e) => {
|
|
22391
|
-
if (e.key === 'Escape') {
|
|
22392
|
-
if (isEventInside(e, panelRef.current) ||
|
|
22393
|
-
isEventInside(e, anchorRef.current)) {
|
|
22394
|
-
e.preventDefault();
|
|
22395
|
-
}
|
|
22396
|
-
onCloseRef.current();
|
|
22397
|
-
}
|
|
22398
|
-
else if (e.altKey && e.key === 'ArrowUp') {
|
|
22399
|
-
e.preventDefault();
|
|
22400
|
-
e.stopPropagation();
|
|
22401
|
-
onCloseRef.current();
|
|
22402
|
-
}
|
|
22403
|
-
};
|
|
22404
|
-
document.addEventListener('keydown', handler);
|
|
22405
|
-
return () => document.removeEventListener('keydown', handler);
|
|
22406
|
-
}, [isOpen, closeOnEscape, anchorRef, onCloseRef]);
|
|
22407
|
-
useFocusTrap(panelRef, {
|
|
22408
|
-
active: isOpen && trapFocus,
|
|
22409
|
-
initialFocusRef,
|
|
22410
|
-
restoreFocus: restoreFocusRef ?? true,
|
|
22411
|
-
});
|
|
22412
|
-
const panelClass = usePrefixedClassNames('picker-popover', {
|
|
22413
|
-
'is-active': isOpen,
|
|
22414
|
-
[`is-${resolved.position}`]: true,
|
|
22415
|
-
'is-portal': appendToBody,
|
|
22416
|
-
});
|
|
22417
|
-
if (!isOpen)
|
|
22418
|
-
return null;
|
|
22419
|
-
const style = appendToBody
|
|
22420
|
-
? {
|
|
22421
|
-
position: 'fixed',
|
|
22422
|
-
top: resolved.top,
|
|
22423
|
-
left: resolved.left,
|
|
22424
|
-
}
|
|
22425
|
-
: {};
|
|
22426
|
-
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,
|
|
22427
|
-
// An Escape pressed in the panel is the picker's, and the listener
|
|
22428
|
-
// above claims it. A Dropdown menu around the picker hears Escape in a
|
|
22429
|
-
// React handler, which runs before that listener, so the panel claims
|
|
22430
|
-
// it here too, where React gets to it first.
|
|
22431
|
-
onKeyDown: e => {
|
|
22432
|
-
if (closeOnEscape && e.key === 'Escape')
|
|
22433
|
-
e.preventDefault();
|
|
22434
|
-
}, children: children }));
|
|
22435
|
-
if (appendToBody && isBrowser) {
|
|
22436
|
-
const target = resolveContainer();
|
|
22437
|
-
return reactDom.createPortal(jsxRuntime.jsx(LocalePortalScope, { target: target, scope: localeScope, children: panel }), target);
|
|
22438
|
-
}
|
|
22439
|
-
return panel;
|
|
23266
|
+
const display = !otherMonth || nearbyMonthDays;
|
|
23267
|
+
return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "aria-describedby": describedBy, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
|
|
23268
|
+
// Reached by Tab while the focused day is disabled, or
|
|
23269
|
+
// by pointer: keys move on from here. A nearby month's
|
|
23270
|
+
// day waits for its click, which turns the grid.
|
|
23271
|
+
if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
|
|
23272
|
+
onFocusedDateChange(cell.date);
|
|
23273
|
+
}
|
|
23274
|
+
},
|
|
23275
|
+
// Mouseover rather than mouseenter: browsers send it to a
|
|
23276
|
+
// disabled day as well, where React holds mouseenter back,
|
|
23277
|
+
// so the preview leaves the last enabled day behind.
|
|
23278
|
+
onMouseOver: anchor ? () => setHoverDate(cell.date) : undefined, onClick: () => {
|
|
23279
|
+
if (disabled)
|
|
23280
|
+
return;
|
|
23281
|
+
onFocusedDateChange(cell.date);
|
|
23282
|
+
if (rangeMode)
|
|
23283
|
+
pickRangeDay(cell.date);
|
|
23284
|
+
else
|
|
23285
|
+
onSelect?.(cell.date);
|
|
23286
|
+
}, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
|
|
23287
|
+
}), weekClass) })] })), view === 'months' && (jsxRuntime.jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsxRuntime.jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
|
|
23288
|
+
const date = makeDate(focusedYear, month);
|
|
23289
|
+
const disabled = disabledMonths[month];
|
|
23290
|
+
const isSelected = !!value && isSameMonth(value, date);
|
|
23291
|
+
const isFocused = month === tabStopMonth;
|
|
23292
|
+
const isCurrent = isSameMonth(date, today);
|
|
23293
|
+
const cellClass = prefixedClassNames(classPrefix, 'dateinput-month-cell', {
|
|
23294
|
+
'is-selected': isSelected,
|
|
23295
|
+
'is-today': isCurrent,
|
|
23296
|
+
'is-disabled': disabled,
|
|
23297
|
+
});
|
|
23298
|
+
return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": isCurrent ? 'date' : undefined, "aria-label": computedMonthNames[month], "data-focused": isFocused ? 'true' : undefined, disabled: disabled, className: cellClass, onFocus: () => {
|
|
23299
|
+
// Reached by Tab while the focused month is disabled,
|
|
23300
|
+
// or by pointer: keys move on from here.
|
|
23301
|
+
if (month !== focusedDate.getMonth()) {
|
|
23302
|
+
onFocusedDateChange(clampDate(date, min, max));
|
|
23303
|
+
}
|
|
23304
|
+
}, onClick: () => {
|
|
23305
|
+
onFocusedDateChange(clampDate(date, min, max));
|
|
23306
|
+
onSelect?.(date);
|
|
23307
|
+
}, children: monthCellNames[month] }, month));
|
|
23308
|
+
}) }, row[0]))) })), view === 'years' && (jsxRuntime.jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
|
|
23309
|
+
const isFocused = year === tabStopYear;
|
|
23310
|
+
const isToday = year === todayYear;
|
|
23311
|
+
// As navigation the focused year is the current one. As the
|
|
23312
|
+
// selection surface the value's year is selected and focus roams.
|
|
23313
|
+
const isSelected = isYearGranularity
|
|
23314
|
+
? !!value && value.getFullYear() === year
|
|
23315
|
+
: isFocused;
|
|
23316
|
+
const disabled = disabledYears.has(year);
|
|
23317
|
+
const cellCls = prefixedClassNames(classPrefix, 'dateinput-year-cell', {
|
|
23318
|
+
'is-selected': isSelected,
|
|
23319
|
+
'is-today': isToday,
|
|
23320
|
+
'is-disabled': disabled,
|
|
23321
|
+
});
|
|
23322
|
+
return (jsxRuntime.jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: year === listTabStop ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
|
|
23323
|
+
// As navigation, focusing a year by key or pointer only
|
|
23324
|
+
// moves the tab stop: nothing is committed until a year is
|
|
23325
|
+
// picked. As the selection surface it is reached by Tab
|
|
23326
|
+
// while the focused year is disabled, or by pointer, and
|
|
23327
|
+
// keys move on from here.
|
|
23328
|
+
if (!isYearGranularity) {
|
|
23329
|
+
setNavYear(year);
|
|
23330
|
+
}
|
|
23331
|
+
else if (year !== focusedYear) {
|
|
23332
|
+
onFocusedDateChange(clampDate(makeDate(year), min, max));
|
|
23333
|
+
}
|
|
23334
|
+
}, onClick: () => isYearGranularity
|
|
23335
|
+
? handleYearPick(year)
|
|
23336
|
+
: handleYearSelect(year), children: year }, year));
|
|
23337
|
+
}) })), rangeMode && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { id: rangeStartId, hidden: true, children: t.rangeStart }), jsxRuntime.jsx("span", { id: rangeEndId, hidden: true, children: t.rangeEnd }), jsxRuntime.jsx("span", { id: rangePreviewEndId, hidden: true, children: t.rangePreviewEnd }), jsxRuntime.jsx("div", { id: rangeSummaryId, "aria-live": "polite", style: visuallyHidden, children: rangeSummary })] }))] }));
|
|
22440
23338
|
};
|
|
22441
|
-
|
|
23339
|
+
Calendar.displayName = 'Calendar';
|
|
22442
23340
|
|
|
22443
23341
|
/**
|
|
22444
23342
|
* Segment-map utility for manual keyboard entry on a date / time picker input.
|
|
@@ -23192,79 +24090,6 @@ function supportsInputType(type) {
|
|
|
23192
24090
|
return supported;
|
|
23193
24091
|
}
|
|
23194
24092
|
|
|
23195
|
-
/**
|
|
23196
|
-
* A picker's locale: its own `locale`, else the one a `LocaleProvider` around
|
|
23197
|
-
* it sets, else `undefined`, which leaves `Intl` to use the runtime's.
|
|
23198
|
-
*/
|
|
23199
|
-
function usePickerLocale(own) {
|
|
23200
|
-
const scope = useLocaleScope();
|
|
23201
|
-
return own || scope?.locale;
|
|
23202
|
-
}
|
|
23203
|
-
const NO_ROOT_ATTRS = {};
|
|
23204
|
-
/**
|
|
23205
|
-
* A picker's language and direction inside a `LocaleProvider`, by the rule a
|
|
23206
|
-
* nested provider follows: its `lang` is its own `locale`, else the
|
|
23207
|
-
* provider's, and its `dir` is the `dir` passed to it, else its own locale's
|
|
23208
|
-
* direction when it has one, else the provider's. Outside a provider it
|
|
23209
|
-
* changes nothing. A `locale` `Intl` refuses counts as none here, since the
|
|
23210
|
-
* picker's own formatting reports it.
|
|
23211
|
-
*/
|
|
23212
|
-
function usePickerLocaleScope(ownLocale, ownDir) {
|
|
23213
|
-
const scope = useLocaleScope();
|
|
23214
|
-
return React.useMemo(() => {
|
|
23215
|
-
if (!scope)
|
|
23216
|
-
return { rootAttrs: NO_ROOT_ATTRS };
|
|
23217
|
-
let own;
|
|
23218
|
-
try {
|
|
23219
|
-
own = canonicalLocale(ownLocale);
|
|
23220
|
-
}
|
|
23221
|
-
catch {
|
|
23222
|
-
own = undefined;
|
|
23223
|
-
}
|
|
23224
|
-
const dir = ownDir || (own ? localeDirection(own) : scope.dir);
|
|
23225
|
-
return {
|
|
23226
|
-
rootAttrs: own ? { lang: own, dir } : NO_ROOT_ATTRS,
|
|
23227
|
-
panel: { locale: own || scope.locale, dir },
|
|
23228
|
-
};
|
|
23229
|
-
}, [scope, ownLocale, ownDir]);
|
|
23230
|
-
}
|
|
23231
|
-
/**
|
|
23232
|
-
* Whether a picker's `dayNames` replace the calendar's own, which `Calendar`
|
|
23233
|
-
* does only for a full week of seven.
|
|
23234
|
-
*/
|
|
23235
|
-
function hasOwnDayNames(dayNames) {
|
|
23236
|
-
return dayNames !== undefined && dayNames.length === 7;
|
|
23237
|
-
}
|
|
23238
|
-
/**
|
|
23239
|
-
* The day a picker's week starts on: its own `firstDayOfWeek`, else, inside a
|
|
23240
|
-
* `LocaleProvider`, the first day of the locale the picker uses, else Sunday.
|
|
23241
|
-
* Outside a provider it stays Sunday whatever the locale, as it always has,
|
|
23242
|
-
* and so does a picker inside one with no locale from either, since the
|
|
23243
|
-
* runtime's own differs between the server and the browser.
|
|
23244
|
-
*
|
|
23245
|
-
* It also stays Sunday for a picker that brings its own `dayNames`. The
|
|
23246
|
-
* calendar shows those exactly as given, in calendar order, and before the
|
|
23247
|
-
* provider an unset `firstDayOfWeek` was always Sunday, so names written
|
|
23248
|
-
* Sunday first would sit a day or more off their columns if the week moved
|
|
23249
|
-
* under them.
|
|
23250
|
-
*/
|
|
23251
|
-
function usePickerFirstDayOfWeek(own, locale, hasDayNames) {
|
|
23252
|
-
const inProvider = useLocaleScope() !== null;
|
|
23253
|
-
return React.useMemo(() => {
|
|
23254
|
-
if (own !== undefined)
|
|
23255
|
-
return own;
|
|
23256
|
-
if (!inProvider || !locale || hasDayNames)
|
|
23257
|
-
return 0;
|
|
23258
|
-
try {
|
|
23259
|
-
return localeFirstDay(canonicalLocale(locale));
|
|
23260
|
-
}
|
|
23261
|
-
catch {
|
|
23262
|
-
// A tag Intl refuses: the picker's own formatting says so.
|
|
23263
|
-
return 0;
|
|
23264
|
-
}
|
|
23265
|
-
}, [own, inProvider, locale, hasDayNames]);
|
|
23266
|
-
}
|
|
23267
|
-
|
|
23268
24093
|
const DEFAULT_FORMATS = {
|
|
23269
24094
|
day: DEFAULT_DATE_FORMAT,
|
|
23270
24095
|
month: DEFAULT_MONTH_FORMAT,
|
|
@@ -27011,6 +27836,421 @@ function ClientOnly({ children, fallback = null }) {
|
|
|
27011
27836
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: typeof children === 'function' ? children() : children });
|
|
27012
27837
|
}
|
|
27013
27838
|
|
|
27839
|
+
const FrameContext = React.createContext(null);
|
|
27840
|
+
/** A width prop as CSS: a number is pixels. */
|
|
27841
|
+
const cssLength = (value) => typeof value === 'number' ? `${value}px` : value;
|
|
27842
|
+
/**
|
|
27843
|
+
* Hands `node` to a part's own ref and to the one forwarded to it. A
|
|
27844
|
+
* forwarded callback that returns a cleanup, as React 19 allows, has it held
|
|
27845
|
+
* in `cleanup` and run on detach in place of a call with `null`, on React 18
|
|
27846
|
+
* as well.
|
|
27847
|
+
*/
|
|
27848
|
+
function setRefs(node, own, forwarded, cleanup) {
|
|
27849
|
+
own.current = node;
|
|
27850
|
+
if (typeof forwarded === 'function') {
|
|
27851
|
+
if (node === null) {
|
|
27852
|
+
const detach = cleanup.current;
|
|
27853
|
+
cleanup.current = null;
|
|
27854
|
+
if (detach)
|
|
27855
|
+
detach();
|
|
27856
|
+
else
|
|
27857
|
+
forwarded(null);
|
|
27858
|
+
return;
|
|
27859
|
+
}
|
|
27860
|
+
const returned = forwarded(node);
|
|
27861
|
+
cleanup.current =
|
|
27862
|
+
typeof returned === 'function' ? returned : null;
|
|
27863
|
+
}
|
|
27864
|
+
else if (forwarded) {
|
|
27865
|
+
forwarded.current = node;
|
|
27866
|
+
}
|
|
27867
|
+
}
|
|
27868
|
+
/**
|
|
27869
|
+
* Warns when a part has an `id` the frame couldn't read, because it isn't a
|
|
27870
|
+
* direct child, so the skip link or the burger points at the wrong one.
|
|
27871
|
+
*/
|
|
27872
|
+
function warnOnUnreadId(part, id, expected) {
|
|
27873
|
+
if (id && id !== expected) {
|
|
27874
|
+
warnOnce(`AppFrame:${part}-id`, `[bestax-bulma] <AppFrame.${part}> has an id <AppFrame> can't read, ` +
|
|
27875
|
+
`so ${part === 'Main' ? 'the skip link' : "the burger's aria-controls"} ` +
|
|
27876
|
+
`misses it. Put AppFrame.${part} directly inside AppFrame.`);
|
|
27877
|
+
}
|
|
27878
|
+
}
|
|
27879
|
+
/**
|
|
27880
|
+
* The top bar of an AppFrame, which sticks while the page scrolls.
|
|
27881
|
+
*
|
|
27882
|
+
* @function
|
|
27883
|
+
* @param {AppFrameHeaderProps} props - Props for AppFrame.Header.
|
|
27884
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<header>`.
|
|
27885
|
+
* @returns {JSX.Element} The rendered header.
|
|
27886
|
+
*/
|
|
27887
|
+
const AppFrameHeader = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
27888
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
27889
|
+
const headerClass = usePrefixedClassNames('app-frame-header');
|
|
27890
|
+
return (jsxRuntime.jsx("header", { ref: ref, className: classNames(headerClass, bulmaHelperClasses, className), ...rest, children: children }));
|
|
27891
|
+
});
|
|
27892
|
+
AppFrameHeader.displayName = 'AppFrame.Header';
|
|
27893
|
+
/**
|
|
27894
|
+
* The navigation of an AppFrame: a column beside the page from the breakpoint up, and a drawer below it.
|
|
27895
|
+
*
|
|
27896
|
+
* @function
|
|
27897
|
+
* @param {AppFrameNavProps} props - Props for AppFrame.Nav.
|
|
27898
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<nav>`.
|
|
27899
|
+
* @returns {JSX.Element} The rendered nav, and the drawer's backdrop.
|
|
27900
|
+
*/
|
|
27901
|
+
const AppFrameNav = React.forwardRef(({ className, style, width, overlay = true, closeOnEscape = true, closeOnClickOutside = true, closeOnNavigate = true, onClick, onPointerDownCapture, children, ...props }, ref) => {
|
|
27902
|
+
const frame = React.useContext(FrameContext);
|
|
27903
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
27904
|
+
const navClass = usePrefixedClassNames('app-frame-nav');
|
|
27905
|
+
const closeClass = usePrefixedClassNames('app-frame-close');
|
|
27906
|
+
const backdropClass = usePrefixedClassNames('app-frame-backdrop');
|
|
27907
|
+
const activeClass = usePrefixedClassNames('is-active');
|
|
27908
|
+
const navRef = React.useRef(null);
|
|
27909
|
+
const backdropRef = React.useRef(null);
|
|
27910
|
+
const navCleanupRef = React.useRef(null);
|
|
27911
|
+
const navRefs = React.useCallback((node) => setRefs(node, navRef, ref, navCleanupRef), [ref]);
|
|
27912
|
+
const navId = rest.id || frame?.state.navId;
|
|
27913
|
+
if (frame)
|
|
27914
|
+
warnOnUnreadId('Nav', rest.id, frame.state.navId);
|
|
27915
|
+
// The drawer is open only below the breakpoint. Above it the nav is a
|
|
27916
|
+
// column, and none of what follows applies.
|
|
27917
|
+
const isDrawer = !!frame?.state.isDrawer;
|
|
27918
|
+
const open = isDrawer && !!frame?.state.navOpen;
|
|
27919
|
+
// The open drawer is Bulma's active state, on the nav as on a navbar's
|
|
27920
|
+
// menu, and on its backdrop.
|
|
27921
|
+
const shown = { [activeClass]: !!frame?.state.navOpen };
|
|
27922
|
+
const modal = open && overlay;
|
|
27923
|
+
const close = () => frame?.state.setNavOpen(false);
|
|
27924
|
+
// The rest of the frame goes inert: the header, the page, the aside and
|
|
27925
|
+
// the footer, but not the backdrop, which takes the presses outside the
|
|
27926
|
+
// drawer rather than let them through to what's under it. Not the whole
|
|
27927
|
+
// page, since a Modal or a toast renders beside the frame, often into an
|
|
27928
|
+
// element that's there before the drawer opens, and a Modal opened from
|
|
27929
|
+
// the drawer has to stay usable. First, so that as the drawer closes it
|
|
27930
|
+
// lets go before focus goes back to the burger: an inert element can't
|
|
27931
|
+
// take focus.
|
|
27932
|
+
useInertSiblings(navRef, modal, false, backdropRef);
|
|
27933
|
+
const isTopmost = useOverlayStack(open, overlay);
|
|
27934
|
+
useScrollLock(modal);
|
|
27935
|
+
// Tab stays inside while the drawer is the topmost modal overlay, as in
|
|
27936
|
+
// Sidebar, so a Modal opened from the drawer holds Tab until it closes.
|
|
27937
|
+
React.useEffect(() => {
|
|
27938
|
+
if (!modal)
|
|
27939
|
+
return undefined;
|
|
27940
|
+
return trapTab(() => navRef.current, window, () => isTopmost('modal'));
|
|
27941
|
+
}, [modal, isTopmost]);
|
|
27942
|
+
useOverlayEscape(open && closeOnEscape, isTopmost, close);
|
|
27943
|
+
// Without an overlay there's no backdrop: a press outside the drawer
|
|
27944
|
+
// closes it, as Popover's does, while it's the topmost overlay, so a
|
|
27945
|
+
// press in a Modal opened over it is the Modal's. A press on a control
|
|
27946
|
+
// that names the nav in its `aria-controls`, such as the burger, is left
|
|
27947
|
+
// to that control, which toggles it.
|
|
27948
|
+
const markInsidePress = useOutsidePress(navRef, open && !overlay && closeOnClickOutside, e => {
|
|
27949
|
+
const id = navRef.current?.id;
|
|
27950
|
+
const controlsNav = (node) => !!node
|
|
27951
|
+
.getAttribute?.('aria-controls')
|
|
27952
|
+
?.split(/\s+/)
|
|
27953
|
+
.includes(id);
|
|
27954
|
+
if (!isTopmost() || e.composedPath().some(controlsNav))
|
|
27955
|
+
return;
|
|
27956
|
+
close();
|
|
27957
|
+
});
|
|
27958
|
+
// Focus moves to the drawer's first tab stop, the close button, as it
|
|
27959
|
+
// opens, and back to whatever had it as it closes, unless it has left
|
|
27960
|
+
// the drawer. After a press outside, the press's own mousedown then takes
|
|
27961
|
+
// it wherever the press goes. When the window widened, the drawer is the
|
|
27962
|
+
// column again, and focus on its links stays; the close button hides
|
|
27963
|
+
// there, so focus on it, or dropped from it to <body>, goes back too.
|
|
27964
|
+
const isDrawerRef = useLatestRef(isDrawer);
|
|
27965
|
+
useOverlayFocus(navRef, open, {
|
|
27966
|
+
modal: overlay,
|
|
27967
|
+
restore: () => {
|
|
27968
|
+
if (isDrawerRef.current)
|
|
27969
|
+
return true;
|
|
27970
|
+
const nav = navRef.current;
|
|
27971
|
+
const active = getActiveElementInTree(nav);
|
|
27972
|
+
return (!nav.contains(active) || !!active?.classList.contains(closeClass));
|
|
27973
|
+
},
|
|
27974
|
+
});
|
|
27975
|
+
const handleClick = (e) => {
|
|
27976
|
+
onClick?.(e);
|
|
27977
|
+
if (!open ||
|
|
27978
|
+
!closeOnNavigate ||
|
|
27979
|
+
e.button !== 0 ||
|
|
27980
|
+
e.metaKey ||
|
|
27981
|
+
e.ctrlKey ||
|
|
27982
|
+
e.shiftKey ||
|
|
27983
|
+
e.altKey) {
|
|
27984
|
+
return;
|
|
27985
|
+
}
|
|
27986
|
+
// Not skipped for a click whose default was prevented: a router's
|
|
27987
|
+
// link prevents it to navigate without a page load. Only a link the nav
|
|
27988
|
+
// holds: one in a Modal portaled from it is the Modal's.
|
|
27989
|
+
const link = e.target.closest('a[href]');
|
|
27990
|
+
if (!link || !navRef.current?.contains(link))
|
|
27991
|
+
return;
|
|
27992
|
+
if (link.hasAttribute('download'))
|
|
27993
|
+
return;
|
|
27994
|
+
const target = link.getAttribute('target');
|
|
27995
|
+
if (target && target !== '_self')
|
|
27996
|
+
return;
|
|
27997
|
+
close();
|
|
27998
|
+
};
|
|
27999
|
+
const navStyle = width === undefined
|
|
28000
|
+
? style
|
|
28001
|
+
: {
|
|
28002
|
+
...style,
|
|
28003
|
+
'--bulma-app-frame-nav-width': cssLength(width),
|
|
28004
|
+
};
|
|
28005
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("nav", { ref: navRefs, className: classNames(navClass, shown, bulmaHelperClasses, className), style: navStyle, onClick: handleClick, onPointerDownCapture: e => {
|
|
28006
|
+
onPointerDownCapture?.(e);
|
|
28007
|
+
markInsidePress(e);
|
|
28008
|
+
}, ...rest, id: navId, "aria-label": rest['aria-label'] ||
|
|
28009
|
+
(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
|
|
28010
|
+
? () => {
|
|
28011
|
+
// Not under an overlay opened over the drawer.
|
|
28012
|
+
if (isTopmost())
|
|
28013
|
+
close();
|
|
28014
|
+
}
|
|
28015
|
+
: undefined }))] }));
|
|
28016
|
+
});
|
|
28017
|
+
AppFrameNav.displayName = 'AppFrame.Nav';
|
|
28018
|
+
/**
|
|
28019
|
+
* The page's main content in an AppFrame, which the skip link moves focus to.
|
|
28020
|
+
*
|
|
28021
|
+
* @function
|
|
28022
|
+
* @param {AppFrameMainProps} props - Props for AppFrame.Main.
|
|
28023
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<main>`.
|
|
28024
|
+
* @returns {JSX.Element} The rendered main.
|
|
28025
|
+
*/
|
|
28026
|
+
const AppFrameMain = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
28027
|
+
const frame = React.useContext(FrameContext);
|
|
28028
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
28029
|
+
const mainClass = usePrefixedClassNames('app-frame-main');
|
|
28030
|
+
if (frame)
|
|
28031
|
+
warnOnUnreadId('Main', rest.id, frame.mainId);
|
|
28032
|
+
return (jsxRuntime.jsx("main", { ref: ref, tabIndex: -1, className: classNames(mainClass, bulmaHelperClasses, className), ...rest, id: rest.id || frame?.mainId, children: children }));
|
|
28033
|
+
});
|
|
28034
|
+
AppFrameMain.displayName = 'AppFrame.Main';
|
|
28035
|
+
/**
|
|
28036
|
+
* Content beside the page in an AppFrame, which moves under it below the breakpoint.
|
|
28037
|
+
*
|
|
28038
|
+
* @function
|
|
28039
|
+
* @param {AppFrameAsideProps} props - Props for AppFrame.Aside.
|
|
28040
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<aside>`.
|
|
28041
|
+
* @returns {JSX.Element} The rendered aside.
|
|
28042
|
+
*/
|
|
28043
|
+
const AppFrameAside = React.forwardRef(({ className, style, width, children, ...props }, ref) => {
|
|
28044
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
28045
|
+
const asideClass = usePrefixedClassNames('app-frame-aside');
|
|
28046
|
+
const asideStyle = width === undefined
|
|
28047
|
+
? style
|
|
28048
|
+
: {
|
|
28049
|
+
...style,
|
|
28050
|
+
'--bulma-app-frame-aside-width': cssLength(width),
|
|
28051
|
+
};
|
|
28052
|
+
return (jsxRuntime.jsx("aside", { ref: ref, className: classNames(asideClass, bulmaHelperClasses, className), style: asideStyle, ...rest, children: children }));
|
|
28053
|
+
});
|
|
28054
|
+
AppFrameAside.displayName = 'AppFrame.Aside';
|
|
28055
|
+
/**
|
|
28056
|
+
* The bottom of an AppFrame, under the page.
|
|
28057
|
+
*
|
|
28058
|
+
* @function
|
|
28059
|
+
* @param {AppFrameFooterProps} props - Props for AppFrame.Footer.
|
|
28060
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<footer>`.
|
|
28061
|
+
* @returns {JSX.Element} The rendered footer.
|
|
28062
|
+
*/
|
|
28063
|
+
const AppFrameFooter = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
28064
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
28065
|
+
const footerClass = usePrefixedClassNames('app-frame-footer');
|
|
28066
|
+
return (jsxRuntime.jsx("footer", { ref: ref, className: classNames(footerClass, bulmaHelperClasses, className), ...rest, children: children }));
|
|
28067
|
+
});
|
|
28068
|
+
AppFrameFooter.displayName = 'AppFrame.Footer';
|
|
28069
|
+
/**
|
|
28070
|
+
* The button that opens the drawer, or collapses the column from the breakpoint up: Navbar.Burger, wired to the frame.
|
|
28071
|
+
*
|
|
28072
|
+
* @function
|
|
28073
|
+
* @param {AppFrameBurgerProps} props - Props for AppFrame.Burger.
|
|
28074
|
+
* @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
|
|
28075
|
+
* @returns {JSX.Element} The rendered burger.
|
|
28076
|
+
*/
|
|
28077
|
+
const AppFrameBurger = React.forwardRef(({ className, onClick, ...props }, ref) => {
|
|
28078
|
+
// Outside a frame it's a Navbar.Burger that controls nothing.
|
|
28079
|
+
const frame = React.useContext(FrameContext);
|
|
28080
|
+
const burgerClass = usePrefixedClassNames('app-frame-burger');
|
|
28081
|
+
const state = frame?.state;
|
|
28082
|
+
// Until the frame knows whether the nav is a drawer, the burger says
|
|
28083
|
+
// nothing it can't stand behind: no `aria-expanded`, and its bars. It can't
|
|
28084
|
+
// act before then anyway, since that's before hydration. Outside a frame
|
|
28085
|
+
// it's a plain Navbar.Burger.
|
|
28086
|
+
const known = !!frame?.modeKnown;
|
|
28087
|
+
const expanded = known && !!state && (state.isDrawer ? state.navOpen : !state.navCollapsed);
|
|
28088
|
+
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 => {
|
|
28089
|
+
onClick?.(e);
|
|
28090
|
+
if (e.defaultPrevented || !frame)
|
|
28091
|
+
return;
|
|
28092
|
+
// Safari, and Firefox on a Mac, don't focus a button that's pressed.
|
|
28093
|
+
// Focused, it's the element the drawer hands focus back to.
|
|
28094
|
+
e.currentTarget.focus();
|
|
28095
|
+
frame.state.toggleNav();
|
|
28096
|
+
} }));
|
|
28097
|
+
});
|
|
28098
|
+
AppFrameBurger.displayName = 'AppFrame.Burger';
|
|
28099
|
+
const isMain = (child) => React.isValidElement(child) && child.type === AppFrameMain;
|
|
28100
|
+
const isNav = (child) => React.isValidElement(child) && child.type === AppFrameNav;
|
|
28101
|
+
/**
|
|
28102
|
+
* 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.
|
|
28103
|
+
*
|
|
28104
|
+
* The header and the column stick while the document scrolls, and a skip
|
|
28105
|
+
* link comes first. The layout is CSS, so the server renders it as the
|
|
28106
|
+
* browser does. It holds two states, each yours to control: whether the
|
|
28107
|
+
* drawer is open, and whether the column is collapsed. `AppFrame.Burger`
|
|
28108
|
+
* and `useAppFrame()` toggle whichever applies at the current width.
|
|
28109
|
+
*
|
|
28110
|
+
* @function
|
|
28111
|
+
* @param {AppFrameProps} props - Props for the AppFrame component.
|
|
28112
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the root `<div>`.
|
|
28113
|
+
* @returns {JSX.Element} The rendered frame.
|
|
28114
|
+
*
|
|
28115
|
+
* @example
|
|
28116
|
+
* <AppFrame>
|
|
28117
|
+
* <AppFrame.Header>
|
|
28118
|
+
* <Navbar aria-label="Site">
|
|
28119
|
+
* <Navbar.Brand>
|
|
28120
|
+
* <Navbar.Item href="/">Acme</Navbar.Item>
|
|
28121
|
+
* <AppFrame.Burger />
|
|
28122
|
+
* </Navbar.Brand>
|
|
28123
|
+
* </Navbar>
|
|
28124
|
+
* </AppFrame.Header>
|
|
28125
|
+
* <AppFrame.Nav>
|
|
28126
|
+
* <Menu>…</Menu>
|
|
28127
|
+
* </AppFrame.Nav>
|
|
28128
|
+
* <AppFrame.Main>…</AppFrame.Main>
|
|
28129
|
+
* </AppFrame>
|
|
28130
|
+
*/
|
|
28131
|
+
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) => {
|
|
28132
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
28133
|
+
color: textColor,
|
|
28134
|
+
backgroundColor: bgColor,
|
|
28135
|
+
...props,
|
|
28136
|
+
});
|
|
28137
|
+
const rootRef = React.useRef(null);
|
|
28138
|
+
const rootCleanupRef = React.useRef(null);
|
|
28139
|
+
const rootRefs = React.useCallback((node) => setRefs(node, rootRef, ref, rootCleanupRef), [ref]);
|
|
28140
|
+
const [openState, setOpenState] = React.useState(defaultNavOpen);
|
|
28141
|
+
const [collapsedState, setCollapsedState] = React.useState(defaultNavCollapsed);
|
|
28142
|
+
const navOpen = navOpenProp ?? openState;
|
|
28143
|
+
const navCollapsed = navCollapsedProp ?? collapsedState;
|
|
28144
|
+
const openControlled = navOpenProp !== undefined;
|
|
28145
|
+
const collapsedControlled = navCollapsedProp !== undefined;
|
|
28146
|
+
const onNavOpenChangeRef = useLatestRef(onNavOpenChange);
|
|
28147
|
+
const onNavCollapsedChangeRef = useLatestRef(onNavCollapsedChange);
|
|
28148
|
+
const setNavOpen = React.useCallback((open) => {
|
|
28149
|
+
onNavOpenChangeRef.current?.(open);
|
|
28150
|
+
if (!openControlled)
|
|
28151
|
+
setOpenState(open);
|
|
28152
|
+
}, [openControlled, onNavOpenChangeRef]);
|
|
28153
|
+
const setNavCollapsed = React.useCallback((collapsed) => {
|
|
28154
|
+
onNavCollapsedChangeRef.current?.(collapsed);
|
|
28155
|
+
if (!collapsedControlled)
|
|
28156
|
+
setCollapsedState(collapsed);
|
|
28157
|
+
}, [collapsedControlled, onNavCollapsedChangeRef]);
|
|
28158
|
+
// The layout switches in the stylesheet. The script needs the width
|
|
28159
|
+
// only to know which state the burger toggles, and to close the drawer
|
|
28160
|
+
// once the window is at or past the breakpoint.
|
|
28161
|
+
const wide = useBulmaBreakpoint(breakpoint);
|
|
28162
|
+
const isDrawer = wide === false;
|
|
28163
|
+
const isWide = wide === true;
|
|
28164
|
+
React.useEffect(() => {
|
|
28165
|
+
// 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
|
|
28166
|
+
if (isWide && navOpen)
|
|
28167
|
+
setNavOpen(false);
|
|
28168
|
+
}, [isWide, navOpen, setNavOpen]);
|
|
28169
|
+
const toggleNav = React.useCallback(() => {
|
|
28170
|
+
if (isDrawer)
|
|
28171
|
+
setNavOpen(!navOpen);
|
|
28172
|
+
else
|
|
28173
|
+
setNavCollapsed(!navCollapsed);
|
|
28174
|
+
}, [isDrawer, navOpen, navCollapsed, setNavOpen, setNavCollapsed]);
|
|
28175
|
+
// The ids of the parts the skip link and the burger point at, read off
|
|
28176
|
+
// the children so the server renders them too.
|
|
28177
|
+
const baseId = React.useId();
|
|
28178
|
+
const parts = flattenChildren(children);
|
|
28179
|
+
const mainId = parts.find(isMain)?.props.id || `${baseId}-main`;
|
|
28180
|
+
const navId = parts.find(isNav)?.props.id || `${baseId}-nav`;
|
|
28181
|
+
const { skipLink: skipText, nav, closeNav, burger } = labels ?? {};
|
|
28182
|
+
const value = React.useMemo(() => ({
|
|
28183
|
+
state: {
|
|
28184
|
+
navOpen,
|
|
28185
|
+
setNavOpen,
|
|
28186
|
+
navCollapsed,
|
|
28187
|
+
setNavCollapsed,
|
|
28188
|
+
isDrawer,
|
|
28189
|
+
toggleNav,
|
|
28190
|
+
navId,
|
|
28191
|
+
},
|
|
28192
|
+
labels: {
|
|
28193
|
+
skipLink: skipText || 'Skip to main content',
|
|
28194
|
+
nav: nav || 'Sections',
|
|
28195
|
+
closeNav: closeNav || 'Close navigation',
|
|
28196
|
+
burger: burger || undefined,
|
|
28197
|
+
},
|
|
28198
|
+
mainId,
|
|
28199
|
+
modeKnown: wide !== null,
|
|
28200
|
+
}), [
|
|
28201
|
+
navOpen,
|
|
28202
|
+
setNavOpen,
|
|
28203
|
+
navCollapsed,
|
|
28204
|
+
setNavCollapsed,
|
|
28205
|
+
isDrawer,
|
|
28206
|
+
toggleNav,
|
|
28207
|
+
navId,
|
|
28208
|
+
skipText,
|
|
28209
|
+
nav,
|
|
28210
|
+
closeNav,
|
|
28211
|
+
burger,
|
|
28212
|
+
mainId,
|
|
28213
|
+
wide,
|
|
28214
|
+
]);
|
|
28215
|
+
// Its header and footer are the page's banner and contentinfo only
|
|
28216
|
+
// outside `main` and sectioning content, as HTML-AAM maps them.
|
|
28217
|
+
React.useEffect(() => {
|
|
28218
|
+
const host = rootRef.current?.parentElement?.closest('main, article, aside, nav, section');
|
|
28219
|
+
if (host) {
|
|
28220
|
+
warnOnce('AppFrame:nested', `[bestax-bulma] <AppFrame> sits inside a <${host.localName}>, where ` +
|
|
28221
|
+
"its header and footer are no longer the page's banner and " +
|
|
28222
|
+
'contentinfo landmarks. Put AppFrame at the top of the page.');
|
|
28223
|
+
}
|
|
28224
|
+
}, []);
|
|
28225
|
+
const rootClass = usePrefixedClassNames('app-frame', {
|
|
28226
|
+
[`is-${breakpoint}`]: true,
|
|
28227
|
+
'is-nav-collapsed': navCollapsed,
|
|
28228
|
+
});
|
|
28229
|
+
const skipClass = usePrefixedClassNames('app-frame-skip-link');
|
|
28230
|
+
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 })] }));
|
|
28231
|
+
});
|
|
28232
|
+
/**
|
|
28233
|
+
* The frame's state, for a control of your own inside it: whether the
|
|
28234
|
+
* drawer is open and the column collapsed, setters that go through the
|
|
28235
|
+
* frame's callbacks, and `toggleNav()`, which does what the burger does.
|
|
28236
|
+
* Outside an AppFrame it's `null`, as the library's other context hooks
|
|
28237
|
+
* are, so a control that can sit outside one renders a fallback or nothing.
|
|
28238
|
+
*
|
|
28239
|
+
* @function useAppFrame
|
|
28240
|
+
* @returns The frame's state, or `null` outside an AppFrame.
|
|
28241
|
+
*/
|
|
28242
|
+
function useAppFrame() {
|
|
28243
|
+
return React.useContext(FrameContext)?.state ?? null;
|
|
28244
|
+
}
|
|
28245
|
+
const AppFrame = withSubComponents(AppFrameRoot, {
|
|
28246
|
+
Header: AppFrameHeader,
|
|
28247
|
+
Nav: AppFrameNav,
|
|
28248
|
+
Main: AppFrameMain,
|
|
28249
|
+
Aside: AppFrameAside,
|
|
28250
|
+
Footer: AppFrameFooter,
|
|
28251
|
+
Burger: AppFrameBurger,
|
|
28252
|
+
}, 'AppFrame');
|
|
28253
|
+
|
|
27014
28254
|
/**
|
|
27015
28255
|
* The `.stack` element behind Stack, Group, Center and Flex.
|
|
27016
28256
|
*
|
|
@@ -28020,6 +29260,13 @@ const Stack = React.forwardRef(function Stack(props, ref) {
|
|
|
28020
29260
|
Stack.displayName = 'Stack';
|
|
28021
29261
|
|
|
28022
29262
|
exports.Affix = Affix;
|
|
29263
|
+
exports.AppFrame = AppFrame;
|
|
29264
|
+
exports.AppFrameAside = AppFrameAside;
|
|
29265
|
+
exports.AppFrameBurger = AppFrameBurger;
|
|
29266
|
+
exports.AppFrameFooter = AppFrameFooter;
|
|
29267
|
+
exports.AppFrameHeader = AppFrameHeader;
|
|
29268
|
+
exports.AppFrameMain = AppFrameMain;
|
|
29269
|
+
exports.AppFrameNav = AppFrameNav;
|
|
28023
29270
|
exports.Autocomplete = Autocomplete;
|
|
28024
29271
|
exports.Avatar = Avatar;
|
|
28025
29272
|
exports.Avatars = Avatars;
|
|
@@ -28233,6 +29480,7 @@ exports.ToastContainer = ToastContainer;
|
|
|
28233
29480
|
exports.ToggleButton = ToggleButton;
|
|
28234
29481
|
exports.Tooltip = Tooltip;
|
|
28235
29482
|
exports.Tr = Tr;
|
|
29483
|
+
exports.TreeSelect = TreeSelect;
|
|
28236
29484
|
exports.TreeView = TreeView;
|
|
28237
29485
|
exports.TreeViewItem = TreeViewItem;
|
|
28238
29486
|
exports.UnorderedList = UnorderedList;
|
|
@@ -28250,6 +29498,7 @@ exports.radioSizes = radioSizes;
|
|
|
28250
29498
|
exports.switchColors = switchColors;
|
|
28251
29499
|
exports.switchSizes = switchSizes;
|
|
28252
29500
|
exports.toast = toast;
|
|
29501
|
+
exports.useAppFrame = useAppFrame;
|
|
28253
29502
|
exports.useBulmaClasses = useBulmaClasses;
|
|
28254
29503
|
exports.useClassPrefix = useClassPrefix;
|
|
28255
29504
|
exports.useColorClasses = useColorClasses;
|