@zag-js/scroll-area
Version:
Core logic for the scroll-area widget implemented as a state machine
217 lines (206 loc) • 7.54 kB
text/typescript
import { contains, dataAttr, getEventPoint, getEventTarget } from "@zag-js/dom-query"
import type { NormalizeProps, PropTypes } from "@zag-js/types"
import { toPx } from "@zag-js/utils"
import { parts } from "./scroll-area.anatomy"
import * as dom from "./scroll-area.dom"
import type { ScrollAreaApi, ScrollAreaService } from "./scroll-area.types"
import { getScrollProgress } from "./utils/scroll-progress"
import { scrollTo } from "./utils/scroll-to"
import { scrollToEdge } from "./utils/scroll-to-edge"
export function connect<T extends PropTypes>(
service: ScrollAreaService,
normalize: NormalizeProps<T>,
): ScrollAreaApi<T> {
const { state, send, context, prop, scope } = service
const dragging = state.matches("dragging")
const hovering = context.get("hovering")
const cornerSize = context.get("cornerSize")
const thumbSize = context.get("thumbSize")
const hiddenState = context.get("hiddenState")
const atSides = context.get("atSides")
return {
isAtTop: atSides.top,
isAtBottom: atSides.bottom,
isAtLeft: atSides.left,
isAtRight: atSides.right,
hasOverflowX: !hiddenState.scrollbarXHidden,
hasOverflowY: !hiddenState.scrollbarYHidden,
getScrollProgress() {
return getScrollProgress(dom.getViewportEl(scope), 0)
},
scrollToEdge(details) {
const { edge, behavior } = details
return scrollToEdge(dom.getViewportEl(scope), edge, prop("dir"), behavior)
},
scrollTo(details) {
return scrollTo(dom.getViewportEl(scope), details)
},
getScrollbarState(props) {
const horizontal = props.orientation === "horizontal"
return {
hovering,
dragging,
scrolling: context.get(horizontal ? "scrollingX" : "scrollingY"),
hidden: horizontal ? hiddenState.scrollbarXHidden : hiddenState.scrollbarYHidden,
}
},
getRootProps() {
return normalize.element({
...parts.root.attrs,
id: dom.getRootId(scope),
dir: prop("dir"),
role: "presentation",
"data-overflow-x": dataAttr(!hiddenState.scrollbarXHidden),
"data-overflow-y": dataAttr(!hiddenState.scrollbarYHidden),
onPointerEnter(event) {
const target = getEventTarget(event)
if (!contains(event.currentTarget, target)) return
send({ type: "root.pointerenter", pointerType: event.pointerType })
},
onPointerMove(event) {
const target = getEventTarget(event)
if (!contains(event.currentTarget, target)) return
send({ type: "root.pointerenter", pointerType: event.pointerType })
},
onPointerDown({ pointerType }) {
send({ type: "root.pointerdown", pointerType })
},
onPointerLeave(event) {
if (contains(event.currentTarget, event.relatedTarget)) return
send({ type: "root.pointerleave" })
},
style: {
position: "relative",
"--corner-width": toPx(cornerSize?.width),
"--corner-height": toPx(cornerSize?.height),
"--thumb-width": toPx(thumbSize?.width),
"--thumb-height": toPx(thumbSize?.height),
},
})
},
getViewportProps() {
const handleUserInteraction = () => {
send({ type: "user.scroll" })
}
return normalize.element({
...parts.viewport.attrs,
role: "presentation",
"data-ownedby": dom.getRootId(scope),
id: dom.getViewportId(scope),
"data-at-top": dataAttr(atSides.top),
"data-at-bottom": dataAttr(atSides.bottom),
"data-at-left": dataAttr(atSides.left),
"data-at-right": dataAttr(atSides.right),
"data-overflow-x": dataAttr(!hiddenState.scrollbarXHidden),
"data-overflow-y": dataAttr(!hiddenState.scrollbarYHidden),
tabIndex: hiddenState.scrollbarXHidden || hiddenState.scrollbarYHidden ? undefined : 0,
style: {
overflow: "auto",
},
onScroll(event) {
send({ type: "viewport.scroll", target: event.currentTarget })
},
onWheel: handleUserInteraction,
onTouchMove: handleUserInteraction,
onPointerMove: handleUserInteraction,
onPointerEnter: handleUserInteraction,
onKeyDown: handleUserInteraction,
})
},
getContentProps() {
return normalize.element({
...parts.content.attrs,
id: dom.getContentId(scope),
role: "presentation",
"data-overflow-x": dataAttr(!hiddenState.scrollbarXHidden),
"data-overflow-y": dataAttr(!hiddenState.scrollbarYHidden),
style: {
minWidth: "fit-content",
},
})
},
getScrollbarProps(props = {}) {
const { orientation = "vertical" } = props
return normalize.element({
...parts.scrollbar.attrs,
"data-ownedby": dom.getRootId(scope),
"data-orientation": orientation,
"data-scrolling": dataAttr(context.get(orientation === "horizontal" ? "scrollingX" : "scrollingY")),
"data-hover": dataAttr(hovering),
"data-dragging": dataAttr(dragging),
"data-overflow-x": dataAttr(!hiddenState.scrollbarXHidden),
"data-overflow-y": dataAttr(!hiddenState.scrollbarYHidden),
onPointerUp() {
send({ type: "scrollbar.pointerup", orientation })
},
onPointerDown(event) {
if (event.button !== 0) {
return
}
if (event.currentTarget !== event.target) {
return
}
const point = getEventPoint(event)
send({ type: "scrollbar.pointerdown", orientation, point })
event.stopPropagation()
},
style: {
position: "absolute",
touchAction: "none",
WebkitUserSelect: "none",
userSelect: "none",
...(orientation === "vertical" && {
top: 0,
bottom: `var(--corner-height)`,
insetInlineEnd: 0,
}),
...(orientation === "horizontal" && {
insetInlineStart: 0,
insetInlineEnd: `var(--corner-width)`,
bottom: 0,
}),
},
})
},
getThumbProps(props = {}) {
const { orientation = "vertical" } = props
return normalize.element({
...parts.thumb.attrs,
"data-ownedby": dom.getRootId(scope),
"data-orientation": orientation,
"data-hover": dataAttr(hovering),
"data-dragging": dataAttr(dragging),
onPointerDown(event) {
if (event.button !== 0) return
const point = getEventPoint(event)
send({ type: "thumb.pointerdown", orientation, point })
},
style: {
...(orientation === "vertical" && {
height: "var(--thumb-height)",
}),
...(orientation === "horizontal" && {
width: "var(--thumb-width)",
}),
},
})
},
getCornerProps() {
return normalize.element({
...parts.corner.attrs,
"data-ownedby": dom.getRootId(scope),
"data-hover": dataAttr(hovering),
"data-state": hiddenState.cornerHidden ? "hidden" : "visible",
"data-overflow-x": dataAttr(!hiddenState.scrollbarXHidden),
"data-overflow-y": dataAttr(!hiddenState.scrollbarYHidden),
style: {
position: "absolute",
bottom: 0,
insetInlineEnd: 0,
width: "var(--corner-width)",
height: "var(--corner-height)",
},
})
},
}
}