@ryvora/react-scrollable-list
Version:
📜↔️ Horizontally scrollable list with navigation buttons for React. Perfect for carousels and tab lists!
8 lines (7 loc) • 13.9 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/scrollable-list.tsx"],
"sourcesContent": ["import React from 'react';\nimport { Primitive } from '@ryvora/react-primitive';\nimport { createContextScope, type Scope } from '@ryvora/react-context';\nimport { useComposedRefs } from '@ryvora/react-compose-refs';\nimport { composeEventHandlers } from '@ryvora/primitive';\n\n/** ----------------------------------------- ScrollableList ----------------------------------------- */\n\nconst SCROLLABLE_LIST_NAME = 'ScrollableList';\n\ntype ScopedProps<P> = P & { __scopeScrollableList?: Scope };\n\nconst [createScrollableListContext, createScrollableListScope] =\n createContextScope(SCROLLABLE_LIST_NAME);\n\ninterface ScrollContextType {\n firstRef: React.RefObject<React.ComponentRef<typeof Primitive.div> | null>;\n lastRef: React.RefObject<React.ComponentRef<typeof Primitive.div> | null>;\n scrollRef: React.RefObject<React.ComponentRef<typeof Primitive.div> | null>;\n items: React.ReactNode[];\n disabledLeft: boolean;\n setDisabledLeft: React.Dispatch<React.SetStateAction<boolean>>;\n disabledRight: boolean;\n setDisabledRight: React.Dispatch<React.SetStateAction<boolean>>;\n isScrollReady?: boolean;\n}\n\nconst [ScrollableListProvider, useScrollableListContextInternal] =\n createScrollableListContext<ScrollContextType>(SCROLLABLE_LIST_NAME);\n\nexport function useScrollContext(callerName: string, scope?: Scope): ScrollContextType {\n const context = useScrollableListContextInternal(callerName, scope);\n if (!context) {\n throw new Error(`\\`${callerName}\\` must be used within \\`${SCROLLABLE_LIST_NAME}\\``);\n }\n return context;\n}\n\ntype ScrollableListElement = React.ComponentRef<typeof Primitive.div>;\ntype PrimitiveDivProps = React.ComponentPropsWithoutRef<typeof Primitive.div>;\ninterface ScrollableListProps extends PrimitiveDivProps {}\n\nconst ScrollableList = React.forwardRef<ScrollableListElement, ScopedProps<ScrollableListProps>>(\n ({ __scopeScrollableList, children, ...props }, forwardedRef) => {\n const firstRef = React.useRef<React.ComponentRef<typeof Primitive.div>>(null);\n const lastRef = React.useRef<React.ComponentRef<typeof Primitive.div>>(null);\n const scrollRef = React.useRef<React.ComponentRef<typeof Primitive.div>>(null);\n\n const [isScrollReady, setIsScrollReady] = React.useState(false);\n const [disabledLeft, setDisabledLeft] = React.useState(true);\n const [disabledRight, setDisabledRight] = React.useState(false);\n\n React.useEffect(() => {\n if (scrollRef.current) {\n setIsScrollReady(true);\n }\n }, []);\n\n let identifiedViewport: React.ReactElement | null = null;\n const otherChildren: React.ReactNode[] = [];\n\n React.Children.forEach(children, (child) => {\n if (\n React.isValidElement(child) &&\n typeof child.type !== 'string' &&\n (child.type as { displayName?: string }).displayName === VIEWPORT_NAME\n ) {\n identifiedViewport = child;\n } else {\n otherChildren.push(child);\n }\n });\n\n let viewportWithRefs: React.ReactElement | null = identifiedViewport;\n\n if (identifiedViewport) {\n const viewportProps = (identifiedViewport as { props: { children?: React.ReactNode } }).props;\n const itemsFromViewport = React.Children.toArray(viewportProps.children);\n\n const itemsWithRefs = itemsFromViewport.map((item, idx) => {\n if (!React.isValidElement(item)) return item;\n if (idx === 0)\n return React.cloneElement(item as React.ReactElement<React.RefAttributes<HTMLElement>>, {\n ref: firstRef,\n });\n if (idx === itemsFromViewport.length - 1)\n return React.cloneElement(item as React.ReactElement<React.RefAttributes<HTMLElement>>, {\n ref: lastRef,\n });\n return item;\n });\n\n viewportWithRefs = React.cloneElement(identifiedViewport, {}, itemsWithRefs);\n }\n\n return (\n <ScrollableListProvider\n scope={__scopeScrollableList}\n firstRef={firstRef}\n lastRef={lastRef}\n scrollRef={scrollRef}\n items={React.Children.toArray(children)}\n disabledLeft={disabledLeft}\n setDisabledLeft={setDisabledLeft}\n disabledRight={disabledRight}\n setDisabledRight={setDisabledRight}\n isScrollReady={isScrollReady}\n >\n <Primitive.div data-state-scrollable-list-root {...props} ref={forwardedRef}>\n {viewportWithRefs}\n {otherChildren}\n </Primitive.div>\n </ScrollableListProvider>\n );\n }\n);\n\nScrollableList.displayName = SCROLLABLE_LIST_NAME;\n\n/** ----------------------------------------- ScrollableListViewport ----------------------------------------- */\n\nconst VIEWPORT_NAME = 'ScrollableListViewport';\ntype ScrollableListViewportElement = React.ComponentRef<typeof Primitive.div>;\ninterface ScrollableListViewportProps extends PrimitiveDivProps {}\n\nconst ScrollableListViewport = React.forwardRef<\n ScrollableListViewportElement,\n ScopedProps<ScrollableListViewportProps>\n>(({ __scopeScrollableList, children, style, onScroll, ...props }, forwardedRef) => {\n const context = useScrollContext(VIEWPORT_NAME, __scopeScrollableList);\n const { setDisabledLeft, setDisabledRight } = context;\n const composedRefs = useComposedRefs(forwardedRef, context.scrollRef);\n\n const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {\n const target = event.currentTarget;\n const scrollLeft = target.scrollLeft;\n const scrollWidth = target.scrollWidth;\n const clientWidth = target.clientWidth;\n\n console.log({\n scrollLeft,\n scrollWidth,\n clientWidth,\n isAtStart: scrollLeft === 0,\n isAtEnd: clientWidth + scrollLeft >= scrollWidth - 1,\n });\n\n setDisabledLeft(scrollLeft === 0);\n setDisabledRight(clientWidth + scrollLeft >= scrollWidth - 1);\n };\n\n return (\n <Primitive.div\n data-state-scrollable-list-viewport\n ref={composedRefs}\n onScroll={composeEventHandlers(onScroll, handleScroll)}\n {...props}\n >\n {children}\n </Primitive.div>\n );\n});\n\nScrollableListViewport.displayName = VIEWPORT_NAME;\n\n/** ----------------------------------------- ScrollableListItem ----------------------------------------- */\n\nconst ITEM_NAME = 'ScrollableListItem';\ntype ScrollableListItemElement = React.ComponentRef<typeof Primitive.div>;\ninterface ScrollableListItemProps extends PrimitiveDivProps {}\n\nconst ScrollableListItem = React.forwardRef<\n ScrollableListItemElement,\n ScopedProps<ScrollableListItemProps>\n>(({ __scopeScrollableList, ...props }, forwardedRef) => {\n return <Primitive.div data-state-scrollable-list-item {...props} ref={forwardedRef} />;\n});\n\nScrollableListItem.displayName = ITEM_NAME;\n\n/** ----------------------------------------- ScrollableListPreviousButton ----------------------------------------- */\n\nconst PREVIOUS_BUTTON_NAME = 'ScrollableListPreviousButton';\ntype ScrollableListPreviousButtonElement = React.ComponentRef<typeof Primitive.button>;\ntype PrimitiveButtonProps = React.ComponentPropsWithoutRef<typeof Primitive.button>;\ninterface ScrollableListPreviousButtonProps extends PrimitiveButtonProps {}\n\nconst ScrollableListPreviousButton = React.forwardRef<\n ScrollableListPreviousButtonElement,\n ScopedProps<ScrollableListPreviousButtonProps>\n>(({ __scopeScrollableList, onClick, ...props }, forwardedRef) => {\n const { disabledLeft, scrollRef, isScrollReady } = useScrollContext(\n PREVIOUS_BUTTON_NAME,\n __scopeScrollableList\n );\n\n const handleClick = () => {\n if (scrollRef.current) {\n scrollRef.current.scrollBy({\n left: -200,\n behavior: 'smooth',\n });\n }\n };\n\n if (!isScrollReady) return null;\n\n return (\n <Primitive.button\n data-state-scrollable-list-prev-button\n data-disabled={disabledLeft ? '' : undefined}\n disabled={disabledLeft}\n onClick={composeEventHandlers(onClick, handleClick)}\n {...props}\n ref={forwardedRef}\n />\n );\n});\n\nScrollableListPreviousButton.displayName = PREVIOUS_BUTTON_NAME;\n\n/** ----------------------------------------- ScrollableListNextButton ----------------------------------------- */\n\nconst NEXT_BUTTON_NAME = 'ScrollableListNextButton';\ntype ScrollableListNextButtonElement = React.ComponentRef<typeof Primitive.button>;\ninterface ScrollableListNextButtonProps extends PrimitiveButtonProps {}\n\nconst ScrollableListNextButton = React.forwardRef<\n ScrollableListNextButtonElement,\n ScopedProps<ScrollableListNextButtonProps>\n>(({ __scopeScrollableList, onClick, ...props }, forwardedRef) => {\n const { disabledRight, scrollRef, isScrollReady } = useScrollContext(\n NEXT_BUTTON_NAME,\n __scopeScrollableList\n );\n\n const handleClick = () => {\n if (scrollRef.current) {\n scrollRef.current.scrollBy({\n left: 200,\n behavior: 'smooth',\n });\n }\n };\n\n if (!isScrollReady) return null;\n\n return (\n <Primitive.button\n data-state-scrollable-list-next-button\n aria-disabled={disabledRight || undefined}\n data-disabled={disabledRight ? '' : undefined}\n disabled={disabledRight}\n onClick={composeEventHandlers(onClick, handleClick)}\n {...props}\n ref={forwardedRef}\n />\n );\n});\n\nScrollableListNextButton.displayName = NEXT_BUTTON_NAME;\n\nexport { createScrollableListScope };\n\nconst Root = ScrollableList;\nconst Viewport = ScrollableListViewport;\nconst Item = ScrollableListItem;\nconst PreviousButton = ScrollableListPreviousButton;\nconst NextButton = ScrollableListNextButton;\n\nexport {\n ScrollableList,\n ScrollableListViewport,\n ScrollableListItem,\n ScrollableListPreviousButton,\n ScrollableListNextButton,\n //\n Root,\n Viewport,\n Item,\n PreviousButton,\n NextButton,\n};\n\nexport type {\n ScrollableListProps,\n ScrollableListViewportProps,\n ScrollableListItemProps,\n ScrollableListPreviousButtonProps,\n ScrollableListNextButtonProps,\n};\n"],
"mappings": ";;;AAAA,OAAO,WAAW;AAClB,SAAS,iBAAiB;AAC1B,SAAS,0BAAsC;AAC/C,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AA4F/B,cAYE,YAZF;AAxFN,IAAM,uBAAuB;AAI7B,IAAM,CAAC,6BAA6B,yBAAyB,IAC3D,mBAAmB,oBAAoB;AAczC,IAAM,CAAC,wBAAwB,gCAAgC,IAC7D,4BAA+C,oBAAoB;AAE9D,SAAS,iBAAiB,YAAoB,OAAkC;AACrF,QAAM,UAAU,iCAAiC,YAAY,KAAK;AAClE,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,KAAK,UAAU,4BAA4B,oBAAoB,IAAI;AAAA,EACrF;AACA,SAAO;AACT;AAMA,IAAM,iBAAiB,MAAM;AAAA,EAC3B,CAAC,EAAE,uBAAuB,UAAU,GAAG,MAAM,GAAG,iBAAiB;AAC/D,UAAM,WAAW,MAAM,OAAiD,IAAI;AAC5E,UAAM,UAAU,MAAM,OAAiD,IAAI;AAC3E,UAAM,YAAY,MAAM,OAAiD,IAAI;AAE7E,UAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,KAAK;AAC9D,UAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,IAAI;AAC3D,UAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,KAAK;AAE9D,UAAM,UAAU,MAAM;AACpB,UAAI,UAAU,SAAS;AACrB,yBAAiB,IAAI;AAAA,MACvB;AAAA,IACF,GAAG,CAAC,CAAC;AAEL,QAAI,qBAAgD;AACpD,UAAM,gBAAmC,CAAC;AAE1C,UAAM,SAAS,QAAQ,UAAU,CAAC,UAAU;AAC1C,UACE,MAAM,eAAe,KAAK,KAC1B,OAAO,MAAM,SAAS,YACrB,MAAM,KAAkC,gBAAgB,eACzD;AACA,6BAAqB;AAAA,MACvB,OAAO;AACL,sBAAc,KAAK,KAAK;AAAA,MAC1B;AAAA,IACF,CAAC;AAED,QAAI,mBAA8C;AAElD,QAAI,oBAAoB;AACtB,YAAM,gBAAiB,mBAAiE;AACxF,YAAM,oBAAoB,MAAM,SAAS,QAAQ,cAAc,QAAQ;AAEvE,YAAM,gBAAgB,kBAAkB,IAAI,CAAC,MAAM,QAAQ;AACzD,YAAI,CAAC,MAAM,eAAe,IAAI,EAAG,QAAO;AACxC,YAAI,QAAQ;AACV,iBAAO,MAAM,aAAa,MAA8D;AAAA,YACtF,KAAK;AAAA,UACP,CAAC;AACH,YAAI,QAAQ,kBAAkB,SAAS;AACrC,iBAAO,MAAM,aAAa,MAA8D;AAAA,YACtF,KAAK;AAAA,UACP,CAAC;AACH,eAAO;AAAA,MACT,CAAC;AAED,yBAAmB,MAAM,aAAa,oBAAoB,CAAC,GAAG,aAAa;AAAA,IAC7E;AAEA,WACE;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO,MAAM,SAAS,QAAQ,QAAQ;AAAA,QACtC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QAEA,+BAAC,UAAU,KAAV,EAAc,mCAA+B,MAAE,GAAG,OAAO,KAAK,cAC5D;AAAA;AAAA,UACA;AAAA,WACH;AAAA;AAAA,IACF;AAAA,EAEJ;AACF;AAEA,eAAe,cAAc;AAI7B,IAAM,gBAAgB;AAItB,IAAM,yBAAyB,MAAM,WAGnC,CAAC,EAAE,uBAAuB,UAAU,OAAO,UAAU,GAAG,MAAM,GAAG,iBAAiB;AAClF,QAAM,UAAU,iBAAiB,eAAe,qBAAqB;AACrE,QAAM,EAAE,iBAAiB,iBAAiB,IAAI;AAC9C,QAAM,eAAe,gBAAgB,cAAc,QAAQ,SAAS;AAEpE,QAAM,eAAe,CAAC,UAAyC;AAC7D,UAAM,SAAS,MAAM;AACrB,UAAM,aAAa,OAAO;AAC1B,UAAM,cAAc,OAAO;AAC3B,UAAM,cAAc,OAAO;AAE3B,YAAQ,IAAI;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,eAAe;AAAA,MAC1B,SAAS,cAAc,cAAc,cAAc;AAAA,IACrD,CAAC;AAED,oBAAgB,eAAe,CAAC;AAChC,qBAAiB,cAAc,cAAc,cAAc,CAAC;AAAA,EAC9D;AAEA,SACE;AAAA,IAAC,UAAU;AAAA,IAAV;AAAA,MACC,uCAAmC;AAAA,MACnC,KAAK;AAAA,MACL,UAAU,qBAAqB,UAAU,YAAY;AAAA,MACpD,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ,CAAC;AAED,uBAAuB,cAAc;AAIrC,IAAM,YAAY;AAIlB,IAAM,qBAAqB,MAAM,WAG/B,CAAC,EAAE,uBAAuB,GAAG,MAAM,GAAG,iBAAiB;AACvD,SAAO,oBAAC,UAAU,KAAV,EAAc,mCAA+B,MAAE,GAAG,OAAO,KAAK,cAAc;AACtF,CAAC;AAED,mBAAmB,cAAc;AAIjC,IAAM,uBAAuB;AAK7B,IAAM,+BAA+B,MAAM,WAGzC,CAAC,EAAE,uBAAuB,SAAS,GAAG,MAAM,GAAG,iBAAiB;AAChE,QAAM,EAAE,cAAc,WAAW,cAAc,IAAI;AAAA,IACjD;AAAA,IACA;AAAA,EACF;AAEA,QAAM,cAAc,MAAM;AACxB,QAAI,UAAU,SAAS;AACrB,gBAAU,QAAQ,SAAS;AAAA,QACzB,MAAM;AAAA,QACN,UAAU;AAAA,MACZ,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,CAAC,cAAe,QAAO;AAE3B,SACE;AAAA,IAAC,UAAU;AAAA,IAAV;AAAA,MACC,0CAAsC;AAAA,MACtC,iBAAe,eAAe,KAAK;AAAA,MACnC,UAAU;AAAA,MACV,SAAS,qBAAqB,SAAS,WAAW;AAAA,MACjD,GAAG;AAAA,MACJ,KAAK;AAAA;AAAA,EACP;AAEJ,CAAC;AAED,6BAA6B,cAAc;AAI3C,IAAM,mBAAmB;AAIzB,IAAM,2BAA2B,MAAM,WAGrC,CAAC,EAAE,uBAAuB,SAAS,GAAG,MAAM,GAAG,iBAAiB;AAChE,QAAM,EAAE,eAAe,WAAW,cAAc,IAAI;AAAA,IAClD;AAAA,IACA;AAAA,EACF;AAEA,QAAM,cAAc,MAAM;AACxB,QAAI,UAAU,SAAS;AACrB,gBAAU,QAAQ,SAAS;AAAA,QACzB,MAAM;AAAA,QACN,UAAU;AAAA,MACZ,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,CAAC,cAAe,QAAO;AAE3B,SACE;AAAA,IAAC,UAAU;AAAA,IAAV;AAAA,MACC,0CAAsC;AAAA,MACtC,iBAAe,iBAAiB;AAAA,MAChC,iBAAe,gBAAgB,KAAK;AAAA,MACpC,UAAU;AAAA,MACV,SAAS,qBAAqB,SAAS,WAAW;AAAA,MACjD,GAAG;AAAA,MACJ,KAAK;AAAA;AAAA,EACP;AAEJ,CAAC;AAED,yBAAyB,cAAc;AAIvC,IAAM,OAAO;AACb,IAAM,WAAW;AACjB,IAAM,OAAO;AACb,IAAM,iBAAiB;AACvB,IAAM,aAAa;",
"names": []
}