@kiwicom/orbit-components
Version:
Orbit-components is a React component library which provides developers with the easiest possible way of building Kiwi.com's products.
177 lines (176 loc) • 5.63 kB
JavaScript
"use client";
import * as React from "react";
import cx from "clsx";
import Stack from "../Stack";
import mergeRefs from "../utils/mergeRefs";
import useTheme from "../hooks/useTheme";
import useScrollBox from "./useScroll";
import ChevronBackward from "../icons/ChevronBackward";
import ChevronForward from "../icons/ChevronForward";
const getSnap = scrollSnap => {
if (scrollSnap === "mandatory") return "x mandatory";
if (scrollSnap === "proximity") return "x proximity";
return scrollSnap;
};
const ArrowButton = ({
children,
className,
isHidden,
onClick,
ariaLabel
}) => {
return /*#__PURE__*/React.createElement("button", {
className: cx("z-default absolute flex h-full items-center", isHidden && "invisible", className),
onClick: onClick,
type: "button",
"aria-label": ariaLabel
}, children);
};
const ElevationHaze = ({
className,
style
}) => {
return /*#__PURE__*/React.createElement("div", {
className: cx("z-default absolute top-0 h-full", className),
style: style
});
};
const HorizontalScroll = ({
children,
spacing = "300",
arrows,
arrowColor,
arrowLeftAriaLabel,
arrowRightAriaLabel,
scrollSnap = "none",
onOverflow,
elevationColor = "paletteCloudDark",
overflowElevation,
scrollPadding,
dataTest,
id,
minHeight,
ref
}) => {
const scrollWrapperRef = React.useRef(null);
const [isOverflowing, setOverflowing] = React.useState(false);
const [reachedStart, setReachedStart] = React.useState(true);
const [reachedEnd, setReachedEnd] = React.useState(false);
const containerRef = React.useRef(null);
const {
isDragging
} = useScrollBox(scrollWrapperRef);
const theme = useTheme();
const scrollEl = scrollWrapperRef.current;
const handleOverflow = React.useCallback(() => {
if (scrollWrapperRef.current?.scrollWidth && containerRef.current?.offsetWidth) {
const {
scrollWidth: containerScrollWidth
} = scrollWrapperRef.current;
const {
offsetWidth
} = containerRef.current;
if (containerScrollWidth > offsetWidth) {
setOverflowing(true);
if (onOverflow) onOverflow();
} else {
setOverflowing(false);
}
}
}, [onOverflow]);
const handleClick = direction => {
if (scrollEl) {
const {
scrollLeft,
offsetWidth
} = scrollEl;
const scrollAmount = scrollLeft + (direction === "left" ? -offsetWidth / 2 : offsetWidth / 2);
scrollEl.scrollTo({
left: scrollAmount,
behavior: "smooth"
});
}
};
const handleScroll = React.useCallback(() => {
if (scrollEl) {
const scrollWidth = scrollEl.scrollWidth - scrollEl.clientWidth;
const {
scrollLeft
} = scrollEl;
if (scrollLeft === 0) {
setReachedStart(true);
} else {
setReachedStart(false);
}
if (scrollLeft >= scrollWidth) {
setReachedEnd(true);
} else {
setReachedEnd(false);
}
}
}, [scrollEl]);
const handleResize = React.useCallback(() => {
handleOverflow();
handleScroll();
}, [handleOverflow, handleScroll]);
React.useEffect(() => {
handleOverflow();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [handleOverflow, handleResize, scrollWrapperRef.current?.scrollWidth]);
return /*#__PURE__*/React.createElement("div", {
className: cx("orbit-horizontal-scroll relative inline-flex w-full items-center overflow-hidden", isOverflowing && (isDragging ? "cursor-grabbing" : "cursor-grab")),
"data-test": dataTest,
"data-overflowing": isOverflowing || undefined,
id: id,
ref: mergeRefs([ref, containerRef]),
style: {
minHeight
}
}, overflowElevation && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ElevationHaze, {
className: cx("left-0", (!isOverflowing || reachedStart) && "invisible"),
style: {
boxShadow: `5px 0px 20px 20px ${theme.orbit[elevationColor]}`
}
}), /*#__PURE__*/React.createElement(ElevationHaze, {
className: cx("right-0", (!isOverflowing || reachedEnd) && "invisible"),
style: {
boxShadow: `-5px 0px 20px 20px ${theme.orbit[elevationColor]}`
}
})), arrows && /*#__PURE__*/React.createElement(ArrowButton, {
className: "left-100",
isHidden: reachedStart || !isOverflowing,
onClick: () => handleClick("left"),
ariaLabel: arrowLeftAriaLabel
}, /*#__PURE__*/React.createElement(ChevronBackward, {
ariaHidden: true,
customColor: arrowColor
})), /*#__PURE__*/React.createElement("div", {
className: "scrollbar-none size-full overflow-x-auto overflow-y-hidden",
ref: scrollWrapperRef,
onScroll: handleScroll,
style: {
scrollPadding,
scrollSnapType: isDragging ? "none" : getSnap(scrollSnap)
}
}, /*#__PURE__*/React.createElement("div", {
className: cx("relative inline-flex size-full", isDragging && "pointer-events-none")
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
,
tabIndex: isOverflowing ? 0 : undefined
}, /*#__PURE__*/React.createElement(Stack, {
inline: true,
spacing: spacing
}, children))), arrows && /*#__PURE__*/React.createElement(ArrowButton, {
className: "right-100",
isHidden: reachedEnd || !isOverflowing,
onClick: () => handleClick("right"),
ariaLabel: arrowRightAriaLabel
}, /*#__PURE__*/React.createElement(ChevronForward, {
ariaHidden: true,
customColor: arrowColor
})));
};
export default HorizontalScroll;