@allxsmith/bestax-bulma 5.41.0 → 5.42.1
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 +282 -103
- package/dist/esm/components/Carousel.js.map +1 -1
- package/dist/esm/form/Checkbox.js.map +1 -1
- package/dist/esm/form/Radio.js.map +1 -1
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +280 -103
- package/dist/index.cjs.map +1 -1
- package/dist/types/components/Carousel.d.ts +96 -8
- package/dist/types/form/Checkbox.d.ts +9 -1
- package/dist/types/form/Radio.d.ts +10 -1
- package/dist/types-cjs/components/Carousel.d.ts +96 -8
- package/dist/types-cjs/form/Checkbox.d.ts +9 -1
- package/dist/types-cjs/form/Radio.d.ts +10 -1
- 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 +6 -6
- package/src/scss/components/_carousel.scss +77 -24
- package/src/scss/form/_checkbox.scss +23 -16
- package/src/scss/form/_radio.scss +4 -1
package/dist/index.cjs
CHANGED
|
@@ -9456,6 +9456,69 @@ const DefaultPrevIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
|
|
|
9456
9456
|
* @returns {JSX.Element} The rendered SVG chevron-right icon.
|
|
9457
9457
|
*/
|
|
9458
9458
|
const DefaultNextIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }));
|
|
9459
|
+
/**
|
|
9460
|
+
* The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
|
|
9461
|
+
* mixins open with, widest first, at Bulma's default breakpoints. The
|
|
9462
|
+
* stylesheet picks the count in view with those mixins; these tell the
|
|
9463
|
+
* script which count that is. Narrower than the last one is `mobile`.
|
|
9464
|
+
*/
|
|
9465
|
+
const VIEWPORT_QUERIES = [
|
|
9466
|
+
'screen and (min-width: 1408px)',
|
|
9467
|
+
'screen and (min-width: 1216px)',
|
|
9468
|
+
'screen and (min-width: 1024px)',
|
|
9469
|
+
'screen and (min-width: 769px), print',
|
|
9470
|
+
];
|
|
9471
|
+
/** `useBulmaViewport`'s answer below the tablet breakpoint. */
|
|
9472
|
+
const MOBILE = VIEWPORT_QUERIES.length;
|
|
9473
|
+
/**
|
|
9474
|
+
* The `--bulma-carousel-items-to-show*` variable each count prop writes, in
|
|
9475
|
+
* the order the component lists them: the viewports as above, mobile, and
|
|
9476
|
+
* last `itemsToShow` itself.
|
|
9477
|
+
*/
|
|
9478
|
+
const COUNT_SUFFIXES = [
|
|
9479
|
+
'-fullhd',
|
|
9480
|
+
'-widescreen',
|
|
9481
|
+
'-desktop',
|
|
9482
|
+
'-tablet',
|
|
9483
|
+
'-mobile',
|
|
9484
|
+
'',
|
|
9485
|
+
];
|
|
9486
|
+
/**
|
|
9487
|
+
* Which of Bulma's viewports the window is in: the index of the widest
|
|
9488
|
+
* `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
|
|
9489
|
+
* when that isn't known (on the server, before the first layout effect, and
|
|
9490
|
+
* whenever `enabled` is off, so a carousel with one count everywhere
|
|
9491
|
+
* subscribes to nothing).
|
|
9492
|
+
*
|
|
9493
|
+
* @function
|
|
9494
|
+
* @param {boolean} enabled - Whether any viewport-specific count is set.
|
|
9495
|
+
* @returns {number} The viewport, as described above.
|
|
9496
|
+
*/
|
|
9497
|
+
function useBulmaViewport(enabled) {
|
|
9498
|
+
const [viewport, setViewport] = React.useState(-1);
|
|
9499
|
+
useClientLayoutEffect(() => {
|
|
9500
|
+
if (!enabled || typeof window.matchMedia !== 'function')
|
|
9501
|
+
return undefined;
|
|
9502
|
+
const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
|
|
9503
|
+
const update = () => {
|
|
9504
|
+
const index = lists.findIndex(list => list.matches);
|
|
9505
|
+
setViewport(index < 0 ? MOBILE : index);
|
|
9506
|
+
};
|
|
9507
|
+
update();
|
|
9508
|
+
lists.forEach(list => list.addEventListener('change', update));
|
|
9509
|
+
return () => lists.forEach(list => list.removeEventListener('change', update));
|
|
9510
|
+
}, [enabled]);
|
|
9511
|
+
return enabled ? viewport : -1;
|
|
9512
|
+
}
|
|
9513
|
+
/**
|
|
9514
|
+
* A count prop as the whole number the carousel works with: rounded down,
|
|
9515
|
+
* and never below 1, so a zero or a stray `NaN` can't divide by nothing.
|
|
9516
|
+
*
|
|
9517
|
+
* @function
|
|
9518
|
+
* @param {number | undefined} count - The prop as given.
|
|
9519
|
+
* @returns {number | undefined} The count, or `undefined` when unset.
|
|
9520
|
+
*/
|
|
9521
|
+
const wholeCount = (count) => count === undefined ? undefined : Math.max(1, Math.floor(count) || 1);
|
|
9459
9522
|
/**
|
|
9460
9523
|
* The `Carousel` component provides an image/content slider with navigation arrows and indicators.
|
|
9461
9524
|
*
|
|
@@ -9482,15 +9545,28 @@ const DefaultNextIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
|
|
|
9482
9545
|
* <img src="image2.jpg" alt="Slide 2" />
|
|
9483
9546
|
* </CarouselItem>
|
|
9484
9547
|
* </Carousel>
|
|
9548
|
+
*
|
|
9549
|
+
* @example
|
|
9550
|
+
* // Three cards in view on desktop, two on tablet, one on mobile
|
|
9551
|
+
* <Carousel itemsToShow={1} itemsToShowTablet={2} itemsToShowDesktop={3}>
|
|
9552
|
+
* {cards.map(card => (
|
|
9553
|
+
* <CarouselItem key={card.id}>{card.body}</CarouselItem>
|
|
9554
|
+
* ))}
|
|
9555
|
+
* </Carousel>
|
|
9485
9556
|
*/
|
|
9486
|
-
const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, interval = 5000, pauseOnHover = true, repeat = true, hasDrag = true, arrow = true, arrowHover = false, indicator = true, indicatorInside = false, indicatorPosition = 'bottom', indicatorStyle = 'dots', iconPrev, iconNext, iconLibrary, iconVariant, iconSize, iconFeatures, arrowBackground = true, arrowColor, ariaLabel = 'Image carousel', onChange, className, children, ...props }, ref) => {
|
|
9557
|
+
const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, interval = 5000, pauseOnHover = true, repeat = true, hasDrag = true, itemsToShow, itemsToShowMobile, itemsToShowTablet, itemsToShowDesktop, itemsToShowWidescreen, itemsToShowFullhd, itemsToScroll, arrow = true, arrowHover = false, indicator = true, indicatorInside = false, indicatorPosition = 'bottom', indicatorStyle = 'dots', iconPrev, iconNext, iconLibrary, iconVariant, iconSize, iconFeatures, arrowBackground = true, arrowColor, ariaLabel = 'Image carousel', onChange, className, style, children, ...props }, ref) => {
|
|
9487
9558
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
9488
9559
|
const carouselRef = React.useRef(null);
|
|
9489
9560
|
// Cleanup returned by a consumer's callback ref on attach, held until detach.
|
|
9490
9561
|
const consumerCleanupRef = React.useRef(null);
|
|
9491
9562
|
const containerRef = React.useRef(null);
|
|
9563
|
+
const slidesRef = React.useRef(null);
|
|
9564
|
+
// The item (a child of the track) that holds focus, until focus leaves
|
|
9565
|
+
// the items for somewhere else
|
|
9566
|
+
const focusedItemRef = React.useRef(undefined);
|
|
9492
9567
|
const [internalValue, setInternalValue] = React.useState(0);
|
|
9493
9568
|
const [isPaused, setIsPaused] = React.useState(false);
|
|
9569
|
+
const [isFocusInside, setIsFocusInside] = React.useState(false);
|
|
9494
9570
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
9495
9571
|
const [dragStart, setDragStart] = React.useState(0);
|
|
9496
9572
|
const [dragOffset, setDragOffset] = React.useState(0);
|
|
@@ -9500,64 +9576,105 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9500
9576
|
// Get valid children
|
|
9501
9577
|
const items = React.Children.toArray(children).filter(React.isValidElement);
|
|
9502
9578
|
const itemCount = items.length;
|
|
9503
|
-
//
|
|
9504
|
-
|
|
9505
|
-
//
|
|
9506
|
-
//
|
|
9507
|
-
|
|
9579
|
+
// The count in view. The viewport props come widest first, in the order
|
|
9580
|
+
// of VIEWPORT_QUERIES, then mobile; each min-width viewport falls back to
|
|
9581
|
+
// the narrower ones, and every viewport to `itemsToShow`, the cascade the
|
|
9582
|
+
// stylesheet's `--bulma-carousel-items-to-show-*` defaults spell out.
|
|
9583
|
+
const counts = [
|
|
9584
|
+
itemsToShowFullhd,
|
|
9585
|
+
itemsToShowWidescreen,
|
|
9586
|
+
itemsToShowDesktop,
|
|
9587
|
+
itemsToShowTablet,
|
|
9588
|
+
itemsToShowMobile,
|
|
9589
|
+
].map(wholeCount);
|
|
9590
|
+
const baseCount = wholeCount(itemsToShow);
|
|
9591
|
+
const viewport = useBulmaViewport(counts.some(c => c !== undefined));
|
|
9592
|
+
const inView = (viewport === MOBILE
|
|
9593
|
+
? counts[MOBILE]
|
|
9594
|
+
: viewport >= 0
|
|
9595
|
+
? counts.slice(viewport, MOBILE).find(c => c !== undefined)
|
|
9596
|
+
: undefined) ??
|
|
9597
|
+
baseCount ??
|
|
9598
|
+
1;
|
|
9599
|
+
// The props as the stylesheet reads them, so the widths are right from
|
|
9600
|
+
// the first paint, on the server too.
|
|
9601
|
+
const countStyle = {};
|
|
9602
|
+
[...counts, baseCount].forEach((count, i) => {
|
|
9603
|
+
if (count !== undefined) {
|
|
9604
|
+
countStyle[`--bulma-carousel-items-to-show${COUNT_SUFFIXES[i]}`] =
|
|
9605
|
+
count;
|
|
9606
|
+
}
|
|
9607
|
+
});
|
|
9608
|
+
const scroll = Math.min(wholeCount(itemsToScroll) ?? 1, inView);
|
|
9609
|
+
const looping = repeat && itemCount > inView;
|
|
9610
|
+
const lastStart = Math.max(0, itemCount - inView);
|
|
9611
|
+
// Where the view can start: every `scroll` items from the first, and
|
|
9612
|
+
// without a loop the last full view as well, which is what keeps the
|
|
9613
|
+
// end of the track from showing empty space.
|
|
9614
|
+
const positions = [];
|
|
9615
|
+
for (let p = 0; p < (looping ? itemCount : lastStart); p += scroll) {
|
|
9616
|
+
positions.push(p);
|
|
9617
|
+
}
|
|
9618
|
+
if (!looping)
|
|
9619
|
+
positions.push(lastStart);
|
|
9620
|
+
const lastPosition = positions[positions.length - 1];
|
|
9621
|
+
const canMove = positions.length > 1;
|
|
9622
|
+
const wrap = (index) => ((index % itemCount) + itemCount) % itemCount;
|
|
9623
|
+
const fit = (index) => looping ? wrap(index) : Math.min(Math.max(index, 0), lastStart);
|
|
9624
|
+
// Use controlled or internal value, as a start the view can show
|
|
9625
|
+
const activeIndex = fit(controlledValue ?? internalValue);
|
|
9626
|
+
// The indicator for the last position at or before the first item in view
|
|
9627
|
+
const activePosition = positions.reduce((found, position, index) => (position <= activeIndex ? index : found), 0);
|
|
9628
|
+
const nextIndex = positions[activePosition + 1];
|
|
9629
|
+
// From between two positions, the previous step is the one just behind
|
|
9630
|
+
const prevIndex = activeIndex > positions[activePosition]
|
|
9631
|
+
? positions[activePosition]
|
|
9632
|
+
: positions[activePosition - 1];
|
|
9633
|
+
// Sync displayIndex when the value changes externally, or when the count
|
|
9634
|
+
// in view moves the start it fits to. Intentional external-prop sync that
|
|
9635
|
+
// must not run mid-wrap animation; covered by tests and certified
|
|
9636
|
+
// in-browser. Uncontrolled, the fitted start becomes the value too, so a
|
|
9637
|
+
// later change of count keeps what was first in view in view.
|
|
9508
9638
|
React.useEffect(() => {
|
|
9509
|
-
if (
|
|
9639
|
+
if (!isWrapping) {
|
|
9510
9640
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncing to controlled prop
|
|
9511
|
-
setDisplayIndex(
|
|
9641
|
+
setDisplayIndex(activeIndex);
|
|
9642
|
+
if (controlledValue === undefined)
|
|
9643
|
+
setInternalValue(activeIndex);
|
|
9512
9644
|
}
|
|
9513
|
-
}, [
|
|
9645
|
+
}, [activeIndex, isWrapping, controlledValue]);
|
|
9514
9646
|
// Update active index
|
|
9515
|
-
const setActiveIndex = React.useCallback((index
|
|
9516
|
-
let newIndex = index;
|
|
9517
|
-
if (repeat) {
|
|
9518
|
-
if (index < 0) {
|
|
9519
|
-
newIndex = itemCount - 1;
|
|
9520
|
-
}
|
|
9521
|
-
else if (index >= itemCount) {
|
|
9522
|
-
newIndex = 0;
|
|
9523
|
-
}
|
|
9524
|
-
}
|
|
9525
|
-
else {
|
|
9526
|
-
newIndex = Math.max(0, Math.min(index, itemCount - 1));
|
|
9527
|
-
}
|
|
9647
|
+
const setActiveIndex = React.useCallback((index) => {
|
|
9528
9648
|
if (controlledValue === undefined) {
|
|
9529
|
-
setInternalValue(
|
|
9530
|
-
}
|
|
9531
|
-
if (syncDisplay) {
|
|
9532
|
-
setDisplayIndex(newIndex);
|
|
9649
|
+
setInternalValue(index);
|
|
9533
9650
|
}
|
|
9534
|
-
|
|
9535
|
-
|
|
9651
|
+
setDisplayIndex(index);
|
|
9652
|
+
setIsWrapping(false);
|
|
9653
|
+
onChange?.(index);
|
|
9654
|
+
}, [controlledValue, onChange]);
|
|
9536
9655
|
// Navigation handlers
|
|
9537
9656
|
const goToPrev = React.useCallback(() => {
|
|
9538
|
-
if (
|
|
9539
|
-
|
|
9540
|
-
setDisplayIndex(-1);
|
|
9541
|
-
setIsWrapping(true);
|
|
9657
|
+
if (prevIndex !== undefined) {
|
|
9658
|
+
setActiveIndex(prevIndex);
|
|
9542
9659
|
}
|
|
9543
|
-
else {
|
|
9544
|
-
|
|
9660
|
+
else if (looping) {
|
|
9661
|
+
// Animate to the clones before the first item, which show the last
|
|
9662
|
+
// position, and snap to the real one when the transition ends
|
|
9663
|
+
setDisplayIndex(lastPosition - itemCount);
|
|
9664
|
+
setIsWrapping(true);
|
|
9545
9665
|
}
|
|
9546
|
-
}, [
|
|
9666
|
+
}, [prevIndex, looping, lastPosition, itemCount, setActiveIndex]);
|
|
9547
9667
|
const goToNext = React.useCallback(() => {
|
|
9548
|
-
if (
|
|
9549
|
-
|
|
9668
|
+
if (nextIndex !== undefined) {
|
|
9669
|
+
setActiveIndex(nextIndex);
|
|
9670
|
+
}
|
|
9671
|
+
else if (looping) {
|
|
9672
|
+
// Animate to the clones after the last item, which show the first
|
|
9673
|
+
// position, and snap to the real one when the transition ends
|
|
9550
9674
|
setDisplayIndex(itemCount);
|
|
9551
9675
|
setIsWrapping(true);
|
|
9552
9676
|
}
|
|
9553
|
-
|
|
9554
|
-
setActiveIndex(activeIndex + 1);
|
|
9555
|
-
}
|
|
9556
|
-
}, [activeIndex, itemCount, repeat, setActiveIndex]);
|
|
9557
|
-
const goToSlide = React.useCallback((index) => {
|
|
9558
|
-
setIsWrapping(false);
|
|
9559
|
-
setActiveIndex(index);
|
|
9560
|
-
}, [setActiveIndex]);
|
|
9677
|
+
}, [nextIndex, looping, itemCount, setActiveIndex]);
|
|
9561
9678
|
// Read when the timer fires or an arrow key arrives, so neither the
|
|
9562
9679
|
// timer nor the key listener starts over while `onChange` changes
|
|
9563
9680
|
// identity, as an inline one does on every render of the parent.
|
|
@@ -9565,15 +9682,28 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9565
9682
|
const goToNextRef = useLatestRef(goToNext);
|
|
9566
9683
|
// Auto-play. Each slide gets the whole interval, so a slide change,
|
|
9567
9684
|
// autoplay's own or the user's, starts the timer over. A render that
|
|
9568
|
-
// leaves the slide where it is doesn't.
|
|
9685
|
+
// leaves the slide where it is doesn't. It waits while focus is inside
|
|
9686
|
+
// an item, so what someone is reading or using doesn't move away. The
|
|
9687
|
+
// timer reads where focus is when it fires, rather than trusting the
|
|
9688
|
+
// last focus event alone: a browser moving focus off an element that
|
|
9689
|
+
// went inert need not fire one, and the pause would never end. A focus
|
|
9690
|
+
// change still starts the timer over, so an item gets the whole
|
|
9691
|
+
// interval once focus leaves it.
|
|
9692
|
+
const isPlaying = autoplay && (looping || nextIndex !== undefined) && !isPaused;
|
|
9693
|
+
const isRotating = isPlaying && !isFocusInside;
|
|
9569
9694
|
React.useEffect(() => {
|
|
9570
|
-
if (!
|
|
9695
|
+
if (!isPlaying)
|
|
9571
9696
|
return undefined;
|
|
9572
9697
|
const timer = setInterval(() => {
|
|
9698
|
+
const track = slidesRef.current;
|
|
9699
|
+
if (track?.contains(getActiveElementInTree(track)))
|
|
9700
|
+
return;
|
|
9701
|
+
if (isFocusInside)
|
|
9702
|
+
setIsFocusInside(false);
|
|
9573
9703
|
goToNextRef.current();
|
|
9574
9704
|
}, interval);
|
|
9575
9705
|
return () => clearInterval(timer);
|
|
9576
|
-
}, [
|
|
9706
|
+
}, [isPlaying, isFocusInside, interval, activeIndex, goToNextRef]);
|
|
9577
9707
|
// Pause on hover
|
|
9578
9708
|
const handleMouseEnter = React.useCallback(() => {
|
|
9579
9709
|
if (pauseOnHover) {
|
|
@@ -9632,12 +9762,16 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9632
9762
|
const handleTouchEnd = React.useCallback(() => {
|
|
9633
9763
|
handleDragEnd();
|
|
9634
9764
|
}, [handleDragEnd]);
|
|
9635
|
-
// Keyboard navigation
|
|
9765
|
+
// Keyboard navigation, from the carousel's own controls. Inside an item
|
|
9766
|
+
// the keys are its content's (a field's caret, a slider), and a step
|
|
9767
|
+
// from there could take the focused item out of view.
|
|
9636
9768
|
React.useEffect(() => {
|
|
9637
9769
|
const handleKeyDown = (e) => {
|
|
9638
9770
|
const node = carouselRef.current;
|
|
9639
|
-
|
|
9771
|
+
const active = getActiveElementInTree(node);
|
|
9772
|
+
if (!node?.contains(active) || slidesRef.current?.contains(active)) {
|
|
9640
9773
|
return;
|
|
9774
|
+
}
|
|
9641
9775
|
switch (e.key) {
|
|
9642
9776
|
case 'ArrowLeft':
|
|
9643
9777
|
e.preventDefault();
|
|
@@ -9652,22 +9786,14 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9652
9786
|
document.addEventListener('keydown', handleKeyDown);
|
|
9653
9787
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
9654
9788
|
}, [goToPrevRef, goToNextRef]);
|
|
9655
|
-
// Handle wrap-around transition end
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9789
|
+
// Handle wrap-around transition end: the track's own, since an item's
|
|
9790
|
+
// content can end a transition of its own that bubbles up to here
|
|
9791
|
+
const handleTransitionEnd = React.useCallback((e) => {
|
|
9792
|
+
if (isWrapping && e.target === e.currentTarget) {
|
|
9793
|
+
// Skip transition for the instant snap from the clones to the
|
|
9794
|
+
// real items they copy
|
|
9659
9795
|
setSkipTransition(true);
|
|
9660
|
-
|
|
9661
|
-
if (displayIndex === itemCount) {
|
|
9662
|
-
// Was at clone of first slide, snap to real first slide
|
|
9663
|
-
setActiveIndex(0, false);
|
|
9664
|
-
setDisplayIndex(0);
|
|
9665
|
-
}
|
|
9666
|
-
else if (displayIndex === -1) {
|
|
9667
|
-
// Was at clone of last slide, snap to real last slide
|
|
9668
|
-
setActiveIndex(itemCount - 1, false);
|
|
9669
|
-
setDisplayIndex(itemCount - 1);
|
|
9670
|
-
}
|
|
9796
|
+
setActiveIndex(((displayIndex % itemCount) + itemCount) % itemCount);
|
|
9671
9797
|
// Re-enable transition after the snap
|
|
9672
9798
|
requestAnimationFrame(() => {
|
|
9673
9799
|
requestAnimationFrame(() => {
|
|
@@ -9709,8 +9835,8 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9709
9835
|
}
|
|
9710
9836
|
}, [ref]);
|
|
9711
9837
|
// Can navigate
|
|
9712
|
-
const canGoPrev =
|
|
9713
|
-
const canGoNext =
|
|
9838
|
+
const canGoPrev = looping || prevIndex !== undefined;
|
|
9839
|
+
const canGoNext = looping || nextIndex !== undefined;
|
|
9714
9840
|
// Generate classes
|
|
9715
9841
|
const carouselClasses = usePrefixedClassNames('carousel', {
|
|
9716
9842
|
'is-overlay': indicatorInside,
|
|
@@ -9728,50 +9854,101 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9728
9854
|
'is-top': indicatorPosition === 'top',
|
|
9729
9855
|
});
|
|
9730
9856
|
const combinedClasses = classNames(carouselClasses, bulmaHelperClasses, className);
|
|
9731
|
-
//
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9857
|
+
// Where the track is drawn from: mid-wrap the clones, otherwise a start
|
|
9858
|
+
// the view can show, so a change in the count in view never paints
|
|
9859
|
+
// empty space while the sync above catches up.
|
|
9860
|
+
const shownIndex = isWrapping ? displayIndex : fit(displayIndex);
|
|
9861
|
+
// Enough clones on each side to fill the widest view any viewport sets,
|
|
9862
|
+
// since the stylesheet, not this count, sizes what's on screen
|
|
9863
|
+
const cloneCount = looping
|
|
9864
|
+
? Math.min(itemCount, Math.max(baseCount ?? 1, ...counts.map(c => c ?? 1)))
|
|
9865
|
+
: 0;
|
|
9866
|
+
// The track: a full view's worth of clones of the last items, the items,
|
|
9867
|
+
// and clones of the first ones, for a seamless loop. Each entry is the
|
|
9868
|
+
// item it shows and its key.
|
|
9869
|
+
const slots = items.map((_, index) => [
|
|
9870
|
+
index,
|
|
9871
|
+
index,
|
|
9872
|
+
]);
|
|
9873
|
+
for (let k = 0; k < cloneCount; k++) {
|
|
9874
|
+
slots.splice(k, 0, [itemCount - cloneCount + k, `clone-before-${k}`]);
|
|
9875
|
+
slots.push([k, `clone-after-${k}`]);
|
|
9876
|
+
}
|
|
9877
|
+
// Render slides with active state (includes clones for infinite loop).
|
|
9878
|
+
// What's in view is a run of slots, so where a looping view runs past
|
|
9879
|
+
// the last item, the clones on screen are the live copies and the items
|
|
9880
|
+
// they copy, off screen, are the hidden ones: no item is exposed twice,
|
|
9881
|
+
// and the copy exposed is the one in view.
|
|
9882
|
+
const renderSlides = () => slots.map(([index, key], slot) => {
|
|
9883
|
+
const item = items[index];
|
|
9884
|
+
const own = item.props;
|
|
9885
|
+
const isShown = slot >= shownIndex + cloneCount &&
|
|
9886
|
+
slot < shownIndex + cloneCount + inView;
|
|
9887
|
+
return React.cloneElement(item, {
|
|
9888
|
+
key,
|
|
9889
|
+
active: isShown,
|
|
9890
|
+
role: own.role ?? 'group',
|
|
9891
|
+
'aria-roledescription': own['aria-roledescription'] ?? 'slide',
|
|
9892
|
+
...(own['aria-label'] || own['aria-labelledby']
|
|
9893
|
+
? {}
|
|
9894
|
+
: { 'aria-label': `${index + 1} of ${itemCount}` }),
|
|
9895
|
+
// Out of view: hidden from assistive tech, and inert, so nothing
|
|
9896
|
+
// in it takes focus or a click
|
|
9897
|
+
...(isShown ? {} : { 'aria-hidden': true, ...inertProps(true) }),
|
|
9741
9898
|
});
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9899
|
+
});
|
|
9900
|
+
// Hand focus on when the item holding it leaves the view. An item out
|
|
9901
|
+
// of view is inert, and a browser drops focus from an inert element to
|
|
9902
|
+
// the page, so a swipe, a change of count with the window's width, a new
|
|
9903
|
+
// controlled value or the item's removal would leave a keyboard or
|
|
9904
|
+
// screen reader user nowhere. Focus goes to the first item in view,
|
|
9905
|
+
// given `tabindex="-1"` for it unless it has a tabindex of its own. A
|
|
9906
|
+
// layout effect runs before the browser gets to drop the focus; the
|
|
9907
|
+
// focused item's removal drops it at once, so focus on the page itself
|
|
9908
|
+
// still counts as the item's.
|
|
9909
|
+
useClientLayoutEffect(() => {
|
|
9910
|
+
const held = focusedItemRef.current;
|
|
9911
|
+
if (!held)
|
|
9912
|
+
return;
|
|
9913
|
+
const track = slidesRef.current;
|
|
9914
|
+
const start = shownIndex + cloneCount;
|
|
9915
|
+
const slot = Array.prototype.indexOf.call(track.children, held);
|
|
9916
|
+
if (slot >= start && slot < start + inView)
|
|
9917
|
+
return;
|
|
9918
|
+
const active = getActiveElementInTree(track);
|
|
9919
|
+
if (active !== held.ownerDocument.body && !held.contains(active)) {
|
|
9920
|
+
return;
|
|
9759
9921
|
}
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9922
|
+
focusedItemRef.current = undefined;
|
|
9923
|
+
const first = track.children[start];
|
|
9924
|
+
if (!first)
|
|
9925
|
+
return;
|
|
9926
|
+
if (!first.hasAttribute('tabindex'))
|
|
9927
|
+
first.tabIndex = -1;
|
|
9928
|
+
first.focus({ preventScroll: true });
|
|
9929
|
+
}, [shownIndex, cloneCount, inView, itemCount]);
|
|
9930
|
+
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 ? handleMouseDown : undefined, onMouseMove: hasDrag && isDragging ? handleMouseMove : undefined, onMouseUp: hasDrag ? handleMouseUp : undefined, onMouseLeave: hasDrag ? handleMouseUp : undefined, onTouchStart: hasDrag ? handleTouchStart : undefined, onTouchMove: hasDrag ? handleTouchMove : undefined, onTouchEnd: hasDrag ? handleTouchEnd : undefined, style: {
|
|
9763
9931
|
transform: isDragging ? `translateX(${dragOffset}px)` : undefined,
|
|
9764
9932
|
cursor: hasDrag ? (isDragging ? 'grabbing' : 'grab') : undefined,
|
|
9765
|
-
}, children: [jsxRuntime.jsx("div", { className: carouselSlidesClass, style: {
|
|
9766
|
-
//
|
|
9767
|
-
|
|
9933
|
+
}, children: [jsxRuntime.jsx("div", { ref: slidesRef, className: carouselSlidesClass, style: {
|
|
9934
|
+
// One item is the track's width plus a gap, over the count the
|
|
9935
|
+
// stylesheet has in view; account for the prepended clones
|
|
9936
|
+
transform: `translateX(calc(${-(shownIndex + cloneCount)} * (100% + var(--bulma-carousel-item-gap, 0px)) / var(--bulma-carousel-items-in-view, 1)))`,
|
|
9768
9937
|
// Disable transition during drag or instant snap after wrap-around
|
|
9769
9938
|
transition: isDragging || skipTransition
|
|
9770
9939
|
? 'none'
|
|
9771
9940
|
: 'transform 0.3s ease-in-out',
|
|
9772
|
-
},
|
|
9773
|
-
|
|
9774
|
-
|
|
9941
|
+
}, "aria-live": isRotating ? 'off' : 'polite', onFocus: e => {
|
|
9942
|
+
setIsFocusInside(true);
|
|
9943
|
+
focusedItemRef.current = Array.from(e.currentTarget.children).find(item => item.contains(e.target));
|
|
9944
|
+
}, onBlur: e => {
|
|
9945
|
+
const inside = e.currentTarget.contains(e.relatedTarget);
|
|
9946
|
+
setIsFocusInside(inside);
|
|
9947
|
+
if (!inside)
|
|
9948
|
+
focusedItemRef.current = undefined;
|
|
9949
|
+
}, 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(DefaultPrevIcon, {})) }), 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(DefaultNextIcon, {})) })] }))] }), indicator && canMove && (jsxRuntime.jsx("div", { className: carouselIndicatorClass, role: "tablist", children: positions.map((position, index) => (jsxRuntime.jsx(Button, { type: "button", className: prefixedClassNames(classPrefix, 'indicator-item', {
|
|
9950
|
+
'is-active': index === activePosition,
|
|
9951
|
+
}), onClick: () => setActiveIndex(position), role: "tab", "aria-selected": index === activePosition, "aria-label": `Go to slide ${position + 1}` }, position))) }))] }));
|
|
9775
9952
|
});
|
|
9776
9953
|
Carousel.displayName = 'Carousel';
|
|
9777
9954
|
|