UNPKG

react-overflow-slider

Version:

A customizable horizontal slider component for React with native scroll

247 lines (241 loc) 8.47 kB
// src/slider/slider.tsx import { Children, useCallback, useEffect, useRef, useState } from "react"; // src/slider/button.tsx import { jsx } from "react/jsx-runtime"; var SliderButton = ({ left }) => { return /* @__PURE__ */ jsx( "div", { className: `react-overflow-slider-btn ${left ? "react-overflow-slider-btn--left" : ""} `, children: /* @__PURE__ */ jsx( "svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polyline", { points: "8 4 16 12 8 20" }) } ) } ); }; var button_default = SliderButton; // src/slider/utils/buttons-states.ts function updateButtonsVisibility(scroller, setButtonsState) { const { offsetWidth, scrollLeft, scrollWidth } = scroller; setButtonsState((state) => ({ ...state, prevButtonVisible: scrollLeft > 0, nextButtonVisible: scrollLeft + offsetWidth < scrollWidth })); } function updateButtonsDisabled(setButtonsState, show) { setButtonsState((state) => ({ ...state, prevButtonDisabled: show, nextButtonDisabled: show })); } // src/slider/utils/get-previous-offset.ts function getPreviousOffset(scroller, items) { const findFirstVisibleItemIndex = (scrollLeft, widths) => { let first = 0, i = 0; while (first < scrollLeft) { first += widths[i]; i++; } return i; }; const findPreviousItemIndex = (visibleScroll, firstVisible2, widths) => { let i = firstVisible2, possible = 0; if (widths[i - 1] >= visibleScroll) return i - 1; do { i--; possible += widths[i]; } while (possible < visibleScroll && i > -1); return i + 1; }; const scrollerVisibleWidth = scroller.offsetWidth; const scrollerScrollLeft = scroller.scrollLeft; const itemsWidths = items.map((item) => item ? item.offsetWidth : 0); const firstVisible = findFirstVisibleItemIndex( scrollerScrollLeft, itemsWidths ); const prevItemIndex = findPreviousItemIndex( scrollerVisibleWidth, firstVisible, itemsWidths ); const itemsOffsetLeft = items.map((item) => item ? item.offsetLeft : 0); return itemsOffsetLeft[prevItemIndex]; } // src/slider/utils/smooth-scroll.ts var easeOutCubic = (t) => 1 - Math.pow(1 - t, 3); var smoothScroll = (scroller, end, duration) => { const start = scroller.scrollLeft; const startTime = performance.now(); const step = (currentTime) => { const elapsed = (currentTime - startTime) / duration; const progress = Math.min(easeOutCubic(elapsed), 1); scroller.scrollLeft = start + (end - start) * progress; if (progress < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); }; // src/slider/utils/get-next-offset.ts function getNextOffset(scroller, items) { const findNextItemIndex = (left, right, widths) => { let availableWidth = 0; let visibleWidth = 0; let i = 0; do { availableWidth += widths[i]; i++; } while (right >= availableWidth && i < widths.length); i = i - 1; for (let j = 0; j < i; j++) { visibleWidth += widths[j]; } return visibleWidth <= left ? i + 1 : i; }; const scrollerVisibleWidth = scroller.offsetWidth; const scrollerScrollLeft = scroller.scrollLeft; const scrollerScrollRight = scrollerScrollLeft + scrollerVisibleWidth; const itemsWidths = items.map((item) => item ? item.offsetWidth : 0); const itemsOffsetLeft = items.map((item) => item ? item.offsetLeft : 0); const nextItemIndex = findNextItemIndex( scrollerScrollLeft, scrollerScrollRight, itemsWidths ); const scrollerWidth = scroller.scrollWidth; const possibleScroll = scrollerWidth - scrollerVisibleWidth + 1; const nextItem = nextItemIndex !== items.length ? itemsOffsetLeft[nextItemIndex] : null; return nextItem !== null && possibleScroll >= nextItem ? nextItem : possibleScroll; } // src/slider/slider.tsx import { jsx as jsx2, jsxs } from "react/jsx-runtime"; var OverflowSlider = ({ children, prevButton, nextButton, gap = 0, duration = 300 }) => { const scrollerRef = useRef(null); const itemsRef = useRef([]); const scrollWidth = useRef(0); const [buttonsState, setButtonsState] = useState({ prevButtonVisible: false, nextButtonVisible: false, prevButtonDisabled: false, nextButtonDisabled: false }); const checkShowButtons = useCallback(() => { if (scrollerRef.current) { updateButtonsVisibility(scrollerRef.current, setButtonsState); } }, [scrollerRef]); const setDisabledButtons = useCallback(() => { updateButtonsDisabled(setButtonsState, true); setTimeout(() => { updateButtonsDisabled(setButtonsState, false); }, duration + 50); }, [duration]); const handleScrollPrevious = useCallback(() => { if (scrollerRef.current && itemsRef?.current?.length) { const previousOffset = getPreviousOffset( scrollerRef.current, itemsRef.current ); smoothScroll(scrollerRef.current, previousOffset, duration); setDisabledButtons(); } }, [scrollerRef, itemsRef, setDisabledButtons, duration]); const handleScrollNext = useCallback(() => { if (scrollerRef.current && itemsRef?.current?.length) { const nextOffset = getNextOffset(scrollerRef.current, itemsRef.current); smoothScroll(scrollerRef.current, nextOffset, duration); setDisabledButtons(); } }, [scrollerRef, itemsRef, setDisabledButtons, duration]); useEffect(() => { const handleResize = checkShowButtons; window.addEventListener("resize", handleResize); checkShowButtons(); return () => window.removeEventListener("resize", handleResize); }, [checkShowButtons]); useEffect(() => { if (scrollerRef.current && scrollerRef.current.scrollWidth !== scrollWidth.current) { checkShowButtons(); scrollWidth.current = scrollerRef.current.scrollWidth; } }, [children, checkShowButtons]); return /* @__PURE__ */ jsx2("div", { className: "react-overflow-slider-container", children: /* @__PURE__ */ jsxs("div", { className: "react-overflow-slider", children: [ /* @__PURE__ */ jsx2("div", { className: "react-overflow-slider__btn-container", children: /* @__PURE__ */ jsx2( "button", { type: "button", role: "button", "aria-label": "Scroll to previous", onClick: handleScrollPrevious, className: `react-overflow-slider__btn react-overflow-slider__btn--prev ${buttonsState.prevButtonDisabled ? "react-overflow-slider__btn--disabled" : ""} ${buttonsState.prevButtonVisible ? "react-overflow-slider__btn--visible" : ""}`, style: { transition: `${duration}ms` }, children: prevButton || /* @__PURE__ */ jsx2(button_default, { left: true }) } ) }), /* @__PURE__ */ jsx2( "div", { ref: scrollerRef, className: "react-overflow-slider__scroller", onScroll: checkShowButtons, children: /* @__PURE__ */ jsx2("div", { className: "react-overflow-slider__body", children: Children.map(children, (child, i) => /* @__PURE__ */ jsx2( "div", { ref: (ref) => itemsRef.current[i] = ref, className: "react-overflow-slider__item", style: { ...gap && i < children.length - 1 && { paddingRight: gap } }, children: child } )) }) } ), /* @__PURE__ */ jsx2("div", { className: "react-overflow-slider__btn-container", children: /* @__PURE__ */ jsx2( "button", { type: "button", role: "button", "aria-label": "Scroll to next", onClick: handleScrollNext, className: `react-overflow-slider__btn react-overflow-slider__btn--next ${buttonsState.nextButtonDisabled ? "react-overflow-slider__btn--disabled" : ""} ${buttonsState.nextButtonVisible ? "react-overflow-slider__btn--visible" : ""}`, style: { transition: `${duration}ms` }, children: nextButton || /* @__PURE__ */ jsx2(button_default, {}) } ) }) ] }) }); }; var slider_default = OverflowSlider; export { slider_default as OverflowSlider }; //# sourceMappingURL=index.mjs.map