UNPKG

@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
"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