@allxsmith/bestax-bulma 5.63.0 → 5.65.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 +12 -66
- package/dist/esm/components/Carousel.js.map +1 -1
- package/dist/esm/components/Lightbox.js +756 -0
- package/dist/esm/components/Lightbox.js.map +1 -0
- package/dist/esm/components/Reveal.js +1 -24
- package/dist/esm/components/Reveal.js.map +1 -1
- package/dist/esm/components/ScrollArea.js +7 -7
- package/dist/esm/components/ScrollArea.js.map +1 -1
- package/dist/esm/components/TableOfContents.js +85 -0
- package/dist/esm/components/TableOfContents.js.map +1 -0
- package/dist/esm/form/PasswordInput.js +105 -0
- package/dist/esm/form/PasswordInput.js.map +1 -0
- package/dist/esm/form/PasswordInputBase.js +177 -0
- package/dist/esm/form/PasswordInputBase.js.map +1 -0
- package/dist/esm/helpers/chevronIcons.js +24 -0
- package/dist/esm/helpers/chevronIcons.js.map +1 -0
- package/dist/esm/helpers/usePrefersReducedMotion.js +34 -0
- package/dist/esm/helpers/usePrefersReducedMotion.js.map +1 -0
- package/dist/esm/helpers/useScrollSpy.js +286 -0
- package/dist/esm/helpers/useScrollSpy.js.map +1 -0
- package/dist/esm/helpers/useSwipe.js +82 -0
- package/dist/esm/helpers/useSwipe.js.map +1 -0
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +1582 -198
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +5 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/Lightbox.d.ts +244 -0
- package/dist/types/components/TableOfContents.d.ts +87 -0
- package/dist/types/form/PasswordInput.d.ts +98 -0
- package/dist/types/form/PasswordInputBase.d.ts +91 -0
- package/dist/types/helpers/chevronIcons.d.ts +14 -0
- package/dist/types/helpers/usePrefersReducedMotion.d.ts +10 -0
- package/dist/types/helpers/useScrollSpy.d.ts +61 -0
- package/dist/types/helpers/useSwipe.d.ts +66 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types-cjs/components/Lightbox.d.ts +244 -0
- package/dist/types-cjs/components/TableOfContents.d.ts +87 -0
- package/dist/types-cjs/form/PasswordInput.d.ts +98 -0
- package/dist/types-cjs/form/PasswordInputBase.d.ts +91 -0
- package/dist/types-cjs/helpers/chevronIcons.d.ts +14 -0
- package/dist/types-cjs/helpers/usePrefersReducedMotion.d.ts +10 -0
- package/dist/types-cjs/helpers/useScrollSpy.d.ts +61 -0
- package/dist/types-cjs/helpers/useSwipe.d.ts +66 -0
- package/dist/types-cjs/index.d.ts +6 -0
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/CLAUDE.md +3 -0
- package/src/scss/_input-trigger.scss +76 -0
- package/src/scss/components/_index.scss +2 -0
- package/src/scss/components/_lightbox.scss +392 -0
- package/src/scss/components/_tableofcontents.scss +42 -0
- package/src/scss/form/_index.scss +1 -0
- package/src/scss/form/_passwordinput.scss +112 -0
- package/src/scss/form/_picker-popover.scss +24 -63
package/dist/index.cjs
CHANGED
|
@@ -10608,33 +10608,113 @@ function useBulmaBreakpoint(breakpoint) {
|
|
|
10608
10608
|
return atOrPast;
|
|
10609
10609
|
}
|
|
10610
10610
|
|
|
10611
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
10612
|
+
/** How far, in CSS pixels, a drag has to travel along an axis to count. */
|
|
10613
|
+
const SWIPE_THRESHOLD = 50;
|
|
10614
|
+
const AT_REST = { x: 0, y: 0 };
|
|
10611
10615
|
/**
|
|
10612
|
-
*
|
|
10616
|
+
* Follows a mouse or touch drag on a surface and reports a swipe when it
|
|
10617
|
+
* ends far enough from where it started. Carousel's drag, shared: it hands
|
|
10618
|
+
* back the handlers to put on the surface and the live offset to draw the
|
|
10619
|
+
* drag with, and leaves what a swipe does to the component.
|
|
10613
10620
|
*
|
|
10614
10621
|
* @function
|
|
10615
|
-
* @param {
|
|
10616
|
-
* @
|
|
10617
|
-
* @returns {JSX.Element} The rendered carousel item.
|
|
10622
|
+
* @param {UseSwipeOptions} options - Whether it's on, the axes, and the callback.
|
|
10623
|
+
* @returns {UseSwipeResult} The drag's state, its handlers, and a way to drop it.
|
|
10618
10624
|
*/
|
|
10619
|
-
|
|
10620
|
-
const
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10625
|
+
function useSwipe({ enabled = true, vertical = false, onSwipe, }) {
|
|
10626
|
+
const [isSwiping, setIsSwiping] = React.useState(false);
|
|
10627
|
+
const [start, setStart] = React.useState(AT_REST);
|
|
10628
|
+
const [offset, setOffset] = React.useState(AT_REST);
|
|
10629
|
+
const begin = React.useCallback((x, y) => {
|
|
10630
|
+
if (!enabled)
|
|
10631
|
+
return;
|
|
10632
|
+
setIsSwiping(true);
|
|
10633
|
+
setStart({ x, y: vertical ? y : 0 });
|
|
10634
|
+
setOffset(AT_REST);
|
|
10635
|
+
}, [enabled, vertical]);
|
|
10636
|
+
const follow = React.useCallback((x, y) => {
|
|
10637
|
+
if (!isSwiping)
|
|
10638
|
+
return;
|
|
10639
|
+
setOffset({ x: x - start.x, y: vertical ? y - start.y : 0 });
|
|
10640
|
+
}, [isSwiping, start, vertical]);
|
|
10641
|
+
const end = React.useCallback(() => {
|
|
10642
|
+
if (!isSwiping)
|
|
10643
|
+
return;
|
|
10644
|
+
setIsSwiping(false);
|
|
10645
|
+
if (vertical && Math.abs(offset.y) > Math.abs(offset.x)) {
|
|
10646
|
+
if (offset.y > SWIPE_THRESHOLD)
|
|
10647
|
+
onSwipe('down');
|
|
10648
|
+
else if (offset.y < -SWIPE_THRESHOLD)
|
|
10649
|
+
onSwipe('up');
|
|
10650
|
+
}
|
|
10651
|
+
else if (offset.x < -SWIPE_THRESHOLD) {
|
|
10652
|
+
onSwipe('left');
|
|
10653
|
+
}
|
|
10654
|
+
else if (offset.x > SWIPE_THRESHOLD) {
|
|
10655
|
+
onSwipe('right');
|
|
10656
|
+
}
|
|
10657
|
+
setOffset(AT_REST);
|
|
10658
|
+
}, [isSwiping, vertical, offset, onSwipe]);
|
|
10659
|
+
const cancel = React.useCallback(() => {
|
|
10660
|
+
setIsSwiping(false);
|
|
10661
|
+
setOffset(AT_REST);
|
|
10662
|
+
}, []);
|
|
10663
|
+
return {
|
|
10664
|
+
isSwiping,
|
|
10665
|
+
offsetX: offset.x,
|
|
10666
|
+
offsetY: offset.y,
|
|
10667
|
+
handlers: {
|
|
10668
|
+
onMouseDown: event => {
|
|
10669
|
+
// Disabled, the press is left as it is: a click still focuses and
|
|
10670
|
+
// text can still be selected.
|
|
10671
|
+
if (!enabled)
|
|
10672
|
+
return;
|
|
10673
|
+
event.preventDefault();
|
|
10674
|
+
begin(event.clientX, event.clientY);
|
|
10675
|
+
},
|
|
10676
|
+
onMouseMove: event => follow(event.clientX, event.clientY),
|
|
10677
|
+
onMouseUp: end,
|
|
10678
|
+
onMouseLeave: end,
|
|
10679
|
+
onTouchStart: event => begin(event.touches[0].clientX, event.touches[0].clientY),
|
|
10680
|
+
onTouchMove: event => follow(event.touches[0].clientX, event.touches[0].clientY),
|
|
10681
|
+
onTouchEnd: end,
|
|
10682
|
+
},
|
|
10683
|
+
cancel,
|
|
10684
|
+
};
|
|
10685
|
+
}
|
|
10686
|
+
|
|
10687
|
+
// INTERNAL: deliberately not exported from src/index.ts. The chevrons the
|
|
10688
|
+
// Carousel's and the Lightbox's previous and next buttons draw, in one place
|
|
10689
|
+
// so the two look the same.
|
|
10624
10690
|
/**
|
|
10625
|
-
*
|
|
10691
|
+
* Chevron pointing left, for a previous button.
|
|
10626
10692
|
*
|
|
10627
10693
|
* @function
|
|
10628
10694
|
* @returns {JSX.Element} The rendered SVG chevron-left icon.
|
|
10629
10695
|
*/
|
|
10630
|
-
const
|
|
10696
|
+
const ChevronPrevIcon = () => (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: "15 18 9 12 15 6" }) }));
|
|
10631
10697
|
/**
|
|
10632
|
-
*
|
|
10698
|
+
* Chevron pointing right, for a next button.
|
|
10633
10699
|
*
|
|
10634
10700
|
* @function
|
|
10635
10701
|
* @returns {JSX.Element} The rendered SVG chevron-right icon.
|
|
10636
10702
|
*/
|
|
10637
|
-
const
|
|
10703
|
+
const ChevronNextIcon = () => (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" }) }));
|
|
10704
|
+
|
|
10705
|
+
/**
|
|
10706
|
+
* Individual carousel item/slide.
|
|
10707
|
+
*
|
|
10708
|
+
* @function
|
|
10709
|
+
* @param {CarouselItemProps} props - Props for the CarouselItem component.
|
|
10710
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the carousel item element.
|
|
10711
|
+
* @returns {JSX.Element} The rendered carousel item.
|
|
10712
|
+
*/
|
|
10713
|
+
const CarouselItem = React.forwardRef(({ active, className, children, ...props }, ref) => {
|
|
10714
|
+
const itemClasses = classNames(usePrefixedClassNames('carousel-item', { 'is-active': active }), className);
|
|
10715
|
+
return (jsxRuntime.jsx("div", { ref: ref, className: itemClasses, ...props, children: children }));
|
|
10716
|
+
});
|
|
10717
|
+
CarouselItem.displayName = 'CarouselItem';
|
|
10638
10718
|
/**
|
|
10639
10719
|
* The `--bulma-carousel-items-to-show*` variable each count prop writes, in
|
|
10640
10720
|
* the order the component lists them: `useBulmaViewport`'s viewports, widest
|
|
@@ -10705,9 +10785,6 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
10705
10785
|
const [internalValue, setInternalValue] = React.useState(0);
|
|
10706
10786
|
const [isPaused, setIsPaused] = React.useState(false);
|
|
10707
10787
|
const [isFocusInside, setIsFocusInside] = React.useState(false);
|
|
10708
|
-
const [isDragging, setIsDragging] = React.useState(false);
|
|
10709
|
-
const [dragStart, setDragStart] = React.useState(0);
|
|
10710
|
-
const [dragOffset, setDragOffset] = React.useState(0);
|
|
10711
10788
|
const [isWrapping, setIsWrapping] = React.useState(false);
|
|
10712
10789
|
const [displayIndex, setDisplayIndex] = React.useState(0);
|
|
10713
10790
|
const [skipTransition, setSkipTransition] = React.useState(false);
|
|
@@ -10853,53 +10930,14 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
10853
10930
|
setIsPaused(false);
|
|
10854
10931
|
}
|
|
10855
10932
|
}, [pauseOnHover]);
|
|
10856
|
-
// Drag
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10864
|
-
const handleDragMove = React.useCallback((clientX) => {
|
|
10865
|
-
if (!isDragging)
|
|
10866
|
-
return;
|
|
10867
|
-
setDragOffset(clientX - dragStart);
|
|
10868
|
-
}, [isDragging, dragStart]);
|
|
10869
|
-
const handleDragEnd = React.useCallback(() => {
|
|
10870
|
-
if (!isDragging)
|
|
10871
|
-
return;
|
|
10872
|
-
setIsDragging(false);
|
|
10873
|
-
const threshold = 50;
|
|
10874
|
-
if (dragOffset < -threshold) {
|
|
10875
|
-
goToNext();
|
|
10876
|
-
}
|
|
10877
|
-
else if (dragOffset > threshold) {
|
|
10878
|
-
goToPrev();
|
|
10879
|
-
}
|
|
10880
|
-
setDragOffset(0);
|
|
10881
|
-
}, [isDragging, dragOffset, goToNext, goToPrev]);
|
|
10882
|
-
// Mouse events
|
|
10883
|
-
const handleMouseDown = React.useCallback((e) => {
|
|
10884
|
-
e.preventDefault();
|
|
10885
|
-
handleDragStart(e.clientX);
|
|
10886
|
-
}, [handleDragStart]);
|
|
10887
|
-
const handleMouseMove = React.useCallback((e) => {
|
|
10888
|
-
handleDragMove(e.clientX);
|
|
10889
|
-
}, [handleDragMove]);
|
|
10890
|
-
const handleMouseUp = React.useCallback(() => {
|
|
10891
|
-
handleDragEnd();
|
|
10892
|
-
}, [handleDragEnd]);
|
|
10893
|
-
// Touch events
|
|
10894
|
-
const handleTouchStart = React.useCallback((e) => {
|
|
10895
|
-
handleDragStart(e.touches[0].clientX);
|
|
10896
|
-
}, [handleDragStart]);
|
|
10897
|
-
const handleTouchMove = React.useCallback((e) => {
|
|
10898
|
-
handleDragMove(e.touches[0].clientX);
|
|
10899
|
-
}, [handleDragMove]);
|
|
10900
|
-
const handleTouchEnd = React.useCallback(() => {
|
|
10901
|
-
handleDragEnd();
|
|
10902
|
-
}, [handleDragEnd]);
|
|
10933
|
+
// Drag and swipe, through the shared hook: a drag left goes on and a
|
|
10934
|
+
// drag right goes back, past its 50px threshold.
|
|
10935
|
+
const swipe = useSwipe({
|
|
10936
|
+
enabled: hasDrag,
|
|
10937
|
+
onSwipe: direction => (direction === 'left' ? goToNext() : goToPrev()),
|
|
10938
|
+
});
|
|
10939
|
+
const isDragging = swipe.isSwiping;
|
|
10940
|
+
const dragOffset = swipe.offsetX;
|
|
10903
10941
|
// Keyboard navigation, from the carousel's own controls. Inside an item
|
|
10904
10942
|
// the keys are its content's (a field's caret, a slider), and a step
|
|
10905
10943
|
// from there could take the focused item out of view.
|
|
@@ -11065,7 +11103,7 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
11065
11103
|
first.tabIndex = -1;
|
|
11066
11104
|
first.focus({ preventScroll: true });
|
|
11067
11105
|
}, [shownIndex, cloneCount, inView, itemCount]);
|
|
11068
|
-
return (jsxRuntime.jsxs("div", { ref: combinedRef, className: combinedClasses, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, role: "region", "aria-roledescription": "carousel", "aria-label": ariaLabel, style: { ...countStyle, ...style }, ...rest, children: [jsxRuntime.jsxs("div", { ref: containerRef, className: carouselContainerClass, onMouseDown: hasDrag ?
|
|
11106
|
+
return (jsxRuntime.jsxs("div", { ref: combinedRef, className: combinedClasses, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, role: "region", "aria-roledescription": "carousel", "aria-label": ariaLabel, style: { ...countStyle, ...style }, ...rest, children: [jsxRuntime.jsxs("div", { ref: containerRef, className: carouselContainerClass, onMouseDown: hasDrag ? swipe.handlers.onMouseDown : undefined, onMouseMove: hasDrag && isDragging ? swipe.handlers.onMouseMove : undefined, onMouseUp: hasDrag ? swipe.handlers.onMouseUp : undefined, onMouseLeave: hasDrag ? swipe.handlers.onMouseLeave : undefined, onTouchStart: hasDrag ? swipe.handlers.onTouchStart : undefined, onTouchMove: hasDrag ? swipe.handlers.onTouchMove : undefined, onTouchEnd: hasDrag ? swipe.handlers.onTouchEnd : undefined, style: {
|
|
11069
11107
|
transform: isDragging ? `translateX(${dragOffset}px)` : undefined,
|
|
11070
11108
|
cursor: hasDrag ? (isDragging ? 'grabbing' : 'grab') : undefined,
|
|
11071
11109
|
}, children: [jsxRuntime.jsx("div", { ref: slidesRef, className: carouselSlidesClass, style: {
|
|
@@ -11084,24 +11122,27 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
11084
11122
|
setIsFocusInside(inside);
|
|
11085
11123
|
if (!inside)
|
|
11086
11124
|
focusedItemRef.current = undefined;
|
|
11087
|
-
}, onTransitionEnd: handleTransitionEnd, children: renderSlides() }), arrow && canMove && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Button, { type: "button", className: carouselArrowPrevClass, onClick: goToPrev, isDisabled: !canGoPrev, "aria-label": "Previous slide", children: iconPrev ? (jsxRuntime.jsx(Icon, { name: iconPrev, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })) : (jsxRuntime.jsx(
|
|
11125
|
+
}, onTransitionEnd: handleTransitionEnd, children: renderSlides() }), arrow && canMove && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Button, { type: "button", className: carouselArrowPrevClass, onClick: goToPrev, isDisabled: !canGoPrev, "aria-label": "Previous slide", children: iconPrev ? (jsxRuntime.jsx(Icon, { name: iconPrev, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })) : (jsxRuntime.jsx(ChevronPrevIcon, {})) }), jsxRuntime.jsx(Button, { type: "button", className: carouselArrowNextClass, onClick: goToNext, isDisabled: !canGoNext, "aria-label": "Next slide", children: iconNext ? (jsxRuntime.jsx(Icon, { name: iconNext, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })) : (jsxRuntime.jsx(ChevronNextIcon, {})) })] }))] }), indicator && canMove && (jsxRuntime.jsx("div", { className: carouselIndicatorClass, role: "tablist", children: positions.map((position, index) => (jsxRuntime.jsx(Button, { type: "button", className: prefixedClassNames(classPrefix, 'indicator-item', {
|
|
11088
11126
|
'is-active': index === activePosition,
|
|
11089
11127
|
}), onClick: () => setActiveIndex(position), role: "tab", "aria-selected": index === activePosition, "aria-label": `Go to slide ${position + 1}` }, position))) }))] }));
|
|
11090
11128
|
});
|
|
11091
11129
|
Carousel.displayName = 'Carousel';
|
|
11092
11130
|
|
|
11131
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
11093
11132
|
/**
|
|
11094
|
-
* Detects the user's `prefers-reduced-motion` preference
|
|
11095
|
-
*
|
|
11096
|
-
*
|
|
11133
|
+
* Detects the user's `prefers-reduced-motion` preference, for motion that
|
|
11134
|
+
* runs in script, where a stylesheet's media query can't reach it. Always
|
|
11135
|
+
* `false` on the server and on the very first client render, so SSR output
|
|
11136
|
+
* and the initial hydration pass always agree.
|
|
11097
11137
|
*
|
|
11098
11138
|
* @function
|
|
11099
11139
|
* @returns {boolean} Whether the user has requested reduced motion.
|
|
11100
11140
|
*/
|
|
11101
11141
|
function usePrefersReducedMotion() {
|
|
11102
11142
|
const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(false);
|
|
11143
|
+
// An effect, so it only ever runs in the browser.
|
|
11103
11144
|
React.useEffect(() => {
|
|
11104
|
-
if (
|
|
11145
|
+
if (!window.matchMedia)
|
|
11105
11146
|
return undefined;
|
|
11106
11147
|
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
11107
11148
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- reads the live media query on mount
|
|
@@ -11114,6 +11155,818 @@ function usePrefersReducedMotion() {
|
|
|
11114
11155
|
}, []);
|
|
11115
11156
|
return prefersReducedMotion;
|
|
11116
11157
|
}
|
|
11158
|
+
|
|
11159
|
+
/**
|
|
11160
|
+
* FigureCaption component for rendering a styled figcaption element.
|
|
11161
|
+
*
|
|
11162
|
+
* A FigureCaption wraps the HTML `<figcaption>` element with Bulma helper class integration.
|
|
11163
|
+
* Use it inside Figure components to provide captions for images or other media.
|
|
11164
|
+
*
|
|
11165
|
+
* @function
|
|
11166
|
+
* @param {FigureCaptionProps} props - Props for the FigureCaption component.
|
|
11167
|
+
* @returns {JSX.Element} The rendered figcaption element.
|
|
11168
|
+
*/
|
|
11169
|
+
const FigureCaption = ({ className, textColor, bgColor, children, ...props }) => {
|
|
11170
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
11171
|
+
color: textColor,
|
|
11172
|
+
backgroundColor: bgColor,
|
|
11173
|
+
...props,
|
|
11174
|
+
});
|
|
11175
|
+
const bulmaClasses = usePrefixedClassNames();
|
|
11176
|
+
const captionClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
11177
|
+
return (jsxRuntime.jsx("figcaption", { className: captionClasses || undefined, ...rest, children: children }));
|
|
11178
|
+
};
|
|
11179
|
+
/**
|
|
11180
|
+
* The `Figure` component renders a styled `<figure>` element with Bulma helper class integration.
|
|
11181
|
+
*
|
|
11182
|
+
* @function
|
|
11183
|
+
* @param {FigureProps} props - Props for the Figure component.
|
|
11184
|
+
* @returns {JSX.Element} The rendered figure element.
|
|
11185
|
+
*/
|
|
11186
|
+
const FigureComponent = ({ className, textColor, bgColor, children, ...props }) => {
|
|
11187
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
11188
|
+
color: textColor,
|
|
11189
|
+
backgroundColor: bgColor,
|
|
11190
|
+
...props,
|
|
11191
|
+
});
|
|
11192
|
+
const bulmaClasses = usePrefixedClassNames();
|
|
11193
|
+
const figureClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
11194
|
+
return (jsxRuntime.jsx("figure", { className: figureClasses || undefined, ...rest, children: children }));
|
|
11195
|
+
};
|
|
11196
|
+
/**
|
|
11197
|
+
* Figure component with Caption subcomponent.
|
|
11198
|
+
*
|
|
11199
|
+
* @example
|
|
11200
|
+
* <Figure>
|
|
11201
|
+
* <img src="image.jpg" alt="Description" />
|
|
11202
|
+
* <Figure.Caption>Image caption text</Figure.Caption>
|
|
11203
|
+
* </Figure>
|
|
11204
|
+
*/
|
|
11205
|
+
const Figure = withSubComponents(FigureComponent, {
|
|
11206
|
+
Caption: FigureCaption,
|
|
11207
|
+
}, 'Figure');
|
|
11208
|
+
|
|
11209
|
+
/**
|
|
11210
|
+
* The `Loader` component renders Bulma's `.loader`, a small spinning ring for inline loading states.
|
|
11211
|
+
*
|
|
11212
|
+
* The ring is `1em` square, so `textSize` scales it. It is exposed as an
|
|
11213
|
+
* indeterminate `progressbar` named by `ariaLabel`, and it stops spinning
|
|
11214
|
+
* under `prefers-reduced-motion: reduce` while staying drawn. In forced
|
|
11215
|
+
* colors its two open sides take the page's color, so it stays open.
|
|
11216
|
+
*
|
|
11217
|
+
* @function
|
|
11218
|
+
* @param {LoaderProps} props - Props for the Loader component.
|
|
11219
|
+
* @param {React.Ref<HTMLSpanElement>} ref - Forwarded ref to the loader element.
|
|
11220
|
+
* @returns {JSX.Element} The rendered loader element.
|
|
11221
|
+
*
|
|
11222
|
+
* @example
|
|
11223
|
+
* <Loader />
|
|
11224
|
+
*
|
|
11225
|
+
* @example
|
|
11226
|
+
* <Loader textSize="3" ariaLabel="Saving" />
|
|
11227
|
+
*/
|
|
11228
|
+
const Loader = React.forwardRef(({ className, ariaLabel = 'Loading', ...props }, ref) => {
|
|
11229
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
11230
|
+
const loaderClass = usePrefixedClassNames('loader');
|
|
11231
|
+
// role="progressbar", not role="status": a status region mounted together
|
|
11232
|
+
// with its text is not reliably announced, so the loader claims only what
|
|
11233
|
+
// it can deliver, a named indicator found by reading the page. No
|
|
11234
|
+
// aria-valuenow, because the progress is indeterminate.
|
|
11235
|
+
return (jsxRuntime.jsx("span", { ref: ref, className: classNames(loaderClass, bulmaHelperClasses, className), role: "progressbar", "aria-label": ariaLabel, ...rest }));
|
|
11236
|
+
});
|
|
11237
|
+
Loader.displayName = 'Loader';
|
|
11238
|
+
|
|
11239
|
+
const LightboxContext = React.createContext(null);
|
|
11240
|
+
/** Each zoom button or key multiplies or divides the zoom by this. */
|
|
11241
|
+
const ZOOM_STEP = 1.5;
|
|
11242
|
+
/** Where a double click or a double tap zooms to. */
|
|
11243
|
+
const DOUBLE_ZOOM = 2;
|
|
11244
|
+
/** How far an arrow key pans a zoomed image, in CSS pixels. */
|
|
11245
|
+
const PAN_STEP = 50;
|
|
11246
|
+
/** How long an image can take before the Loader shows, in milliseconds. */
|
|
11247
|
+
const LOADER_DELAY = 300;
|
|
11248
|
+
/** How long the status line waits after a change, so a quick run of moves is read once. */
|
|
11249
|
+
const STATUS_DELAY = 500;
|
|
11250
|
+
/** How far a finger can move and still tap, in CSS pixels. */
|
|
11251
|
+
const TAP_SLOP = 10;
|
|
11252
|
+
/** The longest gap between the two taps of a double tap, in milliseconds. */
|
|
11253
|
+
const DOUBLE_TAP_TIME = 300;
|
|
11254
|
+
/** How far apart the two taps of a double tap can land, in CSS pixels. */
|
|
11255
|
+
const DOUBLE_TAP_SLOP = 30;
|
|
11256
|
+
/** A pinch that ends this close to the fitted size snaps back to it. */
|
|
11257
|
+
const SNAP_ZOOM = 1.01;
|
|
11258
|
+
const FITTED = { zoom: 1, x: 0, y: 0, animate: true };
|
|
11259
|
+
/**
|
|
11260
|
+
* Reads the image's resting box and the viewer's size.
|
|
11261
|
+
*
|
|
11262
|
+
* @function
|
|
11263
|
+
* @param {HTMLElement} image - The image shown.
|
|
11264
|
+
* @param {HTMLElement} root - The viewer, its `.modal` element.
|
|
11265
|
+
* @returns {Geometry} The geometry the zoom and pan are kept within.
|
|
11266
|
+
*/
|
|
11267
|
+
function measure(image, root) {
|
|
11268
|
+
let x = 0;
|
|
11269
|
+
let y = 0;
|
|
11270
|
+
for (let node = image; node && node !== root; node = node.offsetParent) {
|
|
11271
|
+
x += node.offsetLeft;
|
|
11272
|
+
y += node.offsetTop;
|
|
11273
|
+
}
|
|
11274
|
+
const box = root.getBoundingClientRect();
|
|
11275
|
+
return {
|
|
11276
|
+
cx: x + image.offsetWidth / 2,
|
|
11277
|
+
cy: y + image.offsetHeight / 2,
|
|
11278
|
+
width: image.offsetWidth,
|
|
11279
|
+
height: image.offsetHeight,
|
|
11280
|
+
viewWidth: root.clientWidth,
|
|
11281
|
+
viewHeight: root.clientHeight,
|
|
11282
|
+
left: box.left,
|
|
11283
|
+
top: box.top,
|
|
11284
|
+
};
|
|
11285
|
+
}
|
|
11286
|
+
const clamp$5 = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
11287
|
+
/**
|
|
11288
|
+
* An offset along one axis, kept to the image's edges: a zoomed image wider
|
|
11289
|
+
* than the viewer can't leave a gap at either side, and one that still fits
|
|
11290
|
+
* stays where it rests, moved only as far as it takes to keep it in view.
|
|
11291
|
+
*/
|
|
11292
|
+
function fitAxis(offset, centre, size, room, zoom) {
|
|
11293
|
+
const half = (size * zoom) / 2;
|
|
11294
|
+
// The offsets that put the image's start edge on the viewer's, and its end edge.
|
|
11295
|
+
const start = half - centre;
|
|
11296
|
+
const end = room - centre - half;
|
|
11297
|
+
return half * 2 > room ? clamp$5(offset, end, start) : clamp$5(0, start, end);
|
|
11298
|
+
}
|
|
11299
|
+
/** The view, with its offsets kept to the image's edges. */
|
|
11300
|
+
function fitView(view, geometry) {
|
|
11301
|
+
return {
|
|
11302
|
+
...view,
|
|
11303
|
+
x: fitAxis(view.x, geometry.cx, geometry.width, geometry.viewWidth, view.zoom),
|
|
11304
|
+
y: fitAxis(view.y, geometry.cy, geometry.height, geometry.viewHeight, view.zoom),
|
|
11305
|
+
};
|
|
11306
|
+
}
|
|
11307
|
+
/**
|
|
11308
|
+
* The view at another zoom, with the point under `(px, py)`, in the
|
|
11309
|
+
* viewer's coordinates, still there.
|
|
11310
|
+
*/
|
|
11311
|
+
function zoomView(view, zoom, px, py, geometry, animate) {
|
|
11312
|
+
if (zoom <= 1)
|
|
11313
|
+
return { ...FITTED, animate };
|
|
11314
|
+
const ratio = zoom / view.zoom;
|
|
11315
|
+
const dx = px - geometry.cx;
|
|
11316
|
+
const dy = py - geometry.cy;
|
|
11317
|
+
return fitView({
|
|
11318
|
+
zoom,
|
|
11319
|
+
x: dx - (dx - view.x) * ratio,
|
|
11320
|
+
y: dy - (dy - view.y) * ratio,
|
|
11321
|
+
animate,
|
|
11322
|
+
}, geometry);
|
|
11323
|
+
}
|
|
11324
|
+
/**
|
|
11325
|
+
* Whether a button has an accessible name from what it holds: its own
|
|
11326
|
+
* label, text, an image's alt or a label on an element inside, leaving out
|
|
11327
|
+
* what's hidden from assistive technology and what's inside a `<figure>`,
|
|
11328
|
+
* whose content doesn't name the button around it. A development check, so
|
|
11329
|
+
* it reads the common sources rather than the whole name computation.
|
|
11330
|
+
*/
|
|
11331
|
+
function hasName(element) {
|
|
11332
|
+
if ((element.getAttribute('aria-label') || '').trim() ||
|
|
11333
|
+
(element.getAttribute('aria-labelledby') || '').trim() ||
|
|
11334
|
+
(element.getAttribute('title') || '').trim()) {
|
|
11335
|
+
return true;
|
|
11336
|
+
}
|
|
11337
|
+
return Array.from(element.childNodes).some(node => node instanceof Element
|
|
11338
|
+
? node.getAttribute('aria-hidden') !== 'true' &&
|
|
11339
|
+
node.tagName !== 'FIGURE' &&
|
|
11340
|
+
((node instanceof HTMLImageElement && !!node.alt.trim()) ||
|
|
11341
|
+
hasName(node))
|
|
11342
|
+
: !!node.textContent.trim());
|
|
11343
|
+
}
|
|
11344
|
+
/** Text a key typed into a field is that field's, not the viewer's. */
|
|
11345
|
+
const isTextEntry = (target) => target.matches('input, textarea, select, [contenteditable]:not([contenteditable="false"])');
|
|
11346
|
+
const ZoomInIcon = () => (jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsxRuntime.jsx("circle", { cx: "11", cy: "11", r: "7" }), jsxRuntime.jsx("path", { d: "M21 21l-5-5M11 8v6M8 11h6" })] }));
|
|
11347
|
+
const ZoomOutIcon = () => (jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsxRuntime.jsx("circle", { cx: "11", cy: "11", r: "7" }), jsxRuntime.jsx("path", { d: "M21 21l-5-5M8 11h6" })] }));
|
|
11348
|
+
const ResetZoomIcon = () => (jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsxRuntime.jsx("path", { d: "M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5" }) }));
|
|
11349
|
+
/**
|
|
11350
|
+
* A button around a thumbnail, or a line of text, that opens the viewer at its `index`.
|
|
11351
|
+
*
|
|
11352
|
+
* @function
|
|
11353
|
+
* @param {LightboxTriggerProps} props - Props for the trigger.
|
|
11354
|
+
* @param {React.Ref} ref - Forwarded ref to the element `as` renders.
|
|
11355
|
+
* @returns {JSX.Element} The rendered button.
|
|
11356
|
+
*/
|
|
11357
|
+
const LightboxTrigger = React.forwardRef(function LightboxTrigger(triggerProps, ref) {
|
|
11358
|
+
const { as: Component = 'button', index, className, children, onClick,
|
|
11359
|
+
// Kept from the helper classes, which read `color` as a text color: it
|
|
11360
|
+
// belongs to a component `as`, as Button's `color="link"` does.
|
|
11361
|
+
color, ...props } = triggerProps;
|
|
11362
|
+
const lightbox = React.useContext(LightboxContext);
|
|
11363
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
11364
|
+
const triggerClass = usePrefixedClassNames('lightbox-trigger');
|
|
11365
|
+
const buttonRef = React.useRef(null);
|
|
11366
|
+
const setRefs = React.useCallback((node) => {
|
|
11367
|
+
buttonRef.current = node;
|
|
11368
|
+
if (typeof ref === 'function')
|
|
11369
|
+
return ref(node);
|
|
11370
|
+
if (ref) {
|
|
11371
|
+
ref.current = node;
|
|
11372
|
+
}
|
|
11373
|
+
return undefined;
|
|
11374
|
+
}, [ref]);
|
|
11375
|
+
if (!lightbox) {
|
|
11376
|
+
warnOnce('Lightbox.Trigger:outside', '[bestax-bulma] <Lightbox.Trigger> is outside a <Lightbox>, so it ' +
|
|
11377
|
+
'opens nothing. Put it inside the Lightbox whose items it opens.');
|
|
11378
|
+
}
|
|
11379
|
+
// Its content is its name, and a thumbnail with `alt=""` leaves it
|
|
11380
|
+
// nameless. Checked after each commit, since the content can change.
|
|
11381
|
+
React.useEffect(() => {
|
|
11382
|
+
// An `as` component that keeps its ref, or hands back a handle of its
|
|
11383
|
+
// own, leaves no element to check.
|
|
11384
|
+
const button = buttonRef.current;
|
|
11385
|
+
if (isDev() && button instanceof Element && !hasName(button)) {
|
|
11386
|
+
warnOnce('Lightbox.Trigger:no-name', '[bestax-bulma] <Lightbox.Trigger> has no accessible name. Give ' +
|
|
11387
|
+
'the image inside it an alt, or the button some text or an ' +
|
|
11388
|
+
'aria-label. An image inside a <figure>, which a ratio Image ' +
|
|
11389
|
+
'renders, doesn\'t name a button, so give that Image as="div".');
|
|
11390
|
+
}
|
|
11391
|
+
});
|
|
11392
|
+
return (jsxRuntime.jsx(Component, { ref: setRefs, className: classNames(triggerClass, bulmaHelperClasses, className), "aria-haspopup": "dialog", ...rest, ...(typeof Component === 'string' ? {} : { color }), ...(Component === 'button' || typeof Component !== 'string'
|
|
11393
|
+
? { type: buttonType(rest.type) }
|
|
11394
|
+
: {}), onClick: (event) => {
|
|
11395
|
+
onClick?.(event);
|
|
11396
|
+
if (event.defaultPrevented || !lightbox)
|
|
11397
|
+
return;
|
|
11398
|
+
// Safari doesn't focus a button that's clicked, and the viewer hands
|
|
11399
|
+
// focus back to whatever had it when it opened.
|
|
11400
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
11401
|
+
lightbox.openAt(index);
|
|
11402
|
+
}, children: children }));
|
|
11403
|
+
});
|
|
11404
|
+
LightboxTrigger.displayName = 'Lightbox.Trigger';
|
|
11405
|
+
/**
|
|
11406
|
+
* The open viewer: a modal dialog showing one image, mounted only while the
|
|
11407
|
+
* Lightbox is open, so each opening starts fitted.
|
|
11408
|
+
*
|
|
11409
|
+
* @function
|
|
11410
|
+
* @param {LightboxViewerProps} props - The Lightbox's props and its state.
|
|
11411
|
+
* @returns {JSX.Element} The rendered dialog.
|
|
11412
|
+
*/
|
|
11413
|
+
const LightboxViewer = React.forwardRef(function LightboxViewer({ items, index, onIndexChange, onClose, repeat, zoomable = true, maxZoom = 3, closeOnEscape = true, closeOnClickOutside = true, portal = true, ariaLabel = 'Image viewer', labels, className, onKeyDown, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, ...props }, ref) {
|
|
11414
|
+
const count = items.length;
|
|
11415
|
+
const item = items[index];
|
|
11416
|
+
const limit = Math.max(1, Number.isFinite(maxZoom) ? maxZoom : 3);
|
|
11417
|
+
const formatNumber = useNumberFormat();
|
|
11418
|
+
const number = (value) => formatNumber ? formatNumber(value) : String(value);
|
|
11419
|
+
const words = {
|
|
11420
|
+
close: labels?.close || 'Close',
|
|
11421
|
+
previous: labels?.previous || 'Previous image',
|
|
11422
|
+
next: labels?.next || 'Next image',
|
|
11423
|
+
zoomIn: labels?.zoomIn || 'Zoom in',
|
|
11424
|
+
zoomOut: labels?.zoomOut || 'Zoom out',
|
|
11425
|
+
resetZoom: labels?.resetZoom || 'Reset zoom',
|
|
11426
|
+
loading: labels?.loading || 'Loading image',
|
|
11427
|
+
loadError: labels?.loadError || "This image didn't load.",
|
|
11428
|
+
retry: labels?.retry || 'Retry',
|
|
11429
|
+
progress: labels?.progress ??
|
|
11430
|
+
((current, total) => `Image ${number(current)} of ${number(total)}`),
|
|
11431
|
+
};
|
|
11432
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
11433
|
+
const hydrated = useIsHydrated();
|
|
11434
|
+
const rootRef = React.useRef(null);
|
|
11435
|
+
const setRoot = React.useCallback((node) => {
|
|
11436
|
+
rootRef.current = node;
|
|
11437
|
+
if (typeof ref === 'function')
|
|
11438
|
+
return ref(node);
|
|
11439
|
+
if (ref) {
|
|
11440
|
+
ref.current = node;
|
|
11441
|
+
}
|
|
11442
|
+
return undefined;
|
|
11443
|
+
}, [ref]);
|
|
11444
|
+
const [image, setImage] = React.useState(null);
|
|
11445
|
+
// The image shown and how it's doing. `leaving` is the image before it,
|
|
11446
|
+
// kept on screen while the new one loads and then faded out over it. The
|
|
11447
|
+
// fades are CSS animations, which start when the stylesheet's selector
|
|
11448
|
+
// first matches, even for an image that was already loaded, where a
|
|
11449
|
+
// transition wouldn't run.
|
|
11450
|
+
const [shown, setShown] = React.useState({ index, item });
|
|
11451
|
+
const [status, setStatus] = React.useState('loading');
|
|
11452
|
+
const [attempt, setAttempt] = React.useState(0);
|
|
11453
|
+
const [leaving, setLeaving] = React.useState();
|
|
11454
|
+
const [view, setView] = React.useState(FITTED);
|
|
11455
|
+
const zoomed = view.zoom > 1;
|
|
11456
|
+
// The view as last set, for a gesture to go on from: pointer events can
|
|
11457
|
+
// arrive faster than React renders.
|
|
11458
|
+
const viewRef = React.useRef(view);
|
|
11459
|
+
useClientLayoutEffect(() => {
|
|
11460
|
+
viewRef.current = view;
|
|
11461
|
+
});
|
|
11462
|
+
const updateView = (next) => {
|
|
11463
|
+
viewRef.current = next(viewRef.current);
|
|
11464
|
+
setView(viewRef.current);
|
|
11465
|
+
};
|
|
11466
|
+
// A move starts the new image fitted and loading, and so does another
|
|
11467
|
+
// image at the same index after `items` changes. Adjusted while rendering,
|
|
11468
|
+
// React's pattern for state that follows a prop, so the old zoom or load
|
|
11469
|
+
// state never paints on the new image.
|
|
11470
|
+
if (shown.index !== index ||
|
|
11471
|
+
shown.item.src !== item.src ||
|
|
11472
|
+
shown.item.srcSet !== item.srcSet) {
|
|
11473
|
+
setShown({ index, item });
|
|
11474
|
+
setLeaving(reducedMotion ? undefined : status === 'loaded' ? shown.item : leaving);
|
|
11475
|
+
setStatus('loading');
|
|
11476
|
+
setAttempt(0);
|
|
11477
|
+
setView(FITTED);
|
|
11478
|
+
}
|
|
11479
|
+
const fail = () => {
|
|
11480
|
+
setStatus('error');
|
|
11481
|
+
setLeaving(undefined);
|
|
11482
|
+
};
|
|
11483
|
+
// An image the server rendered loads or fails before hydration, so its
|
|
11484
|
+
// event comes before React listens for it. A broken image is complete
|
|
11485
|
+
// and has no size, and so is a loaded SVG with none of its own, so
|
|
11486
|
+
// `decode()` tells those two apart.
|
|
11487
|
+
useClientLayoutEffect(() => {
|
|
11488
|
+
if (!image?.complete)
|
|
11489
|
+
return undefined;
|
|
11490
|
+
if (image.naturalWidth > 0) {
|
|
11491
|
+
setStatus('loaded');
|
|
11492
|
+
return undefined;
|
|
11493
|
+
}
|
|
11494
|
+
let live = true;
|
|
11495
|
+
image.decode().then(() => live && setStatus('loaded'), () => {
|
|
11496
|
+
if (!live)
|
|
11497
|
+
return;
|
|
11498
|
+
setStatus('error');
|
|
11499
|
+
setLeaving(undefined);
|
|
11500
|
+
});
|
|
11501
|
+
return () => {
|
|
11502
|
+
live = false;
|
|
11503
|
+
};
|
|
11504
|
+
}, [image]);
|
|
11505
|
+
// The Loader shows only for an image that takes a while.
|
|
11506
|
+
React.useEffect(() => {
|
|
11507
|
+
if (status !== 'loading')
|
|
11508
|
+
return undefined;
|
|
11509
|
+
// Cleared by any change of status, so it only fires on a load still
|
|
11510
|
+
// under way.
|
|
11511
|
+
const timer = setTimeout(() => {
|
|
11512
|
+
setStatus('slow');
|
|
11513
|
+
setLeaving(undefined);
|
|
11514
|
+
}, LOADER_DELAY);
|
|
11515
|
+
return () => clearTimeout(timer);
|
|
11516
|
+
}, [status, shown, attempt]);
|
|
11517
|
+
// The images either side, fetched ahead so a move shows them at once.
|
|
11518
|
+
// With `repeat` and two images, both sides are the one other image, so
|
|
11519
|
+
// the indexes are deduplicated and it's fetched once.
|
|
11520
|
+
const neighbours = Array.from(new Set([index - 1, index + 1].map(i => (repeat ? (i + count) % count : i))))
|
|
11521
|
+
.filter(i => i >= 0 && i < count && i !== index)
|
|
11522
|
+
.map(i => items[i]);
|
|
11523
|
+
// None, one or two of them, in no particular order.
|
|
11524
|
+
const [firstNeighbour, secondNeighbour] = neighbours;
|
|
11525
|
+
const firstSrc = firstNeighbour?.src;
|
|
11526
|
+
const firstSrcSet = firstNeighbour?.srcSet;
|
|
11527
|
+
const firstSizes = firstNeighbour?.sizes;
|
|
11528
|
+
const secondSrc = secondNeighbour?.src;
|
|
11529
|
+
const secondSrcSet = secondNeighbour?.srcSet;
|
|
11530
|
+
const secondSizes = secondNeighbour?.sizes;
|
|
11531
|
+
React.useEffect(() => {
|
|
11532
|
+
const fetchAhead = (src, srcSet, sizes) => {
|
|
11533
|
+
if (!src)
|
|
11534
|
+
return;
|
|
11535
|
+
const img = document.createElement('img');
|
|
11536
|
+
if (sizes)
|
|
11537
|
+
img.sizes = sizes;
|
|
11538
|
+
if (srcSet)
|
|
11539
|
+
img.srcset = srcSet;
|
|
11540
|
+
img.src = src;
|
|
11541
|
+
};
|
|
11542
|
+
fetchAhead(firstSrc, firstSrcSet, firstSizes);
|
|
11543
|
+
fetchAhead(secondSrc, secondSrcSet, secondSizes);
|
|
11544
|
+
}, [
|
|
11545
|
+
firstSrc,
|
|
11546
|
+
firstSrcSet,
|
|
11547
|
+
firstSizes,
|
|
11548
|
+
secondSrc,
|
|
11549
|
+
secondSrcSet,
|
|
11550
|
+
secondSizes,
|
|
11551
|
+
]);
|
|
11552
|
+
// The status line, written a moment after it changes, so a quick run of
|
|
11553
|
+
// moves is read once. It's written on opening too, which says which image
|
|
11554
|
+
// the viewer opened at.
|
|
11555
|
+
const progress = words.progress(index + 1, count);
|
|
11556
|
+
const position = item.alt ? `${progress}: ${item.alt}` : progress;
|
|
11557
|
+
// A failed image's line says which image failed too, so a move from one
|
|
11558
|
+
// failed image to another still changes it.
|
|
11559
|
+
const message = status === 'error' ? `${words.loadError} ${position}` : position;
|
|
11560
|
+
const [said, setSaid] = React.useState('');
|
|
11561
|
+
React.useEffect(() => {
|
|
11562
|
+
const timer = setTimeout(() => setSaid(message), STATUS_DELAY);
|
|
11563
|
+
return () => clearTimeout(timer);
|
|
11564
|
+
}, [message]);
|
|
11565
|
+
// ---- Moving -------------------------------------------------------------
|
|
11566
|
+
const canPrev = repeat ? count > 1 : index > 0;
|
|
11567
|
+
const canNext = repeat ? count > 1 : index < count - 1;
|
|
11568
|
+
const go = (next) => {
|
|
11569
|
+
if (next !== index)
|
|
11570
|
+
onIndexChange(next);
|
|
11571
|
+
};
|
|
11572
|
+
const step = (delta) => {
|
|
11573
|
+
if (delta < 0 ? !canPrev : !canNext)
|
|
11574
|
+
return;
|
|
11575
|
+
go((index + delta + count) % count);
|
|
11576
|
+
};
|
|
11577
|
+
const isRtl = (element) => getComputedStyle(element).direction === 'rtl';
|
|
11578
|
+
const swipe = useSwipe({
|
|
11579
|
+
enabled: !zoomed,
|
|
11580
|
+
vertical: true,
|
|
11581
|
+
onSwipe: direction => {
|
|
11582
|
+
if (direction === 'down') {
|
|
11583
|
+
onClose();
|
|
11584
|
+
}
|
|
11585
|
+
else if (direction !== 'up') {
|
|
11586
|
+
// A swipe left brings the next image in from the right, and right
|
|
11587
|
+
// to left the other way round.
|
|
11588
|
+
const forward = isRtl(rootRef.current)
|
|
11589
|
+
? 'right'
|
|
11590
|
+
: 'left';
|
|
11591
|
+
step(direction === forward ? 1 : -1);
|
|
11592
|
+
}
|
|
11593
|
+
},
|
|
11594
|
+
});
|
|
11595
|
+
// ---- Zooming and panning --------------------------------------------------
|
|
11596
|
+
// Zoom waits for the image: a loading one has no size to zoom, and a
|
|
11597
|
+
// failed one isn't there.
|
|
11598
|
+
const canZoom = zoomable && status === 'loaded';
|
|
11599
|
+
// Only called with an image on screen: every way to zoom or pan checks
|
|
11600
|
+
// `canZoom` first, and a pan needs a zoom.
|
|
11601
|
+
const geometry = () => measure(image, rootRef.current);
|
|
11602
|
+
/** Zooms to `zoom`, kept within the limits, about the viewer's centre. */
|
|
11603
|
+
const zoomTo = (next) => {
|
|
11604
|
+
if (!canZoom)
|
|
11605
|
+
return;
|
|
11606
|
+
const g = geometry();
|
|
11607
|
+
updateView(v => zoomView(v, clamp$5(next(v.zoom), 1, limit), g.viewWidth / 2, g.viewHeight / 2, g, true));
|
|
11608
|
+
};
|
|
11609
|
+
const zoomIn = () => zoomTo(zoom => zoom * ZOOM_STEP);
|
|
11610
|
+
const zoomOut = () => zoomTo(zoom => zoom / ZOOM_STEP);
|
|
11611
|
+
const resetZoom = () => updateView(() => FITTED);
|
|
11612
|
+
/** A double click or tap: fitted to 2x at the pointer, and back. */
|
|
11613
|
+
const toggleZoomAt = (clientX, clientY) => {
|
|
11614
|
+
const g = geometry();
|
|
11615
|
+
updateView(v => v.zoom > 1
|
|
11616
|
+
? FITTED
|
|
11617
|
+
: zoomView(v, Math.min(DOUBLE_ZOOM, limit), clientX - g.left, clientY - g.top, g, true));
|
|
11618
|
+
};
|
|
11619
|
+
const pan = (dx, dy) => {
|
|
11620
|
+
const g = geometry();
|
|
11621
|
+
updateView(v => fitView({ ...v, x: v.x + dx, y: v.y + dy, animate: true }, g));
|
|
11622
|
+
};
|
|
11623
|
+
// The wheel zooms at the pointer. A listener of its own, since React's
|
|
11624
|
+
// wheel handlers are passive and can't stop the page from zooming too.
|
|
11625
|
+
const handleWheel = useLatestRef((event) => {
|
|
11626
|
+
const g = geometry();
|
|
11627
|
+
const lines = event.deltaMode === 1 ? 16 : 1;
|
|
11628
|
+
const delta = clamp$5(event.deltaY * lines, -100, 100);
|
|
11629
|
+
updateView(v => zoomView(v, clamp$5(v.zoom * Math.exp(-delta / 300), 1, limit), event.clientX - g.left, event.clientY - g.top, g, false));
|
|
11630
|
+
});
|
|
11631
|
+
React.useEffect(() => {
|
|
11632
|
+
if (!canZoom)
|
|
11633
|
+
return undefined;
|
|
11634
|
+
const target = image;
|
|
11635
|
+
const onWheel = (event) => {
|
|
11636
|
+
event.preventDefault();
|
|
11637
|
+
handleWheel.current(event);
|
|
11638
|
+
};
|
|
11639
|
+
target.addEventListener('wheel', onWheel, { passive: false });
|
|
11640
|
+
return () => target.removeEventListener('wheel', onWheel);
|
|
11641
|
+
}, [image, canZoom, handleWheel]);
|
|
11642
|
+
// Pointers on the image: one pans a zoomed image, two pinch. The first
|
|
11643
|
+
// has to land on the image; the second can land anywhere in the viewer.
|
|
11644
|
+
const pointersRef = React.useRef(new Map());
|
|
11645
|
+
const gestureRef = React.useRef(null);
|
|
11646
|
+
const tapRef = React.useRef(null);
|
|
11647
|
+
const lastTapRef = React.useRef(null);
|
|
11648
|
+
const lastPointerTypeRef = React.useRef('');
|
|
11649
|
+
const startPan = (x, y) => {
|
|
11650
|
+
gestureRef.current = {
|
|
11651
|
+
kind: 'pan',
|
|
11652
|
+
x,
|
|
11653
|
+
y,
|
|
11654
|
+
from: viewRef.current,
|
|
11655
|
+
geometry: geometry(),
|
|
11656
|
+
};
|
|
11657
|
+
};
|
|
11658
|
+
const handlePointerDown = (event) => {
|
|
11659
|
+
onPointerDown?.(event);
|
|
11660
|
+
lastPointerTypeRef.current = event.pointerType;
|
|
11661
|
+
// A second finger is a pinch, never a swipe, whether or not anything
|
|
11662
|
+
// zooms, and wherever in the viewer it lands.
|
|
11663
|
+
if (!event.isPrimary)
|
|
11664
|
+
swipe.cancel();
|
|
11665
|
+
if (!canZoom || event.defaultPrevented)
|
|
11666
|
+
return;
|
|
11667
|
+
const onImage = event.target === image;
|
|
11668
|
+
if (!onImage && pointersRef.current.size === 0)
|
|
11669
|
+
return;
|
|
11670
|
+
pointersRef.current.set(event.pointerId, {
|
|
11671
|
+
x: event.clientX,
|
|
11672
|
+
y: event.clientY,
|
|
11673
|
+
});
|
|
11674
|
+
// Two or more is a pinch, between the first two.
|
|
11675
|
+
if (pointersRef.current.size > 1) {
|
|
11676
|
+
const g = geometry();
|
|
11677
|
+
const [a, b] = Array.from(pointersRef.current.values());
|
|
11678
|
+
tapRef.current = null;
|
|
11679
|
+
swipe.cancel();
|
|
11680
|
+
gestureRef.current = {
|
|
11681
|
+
kind: 'pinch',
|
|
11682
|
+
distance: Math.hypot(a.x - b.x, a.y - b.y) || 1,
|
|
11683
|
+
x: (a.x + b.x) / 2 - g.left,
|
|
11684
|
+
y: (a.y + b.y) / 2 - g.top,
|
|
11685
|
+
from: viewRef.current,
|
|
11686
|
+
geometry: g,
|
|
11687
|
+
};
|
|
11688
|
+
}
|
|
11689
|
+
else {
|
|
11690
|
+
tapRef.current = { x: event.clientX, y: event.clientY };
|
|
11691
|
+
if (zoomed) {
|
|
11692
|
+
// So the pan follows a mouse that leaves the image.
|
|
11693
|
+
event.target.setPointerCapture(event.pointerId);
|
|
11694
|
+
startPan(event.clientX, event.clientY);
|
|
11695
|
+
}
|
|
11696
|
+
}
|
|
11697
|
+
};
|
|
11698
|
+
const handlePointerMove = (event) => {
|
|
11699
|
+
onPointerMove?.(event);
|
|
11700
|
+
const point = pointersRef.current.get(event.pointerId);
|
|
11701
|
+
if (!point)
|
|
11702
|
+
return;
|
|
11703
|
+
point.x = event.clientX;
|
|
11704
|
+
point.y = event.clientY;
|
|
11705
|
+
if (tapRef.current &&
|
|
11706
|
+
Math.hypot(event.clientX - tapRef.current.x, event.clientY - tapRef.current.y) > TAP_SLOP) {
|
|
11707
|
+
tapRef.current = null;
|
|
11708
|
+
}
|
|
11709
|
+
const current = gestureRef.current;
|
|
11710
|
+
if (current?.kind === 'pinch') {
|
|
11711
|
+
const [a, b] = Array.from(pointersRef.current.values());
|
|
11712
|
+
const g = current.geometry;
|
|
11713
|
+
const zoom = clamp$5((current.from.zoom * Math.hypot(a.x - b.x, a.y - b.y)) /
|
|
11714
|
+
current.distance, 1, limit);
|
|
11715
|
+
// The point of the image under the pinch's start stays between the
|
|
11716
|
+
// fingers, so they can pan as they pinch.
|
|
11717
|
+
const midX = (a.x + b.x) / 2 - g.left;
|
|
11718
|
+
const midY = (a.y + b.y) / 2 - g.top;
|
|
11719
|
+
const imageX = (current.x - g.cx - current.from.x) / current.from.zoom;
|
|
11720
|
+
const imageY = (current.y - g.cy - current.from.y) / current.from.zoom;
|
|
11721
|
+
updateView(() => fitView({
|
|
11722
|
+
zoom,
|
|
11723
|
+
x: midX - g.cx - imageX * zoom,
|
|
11724
|
+
y: midY - g.cy - imageY * zoom,
|
|
11725
|
+
animate: false,
|
|
11726
|
+
}, g));
|
|
11727
|
+
}
|
|
11728
|
+
else if (current) {
|
|
11729
|
+
updateView(() => fitView({
|
|
11730
|
+
...current.from,
|
|
11731
|
+
x: current.from.x + event.clientX - current.x,
|
|
11732
|
+
y: current.from.y + event.clientY - current.y,
|
|
11733
|
+
animate: false,
|
|
11734
|
+
}, current.geometry));
|
|
11735
|
+
}
|
|
11736
|
+
};
|
|
11737
|
+
const handlePointerEnd = (event) => {
|
|
11738
|
+
if (!pointersRef.current.delete(event.pointerId))
|
|
11739
|
+
return;
|
|
11740
|
+
const wasPinch = gestureRef.current?.kind === 'pinch';
|
|
11741
|
+
gestureRef.current = null;
|
|
11742
|
+
if (wasPinch) {
|
|
11743
|
+
if (viewRef.current.zoom < SNAP_ZOOM) {
|
|
11744
|
+
updateView(() => FITTED);
|
|
11745
|
+
}
|
|
11746
|
+
else {
|
|
11747
|
+
// A finger left down after a pinch carries on as a pan.
|
|
11748
|
+
const [point] = Array.from(pointersRef.current.values());
|
|
11749
|
+
startPan(point.x, point.y);
|
|
11750
|
+
}
|
|
11751
|
+
}
|
|
11752
|
+
// Two quick taps in the same place zoom, the touch screen's double click.
|
|
11753
|
+
if (event.type === 'pointerup' &&
|
|
11754
|
+
event.pointerType === 'touch' &&
|
|
11755
|
+
tapRef.current) {
|
|
11756
|
+
const previous = lastTapRef.current;
|
|
11757
|
+
if (previous &&
|
|
11758
|
+
event.timeStamp - previous.time < DOUBLE_TAP_TIME &&
|
|
11759
|
+
Math.hypot(event.clientX - previous.x, event.clientY - previous.y) <
|
|
11760
|
+
DOUBLE_TAP_SLOP) {
|
|
11761
|
+
lastTapRef.current = null;
|
|
11762
|
+
toggleZoomAt(event.clientX, event.clientY);
|
|
11763
|
+
}
|
|
11764
|
+
else {
|
|
11765
|
+
lastTapRef.current = {
|
|
11766
|
+
x: event.clientX,
|
|
11767
|
+
y: event.clientY,
|
|
11768
|
+
time: event.timeStamp,
|
|
11769
|
+
};
|
|
11770
|
+
}
|
|
11771
|
+
}
|
|
11772
|
+
tapRef.current = null;
|
|
11773
|
+
};
|
|
11774
|
+
// ---- Keys -----------------------------------------------------------------
|
|
11775
|
+
const handleKeyDown = (event) => {
|
|
11776
|
+
onKeyDown?.(event);
|
|
11777
|
+
if (event.defaultPrevented ||
|
|
11778
|
+
event.altKey ||
|
|
11779
|
+
event.ctrlKey ||
|
|
11780
|
+
event.metaKey ||
|
|
11781
|
+
// A key from content React portals elsewhere, such as a popover's,
|
|
11782
|
+
// is that content's.
|
|
11783
|
+
!event.currentTarget.contains(event.target) ||
|
|
11784
|
+
isTextEntry(event.target)) {
|
|
11785
|
+
return;
|
|
11786
|
+
}
|
|
11787
|
+
const forward = isRtl(event.currentTarget) ? -1 : 1;
|
|
11788
|
+
switch (event.key) {
|
|
11789
|
+
case 'ArrowLeft':
|
|
11790
|
+
if (zoomed)
|
|
11791
|
+
pan(PAN_STEP, 0);
|
|
11792
|
+
else
|
|
11793
|
+
step(forward === 1 ? -1 : 1);
|
|
11794
|
+
break;
|
|
11795
|
+
case 'ArrowRight':
|
|
11796
|
+
if (zoomed)
|
|
11797
|
+
pan(-PAN_STEP, 0);
|
|
11798
|
+
else
|
|
11799
|
+
step(forward === 1 ? 1 : -1);
|
|
11800
|
+
break;
|
|
11801
|
+
case 'ArrowUp':
|
|
11802
|
+
case 'ArrowDown':
|
|
11803
|
+
if (!zoomed)
|
|
11804
|
+
return;
|
|
11805
|
+
pan(0, event.key === 'ArrowUp' ? PAN_STEP : -PAN_STEP);
|
|
11806
|
+
break;
|
|
11807
|
+
case 'Home':
|
|
11808
|
+
go(0);
|
|
11809
|
+
break;
|
|
11810
|
+
case 'End':
|
|
11811
|
+
go(count - 1);
|
|
11812
|
+
break;
|
|
11813
|
+
case '+':
|
|
11814
|
+
case '=':
|
|
11815
|
+
case '-':
|
|
11816
|
+
case '0':
|
|
11817
|
+
if (!canZoom)
|
|
11818
|
+
return;
|
|
11819
|
+
if (event.key === '0')
|
|
11820
|
+
resetZoom();
|
|
11821
|
+
else if (event.key === '-')
|
|
11822
|
+
zoomOut();
|
|
11823
|
+
else
|
|
11824
|
+
zoomIn();
|
|
11825
|
+
break;
|
|
11826
|
+
default:
|
|
11827
|
+
return;
|
|
11828
|
+
}
|
|
11829
|
+
event.preventDefault();
|
|
11830
|
+
};
|
|
11831
|
+
// ---- Loading ----------------------------------------------------------------
|
|
11832
|
+
const retry = () => {
|
|
11833
|
+
setAttempt(n => n + 1);
|
|
11834
|
+
setStatus('loading');
|
|
11835
|
+
// Cleared, so a Retry that fails again is announced again.
|
|
11836
|
+
setSaid('');
|
|
11837
|
+
// The Retry button goes with the error, so focus waits on the dialog.
|
|
11838
|
+
rootRef.current.focus();
|
|
11839
|
+
};
|
|
11840
|
+
// ---- Rendering --------------------------------------------------------------
|
|
11841
|
+
const rootClass = usePrefixedClassNames('lightbox', {
|
|
11842
|
+
'is-zoomed': zoomed,
|
|
11843
|
+
});
|
|
11844
|
+
const toolbarClass = usePrefixedClassNames('lightbox-toolbar');
|
|
11845
|
+
const progressClass = usePrefixedClassNames('lightbox-progress');
|
|
11846
|
+
const figureClass = usePrefixedClassNames('lightbox-figure');
|
|
11847
|
+
const imageClass = usePrefixedClassNames('lightbox-image');
|
|
11848
|
+
const loadingClass = usePrefixedClassNames('is-loading');
|
|
11849
|
+
const leavingClass = usePrefixedClassNames('lightbox-image', 'is-leaving');
|
|
11850
|
+
const captionClass = usePrefixedClassNames('lightbox-caption');
|
|
11851
|
+
const startsSwipe = (target) => !target.closest(`button, a, input, select, textarea, .${captionClass}`);
|
|
11852
|
+
const errorClass = usePrefixedClassNames('lightbox-error');
|
|
11853
|
+
const previousClass = usePrefixedClassNames('lightbox-arrow', 'is-prev');
|
|
11854
|
+
const nextClass = usePrefixedClassNames('lightbox-arrow', 'is-next');
|
|
11855
|
+
const imageStyle = zoomed
|
|
11856
|
+
? {
|
|
11857
|
+
transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`,
|
|
11858
|
+
transition: view.animate ? undefined : 'none',
|
|
11859
|
+
}
|
|
11860
|
+
: view.animate
|
|
11861
|
+
? undefined
|
|
11862
|
+
: { transition: 'none' };
|
|
11863
|
+
const figureStyle = swipe.isSwiping
|
|
11864
|
+
? {
|
|
11865
|
+
// A swipe up doesn't go anywhere, so the image doesn't follow it.
|
|
11866
|
+
transform: `translate(${swipe.offsetX}px, ${Math.max(0, swipe.offsetY)}px)`,
|
|
11867
|
+
transition: 'none',
|
|
11868
|
+
}
|
|
11869
|
+
: undefined;
|
|
11870
|
+
return (jsxRuntime.jsxs(Modal, { ref: setRoot, isActive: true, onClose: onClose, closeOnEscape: closeOnEscape, portal: portal, className: classNames(rootClass, className), "aria-label": ariaLabel, ...props, onKeyDown: handleKeyDown, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: event => {
|
|
11871
|
+
onPointerUp?.(event);
|
|
11872
|
+
handlePointerEnd(event);
|
|
11873
|
+
}, onPointerCancel: event => {
|
|
11874
|
+
onPointerCancel?.(event);
|
|
11875
|
+
handlePointerEnd(event);
|
|
11876
|
+
}, children: [jsxRuntime.jsx(Modal.Background, { onClick: closeOnClickOutside ? onClose : undefined }), jsxRuntime.jsx(Modal.Close, { variant: "floating", size: "large", "aria-label": words.close }), jsxRuntime.jsxs("div", { className: toolbarClass, children: [count > 1 && (jsxRuntime.jsx("bdi", { className: progressClass, "aria-hidden": "true", children: `${number(index + 1)} / ${number(count)}` })), zoomable && (jsxRuntime.jsxs(Buttons, { children: [jsxRuntime.jsx(Button, { type: "button", "aria-label": words.zoomOut, "aria-disabled": !zoomed || undefined, onClick: zoomOut, children: jsxRuntime.jsx(ZoomOutIcon, {}) }), jsxRuntime.jsx(Button, { type: "button", "aria-label": words.zoomIn, "aria-disabled": !canZoom || view.zoom >= limit || undefined, onClick: zoomIn, children: jsxRuntime.jsx(ZoomInIcon, {}) }), jsxRuntime.jsx(Button, { type: "button", "aria-label": words.resetZoom, "aria-disabled": !zoomed || undefined, onClick: resetZoom, children: jsxRuntime.jsx(ResetZoomIcon, {}) })] }))] }), jsxRuntime.jsx(Modal.Content, { children: jsxRuntime.jsxs(Figure, { className: figureClass, style: figureStyle, ...swipe.handlers, onMouseDown: event => {
|
|
11877
|
+
if (startsSwipe(event.target))
|
|
11878
|
+
swipe.handlers.onMouseDown(event);
|
|
11879
|
+
}, onTouchStart: event => {
|
|
11880
|
+
// A second finger is a pinch, not a swipe.
|
|
11881
|
+
if (event.touches.length > 1)
|
|
11882
|
+
swipe.cancel();
|
|
11883
|
+
else if (startsSwipe(event.target)) {
|
|
11884
|
+
swipe.handlers.onTouchStart(event);
|
|
11885
|
+
}
|
|
11886
|
+
}, children: [status !== 'error' && (jsxRuntime.jsx("img", { ref: setImage, className: classNames(imageClass, hydrated && status !== 'loaded' && loadingClass), src: item.src, srcSet: item.srcSet, sizes: item.sizes, alt: item.alt, draggable: false, style: imageStyle, onLoad: () => setStatus('loaded'), onError: fail, onDoubleClick: event => {
|
|
11887
|
+
// A touch screen's double tap is handled as it lands.
|
|
11888
|
+
if (canZoom && lastPointerTypeRef.current !== 'touch') {
|
|
11889
|
+
toggleZoomAt(event.clientX, event.clientY);
|
|
11890
|
+
}
|
|
11891
|
+
} }, `${index}-${item.src}-${item.srcSet}-${attempt}`)), leaving && (jsxRuntime.jsx("img", { className: leavingClass, src: leaving.src, srcSet: leaving.srcSet, sizes: leaving.sizes, alt: "", "aria-hidden": "true", draggable: false, onAnimationEnd: () => setLeaving(undefined) })), status === 'slow' && jsxRuntime.jsx(Loader, { ariaLabel: words.loading }), status === 'error' && (jsxRuntime.jsxs("div", { className: errorClass, children: [jsxRuntime.jsx("p", { children: words.loadError }), item.alt && jsxRuntime.jsx("p", { children: item.alt }), jsxRuntime.jsx(Button, { type: "button", onClick: retry, children: words.retry })] })), item.caption ? (jsxRuntime.jsx(Figure.Caption, { className: captionClass, children: item.caption })) : null] }) }), count > 1 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Button, { type: "button", className: previousClass, "aria-label": words.previous, "aria-disabled": !canPrev || undefined, onClick: () => step(-1), children: jsxRuntime.jsx(ChevronPrevIcon, {}) }), jsxRuntime.jsx(Button, { type: "button", className: nextClass, "aria-label": words.next, "aria-disabled": !canNext || undefined, onClick: () => step(1), children: jsxRuntime.jsx(ChevronNextIcon, {}) })] })), jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", style: visuallyHidden, children: said })] }));
|
|
11892
|
+
});
|
|
11893
|
+
/**
|
|
11894
|
+
* The `Lightbox` component shows a set of images one at a time in a modal viewer, with previous and next, a counter, captions and zoom, opened from the `Lightbox.Trigger` buttons you place.
|
|
11895
|
+
*
|
|
11896
|
+
* It renders its children where it stands, with no wrapper, and the
|
|
11897
|
+
* viewer only while it's open, into a portal by default. The viewer is a
|
|
11898
|
+
* `Modal`: focus moves to its close button, Tab stays inside, Escape and a
|
|
11899
|
+
* click on the background close it, and focus goes back to the trigger.
|
|
11900
|
+
*
|
|
11901
|
+
* @function
|
|
11902
|
+
* @param {LightboxProps} props - Props for the Lightbox component.
|
|
11903
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the viewer's `.modal` element, `null` while it's closed.
|
|
11904
|
+
* @returns {JSX.Element} The children, and the viewer while it's open.
|
|
11905
|
+
*
|
|
11906
|
+
* @example
|
|
11907
|
+
* <Lightbox items={photos}>
|
|
11908
|
+
* {photos.map((photo, i) => (
|
|
11909
|
+
* <Lightbox.Trigger key={photo.src} index={i}>
|
|
11910
|
+
* <Image as="div" src={photo.src} alt={photo.alt} size="4by3" />
|
|
11911
|
+
* </Lightbox.Trigger>
|
|
11912
|
+
* ))}
|
|
11913
|
+
* </Lightbox>
|
|
11914
|
+
*/
|
|
11915
|
+
const LightboxRoot = React.forwardRef(({ items, children, open: controlledOpen, defaultOpen = false, onOpenChange, value: controlledValue, defaultValue = 0, onChange, repeat = true, ...props }, ref) => {
|
|
11916
|
+
const [ownOpen, setOwnOpen] = React.useState(defaultOpen);
|
|
11917
|
+
const open = controlledOpen ?? ownOpen;
|
|
11918
|
+
const [ownValue, setOwnValue] = React.useState(defaultValue);
|
|
11919
|
+
const count = items.length;
|
|
11920
|
+
if (items.some(item => typeof item.alt !== 'string')) {
|
|
11921
|
+
warnOnce('Lightbox:no-alt', '[bestax-bulma] <Lightbox> has an item with no alt. In a viewer the ' +
|
|
11922
|
+
'image is the content, so give each one a text alternative.');
|
|
11923
|
+
}
|
|
11924
|
+
/** An index as one of the images: rounded, then held to the ends or wrapped. */
|
|
11925
|
+
const fit = (value) => {
|
|
11926
|
+
const whole = Number.isFinite(value) ? Math.trunc(value) : 0;
|
|
11927
|
+
return repeat
|
|
11928
|
+
? ((whole % count) + count) % count
|
|
11929
|
+
: clamp$5(whole, 0, count - 1);
|
|
11930
|
+
};
|
|
11931
|
+
const index = fit(controlledValue ?? ownValue);
|
|
11932
|
+
const setOpen = (next) => {
|
|
11933
|
+
if (controlledOpen === undefined)
|
|
11934
|
+
setOwnOpen(next);
|
|
11935
|
+
onOpenChange?.(next);
|
|
11936
|
+
};
|
|
11937
|
+
const setValue = (next) => {
|
|
11938
|
+
if (controlledValue === undefined)
|
|
11939
|
+
setOwnValue(next);
|
|
11940
|
+
onChange?.(next);
|
|
11941
|
+
};
|
|
11942
|
+
// Read when a trigger is pressed, so the context stays the same object
|
|
11943
|
+
// and the triggers don't re-render with every change here.
|
|
11944
|
+
const openAt = useLatestRef((at) => {
|
|
11945
|
+
if (!count)
|
|
11946
|
+
return;
|
|
11947
|
+
setValue(fit(at));
|
|
11948
|
+
if (!open)
|
|
11949
|
+
setOpen(true);
|
|
11950
|
+
});
|
|
11951
|
+
// When the images run out while it's open, it closes the way a user's
|
|
11952
|
+
// close does, so an uncontrolled viewer doesn't open again by itself when
|
|
11953
|
+
// they return. Only on the drop to none, so a viewer that opens before its
|
|
11954
|
+
// images arrive (a deep link, or `defaultOpen` over data still loading)
|
|
11955
|
+
// stays open for them.
|
|
11956
|
+
const closeEmpty = useLatestRef(() => setOpen(false));
|
|
11957
|
+
const countRef = React.useRef(count);
|
|
11958
|
+
React.useEffect(() => {
|
|
11959
|
+
const had = countRef.current;
|
|
11960
|
+
countRef.current = count;
|
|
11961
|
+
if (open && had > 0 && count === 0)
|
|
11962
|
+
closeEmpty.current();
|
|
11963
|
+
}, [open, count, closeEmpty]);
|
|
11964
|
+
const context = React.useMemo(() => ({ openAt: at => openAt.current(at) }), [openAt]);
|
|
11965
|
+
return (jsxRuntime.jsxs(LightboxContext.Provider, { value: context, children: [children, open && count > 0 && (jsxRuntime.jsx(LightboxViewer, { ...props, ref: ref, items: items, repeat: repeat, index: index, onIndexChange: setValue, onClose: () => setOpen(false) }))] }));
|
|
11966
|
+
});
|
|
11967
|
+
LightboxRoot.displayName = 'Lightbox';
|
|
11968
|
+
const Lightbox = withSubComponents(LightboxRoot, { Trigger: LightboxTrigger }, 'Lightbox');
|
|
11969
|
+
|
|
11117
11970
|
/**
|
|
11118
11971
|
* The `Reveal` component animates its content into view as it scrolls into the viewport, backed by `IntersectionObserver`.
|
|
11119
11972
|
*
|
|
@@ -12453,7 +13306,7 @@ const TreeView = withSubComponents(TreeViewRoot, { Item: TreeViewItem }, 'TreeVi
|
|
|
12453
13306
|
const EDGES = ['top', 'bottom', 'start', 'end'];
|
|
12454
13307
|
// Layout can land a fraction of a pixel short of an edge, so anything closer
|
|
12455
13308
|
// than this counts as there.
|
|
12456
|
-
const SLACK = 1;
|
|
13309
|
+
const SLACK$1 = 1;
|
|
12457
13310
|
const sameMeasure = (a, b) => !!a &&
|
|
12458
13311
|
a.overflowing === b.overflowing &&
|
|
12459
13312
|
a.barInline === b.barInline &&
|
|
@@ -12534,8 +13387,8 @@ const ScrollArea = React.forwardRef(({ className, style, textColor, bgColor, hei
|
|
|
12534
13387
|
const scrollsX = orientation !== 'vertical';
|
|
12535
13388
|
const maxTop = el.scrollHeight - el.clientHeight;
|
|
12536
13389
|
const maxLeft = el.scrollWidth - el.clientWidth;
|
|
12537
|
-
const overflowY = scrollsY && maxTop > SLACK;
|
|
12538
|
-
const overflowX = scrollsX && maxLeft > SLACK;
|
|
13390
|
+
const overflowY = scrollsY && maxTop > SLACK$1;
|
|
13391
|
+
const overflowX = scrollsX && maxLeft > SLACK$1;
|
|
12539
13392
|
// Right to left, scrollLeft runs from 0 at the start to negative.
|
|
12540
13393
|
const fromStart = layoutRef.current.rtl
|
|
12541
13394
|
? -el.scrollLeft
|
|
@@ -12543,10 +13396,10 @@ const ScrollArea = React.forwardRef(({ className, style, textColor, bgColor, hei
|
|
|
12543
13396
|
const next = {
|
|
12544
13397
|
overflowing: overflowY || overflowX,
|
|
12545
13398
|
edges: {
|
|
12546
|
-
top: overflowY && el.scrollTop > SLACK,
|
|
12547
|
-
bottom: overflowY && maxTop - el.scrollTop > SLACK,
|
|
12548
|
-
start: overflowX && fromStart > SLACK,
|
|
12549
|
-
end: overflowX && maxLeft - fromStart > SLACK,
|
|
13399
|
+
top: overflowY && el.scrollTop > SLACK$1,
|
|
13400
|
+
bottom: overflowY && maxTop - el.scrollTop > SLACK$1,
|
|
13401
|
+
start: overflowX && fromStart > SLACK$1,
|
|
13402
|
+
end: overflowX && maxLeft - fromStart > SLACK$1,
|
|
12550
13403
|
},
|
|
12551
13404
|
barInline: layoutRef.current.barInline,
|
|
12552
13405
|
barBlock: layoutRef.current.barBlock,
|
|
@@ -15065,59 +15918,412 @@ const DataTable = React.forwardRef(function DataTableRender({ columns, data, tab
|
|
|
15065
15918
|
return column.isRowHeader ? (jsxRuntime.jsx(Th, { ...cellProps, children: content }, column.id)) : (jsxRuntime.jsx(Td, { ...cellProps, children: content }, column.id));
|
|
15066
15919
|
})] }, rowId));
|
|
15067
15920
|
});
|
|
15068
|
-
}
|
|
15069
|
-
const tableElement = (jsxRuntime.jsxs(Table, { isBordered: isBordered, isStriped: isStriped, isNarrow: isNarrow, isHoverable: isHoverable, isFullwidth: isFullwidth, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-busy": loading || undefined, children: [captionNode, header, jsxRuntime.jsx(Tbody, { children: body })] }));
|
|
15070
|
-
// The name the scroll area and the pager take from the table.
|
|
15071
|
-
const textName = ariaLabel || (typeof caption === 'string' ? caption : undefined);
|
|
15072
|
-
const scrollName = ariaLabel
|
|
15073
|
-
? { 'aria-label': ariaLabel }
|
|
15074
|
-
: {
|
|
15075
|
-
'aria-labelledby': ariaLabelledby || (hasCaption ? nameId : undefined),
|
|
15921
|
+
}
|
|
15922
|
+
const tableElement = (jsxRuntime.jsxs(Table, { isBordered: isBordered, isStriped: isStriped, isNarrow: isNarrow, isHoverable: isHoverable, isFullwidth: isFullwidth, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-busy": loading || undefined, children: [captionNode, header, jsxRuntime.jsx(Tbody, { children: body })] }));
|
|
15923
|
+
// The name the scroll area and the pager take from the table.
|
|
15924
|
+
const textName = ariaLabel || (typeof caption === 'string' ? caption : undefined);
|
|
15925
|
+
const scrollName = ariaLabel
|
|
15926
|
+
? { 'aria-label': ariaLabel }
|
|
15927
|
+
: {
|
|
15928
|
+
'aria-labelledby': ariaLabelledby || (hasCaption ? nameId : undefined),
|
|
15929
|
+
};
|
|
15930
|
+
// Pagination names itself "pagination" unless given another name.
|
|
15931
|
+
const pagerLabel = words.pagination || (textName ? `${textName} pages` : undefined);
|
|
15932
|
+
const pagerName = pagerLabel ? { 'aria-label': pagerLabel } : {};
|
|
15933
|
+
const toolbarContent = toolbar?.(table);
|
|
15934
|
+
const hasToolbar = hasSearch || isShown(toolbarContent);
|
|
15935
|
+
return (jsxRuntime.jsxs("div", { ref: ref, className: rootClass, ...rest, children: [hasToolbar && (jsxRuntime.jsxs("div", { className: toolbarClass, children: [hasSearch && (jsxRuntime.jsx(Input, { label: words.search, ...table.getSearchProps() })), isShown(toolbarContent) && (jsxRuntime.jsx("div", { className: actionsClass, children: toolbarContent }))] })), isResponsive ? (jsxRuntime.jsx(ScrollArea, { orientation: "horizontal", ...scrollName, children: tableElement })) : (tableElement), jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", style: visuallyHidden, children: said }), pageCount > 1 && (jsxRuntime.jsx(Pagination, { ...table.getPaginationProps(), ...pagerName }))] }));
|
|
15936
|
+
});
|
|
15937
|
+
DataTable.displayName = 'DataTable';
|
|
15938
|
+
|
|
15939
|
+
/**
|
|
15940
|
+
* The `Affix` component keeps its content fixed to a corner of the viewport while the page scrolls under it.
|
|
15941
|
+
*
|
|
15942
|
+
* It's a `<div>` with no role, so what's inside keeps its own. It sits
|
|
15943
|
+
* `--bulma-affix-offset-vertical` and `--bulma-affix-offset-horizontal`
|
|
15944
|
+
* from its corner's edges, plus the device's safe-area inset, and stacks at
|
|
15945
|
+
* `--bulma-affix-z-index`, over the page's content and under its navbar,
|
|
15946
|
+
* static or fixed, its menus, popovers, the Sidebar, the modals, Tooltip and
|
|
15947
|
+
* Toast. It's as wide as its
|
|
15948
|
+
* content and never wider than the viewport less its offsets. It doesn't
|
|
15949
|
+
* scroll, so nothing inside it is clipped, and content that can be taller
|
|
15950
|
+
* than the screen scrolls itself. Its place in the reading and Tab order is
|
|
15951
|
+
* its place in the DOM, whatever corner it shows in.
|
|
15952
|
+
*
|
|
15953
|
+
* @function
|
|
15954
|
+
* @param {AffixProps} props - Props for the Affix component.
|
|
15955
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `<div>`.
|
|
15956
|
+
* @returns The `<div>`, in place or in its portal.
|
|
15957
|
+
*
|
|
15958
|
+
* @example
|
|
15959
|
+
* <Affix position="bottom-left">
|
|
15960
|
+
* <Button color="primary">Feedback</Button>
|
|
15961
|
+
* </Affix>
|
|
15962
|
+
*/
|
|
15963
|
+
const Affix = React.forwardRef(({ position = 'bottom-right', portal = false, className, children, ...props }, ref) => {
|
|
15964
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
15965
|
+
const affixClasses = usePrefixedClassNames('affix', {
|
|
15966
|
+
[`is-${position}`]: true,
|
|
15967
|
+
});
|
|
15968
|
+
// In place on the server and while hydrating, then into the portal, as
|
|
15969
|
+
// Modal does. The move remounts the `<div>`.
|
|
15970
|
+
const { isPortaled, container } = usePortalProp(portal);
|
|
15971
|
+
return (jsxRuntime.jsx(Portal, { disabled: !isPortaled, container: container, children: jsxRuntime.jsx("div", { ref: ref, className: classNames(affixClasses, bulmaHelperClasses, className), ...rest, children: children }) }));
|
|
15972
|
+
});
|
|
15973
|
+
Affix.displayName = 'Affix';
|
|
15974
|
+
|
|
15975
|
+
/**
|
|
15976
|
+
* How long the page has to stay still before a jump counts as over: long
|
|
15977
|
+
* enough to span the gap between two frames of a smooth scroll.
|
|
15978
|
+
*/
|
|
15979
|
+
const SETTLE_MS = 100;
|
|
15980
|
+
/**
|
|
15981
|
+
* A pixel of slack at the line, for a jump that lands a fraction of a pixel
|
|
15982
|
+
* away from it.
|
|
15983
|
+
*/
|
|
15984
|
+
const SLACK = 1;
|
|
15985
|
+
/** The page's `scroll-padding-top` in pixels, or 0 for `auto`. */
|
|
15986
|
+
function scrollPaddingTop() {
|
|
15987
|
+
const root = document.documentElement;
|
|
15988
|
+
const value = getComputedStyle(root).scrollPaddingTop;
|
|
15989
|
+
const number = parseFloat(value);
|
|
15990
|
+
if (!Number.isFinite(number))
|
|
15991
|
+
return 0;
|
|
15992
|
+
// A percentage is of the page's scrollport: the viewport less a classic
|
|
15993
|
+
// scrollbar.
|
|
15994
|
+
return value.endsWith('%') ? (number * root.clientHeight) / 100 : number;
|
|
15995
|
+
}
|
|
15996
|
+
/**
|
|
15997
|
+
* The height of the viewport the reader sees the page through, less a
|
|
15998
|
+
* classic scrollbar: the larger of the root's `clientHeight` and the visual
|
|
15999
|
+
* viewport's height. The visual viewport grows when a phone hides its
|
|
16000
|
+
* toolbar, where the root keeps the height it had with the toolbar shown;
|
|
16001
|
+
* under a pinch zoom it's the smaller, and the root's stands. Its `pageTop`
|
|
16002
|
+
* isn't read, since Firefox can leave it behind `scrollY` for a frame after
|
|
16003
|
+
* it puts back the scroll position on a reload.
|
|
16004
|
+
*/
|
|
16005
|
+
function viewportHeight() {
|
|
16006
|
+
return Math.max(document.documentElement.clientHeight, window.visualViewport?.height ?? 0);
|
|
16007
|
+
}
|
|
16008
|
+
/** Whether the page is scrolled as far down as it goes. */
|
|
16009
|
+
const atEnd = () => window.scrollY > 0 &&
|
|
16010
|
+
window.scrollY + viewportHeight() >=
|
|
16011
|
+
document.documentElement.scrollHeight - 1;
|
|
16012
|
+
/** Whether the browser draws an element: `display: none` draws no box. */
|
|
16013
|
+
const isDrawn = (element) => element.getClientRects().length > 0;
|
|
16014
|
+
/** The id a URL fragment names, such as `#caf%C3%A9`. */
|
|
16015
|
+
function fragmentId(hash) {
|
|
16016
|
+
const raw = hash.slice(1);
|
|
16017
|
+
try {
|
|
16018
|
+
return decodeURIComponent(raw);
|
|
16019
|
+
}
|
|
16020
|
+
catch {
|
|
16021
|
+
return raw;
|
|
16022
|
+
}
|
|
16023
|
+
}
|
|
16024
|
+
/**
|
|
16025
|
+
* Whether the page was opened afresh, as by a link, rather than reloaded or
|
|
16026
|
+
* reached by Back or Forward, where the browser puts it back where it was.
|
|
16027
|
+
* Where the navigation isn't recorded (jsdom), it counts as opened afresh.
|
|
16028
|
+
*/
|
|
16029
|
+
function openedAfresh() {
|
|
16030
|
+
const [entry] = performance.getEntriesByType?.('navigation') ?? [];
|
|
16031
|
+
const type = entry?.type;
|
|
16032
|
+
return type !== 'reload' && type !== 'back_forward';
|
|
16033
|
+
}
|
|
16034
|
+
/**
|
|
16035
|
+
* The link a click was on, if it's a plain click that follows it in this
|
|
16036
|
+
* page: the main button, no modifier key, and no `target` sending it
|
|
16037
|
+
* elsewhere. The path is read rather than the target, so a link inside a
|
|
16038
|
+
* shadow root counts.
|
|
16039
|
+
*/
|
|
16040
|
+
function followedLink(event) {
|
|
16041
|
+
if (event.button !== 0 ||
|
|
16042
|
+
event.metaKey ||
|
|
16043
|
+
event.ctrlKey ||
|
|
16044
|
+
event.shiftKey ||
|
|
16045
|
+
event.altKey) {
|
|
16046
|
+
return undefined;
|
|
16047
|
+
}
|
|
16048
|
+
const link = event
|
|
16049
|
+
.composedPath()
|
|
16050
|
+
.find((node) => node instanceof HTMLAnchorElement);
|
|
16051
|
+
return link && (!link.target || link.target === '_self') ? link : undefined;
|
|
16052
|
+
}
|
|
16053
|
+
/**
|
|
16054
|
+
* The `useScrollSpy` hook returns the id of the section being read: the last
|
|
16055
|
+
* of `ids` whose top is at or above the offset line, or the last one once the
|
|
16056
|
+
* page is scrolled to its end. After a jump to one of them, it's that one
|
|
16057
|
+
* until the reader scrolls on. `null` above the first section, on the server
|
|
16058
|
+
* and until the page hydrates.
|
|
16059
|
+
*
|
|
16060
|
+
* It checks once per frame while the page scrolls, and on resize. A jump is
|
|
16061
|
+
* a plain click on a link to one of the ids (the main button, no modifier
|
|
16062
|
+
* key, opening in this page), noted and never stopped, so the browser jumps
|
|
16063
|
+
* as it always does; and the `#id` a page opened afresh opens at. Back,
|
|
16064
|
+
* Forward and a reload aren't jumps. That `#id` is read once, when `ids`
|
|
16065
|
+
* first has some: a section rendered after that isn't held, even if the
|
|
16066
|
+
* page opened at it, and the reading rule marks where the page is. Once the
|
|
16067
|
+
* page has been still for a
|
|
16068
|
+
* moment, the section stays current only if it's where a jump puts it: its
|
|
16069
|
+
* top at the line, give or take a shift of the layout smaller than its own
|
|
16070
|
+
* height, or, at the end of the page, on screen below the line. Otherwise,
|
|
16071
|
+
* as after a flick right after the click, the reading rule takes over. The
|
|
16072
|
+
* next scroll after that lets it go, the browser's own included, so when
|
|
16073
|
+
* content loading above it makes the browser scroll to keep the reader's
|
|
16074
|
+
* place, the rule takes over too.
|
|
16075
|
+
*
|
|
16076
|
+
* A section that isn't drawn (`display: none`) is never current. A section
|
|
16077
|
+
* that moves while nothing scrolls is seen at the next scroll or resize. It
|
|
16078
|
+
* watches the document only: not a panel that scrolls inside it, and not
|
|
16079
|
+
* sections inside a shadow root, which an `#id` link can't reach either.
|
|
16080
|
+
* Whether a page was opened afresh is read from how the document loaded, so
|
|
16081
|
+
* in an app that changes routes on the client, a route change can briefly
|
|
16082
|
+
* mark the address bar's section until the page settles, when the reading
|
|
16083
|
+
* rule takes over, and after a reload a route change to an `#id` holds
|
|
16084
|
+
* nothing.
|
|
16085
|
+
*
|
|
16086
|
+
* @function
|
|
16087
|
+
* @param {UseScrollSpyOptions} options - The ids to watch, the offset, and a change callback.
|
|
16088
|
+
* @returns {string | null} The current section's id, or `null`.
|
|
16089
|
+
*
|
|
16090
|
+
* @example
|
|
16091
|
+
* const current = useScrollSpy({ ids: ['profile', 'security', 'billing'] });
|
|
16092
|
+
*/
|
|
16093
|
+
function useScrollSpy({ ids, offset, onCurrentChange, }) {
|
|
16094
|
+
const [active, setActive] = React.useState(null);
|
|
16095
|
+
// What was last reported, kept across a re-subscribe so the same id isn't
|
|
16096
|
+
// reported twice.
|
|
16097
|
+
const reportedRef = React.useRef(null);
|
|
16098
|
+
// The section a jump went to. It belongs to the hook rather than to one run
|
|
16099
|
+
// of the effect, so new ids, a new offset or React's development double run
|
|
16100
|
+
// don't lose it, and the `#id` the page opened at is read once, when the
|
|
16101
|
+
// ids first include some.
|
|
16102
|
+
const heldRef = React.useRef(null);
|
|
16103
|
+
const openedRef = React.useRef(false);
|
|
16104
|
+
const onCurrentChangeRef = useLatestRef(onCurrentChange);
|
|
16105
|
+
// The ids as a value, so a new array with the same ids each render doesn't
|
|
16106
|
+
// re-subscribe.
|
|
16107
|
+
const key = JSON.stringify(ids);
|
|
16108
|
+
const fixedOffset = typeof offset === 'number' && Number.isFinite(offset) ? offset : undefined;
|
|
16109
|
+
React.useEffect(() => {
|
|
16110
|
+
const watched = JSON.parse(key);
|
|
16111
|
+
let line = fixedOffset ?? scrollPaddingTop();
|
|
16112
|
+
let settled = true;
|
|
16113
|
+
let settleTimer;
|
|
16114
|
+
let frame = 0;
|
|
16115
|
+
const report = (id) => {
|
|
16116
|
+
if (id === reportedRef.current)
|
|
16117
|
+
return;
|
|
16118
|
+
reportedRef.current = id;
|
|
16119
|
+
setActive(id);
|
|
16120
|
+
onCurrentChangeRef.current?.(id);
|
|
16121
|
+
};
|
|
16122
|
+
/** The element for one of the ids, if it's on the page and drawn. */
|
|
16123
|
+
const sectionOf = (id) => {
|
|
16124
|
+
const element = watched.includes(id) ? document.getElementById(id) : null;
|
|
16125
|
+
return element && isDrawn(element) ? element : null;
|
|
16126
|
+
};
|
|
16127
|
+
/**
|
|
16128
|
+
* Whether a section is where a jump puts it: its top at the line, give or
|
|
16129
|
+
* take a shift of the layout smaller than the section's own height, or,
|
|
16130
|
+
* at the end of the page, as near as the page lets it come, on screen
|
|
16131
|
+
* below the line.
|
|
16132
|
+
*/
|
|
16133
|
+
const atJump = (id) => {
|
|
16134
|
+
const element = sectionOf(id);
|
|
16135
|
+
if (!element)
|
|
16136
|
+
return false;
|
|
16137
|
+
const { top, bottom, height } = element.getBoundingClientRect();
|
|
16138
|
+
if (top <= line + height && bottom >= line)
|
|
16139
|
+
return true;
|
|
16140
|
+
return top > line && top < viewportHeight() && atEnd();
|
|
16141
|
+
};
|
|
16142
|
+
const check = () => {
|
|
16143
|
+
if (heldRef.current !== null)
|
|
16144
|
+
return report(heldRef.current);
|
|
16145
|
+
const drawn = [];
|
|
16146
|
+
for (const id of watched) {
|
|
16147
|
+
const element = document.getElementById(id);
|
|
16148
|
+
if (element && isDrawn(element))
|
|
16149
|
+
drawn.push([id, element]);
|
|
16150
|
+
}
|
|
16151
|
+
let current = null;
|
|
16152
|
+
if (atEnd()) {
|
|
16153
|
+
// A short last section can't reach the line, so the end of the page
|
|
16154
|
+
// stands in for it.
|
|
16155
|
+
if (drawn.length)
|
|
16156
|
+
current = drawn[drawn.length - 1][0];
|
|
16157
|
+
}
|
|
16158
|
+
else {
|
|
16159
|
+
for (const [id, element] of drawn) {
|
|
16160
|
+
if (element.getBoundingClientRect().top <= line + SLACK)
|
|
16161
|
+
current = id;
|
|
16162
|
+
}
|
|
16163
|
+
}
|
|
16164
|
+
report(current);
|
|
16165
|
+
};
|
|
16166
|
+
/** Runs the check in the next frame, once however many events ask. */
|
|
16167
|
+
const schedule = () => {
|
|
16168
|
+
if (frame)
|
|
16169
|
+
return;
|
|
16170
|
+
frame = requestAnimationFrame(() => {
|
|
16171
|
+
frame = 0;
|
|
16172
|
+
check();
|
|
16173
|
+
});
|
|
16174
|
+
};
|
|
16175
|
+
/**
|
|
16176
|
+
* Starts, or starts again, the wait for the page to be still, when a
|
|
16177
|
+
* held section that isn't where a jump puts it is let go.
|
|
16178
|
+
*/
|
|
16179
|
+
const waitToSettle = () => {
|
|
16180
|
+
settled = false;
|
|
16181
|
+
clearTimeout(settleTimer);
|
|
16182
|
+
settleTimer = setTimeout(() => {
|
|
16183
|
+
settled = true;
|
|
16184
|
+
if (heldRef.current !== null && !atJump(heldRef.current)) {
|
|
16185
|
+
heldRef.current = null;
|
|
16186
|
+
}
|
|
16187
|
+
check();
|
|
16188
|
+
}, SETTLE_MS);
|
|
16189
|
+
};
|
|
16190
|
+
/** Holds the section a jump goes to, if it's one of these on the page. */
|
|
16191
|
+
const hold = (id) => {
|
|
16192
|
+
if (!sectionOf(id))
|
|
16193
|
+
return;
|
|
16194
|
+
heldRef.current = id;
|
|
16195
|
+
waitToSettle();
|
|
16196
|
+
};
|
|
16197
|
+
const onScroll = () => {
|
|
16198
|
+
if (heldRef.current !== null) {
|
|
16199
|
+
// Once the page has been still, the next scroll lets the jump go;
|
|
16200
|
+
// until then, each scroll pushes the end of the jump back.
|
|
16201
|
+
if (settled)
|
|
16202
|
+
heldRef.current = null;
|
|
16203
|
+
else
|
|
16204
|
+
waitToSettle();
|
|
16205
|
+
}
|
|
16206
|
+
schedule();
|
|
16207
|
+
};
|
|
16208
|
+
const onResize = () => {
|
|
16209
|
+
line = fixedOffset ?? scrollPaddingTop();
|
|
16210
|
+
schedule();
|
|
16211
|
+
};
|
|
16212
|
+
const onClick = (event) => {
|
|
16213
|
+
const link = followedLink(event);
|
|
16214
|
+
if (link?.hash &&
|
|
16215
|
+
link.href.split('#')[0] === window.location.href.split('#')[0]) {
|
|
16216
|
+
hold(fragmentId(link.hash));
|
|
16217
|
+
check();
|
|
16218
|
+
}
|
|
16219
|
+
};
|
|
16220
|
+
if (!openedRef.current) {
|
|
16221
|
+
// Ids built from content fetched after mount arrive later, so the read
|
|
16222
|
+
// waits until there are some.
|
|
16223
|
+
if (watched.length) {
|
|
16224
|
+
openedRef.current = true;
|
|
16225
|
+
if (openedAfresh())
|
|
16226
|
+
hold(fragmentId(window.location.hash));
|
|
16227
|
+
}
|
|
16228
|
+
}
|
|
16229
|
+
else if (heldRef.current !== null) {
|
|
16230
|
+
// A jump held across new ids carries on if its section still counts.
|
|
16231
|
+
if (sectionOf(heldRef.current))
|
|
16232
|
+
waitToSettle();
|
|
16233
|
+
else
|
|
16234
|
+
heldRef.current = null;
|
|
16235
|
+
}
|
|
16236
|
+
check();
|
|
16237
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
16238
|
+
window.addEventListener('resize', onResize);
|
|
16239
|
+
document.addEventListener('click', onClick, {
|
|
16240
|
+
capture: true,
|
|
16241
|
+
passive: true,
|
|
16242
|
+
});
|
|
16243
|
+
return () => {
|
|
16244
|
+
cancelAnimationFrame(frame);
|
|
16245
|
+
clearTimeout(settleTimer);
|
|
16246
|
+
window.removeEventListener('scroll', onScroll);
|
|
16247
|
+
window.removeEventListener('resize', onResize);
|
|
16248
|
+
document.removeEventListener('click', onClick, { capture: true });
|
|
15076
16249
|
};
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
const toolbarContent = toolbar?.(table);
|
|
15081
|
-
const hasToolbar = hasSearch || isShown(toolbarContent);
|
|
15082
|
-
return (jsxRuntime.jsxs("div", { ref: ref, className: rootClass, ...rest, children: [hasToolbar && (jsxRuntime.jsxs("div", { className: toolbarClass, children: [hasSearch && (jsxRuntime.jsx(Input, { label: words.search, ...table.getSearchProps() })), isShown(toolbarContent) && (jsxRuntime.jsx("div", { className: actionsClass, children: toolbarContent }))] })), isResponsive ? (jsxRuntime.jsx(ScrollArea, { orientation: "horizontal", ...scrollName, children: tableElement })) : (tableElement), jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", style: visuallyHidden, children: said }), pageCount > 1 && (jsxRuntime.jsx(Pagination, { ...table.getPaginationProps(), ...pagerName }))] }));
|
|
15083
|
-
});
|
|
15084
|
-
DataTable.displayName = 'DataTable';
|
|
16250
|
+
}, [key, fixedOffset, onCurrentChangeRef]);
|
|
16251
|
+
return active;
|
|
16252
|
+
}
|
|
15085
16253
|
|
|
16254
|
+
/** Every id in the tree, in page order. */
|
|
16255
|
+
const idsOf = (items) => items.flatMap(item => [item.id, ...idsOf(item.children ?? [])]);
|
|
15086
16256
|
/**
|
|
15087
|
-
* The `
|
|
16257
|
+
* The entries as `MenuItem` links, each entry's own entries in a nested
|
|
16258
|
+
* `MenuList`, and the current one marked.
|
|
16259
|
+
*/
|
|
16260
|
+
const entries = (items, active) => items.map(item => {
|
|
16261
|
+
const current = item.id === active;
|
|
16262
|
+
return (jsxRuntime.jsxs(MenuItem, { href: `#${encodeURIComponent(item.id)}`, active: current, "aria-current": current ? 'location' : undefined, children: [item.label, item.children?.length ? (jsxRuntime.jsx(MenuList, { children: entries(item.children, active) })) : null] }, item.id));
|
|
16263
|
+
});
|
|
16264
|
+
/**
|
|
16265
|
+
* The `TableOfContents` component lists a long page's sections as links in Bulma's menu look, and marks the one being read.
|
|
15088
16266
|
*
|
|
15089
|
-
* It's a `<
|
|
15090
|
-
*
|
|
15091
|
-
*
|
|
15092
|
-
*
|
|
15093
|
-
*
|
|
15094
|
-
* Toast. It's as wide as its
|
|
15095
|
-
* content and never wider than the viewport less its offsets. It doesn't
|
|
15096
|
-
* scroll, so nothing inside it is clipped, and content that can be taller
|
|
15097
|
-
* than the screen scrolls itself. Its place in the reading and Tab order is
|
|
15098
|
-
* its place in the DOM, whatever corner it shows in.
|
|
16267
|
+
* It's a `<nav>` named by its title, holding `MenuList` and `MenuItem` links
|
|
16268
|
+
* to each `#id`, rendered in full on the server. `useScrollSpy` picks the
|
|
16269
|
+
* current section, and its link alone gets `is-active` and
|
|
16270
|
+
* `aria-current="location"`. Use the hook on its own to mark the section in
|
|
16271
|
+
* view somewhere else, such as an app's nav `Menu`.
|
|
15099
16272
|
*
|
|
15100
16273
|
* @function
|
|
15101
|
-
* @param {
|
|
15102
|
-
* @param {React.Ref<
|
|
15103
|
-
* @returns The
|
|
16274
|
+
* @param {TableOfContentsProps} props - Props for the TableOfContents component.
|
|
16275
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<nav>`.
|
|
16276
|
+
* @returns {JSX.Element} The rendered table of contents.
|
|
15104
16277
|
*
|
|
15105
16278
|
* @example
|
|
15106
|
-
* <
|
|
15107
|
-
*
|
|
15108
|
-
*
|
|
16279
|
+
* <TableOfContents
|
|
16280
|
+
* isSticky
|
|
16281
|
+
* items={[
|
|
16282
|
+
* { id: 'install', label: 'Install' },
|
|
16283
|
+
* { id: 'usage', label: 'Usage', children: [{ id: 'props', label: 'Props' }] },
|
|
16284
|
+
* ]}
|
|
16285
|
+
* />
|
|
15109
16286
|
*/
|
|
15110
|
-
const
|
|
15111
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses(
|
|
15112
|
-
|
|
15113
|
-
|
|
16287
|
+
const TableOfContents = React.forwardRef(({ className, textColor, bgColor, items, onCurrentChange, offset, isSticky, labels, ...props }, ref) => {
|
|
16288
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
16289
|
+
color: textColor,
|
|
16290
|
+
backgroundColor: bgColor,
|
|
16291
|
+
...props,
|
|
15114
16292
|
});
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
const
|
|
15118
|
-
|
|
16293
|
+
const titleId = React.useId();
|
|
16294
|
+
const active = useScrollSpy({ ids: idsOf(items), offset, onCurrentChange });
|
|
16295
|
+
const navRef = React.useRef(null);
|
|
16296
|
+
const setRefs = React.useCallback((node) => {
|
|
16297
|
+
navRef.current = node;
|
|
16298
|
+
if (typeof ref === 'function')
|
|
16299
|
+
return ref(node);
|
|
16300
|
+
if (ref)
|
|
16301
|
+
ref.current = node;
|
|
16302
|
+
}, [ref]);
|
|
16303
|
+
// A list taller than the room it has scrolls itself to keep the current
|
|
16304
|
+
// link in view. Only the list moves: scrollIntoView would move the page
|
|
16305
|
+
// too whenever the link is off screen.
|
|
16306
|
+
React.useEffect(() => {
|
|
16307
|
+
const nav = navRef.current;
|
|
16308
|
+
const link = nav.querySelector('a[aria-current="location"]');
|
|
16309
|
+
if (!link || nav.scrollHeight <= nav.clientHeight)
|
|
16310
|
+
return;
|
|
16311
|
+
const top = nav.getBoundingClientRect().top + nav.clientTop;
|
|
16312
|
+
const bottom = top + nav.clientHeight;
|
|
16313
|
+
const at = link.getBoundingClientRect();
|
|
16314
|
+
if (at.top < top)
|
|
16315
|
+
nav.scrollTop -= top - at.top;
|
|
16316
|
+
else if (at.bottom > bottom)
|
|
16317
|
+
nav.scrollTop += at.bottom - bottom;
|
|
16318
|
+
}, [active]);
|
|
16319
|
+
// Bulma's menu, which draws it, and its own class beside it.
|
|
16320
|
+
const menuClass = usePrefixedClassNames('menu');
|
|
16321
|
+
const rootClass = usePrefixedClassNames('table-of-contents', {
|
|
16322
|
+
'is-sticky': !!isSticky,
|
|
16323
|
+
});
|
|
16324
|
+
return (jsxRuntime.jsxs("nav", { ref: setRefs, className: classNames(menuClass, rootClass, bulmaHelperClasses, className), "aria-labelledby": titleId, ...rest, children: [jsxRuntime.jsx(MenuLabel, { id: titleId, children: labels?.title || 'On this page' }), jsxRuntime.jsx(MenuList, { children: entries(items, active) })] }));
|
|
15119
16325
|
});
|
|
15120
|
-
|
|
16326
|
+
TableOfContents.displayName = 'TableOfContents';
|
|
15121
16327
|
|
|
15122
16328
|
/**
|
|
15123
16329
|
* The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
|
|
@@ -15433,56 +16639,6 @@ const Emphasis = ({ className, textColor, bgColor, children, ...props }) => {
|
|
|
15433
16639
|
return (jsxRuntime.jsx("em", { className: emphasisClasses || undefined, ...rest, children: children }));
|
|
15434
16640
|
};
|
|
15435
16641
|
|
|
15436
|
-
/**
|
|
15437
|
-
* FigureCaption component for rendering a styled figcaption element.
|
|
15438
|
-
*
|
|
15439
|
-
* A FigureCaption wraps the HTML `<figcaption>` element with Bulma helper class integration.
|
|
15440
|
-
* Use it inside Figure components to provide captions for images or other media.
|
|
15441
|
-
*
|
|
15442
|
-
* @function
|
|
15443
|
-
* @param {FigureCaptionProps} props - Props for the FigureCaption component.
|
|
15444
|
-
* @returns {JSX.Element} The rendered figcaption element.
|
|
15445
|
-
*/
|
|
15446
|
-
const FigureCaption = ({ className, textColor, bgColor, children, ...props }) => {
|
|
15447
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
15448
|
-
color: textColor,
|
|
15449
|
-
backgroundColor: bgColor,
|
|
15450
|
-
...props,
|
|
15451
|
-
});
|
|
15452
|
-
const bulmaClasses = usePrefixedClassNames();
|
|
15453
|
-
const captionClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
15454
|
-
return (jsxRuntime.jsx("figcaption", { className: captionClasses || undefined, ...rest, children: children }));
|
|
15455
|
-
};
|
|
15456
|
-
/**
|
|
15457
|
-
* The `Figure` component renders a styled `<figure>` element with Bulma helper class integration.
|
|
15458
|
-
*
|
|
15459
|
-
* @function
|
|
15460
|
-
* @param {FigureProps} props - Props for the Figure component.
|
|
15461
|
-
* @returns {JSX.Element} The rendered figure element.
|
|
15462
|
-
*/
|
|
15463
|
-
const FigureComponent = ({ className, textColor, bgColor, children, ...props }) => {
|
|
15464
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
15465
|
-
color: textColor,
|
|
15466
|
-
backgroundColor: bgColor,
|
|
15467
|
-
...props,
|
|
15468
|
-
});
|
|
15469
|
-
const bulmaClasses = usePrefixedClassNames();
|
|
15470
|
-
const figureClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
15471
|
-
return (jsxRuntime.jsx("figure", { className: figureClasses || undefined, ...rest, children: children }));
|
|
15472
|
-
};
|
|
15473
|
-
/**
|
|
15474
|
-
* Figure component with Caption subcomponent.
|
|
15475
|
-
*
|
|
15476
|
-
* @example
|
|
15477
|
-
* <Figure>
|
|
15478
|
-
* <img src="image.jpg" alt="Description" />
|
|
15479
|
-
* <Figure.Caption>Image caption text</Figure.Caption>
|
|
15480
|
-
* </Figure>
|
|
15481
|
-
*/
|
|
15482
|
-
const Figure = withSubComponents(FigureComponent, {
|
|
15483
|
-
Caption: FigureCaption,
|
|
15484
|
-
}, 'Figure');
|
|
15485
|
-
|
|
15486
16642
|
/**
|
|
15487
16643
|
* The `IconText` component provides a Bulma-styled horizontal arrangement of one or more `Icon` components and optional text.
|
|
15488
16644
|
*
|
|
@@ -15662,36 +16818,6 @@ const ListItem = ({ className, textColor, bgColor, children, ...props }) => {
|
|
|
15662
16818
|
return (jsxRuntime.jsx("li", { className: itemClasses || undefined, ...rest, children: children }));
|
|
15663
16819
|
};
|
|
15664
16820
|
|
|
15665
|
-
/**
|
|
15666
|
-
* The `Loader` component renders Bulma's `.loader`, a small spinning ring for inline loading states.
|
|
15667
|
-
*
|
|
15668
|
-
* The ring is `1em` square, so `textSize` scales it. It is exposed as an
|
|
15669
|
-
* indeterminate `progressbar` named by `ariaLabel`, and it stops spinning
|
|
15670
|
-
* under `prefers-reduced-motion: reduce` while staying drawn. In forced
|
|
15671
|
-
* colors its two open sides take the page's color, so it stays open.
|
|
15672
|
-
*
|
|
15673
|
-
* @function
|
|
15674
|
-
* @param {LoaderProps} props - Props for the Loader component.
|
|
15675
|
-
* @param {React.Ref<HTMLSpanElement>} ref - Forwarded ref to the loader element.
|
|
15676
|
-
* @returns {JSX.Element} The rendered loader element.
|
|
15677
|
-
*
|
|
15678
|
-
* @example
|
|
15679
|
-
* <Loader />
|
|
15680
|
-
*
|
|
15681
|
-
* @example
|
|
15682
|
-
* <Loader textSize="3" ariaLabel="Saving" />
|
|
15683
|
-
*/
|
|
15684
|
-
const Loader = React.forwardRef(({ className, ariaLabel = 'Loading', ...props }, ref) => {
|
|
15685
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
15686
|
-
const loaderClass = usePrefixedClassNames('loader');
|
|
15687
|
-
// role="progressbar", not role="status": a status region mounted together
|
|
15688
|
-
// with its text is not reliably announced, so the loader claims only what
|
|
15689
|
-
// it can deliver, a named indicator found by reading the page. No
|
|
15690
|
-
// aria-valuenow, because the progress is indeterminate.
|
|
15691
|
-
return (jsxRuntime.jsx("span", { ref: ref, className: classNames(loaderClass, bulmaHelperClasses, className), role: "progressbar", "aria-label": ariaLabel, ...rest }));
|
|
15692
|
-
});
|
|
15693
|
-
Loader.displayName = 'Loader';
|
|
15694
|
-
|
|
15695
16821
|
/**
|
|
15696
16822
|
* The `Notification` component is a Bulma-styled alert/message area for providing feedback, warnings, or information to users.
|
|
15697
16823
|
*
|
|
@@ -22386,6 +23512,167 @@ const TextAreaBase = React.forwardRef(({ color, size, isRounded, isStatic, isHov
|
|
|
22386
23512
|
});
|
|
22387
23513
|
TextAreaBase.displayName = 'TextAreaBase';
|
|
22388
23514
|
|
|
23515
|
+
/** An eye, drawn while the password is hidden. */
|
|
23516
|
+
const EyeIcon = () => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "1.25em", height: "1.25em", "aria-hidden": "true", children: jsxRuntime.jsxs("g", { fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "M1.5 8s2.5-4.5 6.5-4.5 6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8z" }), jsxRuntime.jsx("circle", { cx: "8", cy: "8", r: "2" })] }) }));
|
|
23517
|
+
/** The eye crossed out, drawn while the password shows. */
|
|
23518
|
+
const EyeOffIcon = () => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "1.25em", height: "1.25em", "aria-hidden": "true", children: jsxRuntime.jsxs("g", { fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "M1.5 8s2.5-4.5 6.5-4.5 6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8z" }), jsxRuntime.jsx("circle", { cx: "8", cy: "8", r: "2" }), jsxRuntime.jsx("path", { d: "M2.5 2.5l11 11" })] }) }));
|
|
23519
|
+
/**
|
|
23520
|
+
* Warns in development when the `text` toggle's name doesn't contain the
|
|
23521
|
+
* word on it, which a voice control user says to press it.
|
|
23522
|
+
*/
|
|
23523
|
+
const checkLabelInName = (name, word) => {
|
|
23524
|
+
if (name.toLocaleLowerCase().includes(word.toLocaleLowerCase()))
|
|
23525
|
+
return;
|
|
23526
|
+
warnOnce(`PasswordInput:label-in-name:${name}:${word}`, `[bestax-bulma] A PasswordInput with variant="text" is named ` +
|
|
23527
|
+
`"${name}" while its button says "${word}". A voice control user ` +
|
|
23528
|
+
`says the word they see, so the name has to contain it (WCAG 2.5.3, ` +
|
|
23529
|
+
`Label in Name). Set labels so each name contains its word.`);
|
|
23530
|
+
};
|
|
23531
|
+
/**
|
|
23532
|
+
* The raw PasswordInput: a password field with a button inside it that shows what was typed, without Field/Control wrapping.
|
|
23533
|
+
*
|
|
23534
|
+
* Use `PasswordInput` for the convenience wrapper.
|
|
23535
|
+
*
|
|
23536
|
+
* @function
|
|
23537
|
+
* @param {PasswordInputBaseProps} props - Props for PasswordInputBase.
|
|
23538
|
+
* @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.
|
|
23539
|
+
* @returns {JSX.Element} The input and its toggle, in a container.
|
|
23540
|
+
*/
|
|
23541
|
+
const PasswordInputBase = React.forwardRef((props, ref) => {
|
|
23542
|
+
const { visible: visibleProp, defaultVisible = false, onVisibleChange, variant = 'icon', labels, autoComplete = 'current-password', spellCheck = false, autoCorrect = 'off', autoCapitalize = 'none', disabled: disabledProp, color, className, style, ...others } = props;
|
|
23543
|
+
// Helper props, `className` and `style` go on the container, so a margin,
|
|
23544
|
+
// a width or a display moves the field and its toggle together. `color`
|
|
23545
|
+
// stays with the input, whose border it colors.
|
|
23546
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(others);
|
|
23547
|
+
const disabled = useFieldsetDisabled(disabledProp);
|
|
23548
|
+
// The toggle gives way to the loading spinner of a Control this sits in,
|
|
23549
|
+
// which takes its slot, and the field hides the password meanwhile, as
|
|
23550
|
+
// there's no button to hide it with.
|
|
23551
|
+
const controlLoading = useControlLoading();
|
|
23552
|
+
const isText = variant === 'text';
|
|
23553
|
+
const showPassword = labels?.showPassword || 'Show password';
|
|
23554
|
+
const hidePassword = labels?.hidePassword || 'Hide password';
|
|
23555
|
+
const show = labels?.show || 'Show';
|
|
23556
|
+
const hide = labels?.hide || 'Hide';
|
|
23557
|
+
if (isText) {
|
|
23558
|
+
checkLabelInName(showPassword, show);
|
|
23559
|
+
checkLabelInName(hidePassword, hide);
|
|
23560
|
+
}
|
|
23561
|
+
const isControlled = visibleProp !== undefined;
|
|
23562
|
+
const [internalVisible, setInternalVisible] = React.useState(defaultVisible);
|
|
23563
|
+
const visible = isControlled ? visibleProp : internalVisible;
|
|
23564
|
+
// Whether the field shows now: as last reported, until `visible` changes.
|
|
23565
|
+
// Two hide events before React renders, as from an effect, or a caller
|
|
23566
|
+
// holding `visible` that keeps it `true`, report `false` once.
|
|
23567
|
+
const shownRef = React.useRef(visible);
|
|
23568
|
+
useClientLayoutEffect(() => {
|
|
23569
|
+
shownRef.current = visible;
|
|
23570
|
+
}, [visible]);
|
|
23571
|
+
const setVisible = (next) => {
|
|
23572
|
+
shownRef.current = next;
|
|
23573
|
+
if (!isControlled)
|
|
23574
|
+
setInternalVisible(next);
|
|
23575
|
+
onVisibleChange?.(next);
|
|
23576
|
+
};
|
|
23577
|
+
const inputRef = React.useRef(null);
|
|
23578
|
+
// A caller's callback ref may return a cleanup, as React 19 allows. It's
|
|
23579
|
+
// held here and run on detach in place of a call with `null`, on React 18
|
|
23580
|
+
// as well, since this ref is the one React calls.
|
|
23581
|
+
const refCleanup = React.useRef(null);
|
|
23582
|
+
const combinedRef = React.useCallback((node) => {
|
|
23583
|
+
inputRef.current = node;
|
|
23584
|
+
if (typeof ref === 'function') {
|
|
23585
|
+
if (node === null) {
|
|
23586
|
+
const cleanup = refCleanup.current;
|
|
23587
|
+
refCleanup.current = null;
|
|
23588
|
+
if (cleanup)
|
|
23589
|
+
cleanup();
|
|
23590
|
+
else
|
|
23591
|
+
ref(null);
|
|
23592
|
+
return;
|
|
23593
|
+
}
|
|
23594
|
+
const cleanup = ref(node);
|
|
23595
|
+
refCleanup.current =
|
|
23596
|
+
typeof cleanup === 'function' ? cleanup : null;
|
|
23597
|
+
}
|
|
23598
|
+
else if (ref) {
|
|
23599
|
+
ref.current = node;
|
|
23600
|
+
}
|
|
23601
|
+
}, [ref]);
|
|
23602
|
+
// A toggle with focus that leaves while the input stays, as it does when a
|
|
23603
|
+
// Control starts loading, hands focus to the input rather than dropping it
|
|
23604
|
+
// to the page. React lets go of the toggle before it takes it out of the
|
|
23605
|
+
// page, so the toggle still has focus here; when the whole field unmounts,
|
|
23606
|
+
// React has let go of the input first.
|
|
23607
|
+
const toggleRef = React.useRef(null);
|
|
23608
|
+
const setToggle = React.useCallback((node) => {
|
|
23609
|
+
// React lets go only of a toggle it handed over, so there is one.
|
|
23610
|
+
const leaving = toggleRef.current;
|
|
23611
|
+
toggleRef.current = node;
|
|
23612
|
+
if (node)
|
|
23613
|
+
return;
|
|
23614
|
+
const root = leaving.getRootNode();
|
|
23615
|
+
if (root.activeElement === leaving)
|
|
23616
|
+
inputRef.current?.focus();
|
|
23617
|
+
}, []);
|
|
23618
|
+
// Hides the password again: reports `false`, and sets its own state when it
|
|
23619
|
+
// holds it. Inside `flushSync`, React renders that, a caller's own state
|
|
23620
|
+
// update included, before the listener returns, so the browser reads the
|
|
23621
|
+
// form, or saves the page's form state, from a password field. The
|
|
23622
|
+
// component never sets the field's type itself, so a caller holding
|
|
23623
|
+
// `visible` that keeps it `true` keeps the field shown. The one catch: an
|
|
23624
|
+
// event fired while React is rendering or committing, which includes
|
|
23625
|
+
// `useEffect` and `useLayoutEffect` callbacks (a `requestSubmit()` from
|
|
23626
|
+
// an effect), can't flush there, so React warns in development and the
|
|
23627
|
+
// hide lands after the form is read. Only the listeners below call it,
|
|
23628
|
+
// while the input is mounted.
|
|
23629
|
+
const hideRef = useLatestRef(() => {
|
|
23630
|
+
if (!shownRef.current)
|
|
23631
|
+
return;
|
|
23632
|
+
// eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the form is read, or the page saved, before React would render this on its own
|
|
23633
|
+
reactDom.flushSync(() => setVisible(false));
|
|
23634
|
+
});
|
|
23635
|
+
// When the form it belongs to submits or resets: whichever form that is
|
|
23636
|
+
// when it happens, so a form mounted after the field, or mounted again,
|
|
23637
|
+
// counts. Neither event leaves a shadow root, so it listens on the field's
|
|
23638
|
+
// own root, the document or the shadow root it renders in, and in the
|
|
23639
|
+
// capture phase, before any listener on the form.
|
|
23640
|
+
React.useEffect(() => {
|
|
23641
|
+
const input = inputRef.current;
|
|
23642
|
+
const root = input.getRootNode();
|
|
23643
|
+
const onFormEvent = (event) => {
|
|
23644
|
+
if (event.target === input.form)
|
|
23645
|
+
hideRef.current();
|
|
23646
|
+
};
|
|
23647
|
+
root.addEventListener('submit', onFormEvent, true);
|
|
23648
|
+
root.addEventListener('reset', onFormEvent, true);
|
|
23649
|
+
return () => {
|
|
23650
|
+
root.removeEventListener('submit', onFormEvent, true);
|
|
23651
|
+
root.removeEventListener('reset', onFormEvent, true);
|
|
23652
|
+
};
|
|
23653
|
+
}, [hideRef]);
|
|
23654
|
+
// When the page is left, so the history the browser keeps of the form
|
|
23655
|
+
// holds a password field rather than text. It fires on the way into the
|
|
23656
|
+
// back-forward cache too, so a page coming back from there is hidden.
|
|
23657
|
+
React.useEffect(() => {
|
|
23658
|
+
const onPageHide = () => hideRef.current();
|
|
23659
|
+
window.addEventListener('pagehide', onPageHide);
|
|
23660
|
+
return () => window.removeEventListener('pagehide', onPageHide);
|
|
23661
|
+
}, [hideRef]);
|
|
23662
|
+
const containerClass = usePrefixedClassNames('password-input-container', {
|
|
23663
|
+
'is-text': isText,
|
|
23664
|
+
});
|
|
23665
|
+
const toggleClass = usePrefixedClassNames('password-input-toggle');
|
|
23666
|
+
// The icon keeps one name and reports its state; the words change, so
|
|
23667
|
+
// the name follows them and there's no state to report.
|
|
23668
|
+
const toggleName = isText && visible ? hidePassword : showPassword;
|
|
23669
|
+
const glyph = visible ? jsxRuntime.jsx(EyeOffIcon, {}) : jsxRuntime.jsx(EyeIcon, {});
|
|
23670
|
+
return (jsxRuntime.jsxs("div", { className: classNames(containerClass, bulmaHelperClasses, className), style: style, children: [jsxRuntime.jsx(InputBase, { ref: combinedRef, autoComplete: autoComplete, spellCheck: spellCheck, autoCorrect: autoCorrect, autoCapitalize: autoCapitalize, disabled: disabled, color: color, ...rest, type: visible && !controlLoading ? 'text' : 'password' }), !controlLoading && (jsxRuntime.jsx("button", { ref: setToggle, type: "button", className: toggleClass, "aria-label": toggleName, "aria-pressed": isText ? undefined : visible, disabled: disabled,
|
|
23671
|
+
// A pointer press leaves focus, and the caret, in the input.
|
|
23672
|
+
onMouseDown: event => event.preventDefault(), onClick: () => setVisible(!visible), children: isText ? (visible ? hide : show) : glyph }))] }));
|
|
23673
|
+
});
|
|
23674
|
+
PasswordInputBase.displayName = 'PasswordInputBase';
|
|
23675
|
+
|
|
22389
23676
|
/**
|
|
22390
23677
|
* The `Select` component provides a Bulma-styled dropdown for selecting one or more options.
|
|
22391
23678
|
*
|
|
@@ -22520,6 +23807,97 @@ fieldClassName, controlClassName,
|
|
|
22520
23807
|
});
|
|
22521
23808
|
TextArea.displayName = 'TextArea';
|
|
22522
23809
|
|
|
23810
|
+
/** The right-icon props, which the toggle's place leaves no room for. */
|
|
23811
|
+
const RIGHT_ICON_PROPS = [
|
|
23812
|
+
'iconRight',
|
|
23813
|
+
'iconRightName',
|
|
23814
|
+
'iconRightSize',
|
|
23815
|
+
'hasIconsRight',
|
|
23816
|
+
];
|
|
23817
|
+
/**
|
|
23818
|
+
* The `PasswordInput` component is a password field with a button inside it that shows what was typed.
|
|
23819
|
+
*
|
|
23820
|
+
* By default the button is an eye named "Show password" that reports
|
|
23821
|
+
* whether the password shows with `aria-pressed`; `variant="text"` makes it
|
|
23822
|
+
* the words "Show" and "Hide". The field asks for
|
|
23823
|
+
* `autocomplete="current-password"` and no spellcheck, and hides the
|
|
23824
|
+
* password again when its form submits.
|
|
23825
|
+
*
|
|
23826
|
+
* @function
|
|
23827
|
+
* @param {PasswordInputProps} props - Props for PasswordInput.
|
|
23828
|
+
* @returns {JSX.Element} The composed field element.
|
|
23829
|
+
*
|
|
23830
|
+
* @example
|
|
23831
|
+
* <PasswordInput label="Password" />
|
|
23832
|
+
*
|
|
23833
|
+
* @example
|
|
23834
|
+
* <PasswordInput label="New password" autoComplete="new-password" variant="text" />
|
|
23835
|
+
*/
|
|
23836
|
+
const PasswordInput = React.forwardRef((props, ref) => {
|
|
23837
|
+
const {
|
|
23838
|
+
// Field props
|
|
23839
|
+
label, labelSize, labelProps, horizontal,
|
|
23840
|
+
// Control props
|
|
23841
|
+
iconLeft, iconLeftName, iconLeftSize, hasIconsLeft, isLoading, isExpanded, controlSize,
|
|
23842
|
+
// Message props
|
|
23843
|
+
message, messageColor,
|
|
23844
|
+
// Container class overrides
|
|
23845
|
+
fieldClassName, controlClassName,
|
|
23846
|
+
// Not offered, and not passed on
|
|
23847
|
+
iconRight: _iconRight, iconRightName: _iconRightName, iconRightSize: _iconRightSize, hasIconsRight: _hasIconsRight,
|
|
23848
|
+
// Everything else goes to the base
|
|
23849
|
+
...baseProps } = props;
|
|
23850
|
+
const rightIcon = RIGHT_ICON_PROPS.filter(prop => props[prop]);
|
|
23851
|
+
if (rightIcon.length > 0) {
|
|
23852
|
+
const names = rightIcon.join(' ');
|
|
23853
|
+
warnOnce(`PasswordInput:right-icon:${names}`, `[bestax-bulma] <PasswordInput ${names}> draws no right icon: the ` +
|
|
23854
|
+
`visibility toggle sits at that edge. Leave ` +
|
|
23855
|
+
`${rightIcon.length > 1 ? 'them' : 'it'} out, or use a left icon.`);
|
|
23856
|
+
}
|
|
23857
|
+
const insideField = useInsideField();
|
|
23858
|
+
const insideControl = useInsideControl();
|
|
23859
|
+
const ownField = rendersOwnField('PasswordInput', {
|
|
23860
|
+
insideField,
|
|
23861
|
+
insideControl,
|
|
23862
|
+
label,
|
|
23863
|
+
message,
|
|
23864
|
+
horizontal,
|
|
23865
|
+
fieldClassName,
|
|
23866
|
+
});
|
|
23867
|
+
const ownControl = rendersOwnControl('PasswordInput', {
|
|
23868
|
+
insideControl,
|
|
23869
|
+
isLoading,
|
|
23870
|
+
iconLeft,
|
|
23871
|
+
iconLeftName,
|
|
23872
|
+
iconLeftSize,
|
|
23873
|
+
hasIconsLeft,
|
|
23874
|
+
isExpanded,
|
|
23875
|
+
controlSize,
|
|
23876
|
+
controlClassName,
|
|
23877
|
+
});
|
|
23878
|
+
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
23879
|
+
label,
|
|
23880
|
+
id: baseProps.id,
|
|
23881
|
+
labelProps,
|
|
23882
|
+
rendersLabel: ownField,
|
|
23883
|
+
});
|
|
23884
|
+
const helpClass = usePrefixedClassNames('help', {
|
|
23885
|
+
[`is-${messageColor}`]: !!messageColor,
|
|
23886
|
+
});
|
|
23887
|
+
// The base hides its toggle while the Control it sits in is loading,
|
|
23888
|
+
// whether that is the one rendered below or an enclosing one.
|
|
23889
|
+
let content = (jsxRuntime.jsx(PasswordInputBase, { ref: ref, ...baseProps, id: controlId }));
|
|
23890
|
+
if (ownControl) {
|
|
23891
|
+
content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
23892
|
+
}
|
|
23893
|
+
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
23894
|
+
if (ownField) {
|
|
23895
|
+
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
23896
|
+
}
|
|
23897
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
23898
|
+
});
|
|
23899
|
+
PasswordInput.displayName = 'PasswordInput';
|
|
23900
|
+
|
|
22523
23901
|
function isSameDay(a, b) {
|
|
22524
23902
|
return (a.getFullYear() === b.getFullYear() &&
|
|
22525
23903
|
a.getMonth() === b.getMonth() &&
|
|
@@ -29881,6 +31259,8 @@ exports.Level = Level;
|
|
|
29881
31259
|
exports.LevelItem = LevelItem;
|
|
29882
31260
|
exports.LevelLeft = LevelLeft;
|
|
29883
31261
|
exports.LevelRight = LevelRight;
|
|
31262
|
+
exports.Lightbox = Lightbox;
|
|
31263
|
+
exports.LightboxTrigger = LightboxTrigger;
|
|
29884
31264
|
exports.Link = Link;
|
|
29885
31265
|
exports.LinkButton = LinkButton;
|
|
29886
31266
|
exports.ListItem = ListItem;
|
|
@@ -29930,6 +31310,8 @@ exports.PanelIcon = PanelIcon;
|
|
|
29930
31310
|
exports.PanelInputBlock = PanelInputBlock;
|
|
29931
31311
|
exports.PanelTabs = PanelTabs;
|
|
29932
31312
|
exports.Paragraph = Paragraph;
|
|
31313
|
+
exports.PasswordInput = PasswordInput;
|
|
31314
|
+
exports.PasswordInputBase = PasswordInputBase;
|
|
29933
31315
|
exports.PinInput = PinInput;
|
|
29934
31316
|
exports.Popover = Popover;
|
|
29935
31317
|
exports.PopoverBody = PopoverBody;
|
|
@@ -29973,6 +31355,7 @@ exports.TabContentItem = TabContentItem;
|
|
|
29973
31355
|
exports.TabItem = TabItem;
|
|
29974
31356
|
exports.TabList = TabList;
|
|
29975
31357
|
exports.Table = Table;
|
|
31358
|
+
exports.TableOfContents = TableOfContents;
|
|
29976
31359
|
exports.Tabs = Tabs;
|
|
29977
31360
|
exports.TabsContent = TabsContent;
|
|
29978
31361
|
exports.Tag = Tag;
|
|
@@ -30034,6 +31417,7 @@ exports.useNumberFormat = useNumberFormat;
|
|
|
30034
31417
|
exports.useOtherClasses = useOtherClasses;
|
|
30035
31418
|
exports.usePrefixedClass = usePrefixedClass;
|
|
30036
31419
|
exports.usePrefixedClassNames = usePrefixedClassNames;
|
|
31420
|
+
exports.useScrollSpy = useScrollSpy;
|
|
30037
31421
|
exports.useSpacingClasses = useSpacingClasses;
|
|
30038
31422
|
exports.useTypographyClasses = useTypographyClasses;
|
|
30039
31423
|
exports.useVisibilityClasses = useVisibilityClasses;
|