@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
JavaScript
"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;