Skip to content

Commit

Permalink
feat: new carousel
Browse files Browse the repository at this point in the history
  • Loading branch information
anubra266 committed Dec 2, 2024
1 parent a354af2 commit 134b6ed
Show file tree
Hide file tree
Showing 19 changed files with 472 additions and 435 deletions.
19 changes: 11 additions & 8 deletions examples/next-ts/pages/carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ export default function Page() {
<div {...api.getRootProps()}>
<button {...api.getPrevTriggerProps()}>Prev</button>
<button {...api.getNextTriggerProps()}>Next</button>
<div {...api.getViewportProps()}>
<div {...api.getItemGroupProps()}>
{carouselData.map((image, index) => (
<div {...api.getItemProps({ index })} key={index}>
<img src={image} alt="" style={{ height: "300px", width: "100%", objectFit: "cover" }} />
</div>
))}
</div>
<div {...api.getItemGroupProps()}>
{carouselData.map((image, index) => (
<div {...api.getItemProps({ index })} key={index}>
<img src={image} alt="" style={{ height: "300px", width: "100%", objectFit: "cover" }} />
</div>
))}
</div>
<div {...api.getIndicatorGroupProps()}>
{api.views.map(({ index }) => (
<button {...api.getIndicatorProps({ index })} key={index} />
))}
</div>
</div>
</main>
Expand Down
1 change: 1 addition & 0 deletions packages/machines/carousel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@zag-js/anatomy": "workspace:*",
"@zag-js/core": "workspace:*",
"@zag-js/types": "workspace:*",
"@zag-js/dom-event": "workspace:*",
"@zag-js/dom-query": "workspace:*",
"@zag-js/utils": "workspace:*"
},
Expand Down
1 change: 0 additions & 1 deletion packages/machines/carousel/src/carousel.anatomy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { createAnatomy } from "@zag-js/anatomy"

