UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

548 lines (534 loc) 24.8 kB
import _slicedToArray from '@babel/runtime/helpers/slicedToArray'; import _objectWithoutProperties from '@babel/runtime/helpers/objectWithoutProperties'; import _defineProperty from '@babel/runtime/helpers/defineProperty'; import styled from 'styled-components'; import React__default from 'react'; import { Indicators } from './Indicators/Indicators.js'; import './NavigationButton/index.js'; import { CarouselContext } from './CarouselContext.js'; import { getCarouselItemId } from './utils.js'; import { componentIds, CAROUSEL_AUTOPLAY_INTERVAL } from './constants.js'; import getIn from '../../utils/lodashButBetter/get.js'; import throttle from '../../utils/lodashButBetter/throttle.js'; import debounce from '../../utils/lodashButBetter/debounce.js'; import '../Box/index.js'; import '../Box/BaseBox/index.js'; import '../../utils/index.js'; import { useId } from '../../utils/useId.js'; import '../../utils/makeAccessible/index.js'; import '../../utils/metaAttribute/index.js'; import { useVerifyAllowedChildren } from '../../utils/useVerifyAllowedChildren/useVerifyAllowedChildren.js'; import '../BladeProvider/index.js'; import '../Box/styledProps/index.js'; import { useControllableState } from '../../utils/useControllable.js'; import { useIsomorphicLayoutEffect } from '../../utils/useIsomorphicLayoutEffect.js'; import { useDidUpdate } from '../../utils/useDidUpdate.js'; import '../../utils/makeAnalyticsAttribute/index.js'; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import { Box } from '../Box/Box.js'; import { NavigationButton } from './NavigationButton/NavigationButton.js'; import { BaseBox } from '../Box/BaseBox/BaseBox.web.js'; import { castWebType } from '../../utils/platform/castUtils.js'; import { makeMotionTime } from '../../utils/makeMotionTime/makeMotionTime.web.js'; import { makeAccessible } from '../../utils/makeAccessible/makeAccessible.web.js'; import useTheme from '../BladeProvider/useTheme.js'; import { useInterval } from '../../utils/useInterval.js'; import { metaAttribute } from '../../utils/metaAttribute/metaAttribute.web.js'; import { MetaConstants } from '../../utils/metaAttribute/metaConstants.js'; import { getStyledProps } from '../Box/styledProps/getStyledProps.js'; import { makeAnalyticsAttribute } from '../../utils/makeAnalyticsAttribute/makeAnalyticsAttribute.js'; var _excluded = ["autoPlay", "visibleItems", "showIndicators", "navigationButtonPosition", "children", "shouldAddStartEndSpacing", "carouselItemWidth", "scrollOverlayColor", "accessibilityLabel", "onChange", "indicatorVariant", "navigationButtonVariant", "carouselItemAlignment", "height", "defaultActiveSlide", "activeSlide", "showNavigationButtons"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } var Controls = function Controls(_ref) { var showIndicators = _ref.showIndicators, navigationButtonPosition = _ref.navigationButtonPosition, activeIndicator = _ref.activeIndicator, totalSlides = _ref.totalSlides, onIndicatorButtonClick = _ref.onIndicatorButtonClick, onNextButtonClick = _ref.onNextButtonClick, onPreviousButtonClick = _ref.onPreviousButtonClick, indicatorVariant = _ref.indicatorVariant, navigationButtonVariant = _ref.navigationButtonVariant, showNavigationButtons = _ref.showNavigationButtons; if (navigationButtonPosition === 'bottom') { return /*#__PURE__*/jsxs(Box, { marginTop: "spacing.7", display: "flex", alignItems: "center", gap: "spacing.4", children: [showNavigationButtons ? /*#__PURE__*/jsx(NavigationButton, { type: "previous", variant: navigationButtonVariant, onClick: onPreviousButtonClick }) : null, showIndicators ? /*#__PURE__*/jsx(Indicators, { onClick: onIndicatorButtonClick, activeIndex: activeIndicator, totalItems: totalSlides, variant: indicatorVariant }) : null, showNavigationButtons ? /*#__PURE__*/jsx(NavigationButton, { onClick: onNextButtonClick, type: "next", variant: navigationButtonVariant }) : null] }); } if (showIndicators && navigationButtonPosition === 'side') { return /*#__PURE__*/jsx(Box, { marginTop: "spacing.7", children: /*#__PURE__*/jsx(Indicators, { onClick: onIndicatorButtonClick, activeIndex: activeIndicator, totalItems: totalSlides, variant: indicatorVariant }) }); } return /*#__PURE__*/jsx(Fragment, {}); }; var CarouselContainer = /*#__PURE__*/styled(BaseBox).withConfig({ displayName: "Carouselweb__CarouselContainer", componentId: "v6xykj-0" })(function (_ref2) { var theme = _ref2.theme, showOverlay = _ref2.showOverlay, scrollOverlayColor = _ref2.scrollOverlayColor, isScrollAtStart = _ref2.isScrollAtStart, isScrollAtEnd = _ref2.isScrollAtEnd; var gradientStop1 = getIn(theme.colors, scrollOverlayColor); var gradientStop2 = 'hsla(0, 0%, 100%, 0)'; var overlayCommonStyle = { content: "''", position: 'absolute', top: 0, width: '100px', height: '100%', transitionDuration: castWebType(makeMotionTime(theme.motion.duration.gentle)), transitionTimingFunction: castWebType(theme.motion.easing.standard), transitionProperty: 'opacity' }; return _objectSpread({ width: '100%', height: '100%', overflowX: 'scroll', display: 'flex', flexWrap: 'nowrap', scrollSnapType: 'x mandatory', scrollSnapPointsY: "repeat(100%)", msOverflowStyle: 'none' /* IE and Edge */, scrollbarWidth: 'none' /* Firefox */, /* Needed to work on iOS Safari */ webkitOverflowScrolling: 'touch', msScrollSnapType: 'mandatory', scrollSnapPointsX: 'repeat(100%)', msScrollSnapPointsX: 'repeat(100%)', '&::-webkit-scrollbar': { display: 'none' } }, showOverlay && { '&::before': _objectSpread(_objectSpread({}, overlayCommonStyle), {}, { background: "linear-gradient(to right, ".concat(gradientStop1, ", ").concat(gradientStop2, ")"), left: -1, opacity: isScrollAtStart ? 0 : 1, pointerEvents: 'none' }), '&::after': _objectSpread(_objectSpread({}, overlayCommonStyle), {}, { background: "linear-gradient(to left, ".concat(gradientStop1, ", ").concat(gradientStop2, ")"), right: -1, opacity: isScrollAtEnd ? 0 : 1, pointerEvents: 'none' }) }); }); var CarouselBody = /*#__PURE__*/React__default.forwardRef(function (_ref3, ref) { var children = _ref3.children, totalSlides = _ref3.totalSlides, shouldAddStartEndSpacing = _ref3.shouldAddStartEndSpacing, idPrefix = _ref3.idPrefix, scrollOverlayColor = _ref3.scrollOverlayColor, isScrollAtStart = _ref3.isScrollAtStart, isScrollAtEnd = _ref3.isScrollAtEnd, carouselItemAlignment = _ref3.carouselItemAlignment, accessibilityLabel = _ref3.accessibilityLabel, startEndMargin = _ref3.startEndMargin; return /*#__PURE__*/jsx(CarouselContainer, _objectSpread(_objectSpread({ tabIndex: 0, ref: ref, showOverlay: Boolean(scrollOverlayColor), scrollOverlayColor: scrollOverlayColor, gap: { base: 'spacing.4', m: 'spacing.5' }, isScrollAtStart: isScrollAtStart, isScrollAtEnd: isScrollAtEnd, alignItems: carouselItemAlignment }, makeAccessible({ role: 'group', roleDescription: 'carousel', label: accessibilityLabel })), {}, { children: React__default.Children.map(children, function (child, index) { var shouldHaveStartSpacing = shouldAddStartEndSpacing && index === 0; var shouldHaveEndSpacing = shouldAddStartEndSpacing && index === totalSlides - 1; var carouselItemNode = /*#__PURE__*/React__default.cloneElement(child, { index: index, id: "".concat(idPrefix, "-carousel-item-").concat(index), shouldHaveStartSpacing: shouldHaveStartSpacing, shouldHaveEndSpacing: shouldHaveEndSpacing }); // Safari doesn't include the margin in the bounding box calculation // Thus have to add an additional box to the end of the carousel to ensure we can scroll past the last item // https://stackoverflow.com/questions/75509058/safari-does-not-include-margins-to-the-scroll-width if (shouldHaveEndSpacing) { return /*#__PURE__*/jsxs(Fragment, { children: [carouselItemNode, /*#__PURE__*/jsx(BaseBox, { minWidth: "".concat(startEndMargin, "px") })] }); } return carouselItemNode; }) })); }); var _Carousel = function _Carousel(_ref4, ref) { var autoPlay = _ref4.autoPlay, _ref4$visibleItems = _ref4.visibleItems, visibleItems = _ref4$visibleItems === void 0 ? 1 : _ref4$visibleItems, _ref4$showIndicators = _ref4.showIndicators, showIndicators = _ref4$showIndicators === void 0 ? true : _ref4$showIndicators, _ref4$navigationButto = _ref4.navigationButtonPosition, navigationButtonPosition = _ref4$navigationButto === void 0 ? 'bottom' : _ref4$navigationButto, children = _ref4.children, _ref4$shouldAddStartE = _ref4.shouldAddStartEndSpacing, shouldAddStartEndSpacing = _ref4$shouldAddStartE === void 0 ? false : _ref4$shouldAddStartE, carouselItemWidth = _ref4.carouselItemWidth, scrollOverlayColor = _ref4.scrollOverlayColor, accessibilityLabel = _ref4.accessibilityLabel, _onChange = _ref4.onChange, _ref4$indicatorVarian = _ref4.indicatorVariant, indicatorVariant = _ref4$indicatorVarian === void 0 ? 'gray' : _ref4$indicatorVarian, _ref4$navigationButto2 = _ref4.navigationButtonVariant, navigationButtonVariant = _ref4$navigationButto2 === void 0 ? 'filled' : _ref4$navigationButto2, _ref4$carouselItemAli = _ref4.carouselItemAlignment, carouselItemAlignment = _ref4$carouselItemAli === void 0 ? 'start' : _ref4$carouselItemAli, height = _ref4.height, defaultActiveSlide = _ref4.defaultActiveSlide, activeSlideProp = _ref4.activeSlide, _ref4$showNavigationB = _ref4.showNavigationButtons, showNavigationButtonProp = _ref4$showNavigationB === void 0 ? true : _ref4$showNavigationB, rest = _objectWithoutProperties(_ref4, _excluded); var _useTheme = useTheme(), platform = _useTheme.platform; var _React$useState = React__default.useState(0), _React$useState2 = _slicedToArray(_React$useState, 2), activeIndicator = _React$useState2[0], setActiveIndicator = _React$useState2[1]; var _useControllableState = useControllableState({ defaultValue: defaultActiveSlide !== null && defaultActiveSlide !== void 0 ? defaultActiveSlide : 0, value: activeSlideProp, onChange: function onChange(value) { _onChange === null || _onChange === void 0 ? void 0 : _onChange(value); } }), _useControllableState2 = _slicedToArray(_useControllableState, 2), activeSlide = _useControllableState2[0], setActiveSlide = _useControllableState2[1]; var _React$useState3 = React__default.useState(false), _React$useState4 = _slicedToArray(_React$useState3, 2), shouldPauseAutoplay = _React$useState4[0], setShouldPauseAutoplay = _React$useState4[1]; var _React$useState5 = React__default.useState(0), _React$useState6 = _slicedToArray(_React$useState5, 2), startEndMargin = _React$useState6[0], setStartEndMargin = _React$useState6[1]; var containerRef = React__default.useRef(null); var isMobile = platform === 'onMobile'; var id = useId(); var carouselId = "carousel-".concat(id); useVerifyAllowedChildren({ componentName: 'Carousel', allowedComponents: [componentIds.CarouselItem], children: children }); var _React$useState7 = React__default.useState( // on mobile we do not want to render the overlay isMobile ? true : !shouldAddStartEndSpacing), _React$useState8 = _slicedToArray(_React$useState7, 2), isScrollAtStart = _React$useState8[0], setScrollStart = _React$useState8[1]; var _React$useState9 = React__default.useState(isMobile), _React$useState10 = _slicedToArray(_React$useState9, 2), isScrollAtEnd = _React$useState10[0], setScrollEnd = _React$useState10[1]; var isResponsive = visibleItems === 'autofit'; var _visibleItems = visibleItems; if (isMobile) { _visibleItems = 1; navigationButtonPosition = 'bottom'; } if (isResponsive) { _visibleItems = 1; } // A special case where we hide the indicators when the carousel is responsive // Because indicators become useless since it's not aparent which carousel item is active // and how many carousel items are visible at a time if (isResponsive && !shouldAddStartEndSpacing && !isMobile) { showIndicators = false; } var showNavigationButtons = showNavigationButtonProp || !isMobile; var isNavButtonsOnSide = !isResponsive && navigationButtonPosition === 'side'; var shouldNavButtonsFloat = isResponsive && navigationButtonPosition === 'side'; var totalNumberOfSlides = React__default.Children.count(children); var numberOfIndicators = Math.ceil(totalNumberOfSlides / _visibleItems); // hide next/prev button on reaching start/end when carousel is responsive // in non-responsive carousel we always show the next/prev buttons to allow looping var shouldShowPrevButton = isResponsive ? activeSlide !== 0 : true; var shouldShowNextButton = isResponsive ? activeSlide !== totalNumberOfSlides - 1 : true; // calculate the start/end margin so that we can // deduct that margin when scrolling to a carousel item with goToSlideIndex useIsomorphicLayoutEffect(function () { var _carouselItem$getBoun, _containerRef$current; // Do not calculate if not needed if (!isResponsive && !shouldAddStartEndSpacing) return; if (!containerRef.current) return; var carouselItemId = getCarouselItemId(carouselId, 0); var carouselItem = containerRef.current.querySelector(carouselItemId); if (!carouselItem) return; var carouselItemLeft = (_carouselItem$getBoun = carouselItem.getBoundingClientRect().left) !== null && _carouselItem$getBoun !== void 0 ? _carouselItem$getBoun : 0; var carouselContainerLeft = (_containerRef$current = containerRef.current.getBoundingClientRect().left) !== null && _containerRef$current !== void 0 ? _containerRef$current : 0; setStartEndMargin(carouselItemLeft - carouselContainerLeft); }, [carouselId, isResponsive, shouldAddStartEndSpacing]); var scrollToSlide = function scrollToSlide(slideIndex) { var _ref5; var shouldAnimate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; if (!containerRef.current) return; var carouselItemId = getCarouselItemId(carouselId, slideIndex * _visibleItems); var carouselItem = containerRef.current.querySelector(carouselItemId); if (!carouselItem) return; var carouselItemLeft = (_ref5 = carouselItem.getBoundingClientRect().left - containerRef.current.getBoundingClientRect().left) !== null && _ref5 !== void 0 ? _ref5 : 0; var left = containerRef.current.scrollLeft + carouselItemLeft; containerRef.current.scroll({ left: left - startEndMargin, behavior: shouldAnimate ? 'smooth' : 'auto' }); }; var goToSlideIndex = function goToSlideIndex(slideIndex) { setActiveSlide(function () { return slideIndex; }); setActiveIndicator(slideIndex); }; var goToNextSlide = function goToNextSlide() { var slideIndex = activeSlide + 1; if (slideIndex >= numberOfIndicators) { slideIndex = 0; } // an edge case where if carousel is responsive // and shouldHaveStartEndSpacing is set to false // there can be a case where numberOfIndicators is set to 10 but // visually there is 3 or 4 items, in those cases we want to check if we reached the // end of the scroll container if so we wrap around if (containerRef.current) { var container = containerRef.current; var scrollLeft = container.scrollLeft; var scrollWidth = container.scrollWidth - container.offsetWidth; if (scrollLeft === scrollWidth) { slideIndex = 0; } } goToSlideIndex(slideIndex); }; var goToPreviousSlide = function goToPreviousSlide() { var slideIndex = activeSlide - 1; if (activeSlide <= 0) { slideIndex = numberOfIndicators - 1; } goToSlideIndex(slideIndex); }; // Scroll overlay gradient show/hide based on if scrolled to start or end React__default.useEffect(function () { // if shouldAddStartEndSpacing is true, we don't need to hide/show the overlay based on the scroll position // because the gap is there so it won't overlap with the card anyway if (shouldAddStartEndSpacing) return; if (isMobile) return; var carouselContainer = containerRef.current; if (!carouselContainer) return; var handleScroll = throttle(function () { var scrollWidth = (carouselContainer === null || carouselContainer === void 0 ? void 0 : carouselContainer.scrollWidth) - carouselContainer.offsetWidth; setScrollStart((carouselContainer === null || carouselContainer === void 0 ? void 0 : carouselContainer.scrollLeft) === 0); setScrollEnd((carouselContainer === null || carouselContainer === void 0 ? void 0 : carouselContainer.scrollLeft) === scrollWidth); }, 500); carouselContainer.addEventListener('scroll', handleScroll); return function () { carouselContainer === null || carouselContainer === void 0 ? void 0 : carouselContainer.removeEventListener('scroll', handleScroll); }; }, [isMobile, shouldAddStartEndSpacing]); // Sync the indicators with scroll React__default.useEffect(function () { var carouselContainer = containerRef.current; if (!carouselContainer) return; var handleScroll = debounce(function () { // carousel bounding box var carouselBB = carouselContainer.getBoundingClientRect(); // By default we check the far left side of the screen var xOffset = 0.1; // when the carousel is responsive & has spacing // we want to check the center of the screen if (isResponsive && shouldAddStartEndSpacing) { xOffset = 0.5; } var pointX = carouselBB.left + carouselBB.width * xOffset; var pointY = carouselBB.top + carouselBB.height * 0.5; var element = document.elementFromPoint(pointX, pointY); var carouselItem = element === null || element === void 0 ? void 0 : element.closest('[data-slide-index]'); if (!carouselItem) { return; } var slideIndex = Number(carouselItem === null || carouselItem === void 0 ? void 0 : carouselItem.getAttribute('data-slide-index')); var goTo = Math.ceil(slideIndex / _visibleItems); setActiveSlide(function () { return goTo; }); setActiveIndicator(goTo); }, 50); carouselContainer.addEventListener('scroll', handleScroll); return function () { carouselContainer === null || carouselContainer === void 0 ? void 0 : carouselContainer.removeEventListener('scroll', handleScroll); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [_visibleItems, isMobile, isResponsive, shouldAddStartEndSpacing]); // auto play useInterval(function () { goToNextSlide(); }, { delay: CAROUSEL_AUTOPLAY_INTERVAL, // only enable if autoplay is true & user's intent isn't to interact with carousel enable: autoPlay && !shouldPauseAutoplay }); // set initial active slide on mount useIsomorphicLayoutEffect(function () { if (!id) return; goToSlideIndex(activeSlide); scrollToSlide(activeSlide, false); }, [id]); // Scroll the carousel to the active slide useDidUpdate(function () { scrollToSlide(activeSlide); }, [activeSlide]); var carouselContext = React__default.useMemo(function () { return { isResponsive: isResponsive, visibleItems: _visibleItems, carouselItemWidth: carouselItemWidth, carouselContainerRef: containerRef, setActiveIndicator: setActiveIndicator, carouselId: carouselId, totalNumberOfSlides: totalNumberOfSlides, activeSlide: activeSlide, startEndMargin: startEndMargin, shouldAddStartEndSpacing: shouldAddStartEndSpacing }; }, [carouselId, startEndMargin, isResponsive, _visibleItems, carouselItemWidth, totalNumberOfSlides, activeSlide, shouldAddStartEndSpacing]); return /*#__PURE__*/jsx(CarouselContext.Provider, { value: carouselContext, children: /*#__PURE__*/jsxs(BaseBox, _objectSpread(_objectSpread(_objectSpread(_objectSpread({ ref: ref }, metaAttribute({ name: MetaConstants.Carousel })), {}, { // stop autoplaying when any elements in carousel is in focus onFocus: function onFocus(e) { if (!e.currentTarget.contains(e.relatedTarget)) { setShouldPauseAutoplay(true); } }, onBlur: function onBlur(e) { if (!e.currentTarget.contains(e.relatedTarget)) { setShouldPauseAutoplay(false); } } // stop autplay when user hover overs the carousel , onMouseEnter: function onMouseEnter() { setShouldPauseAutoplay(true); }, onMouseLeave: function onMouseLeave() { setShouldPauseAutoplay(false); }, onTouchStart: function onTouchStart() { setShouldPauseAutoplay(true); }, onTouchEnd: function onTouchEnd() { setShouldPauseAutoplay(false); }, display: "flex", alignItems: "center", flexDirection: "column", height: height }, getStyledProps(rest)), makeAnalyticsAttribute(rest)), {}, { children: [/*#__PURE__*/jsxs(BaseBox, { width: "100%", position: "relative", display: "flex", alignItems: "center", gap: "spacing.4", flexDirection: "row", height: "100%", children: [shouldShowPrevButton && shouldNavButtonsFloat ? /*#__PURE__*/jsx(BaseBox, { zIndex: 2, position: "absolute", left: "spacing.11", children: /*#__PURE__*/jsx(NavigationButton, { type: "previous", variant: navigationButtonVariant, onClick: goToPreviousSlide }) }) : null, isNavButtonsOnSide ? /*#__PURE__*/jsx(NavigationButton, { type: "previous", variant: navigationButtonVariant, onClick: goToPreviousSlide }) : null, /*#__PURE__*/jsx(CarouselBody, { idPrefix: carouselId, startEndMargin: startEndMargin, totalSlides: totalNumberOfSlides, shouldAddStartEndSpacing: shouldAddStartEndSpacing, scrollOverlayColor: scrollOverlayColor, isScrollAtStart: isScrollAtStart, isScrollAtEnd: isScrollAtEnd, ref: containerRef, carouselItemAlignment: carouselItemAlignment, accessibilityLabel: accessibilityLabel, children: children }), shouldShowNextButton && shouldNavButtonsFloat ? /*#__PURE__*/jsx(BaseBox, { zIndex: 2, position: "absolute", right: "spacing.11", children: /*#__PURE__*/jsx(NavigationButton, { onClick: goToNextSlide, type: "next", variant: navigationButtonVariant }) }) : null, isNavButtonsOnSide ? /*#__PURE__*/jsx(NavigationButton, { onClick: goToNextSlide, type: "next", variant: navigationButtonVariant }) : null] }), /*#__PURE__*/jsx(Controls, { totalSlides: numberOfIndicators, activeIndicator: activeIndicator, showIndicators: showIndicators, navigationButtonPosition: navigationButtonPosition, onIndicatorButtonClick: goToSlideIndex, onNextButtonClick: goToNextSlide, onPreviousButtonClick: goToPreviousSlide, indicatorVariant: indicatorVariant, navigationButtonVariant: navigationButtonVariant, showNavigationButtons: showNavigationButtons })] })) }); }; var Carousel = /*#__PURE__*/React__default.forwardRef(_Carousel); export { Carousel }; //# sourceMappingURL=Carousel.web.js.map