react-overflow-slider
Version:
A customizable horizontal slider component for React with native scroll
247 lines (241 loc) • 8.47 kB
JavaScript
// 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