export const anatomy = createAnatomy("carousel").parts(
"root",
"viewport",
"itemGroup",
"item",
"nextTrigger",
Expand Down
118 changes: 81 additions & 37 deletions packages/machines/carousel/src/carousel.connect.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,50 @@
import { dataAttr, isDom } from "@zag-js/dom-query"
import { getEventKey, type EventKeyMap } from "@zag-js/dom-event"
import { ariaAttr, dataAttr } from "@zag-js/dom-query"
import type { NormalizeProps, PropTypes } from "@zag-js/types"
import { parts } from "./carousel.anatomy"
import { dom } from "./carousel.dom"
import type { ItemProps, ItemState, MachineApi, Send, State } from "./carousel.types"
import { getSlidesInView } from "./utils/get-slide-in-view"

export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>): MachineApi<T> {
const canScrollNext = state.context.canScrollNext
const canScrollPrev = state.context.canScrollPrev
const horizontal = state.context.isHorizontal
const autoPlaying = state.matches("autoplay")
const slidesPerView = state.context.slidesPerView
const spacing = state.context.spacing
const padding = state.context.padding
const scrollBy = state.context.scrollBy

const activeSnap = state.context.scrollSnaps[state.context.index]
const slidesInView = isDom() ? getSlidesInView(state.context)(activeSnap) : []
const autoPlaying = state.matches("autoplay")

function getItemState(props: ItemProps): ItemState {
const index = state.context.index
const slidesInView = state.context.views.at(index) ?? []

return {
valueText: `Slide ${props.index + 1}`,
current: props.index === state.context.index,
next: props.index === state.context.index + 1,
previous: props.index === state.context.index - 1,
current: props.index === index && slidesPerView === 1,
next: props.index === index + 1,
previous: props.index === index - 1,
inView: slidesInView.includes(props.index),
valueText: `Slide ${props.index + 1}`,
}
}

return {
index: state.context.index,
scrollProgress: state.context.scrollProgress,
views: state.context.views.map((_, index) => ({ index })),
autoPlaying,
canScrollNext,
canScrollPrev,
scrollTo(index, jump) {
send({ type: "GOTO", index, jump })
scrollTo(index) {
send({ type: "GOTO", index })
},
scrollToNext() {
send("NEXT")
},
scrollToPrevious() {
send("PREV")
},
getItemState: getItemState,
getItemState,
play() {
send("PLAY")
},
Expand All @@ -58,57 +63,96 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
"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,
tabIndex: 0,
onPointerDown(event) {
if (event.button !== 0) return
event.preventDefault()
send({ type: "POINTER_DOWN" })
},

onKeyDown(event) {
if (event.defaultPrevented) return

const keyMap: EventKeyMap = {
ArrowDown() {
if (horizontal) return
send({ type: "GOTO.NEXT" })
},
ArrowUp() {
if (horizontal) return
send({ type: "GOTO.PREV" })
},
ArrowRight() {
if (!horizontal) return
send({ type: "GOTO.NEXT" })
},
ArrowLeft() {
if (!horizontal) return
send({ type: "GOTO.PREV" })
},
Home() {
send({ type: "GOTO.FIRST" })
},
End() {
send({ type: "GOTO.LAST" })
},
}

const key = getEventKey(event, {
dir: state.context.dir,
orientation: state.context.orientation,
})

const exec = keyMap[key]

if (exec) {
exec(event)
event.preventDefault()
}
},

style: {
display: "flex",
flexDirection: horizontal ? "row" : "column",
[horizontal ? "height" : "width"]: "auto",
[horizontal ? "gridAutoColumns" : "gridAutoRows"]:
`calc(100% / ${slidesPerView} - ${spacing} * ${slidesPerView - 1} / ${slidesPerView})`,
[horizontal ? "scrollPaddingInline" : "scrollPaddingBlock"]: padding,
[horizontal ? "paddingInline" : "paddingBlock"]: padding,
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(props) {
const itemState = getItemState(props)
const slides = Math.floor(slidesPerView)
const shouldSnap = scrollBy === "item" || (props.index + slides) % slides === 0

return normalize.element({
...parts.item.attrs,
id: dom.getItemId(state.context, props.index),
dir: state.context.dir,
"data-current": dataAttr(itemState.current),
"data-inview": dataAttr(itemState.inView),
// Used to detect active slide after scroll
"data-index": props.index,
role: "group",
"aria-roledescription": "slide",
"aria-roledescription": "carousel item",
"data-orientation": state.context.orientation,
"aria-label": itemState.valueText,
inert: itemState.inView ? undefined : "true",
"aria-hidden": ariaAttr(!itemState.inView),

style: {
position: "relative",
flex: "0 0 var(--slide-size)",
[horizontal ? "minWidth" : "minHeight"]: "0px",
scrollSnapAlign: shouldSnap ? "start" : undefined,
},
})
},
Expand All @@ -125,7 +169,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
"data-orientation": state.context.orientation,
"aria-controls": dom.getItemGroupId(state.context),
onClick() {
send("PREV")
send("GOTO.PREV")
},
})
},
Expand All @@ -142,7 +186,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
"aria-controls": dom.getItemGroupId(state.context),
disabled: !canScrollNext,
onClick() {
send("NEXT")
send("GOTO.NEXT")
},
})
},
Expand Down
2 changes: 0 additions & 2 deletions packages/machines/carousel/src/carousel.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { MachineContext as Ctx } from "./carousel.types"

export const dom = createScope({
getRootId: (ctx: Ctx) => ctx.ids?.root ?? `carousel:${ctx.id}`,
getViewportId: (ctx: Ctx) => ctx.ids?.viewport ?? `carousel:${ctx.id}:viewport`,
getItemId: (ctx: Ctx, index: number) => ctx.ids?.item?.(index) ?? `carousel:${ctx.id}:item:${index}`,
getItemGroupId: (ctx: Ctx) => ctx.ids?.itemGroup ?? `carousel:${ctx.id}:item-group`,
getNextTriggerId: (ctx: Ctx) => ctx.ids?.nextTrigger ?? `carousel:${ctx.id}:next-trigger`,
Expand All @@ -12,7 +11,6 @@ export const dom = createScope({
getIndicatorId: (ctx: Ctx, index: number) => ctx.ids?.indicator?.(index) ?? `carousel:${ctx.id}:indicator:${index}`,

getRootEl: (ctx: Ctx) => dom.getById(ctx, dom.getRootId(ctx)),
getViewportEl: (ctx: Ctx) => dom.getById(ctx, dom.getViewportId(ctx)),
getSlideGroupEl: (ctx: Ctx) => dom.getById(ctx, dom.getItemGroupId(ctx)),
getSlideEls: (ctx: Ctx) => queryAll(dom.getSlideGroupEl(ctx), `[data-part=item]`),
})
Loading

0 comments on commit 134b6ed

Please sign in to comment.