@zag-js/scroll-snap
Version:
Scroll snap utilities
207 lines (206 loc) • 8.15 kB
JavaScript
// src/index.ts
import { getComputedStyle, getScale } from "@zag-js/dom-query";
var getDirection = (element) => getComputedStyle(element).direction;
var convert = (raw, size) => {
let n = parseFloat(raw);
if (/%/.test(raw)) {
n /= 100;
n *= size;
}
return Number.isNaN(n) ? 0 : n;
};
function getScrollPadding(element) {
const style = getComputedStyle(element);
const layoutWidth = element.offsetWidth;
const layoutHeight = element.offsetHeight;
let xBeforeRaw = style.getPropertyValue("scroll-padding-left").replace("auto", "0px");
let yBeforeRaw = style.getPropertyValue("scroll-padding-top").replace("auto", "0px");
let xAfterRaw = style.getPropertyValue("scroll-padding-right").replace("auto", "0px");
let yAfterRaw = style.getPropertyValue("scroll-padding-bottom").replace("auto", "0px");
let xBefore = convert(xBeforeRaw, layoutWidth);
let yBefore = convert(yBeforeRaw, layoutHeight);
let xAfter = convert(xAfterRaw, layoutWidth);
let yAfter = convert(yAfterRaw, layoutHeight);
return {
x: { before: xBefore, after: xAfter },
y: { before: yBefore, after: yAfter }
};
}
function isRectIntersecting(a, b, axis = "both") {
return axis === "x" && a.right >= b.left && a.left <= b.right || axis === "y" && a.bottom >= b.top && a.top <= b.bottom || axis === "both" && a.right >= b.left && a.left <= b.right && a.bottom >= b.top && a.top <= b.bottom;
}
function getDescendants(parent) {
let children = [];
for (const child of parent.children) {
children = children.concat(child, getDescendants(child));
}
return children;
}
function getSnapPositions(parent, subtree = false) {
const parentRect = parent.getBoundingClientRect();
const dir = getDirection(parent);
const isRtl = dir === "rtl";
const scale = getScale(parent);
const positions = {
x: { start: [], center: [], end: [] },
y: { start: [], center: [], end: [] }
};
const children = subtree ? getDescendants(parent) : parent.children;
for (const axis of ["x", "y"]) {
const orthogonalAxis = axis === "x" ? "y" : "x";
const axisStart = axis === "x" ? "left" : "top";
const axisEnd = axis === "x" ? "right" : "bottom";
const axisSize = axis === "x" ? "width" : "height";
const axisScroll = axis === "x" ? "scrollLeft" : "scrollTop";
const axisScale = axis === "x" ? scale.x : scale.y;
const useRtlCalc = isRtl && axis === "x";
for (const child of children) {
const childRect = child.getBoundingClientRect();
if (!isRectIntersecting(parentRect, childRect, orthogonalAxis)) {
continue;
}
const childStyle = getComputedStyle(child);
let [childAlignY, childAlignX] = childStyle.getPropertyValue("scroll-snap-align").split(" ");
if (typeof childAlignX === "undefined") {
childAlignX = childAlignY;
}
const childAlign = axis === "x" ? childAlignX : childAlignY;
let childOffsetStart;
let childOffsetEnd;
let childOffsetCenter;
if (useRtlCalc) {
const scrollOffset = Math.abs(parent[axisScroll]);
const rightOffset = (parentRect[axisEnd] - childRect[axisEnd]) / axisScale + scrollOffset;
childOffsetStart = rightOffset;
childOffsetEnd = rightOffset + childRect[axisSize] / axisScale;
childOffsetCenter = rightOffset + childRect[axisSize] / (2 * axisScale);
} else {
childOffsetStart = (childRect[axisStart] - parentRect[axisStart]) / axisScale + parent[axisScroll];
childOffsetEnd = childOffsetStart + childRect[axisSize] / axisScale;
childOffsetCenter = childOffsetStart + childRect[axisSize] / (2 * axisScale);
}
switch (childAlign) {
case "none":
break;
case "start":
positions[axis].start.push({ node: child, position: childOffsetStart });
break;
case "center":
positions[axis].center.push({ node: child, position: childOffsetCenter });
break;
case "end":
positions[axis].end.push({ node: child, position: childOffsetEnd });
break;
}
}
}
return positions;
}
function getScrollSnapPositions(element) {
const dir = getDirection(element);
const scrollPadding = getScrollPadding(element);
const snapPositions = getSnapPositions(element);
const layoutWidth = element.offsetWidth;
const layoutHeight = element.offsetHeight;
const maxScroll = {
x: element.scrollWidth - element.offsetWidth,
y: element.scrollHeight - element.offsetHeight
};
const isRtl = dir === "rtl";
const usesNegativeScrollLeft = isRtl && element.scrollLeft <= 0;
let xPositions;
if (isRtl) {
xPositions = uniq(
[
...snapPositions.x.start.map((v) => v.position - scrollPadding.x.after),
...snapPositions.x.center.map((v) => v.position - layoutWidth / 2),
...snapPositions.x.end.map((v) => v.position - layoutWidth + scrollPadding.x.before)
].map(clamp(0, maxScroll.x))
);
if (usesNegativeScrollLeft) {
xPositions = xPositions.map((pos) => -pos);
}
} else {
xPositions = uniq(
[
...snapPositions.x.start.map((v) => v.position - scrollPadding.x.before),
...snapPositions.x.center.map((v) => v.position - layoutWidth / 2),
...snapPositions.x.end.map((v) => v.position - layoutWidth + scrollPadding.x.after)
].map(clamp(0, maxScroll.x))
);
}
return {
x: xPositions,
y: uniq(
[
...snapPositions.y.start.map((v) => v.position - scrollPadding.y.before),
...snapPositions.y.center.map((v) => v.position - layoutHeight / 2),
...snapPositions.y.end.map((v) => v.position - layoutHeight + scrollPadding.y.after)
].map(clamp(0, maxScroll.y))
)
};
}
function findSnapPoint(parent, axis, predicate) {
const dir = getDirection(parent);
const scrollPadding = getScrollPadding(parent);
const snapPositions = getSnapPositions(parent);
const items = [...snapPositions[axis].start, ...snapPositions[axis].center, ...snapPositions[axis].end];
const isRtl = dir === "rtl";
const usesNegativeScrollLeft = isRtl && axis === "x" && parent.scrollLeft <= 0;
for (const item of items) {
if (predicate(item.node)) {
let position;
if (axis === "x" && isRtl) {
position = item.position - scrollPadding.x.after;
if (usesNegativeScrollLeft) {
position = -position;
}
} else {
position = item.position - (axis === "x" ? scrollPadding.x.before : scrollPadding.y.before);
}
return position;
}
}
}
function getSnapPointTarget(parent, snapPoint) {
const rect = parent.getBoundingClientRect();
const scale = getScale(parent);
const scrollPadding = getScrollPadding(parent);
const children = Array.from(parent.children);
const layoutWidth = parent.offsetWidth;
const layoutHeight = parent.offsetHeight;
for (const child of children) {
const childRect = child.getBoundingClientRect();
const childOffsetStart = {
x: (childRect.left - rect.left) / scale.x + parent.scrollLeft,
y: (childRect.top - rect.top) / scale.y + parent.scrollTop
};
const childLayoutWidth = childRect.width / scale.x;
const childLayoutHeight = childRect.height / scale.y;
const matchesX = [
childOffsetStart.x - scrollPadding.x.before,
// start
childOffsetStart.x + childLayoutWidth / 2 - layoutWidth / 2,
// center
childOffsetStart.x + childLayoutWidth - layoutWidth + scrollPadding.x.after
// end
].some((pos) => Math.abs(pos - snapPoint) < 1);
const matchesY = [
childOffsetStart.y - scrollPadding.y.before,
childOffsetStart.y + childLayoutHeight / 2 - layoutHeight / 2,
childOffsetStart.y + childLayoutHeight - layoutHeight + scrollPadding.y.after
].some((pos) => Math.abs(pos - snapPoint) < 1);
if (matchesX || matchesY) {
return child;
}
}
return children[0];
}
var uniq = (arr) => [...new Set(arr)];
var clamp = (min, max) => (value) => Math.max(min, Math.min(max, value));
export {
findSnapPoint,
getScrollSnapPositions,
getSnapPointTarget,
getSnapPositions
};