UNPKG

@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.

284 lines (282 loc) 11 kB
"use strict"; "use client"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; exports.__esModule = true; exports.default = void 0; var React = _interopRequireWildcard(require("react")); var _clsx = _interopRequireDefault(require("clsx")); var _Stack = _interopRequireDefault(require("../Stack")); var _mergeRefs = _interopRequireDefault(require("../utils/mergeRefs")); var _useTheme = _interopRequireDefault(require("../hooks/useTheme")); var _useScroll = _interopRequireDefault(require("./useScroll")); var _ChevronBackward = _interopRequireDefault(require("../icons/ChevronBackward")); var _ChevronForward = _interopRequireDefault(require("../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: (0, _clsx.default)("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: (0, _clsx.default)("z-default absolute top-0 h-full", className), style: style }); }; /** * @orbit-doc-start * README * ---------- * # HorizontalScroll * * To implement HorizontalScroll component into your project you'll need to add the import: * * ```jsx * import HorizontalScroll from "@kiwicom/orbit-components/lib/HorizontalScroll"; * ``` * * After adding import into your project you can use it simply like: * * ```jsx * <HorizontalScroll> * <FirstComponent /> * <SecondComponent /> * <ThirdComponent /> * ...etc * </HorizontalScroll> * ``` * * ## Props * * | Name | Type | Required | Default | Description | * | ------------------- | --------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------- | * | minHeight | `number` | | | set minimal height | * | dataTest | `string` | | | prop for testing purposes | * | id | `string` | | | Set `id` for `HorizontalScroll` | * | spacing | [`Spacing`](#spacing) | | "300" | the spacing between children elements | * | children | `React.ReactNode` | ✔️ | | content of HorizontalScroll | * | scrollSnap | [`ScrollSnap`](#scrollsnap) | | "none" | set value for `scroll-snap-type` property | * | scrollPadding | `number` | | | set value for `scroll-padding` property | * | overflowElevation | `boolean` | | | set box-shadow on sides during scroll | * | elevationColor | `string` | | `paletteCloudDark` | set box-shadow color. Value must be the name of a color token from the theme. | * | onOverflow | `() => void` | | | callback function, fires, if content is overflowed | * | arrows | `boolean` | | | show arrows | * | arrowColor | `string` | | | set arrows color | * | arrowLeftAriaLabel | `string` | | | set aria-label for left arrow | * | arrowRightAriaLabel | `string` | | | set aria-label for right arrow | * * ## ScrollSnap * * | ScrollSnap | * | ------------- | * | `"mandatory"` | * | `"proximity"` | * | `"inline"` | * | `"none"` | * * ## Spacing * * | Spacing | * | -------- | * | `"none"` | * | `"50"` | * | `"100"` | * | `"150"` | * | `"200"` | * | `"300"` | * | `"400"` | * | `"500"` | * | `"600"` | * | `"800"` | * | `"1000"` | * | `"1200"` | * | `"1600"` | * * * Accessibility * ------------- * ## Accessibility * * The HorizontalScroll component comes with built-in accessibility features for screen reader users through ARIA labels. * * ### ARIA labels * * When using arrows (`arrows`), you must provide accessibility labels for both arrow buttons using the `arrowLeftAriaLabel` and `arrowRightAriaLabel` props. These labels should be properly translated strings, as they will be read by screen readers to users in their preferred language. * * ```jsx * <HorizontalScroll * arrows * arrowLeftAriaLabel="Scroll left" // Should be a translated string * arrowRightAriaLabel="Scroll right" // Should be a translated string * > * {/* content *\/} * </HorizontalScroll> * ``` * * The labels should clearly describe the action that will occur when the button is pressed. For example: * * - "See previous items" * - "Scroll to previous cards" * - "Show previous products" * * Remember that these strings must be translated to provide an accessible experience for screen reader users across different languages. * * * @orbit-doc-end */ 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 } = (0, _useScroll.default)(scrollWrapperRef); const theme = (0, _useTheme.default)(); 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: (0, _clsx.default)("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: (0, _mergeRefs.default)([ref, containerRef]), style: { minHeight } }, overflowElevation && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ElevationHaze, { className: (0, _clsx.default)("left-0", (!isOverflowing || reachedStart) && "invisible"), style: { boxShadow: `5px 0px 20px 20px ${theme.orbit[elevationColor]}` } }), /*#__PURE__*/React.createElement(ElevationHaze, { className: (0, _clsx.default)("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.default, { 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: (0, _clsx.default)("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.default, { 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.default, { ariaHidden: true, customColor: arrowColor }))); }; var _default = exports.default = HorizontalScroll;