UNPKG

react-overflow-slider

Version:

A customizable horizontal slider component for React with native scroll

294 lines (285 loc) 11 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/slider/index.ts var index_exports = {}; __export(index_exports, { OverflowSlider: () => slider_default }); module.exports = __toCommonJS(index_exports); // src/slider/slider.tsx var import_react = require("react"); // src/slider/button.tsx var import_jsx_runtime = require("react/jsx-runtime"); var SliderButton = ({ left }) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: `react-overflow-slider-btn ${left ? "react-overflow-slider-btn--left" : ""} `, children: /* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.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/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/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/scroll-to-first-visible.ts function findFirstVisibleIndex(items) { return items.findIndex((item) => { if (!item) return false; if (item.dataset.firstVisible === "true") return true; if (item.firstElementChild?.getAttribute("data-first-visible") === "true") return true; return Array.from(item.children).some( (el) => el.dataset.firstVisible === "true" ); }); } function scrollToFirstVisible(itemsRef, scrollToOffset, initialScrollDone) { if (initialScrollDone.current) return; const index = findFirstVisibleIndex(itemsRef.current); if (index !== -1) { const targetOffset = itemsRef.current[index]?.offsetLeft ?? 0; scrollToOffset(targetOffset); initialScrollDone.current = true; } } // src/slider/slider.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); var OverflowSlider = ({ children, prevButton, nextButton, gap = 0, duration = 300 }) => { const scrollerRef = (0, import_react.useRef)(null); const itemsRef = (0, import_react.useRef)([]); const scrollWidth = (0, import_react.useRef)(0); const initialScrollDone = (0, import_react.useRef)(false); const [buttonsState, setButtonsState] = (0, import_react.useState)({ prevButtonVisible: false, nextButtonVisible: false, prevButtonDisabled: false, nextButtonDisabled: false }); const checkShowButtons = (0, import_react.useCallback)(() => { updateButtonsVisibility(scrollerRef.current, setButtonsState); }, []); const setDisabledButtons = (0, import_react.useCallback)(() => { updateButtonsDisabled(setButtonsState, true); setTimeout( () => updateButtonsDisabled(setButtonsState, false), duration + 50 ); }, [duration]); const scrollToOffset = (0, import_react.useCallback)( (offset) => { smoothScroll(scrollerRef.current, offset, duration); setDisabledButtons(); }, [duration, setDisabledButtons] ); const handleScrollPrevious = (0, import_react.useCallback)(() => { const offset = getPreviousOffset(scrollerRef.current, itemsRef.current); scrollToOffset(offset); }, [scrollToOffset]); const handleScrollNext = (0, import_react.useCallback)(() => { const offset = getNextOffset(scrollerRef.current, itemsRef.current); scrollToOffset(offset); }, [scrollToOffset]); const scrollToFirstVisibleCallback = (0, import_react.useCallback)(() => { scrollToFirstVisible(itemsRef, scrollToOffset, initialScrollDone); }, [scrollToOffset]); (0, import_react.useEffect)(() => { const handleResize = checkShowButtons; window.addEventListener("resize", handleResize); checkShowButtons(); return () => window.removeEventListener("resize", handleResize); }, [checkShowButtons]); (0, import_react.useEffect)(() => { const currentScrollWidth = scrollerRef.current?.scrollWidth ?? 0; if (currentScrollWidth !== scrollWidth.current) { scrollWidth.current = currentScrollWidth; checkShowButtons(); scrollToFirstVisibleCallback(); } }, [children, checkShowButtons, scrollToFirstVisibleCallback]); return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "react-overflow-slider-container", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "react-overflow-slider", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "react-overflow-slider__btn-container", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "button", { type: "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__ */ (0, import_jsx_runtime2.jsx)(button_default, { left: true }) } ) }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "div", { ref: scrollerRef, className: "react-overflow-slider__scroller", onScroll: checkShowButtons, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "react-overflow-slider__body", children: import_react.Children.map(children, (child, i) => { const isObject = typeof child === "object" && child !== null; const dataFirstVisible = isObject && "props" in child && child.props?.["data-first-visible"]; return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "div", { ref: (ref) => itemsRef.current[i] = ref, className: "react-overflow-slider__item", "data-first-visible": dataFirstVisible, style: gap && i < children.length - 1 ? { paddingRight: gap } : {}, children: child } ); }) }) } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "react-overflow-slider__btn-container", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "button", { type: "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__ */ (0, import_jsx_runtime2.jsx)(button_default, {}) } ) }) ] }) }); }; var slider_default = OverflowSlider; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { OverflowSlider }); //# sourceMappingURL=index.cjs.map