@ryvora/react-scrollable-list
Version:
📜↔️ Horizontally scrollable list with navigation buttons for React. Perfect for carousels and tab lists!
195 lines (193 loc) • 6.74 kB
JavaScript
"use client";
// src/scrollable-list.tsx
import React from "react";
import { Primitive } from "@ryvora/react-primitive";
import { createContextScope } from "@ryvora/react-context";
import { useComposedRefs } from "@ryvora/react-compose-refs";
import { composeEventHandlers } from "@ryvora/primitive";
import { jsx, jsxs } from "react/jsx-runtime";
var SCROLLABLE_LIST_NAME = "ScrollableList";
var [createScrollableListContext, createScrollableListScope] = createContextScope(SCROLLABLE_LIST_NAME);
var [ScrollableListProvider, useScrollableListContextInternal] = createScrollableListContext(SCROLLABLE_LIST_NAME);
function useScrollContext(callerName, scope) {
const context = useScrollableListContextInternal(callerName, scope);
if (!context) {
throw new Error(`\`${callerName}\` must be used within \`${SCROLLABLE_LIST_NAME}\``);
}
return context;
}
var ScrollableList = React.forwardRef(
({ __scopeScrollableList, children, ...props }, forwardedRef) => {
const firstRef = React.useRef(null);
const lastRef = React.useRef(null);
const scrollRef = React.useRef(null);
const [isScrollReady, setIsScrollReady] = React.useState(false);
const [disabledLeft, setDisabledLeft] = React.useState(true);
const [disabledRight, setDisabledRight] = React.useState(false);
React.useEffect(() => {
if (scrollRef.current) {
setIsScrollReady(true);
}
}, []);
let identifiedViewport = null;
const otherChildren = [];
React.Children.forEach(children, (child) => {
if (React.isValidElement(child) && typeof child.type !== "string" && child.type.displayName === VIEWPORT_NAME) {
identifiedViewport = child;
} else {
otherChildren.push(child);
}
});
let viewportWithRefs = identifiedViewport;
if (identifiedViewport) {
const viewportProps = identifiedViewport.props;
const itemsFromViewport = React.Children.toArray(viewportProps.children);
const itemsWithRefs = itemsFromViewport.map((item, idx) => {
if (!React.isValidElement(item)) return item;
if (idx === 0)
return React.cloneElement(item, {
ref: firstRef
});
if (idx === itemsFromViewport.length - 1)
return React.cloneElement(item, {
ref: lastRef
});
return item;
});
viewportWithRefs = React.cloneElement(identifiedViewport, {}, itemsWithRefs);
}
return /* @__PURE__ */ jsx(
ScrollableListProvider,
{
scope: __scopeScrollableList,
firstRef,
lastRef,
scrollRef,
items: React.Children.toArray(children),
disabledLeft,
setDisabledLeft,
disabledRight,
setDisabledRight,
isScrollReady,
children: /* @__PURE__ */ jsxs(Primitive.div, { "data-state-scrollable-list-root": true, ...props, ref: forwardedRef, children: [
viewportWithRefs,
otherChildren
] })
}
);
}
);
ScrollableList.displayName = SCROLLABLE_LIST_NAME;
var VIEWPORT_NAME = "ScrollableListViewport";
var ScrollableListViewport = React.forwardRef(({ __scopeScrollableList, children, style, onScroll, ...props }, forwardedRef) => {
const context = useScrollContext(VIEWPORT_NAME, __scopeScrollableList);
const { setDisabledLeft, setDisabledRight } = context;
const composedRefs = useComposedRefs(forwardedRef, context.scrollRef);
const handleScroll = (event) => {
const target = event.currentTarget;
const scrollLeft = target.scrollLeft;
const scrollWidth = target.scrollWidth;
const clientWidth = target.clientWidth;
console.log({
scrollLeft,
scrollWidth,
clientWidth,
isAtStart: scrollLeft === 0,
isAtEnd: clientWidth + scrollLeft >= scrollWidth - 1
});
setDisabledLeft(scrollLeft === 0);
setDisabledRight(clientWidth + scrollLeft >= scrollWidth - 1);
};
return /* @__PURE__ */ jsx(
Primitive.div,
{
"data-state-scrollable-list-viewport": true,
ref: composedRefs,
onScroll: composeEventHandlers(onScroll, handleScroll),
...props,
children
}
);
});
ScrollableListViewport.displayName = VIEWPORT_NAME;
var ITEM_NAME = "ScrollableListItem";
var ScrollableListItem = React.forwardRef(({ __scopeScrollableList, ...props }, forwardedRef) => {
return /* @__PURE__ */ jsx(Primitive.div, { "data-state-scrollable-list-item": true, ...props, ref: forwardedRef });
});
ScrollableListItem.displayName = ITEM_NAME;
var PREVIOUS_BUTTON_NAME = "ScrollableListPreviousButton";
var ScrollableListPreviousButton = React.forwardRef(({ __scopeScrollableList, onClick, ...props }, forwardedRef) => {
const { disabledLeft, scrollRef, isScrollReady } = useScrollContext(
PREVIOUS_BUTTON_NAME,
__scopeScrollableList
);
const handleClick = () => {
if (scrollRef.current) {
scrollRef.current.scrollBy({
left: -200,
behavior: "smooth"
});
}
};
if (!isScrollReady) return null;
return /* @__PURE__ */ jsx(
Primitive.button,
{
"data-state-scrollable-list-prev-button": true,
"data-disabled": disabledLeft ? "" : void 0,
disabled: disabledLeft,
onClick: composeEventHandlers(onClick, handleClick),
...props,
ref: forwardedRef
}
);
});
ScrollableListPreviousButton.displayName = PREVIOUS_BUTTON_NAME;
var NEXT_BUTTON_NAME = "ScrollableListNextButton";
var ScrollableListNextButton = React.forwardRef(({ __scopeScrollableList, onClick, ...props }, forwardedRef) => {
const { disabledRight, scrollRef, isScrollReady } = useScrollContext(
NEXT_BUTTON_NAME,
__scopeScrollableList
);
const handleClick = () => {
if (scrollRef.current) {
scrollRef.current.scrollBy({
left: 200,
behavior: "smooth"
});
}
};
if (!isScrollReady) return null;
return /* @__PURE__ */ jsx(
Primitive.button,
{
"data-state-scrollable-list-next-button": true,
"aria-disabled": disabledRight || void 0,
"data-disabled": disabledRight ? "" : void 0,
disabled: disabledRight,
onClick: composeEventHandlers(onClick, handleClick),
...props,
ref: forwardedRef
}
);
});
ScrollableListNextButton.displayName = NEXT_BUTTON_NAME;
var Root = ScrollableList;
var Viewport = ScrollableListViewport;
var Item = ScrollableListItem;
var PreviousButton = ScrollableListPreviousButton;
var NextButton = ScrollableListNextButton;
export {
Item,
NextButton,
PreviousButton,
Root,
ScrollableList,
ScrollableListItem,
ScrollableListNextButton,
ScrollableListPreviousButton,
ScrollableListViewport,
Viewport,
createScrollableListScope
};
//# sourceMappingURL=index.mjs.map