package.dist.index.mjs Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of carousel Show documentation
Show all versions of carousel Show documentation
Core logic for the carousel widget implemented as a state machine
The newest version!
import { createAnatomy } from '@zag-js/anatomy';
import { createScope, queryAll, isDom, dataAttr } from '@zag-js/dom-query';
import { createSplitProps, compact, nextIndex, prevIndex, isEqual, isNumber } from '@zag-js/utils';
import { createMachine, ref } from '@zag-js/core';
import { createProps } from '@zag-js/types';
// src/carousel.anatomy.ts
var anatomy = createAnatomy("carousel").parts(
"root",
"viewport",
"itemGroup",
"item",
"nextTrigger",
"prevTrigger",
"indicatorGroup",
"indicator"
);
var parts = anatomy.build();
var dom = createScope({
getRootId: (ctx) => ctx.ids?.root ?? `carousel:${ctx.id}`,
getViewportId: (ctx) => ctx.ids?.viewport ?? `carousel:${ctx.id}:viewport`,
getItemId: (ctx, index) => ctx.ids?.item?.(index) ?? `carousel:${ctx.id}:item:${index}`,
getItemGroupId: (ctx) => ctx.ids?.itemGroup ?? `carousel:${ctx.id}:item-group`,
getNextTriggerId: (ctx) => ctx.ids?.nextTrigger ?? `carousel:${ctx.id}:next-trigger`,
getPrevTriggerId: (ctx) => ctx.ids?.prevTrigger ?? `carousel:${ctx.id}:prev-trigger`,
getIndicatorGroupId: (ctx) => ctx.ids?.indicatorGroup ?? `carousel:${ctx.id}:indicator-group`,
getIndicatorId: (ctx, index) => ctx.ids?.indicator?.(index) ?? `carousel:${ctx.id}:indicator:${index}`,
getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
getViewportEl: (ctx) => dom.getById(ctx, dom.getViewportId(ctx)),
getSlideGroupEl: (ctx) => dom.getById(ctx, dom.getItemGroupId(ctx)),
getSlideEls: (ctx) => queryAll(dom.getSlideGroupEl(ctx), `[data-part=item]`)
});
// src/utils/get-limit.ts
function getLimit(min, max) {
const length = Math.abs(min - max);
function reachedMin(n) {
return n < min;
}
function reachedMax(n) {
return n > max;
}
function reachedAny(n) {
return reachedMin(n) || reachedMax(n);
}
function constrain(n) {
if (!reachedAny(n)) return n;
return reachedMin(n) ? min : max;
}
function removeOffset(n) {
if (!length) return n;
return n - length * Math.ceil((n - max) / length);
}
return {
length,
max,
min,
constrain,
reachedAny,
reachedMax,
reachedMin,
removeOffset
};
}
var getAlignment = (align, containerSize) => {
const predefined = { start, center, end };
function start() {
return 0;
}
function center(n) {
return end(n) / 2;
}
function end(n) {
return containerSize - n;
}
function percent() {
return containerSize * Number(align);
}
return (n) => {
if (isNumber(align)) return percent();
return predefined[align](n);
};
};
function getSlidesToScroll(containerSize, slideSizesWithGaps, slidesPerView) {
function byNumber(array, groupSize) {
return Array.from(array.keys()).filter((i) => i % groupSize === 0).map((i) => array.slice(i, i + groupSize));
}
function bySize(array) {
return Array.from(array.keys()).reduce((groups, i) => {
const chunk = slideSizesWithGaps.slice(groups.at(-1), i + 1);
const chunkSize = chunk.reduce((a, s) => a + s, 0);
return !i || chunkSize > containerSize ? groups.concat(i) : groups;
}, []).map((start, i, groups) => array.slice(start, groups[i + 1]));
}
return function groupSlides(array) {
return isNumber(slidesPerView) ? byNumber(array, slidesPerView) : bySize(array);
};
}
// src/utils/get-slide-sizes.ts
function getSlideSizes(ctx) {
const startGap = measureStartGap();
function measureStartGap() {
if (!ctx.containerRect) return 0;
const slideRect = ctx.slideRects[0];
return Math.abs(ctx.containerRect[ctx.startEdge] - slideRect[ctx.startEdge]);
}
function measureWithGaps() {
return ctx.slideRects.map((rect, index, rects) => {
const isFirst = !index;
if (isFirst) return Math.abs(slideSizes[index] + startGap);
const isLast = index === rects.length - 1;
if (isLast) return Math.abs(slideSizes[index]);
return Math.abs(rects[index + 1][ctx.startEdge] - rect[ctx.startEdge]);
});
}
const slideSizes = ctx.slideRects.map((slideRect) => {
return ctx.isVertical ? slideRect.height : slideRect.width;
});
const slideSizesWithGaps = measureWithGaps();
return {
slideSizes,
slideSizesWithGaps
};
}
// src/utils/get-scroll-snaps.ts
var arrayLast = (array) => array[arrayLastIndex(array)];
var arrayLastIndex = (array) => Math.max(0, array.length - 1);
function getScrollSnaps(ctx) {
const { slideSizes, slideSizesWithGaps } = getSlideSizes(ctx);
const groupSlides = getSlidesToScroll(ctx.containerSize, slideSizesWithGaps, ctx.slidesPerView);
function measureSizes() {
return groupSlides(ctx.slideRects).map((rects) => arrayLast(rects)[ctx.endEdge] - rects[0][ctx.startEdge]).map(Math.abs);
}
function measureUnaligned() {
return ctx.slideRects.map((slideRect) => ctx.containerRect[ctx.startEdge] - slideRect[ctx.startEdge]).map((snap) => -Math.abs(snap));
}
function measureAligned() {
const measureFn = getAlignment(ctx.align, ctx.containerSize);
const alignments = measureSizes().map(measureFn);
return groupSlides(snaps).map((snap) => snap[0]).map((snap, index) => snap + alignments[index]);
}
const snaps = measureUnaligned();
const snapsAligned = measureAligned();
const contentSize = -arrayLast(snaps) + arrayLast(slideSizesWithGaps);
const scrollLimit = getLimit(snaps[snaps.length - 1], snaps[0]);
const scrollProgress = (snapsAligned[ctx.index] - scrollLimit.max) / -scrollLimit.length;
return {
snaps,
snapsAligned,
slideSizes,
slideSizesWithGaps,
contentSize,
scrollLimit,
scrollProgress: Math.abs(scrollProgress)
};
}
// src/utils/get-slide-in-view.ts
var slideThreshold = 0;
function getSlidesInView(ctx) {
const roundingSafety = 0.5;
const slideOffsets = [0];
const { snaps, slideSizes, scrollLimit } = getScrollSnaps(ctx);
const slideThresholds = slideSizes.map((slideSize) => {
const thresholdLimit = getLimit(roundingSafety, slideSize - roundingSafety);
return thresholdLimit.constrain(slideSize * slideThreshold);
});
const slideBounds = slideOffsets.reduce((acc, offset) => {
const bounds = snaps.map((snap, index) => ({
start: snap - slideSizes[index] + slideThresholds[index] + offset,
end: snap + ctx.containerSize - slideThresholds[index] + offset,
index
}));
return acc.concat(bounds);
}, []);
return (location) => {
const loc = scrollLimit.constrain(location);
return slideBounds.reduce((list, bound) => {
const { index, start, end } = bound;
const inList = list.includes(index);
const inView = start < loc && end > loc;
return !inList && inView ? list.concat([index]) : list;
}, []);
};
}
// src/carousel.connect.ts
function connect(state, send, normalize) {
const canScrollNext = state.context.canScrollNext;
const canScrollPrev = state.context.canScrollPrev;
const horizontal = state.context.isHorizontal;
const autoPlaying = state.matches("autoplay");
const activeSnap = state.context.scrollSnaps[state.context.index];
const slidesInView = isDom() ? getSlidesInView(state.context)(activeSnap) : [];
function getItemState(props2) {
return {
valueText: `Slide ${props2.index + 1}`,
current: props2.index === state.context.index,
next: props2.index === state.context.index + 1,
previous: props2.index === state.context.index - 1,
inView: slidesInView.includes(props2.index)
};
}
return {
index: state.context.index,
scrollProgress: state.context.scrollProgress,
autoPlaying,
canScrollNext,
canScrollPrev,
scrollTo(index, jump) {
send({ type: "GOTO", index, jump });
},
scrollToNext() {
send("NEXT");
},
scrollToPrevious() {
send("PREV");
},
getItemState,
play() {
send("PLAY");
},
pause() {
send("PAUSE");
},
getRootProps() {
return normalize.element({
...parts.root.attrs,
id: dom.getRootId(state.context),
role: "region",
"aria-roledescription": "carousel",
"data-orientation": state.context.orientation,
dir: state.context.dir,
"aria-label": "Carousel",
style: {
"--slide-spacing": state.context.spacing,
"--slide-size": `calc(100% / ${state.context.slidesPerView} - var(--slide-spacing))`
}
});
},
getViewportProps() {
return normalize.element({
...parts.viewport.attrs,
dir: state.context.dir,
id: dom.getViewportId(state.context),
"data-orientation": state.context.orientation
});
},
getItemGroupProps() {
return normalize.element({
...parts.itemGroup.attrs,
id: dom.getItemGroupId(state.context),
"data-orientation": state.context.orientation,
dir: state.context.dir,
style: {
display: "flex",
flexDirection: horizontal ? "row" : "column",
[horizontal ? "height" : "width"]: "auto",
gap: "var(--slide-spacing)",
transform: state.context.translateValue,
transitionProperty: "transform",
willChange: "transform",
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
transitionDuration: "0.3s"
}
});
},
getItemProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
...parts.item.attrs,
id: dom.getItemId(state.context, props2.index),
dir: state.context.dir,
"data-current": dataAttr(itemState.current),
"data-inview": dataAttr(itemState.inView),
role: "group",
"aria-roledescription": "slide",
"data-orientation": state.context.orientation,
"aria-label": itemState.valueText,
style: {
position: "relative",
flex: "0 0 var(--slide-size)",
[horizontal ? "minWidth" : "minHeight"]: "0px"
}
});
},
getPrevTriggerProps() {
return normalize.button({
...parts.prevTrigger.attrs,
id: dom.getPrevTriggerId(state.context),
type: "button",
tabIndex: -1,
disabled: !canScrollPrev,
dir: state.context.dir,
"aria-label": "Previous Slide",
"data-orientation": state.context.orientation,
"aria-controls": dom.getItemGroupId(state.context),
onClick() {
send("PREV");
}
});
},
getNextTriggerProps() {
return normalize.button({
...parts.nextTrigger.attrs,
dir: state.context.dir,
id: dom.getNextTriggerId(state.context),
type: "button",
tabIndex: -1,
"aria-label": "Next Slide",
"data-orientation": state.context.orientation,
"aria-controls": dom.getItemGroupId(state.context),
disabled: !canScrollNext,
onClick() {
send("NEXT");
}
});
},
getIndicatorGroupProps() {
return normalize.element({
...parts.indicatorGroup.attrs,
dir: state.context.dir,
id: dom.getIndicatorGroupId(state.context),
"data-orientation": state.context.orientation
});
},
getIndicatorProps(props2) {
return normalize.button({
...parts.indicator.attrs,
dir: state.context.dir,
id: dom.getIndicatorId(state.context, props2.index),
type: "button",
"data-orientation": state.context.orientation,
"data-index": props2.index,
"data-readonly": dataAttr(props2.readOnly),
"data-current": dataAttr(props2.index === state.context.index),
onClick() {
if (props2.readOnly) return;
send({ type: "GOTO", index: props2.index });
}
});
}
};
}
function machine(userContext) {
const ctx = compact(userContext);
return createMachine(
{
id: "carousel",
initial: "idle",
context: {
index: 0,
orientation: "horizontal",
align: "start",
loop: false,
slidesPerView: 1,
spacing: "0px",
...ctx,
scrollSnaps: [],
scrollProgress: 0,
containerSize: 0,
slideRects: []
},
watch: {
index: ["setScrollSnaps"]
},
on: {
NEXT: {
actions: ["scrollToNext"]
},
PREV: {
actions: ["scrollToPrev"]
},
GOTO: {
actions: ["scrollTo"]
},
MEASURE_DOM: {
actions: ["measureElements", "setScrollSnaps"]
},
PLAY: "autoplay"
},
states: {
idle: {
on: {
POINTER_DOWN: "dragging"
}
},
autoplay: {
activities: ["trackDocumentVisibility"],
every: {
2e3: ["scrollToNext"]
},
on: {
PAUSE: "idle"
}
},
dragging: {
on: {
POINTER_UP: "idle",
POINTER_MOVE: {
actions: ["setScrollSnaps"]
}
}
}
},
activities: ["trackContainerResize", "trackSlideMutation"],
entry: ["measureElements", "setScrollSnaps"],
computed: {
isRtl: (ctx2) => ctx2.dir === "rtl",
isHorizontal: (ctx2) => ctx2.orientation === "horizontal",
isVertical: (ctx2) => ctx2.orientation === "vertical",
canScrollNext: (ctx2) => ctx2.loop || ctx2.index < ctx2.scrollSnaps.length - 1,
canScrollPrev: (ctx2) => ctx2.loop || ctx2.index > 0,
startEdge(ctx2) {
if (ctx2.isVertical) return "top";
return ctx2.isRtl ? "right" : "left";
},
endEdge(ctx2) {
if (ctx2.isVertical) return "bottom";
return ctx2.isRtl ? "left" : "right";
},
translateValue: (ctx2) => {
const scrollSnap = ctx2.scrollSnaps[ctx2.index];
return ctx2.isHorizontal ? `translate3d(${scrollSnap}px, 0, 0)` : `translate3d(0, ${scrollSnap}px, 0)`;
}
}
},
{
activities: {
trackSlideMutation(ctx2, _evt, { send }) {
const slideGroupEl = dom.getSlideGroupEl(ctx2);
if (!slideGroupEl) return;
const win = dom.getWin(ctx2);
const observer = new win.MutationObserver(() => {
send({ type: "MEASURE_DOM", src: "mutation" });
});
observer.observe(slideGroupEl, { childList: true });
return () => {
observer.disconnect();
};
},
trackContainerResize(ctx2, _evt, { send }) {
const slideGroupEl = dom.getSlideGroupEl(ctx2);
if (!slideGroupEl) return;
const win = dom.getWin(ctx2);
const observer = new win.ResizeObserver((entries) => {
entries.forEach((entry) => {
if (entry.target === slideGroupEl) {
send({ type: "MEASURE_DOM", src: "resize" });
}
});
});
observer.observe(slideGroupEl);
return () => {
observer.disconnect();
};
},
trackDocumentVisibility(ctx2, _evt, { send }) {
const doc = dom.getDoc(ctx2);
const onVisibilityChange = () => {
if (doc.visibilityState !== "visible") {
send({ type: "PAUSE", src: "document-hidden" });
}
};
doc.addEventListener("visibilitychange", onVisibilityChange);
return () => {
doc.removeEventListener("visibilitychange", onVisibilityChange);
};
}
},
guards: {
loop: (ctx2) => ctx2.loop,
isLastSlide: (ctx2) => ctx2.index === ctx2.scrollSnaps.length - 1,
isFirstSlide: (ctx2) => ctx2.index === 0
},
actions: {
scrollToNext(ctx2) {
const index = nextIndex(ctx2.scrollSnaps, ctx2.index);
set.index(ctx2, index);
},
scrollToPrev(ctx2) {
const index = prevIndex(ctx2.scrollSnaps, ctx2.index);
set.index(ctx2, index);
},
setScrollSnaps(ctx2) {
const { snapsAligned, scrollProgress } = getScrollSnaps(ctx2);
ctx2.scrollSnaps = snapsAligned;
ctx2.scrollProgress = scrollProgress;
},
scrollTo(ctx2, evt) {
const index = Math.max(0, Math.min(evt.index, ctx2.scrollSnaps.length - 1));
set.index(ctx2, index);
},
measureElements
}
}
);
}
var measureElements = (ctx) => {
const slideGroupEl = dom.getSlideGroupEl(ctx);
if (!slideGroupEl) return;
ctx.containerRect = ref(slideGroupEl.getBoundingClientRect());
ctx.containerSize = ctx.isHorizontal ? ctx.containerRect.width : ctx.containerRect.height;
ctx.slideRects = ref(dom.getSlideEls(ctx).map((slide) => slide.getBoundingClientRect()));
};
var invoke = {
change: (ctx) => {
ctx.onIndexChange?.({ index: ctx.index });
}
};
var set = {
index: (ctx, index) => {
if (isEqual(ctx.index, index)) return;
ctx.index = index;
invoke.change(ctx);
}
};
var props = createProps()([
"align",
"dir",
"getRootNode",
"id",
"ids",
"index",
"loop",
"onIndexChange",
"orientation",
"slidesPerView",
"spacing"
]);
var splitProps = createSplitProps(props);
var indicatorProps = createProps()(["index", "readOnly"]);
var splitIndicatorProps = createSplitProps(indicatorProps);
export { anatomy, connect, indicatorProps, machine, props, splitIndicatorProps, splitProps };
© 2015 - 2025 Weber Informatics LLC | Privacy Policy