@shopgate/pwa-common
Version:
Common library for the Shopgate Connect PWA.
229 lines (220 loc) • 7.67 kB
JavaScript
import React, { useMemo, useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import cls from 'classnames';
import { A11y, Autoplay, FreeMode, Navigation, Pagination, Zoom } from 'swiper/modules';
import { Swiper as OriginalSwiper } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/a11y';
import 'swiper/css/pagination';
import 'swiper/css/navigation';
import 'swiper/css/zoom';
import { useReduceMotion } from '@shopgate/engage/a11y/hooks';
import SwiperItem from "./components/SwiperItem";
import { container, innerContainer, zoomFix, buttonNext, buttonPrev } from "./styles";
/**
* @typedef {import('swiper/react').SwiperProps} SwiperCmpProps
*/
/**
* @typedef {import('swiper/react').SwiperClass} SwiperClass
*/
/**
* Performs steps that are required when the loop prop of the Swiper is updated.
* @param {SwiperClass} swiper Swiper instance
* @param {boolean} loop Whether the loop mode should be enabled or not.
*/
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
const handleLoopPropUpdate = (swiper, loop) => {
const realIndex = swiper?.realIndex || 0;
// eslint-disable-next-line no-param-reassign
swiper.params.loop = loop;
if (loop) {
swiper.loopDestroy();
swiper.loopCreate(realIndex);
swiper.updateSlides();
} else {
swiper.loopDestroy();
}
};
/**
* The basic Swiper component. It acts as a wrapper for the Swiper JS library component.
*
* This component wraps the [Swiper](https://swiperjs.com/) library's main component.
* Refer to the [Swiper documentation](https://swiperjs.com/react) for details on the available props.
*
* @param {SwiperCmpProps} props The component props.
* @returns {JSX.Element}
*/
const Swiper = ({
maxIndicators,
indicators,
controls,
'aria-hidden': ariaHidden,
disabled,
autoPlay,
interval,
classNames,
className,
onSlideChange,
onBreakpoint,
additionalModules,
loop: loopProp,
children,
paginationType: paginationTypeProp,
...swiperProps
}) => {
const useFraction = maxIndicators && maxIndicators < children.length;
const paginationType = useFraction ? 'fraction' : 'bullets';
const showPagination = indicators && children.length > 1;
const hasControls = typeof controls === 'boolean' && controls === true;
const reduceMotion = useReduceMotion();
/** @type {React.RefObject<{ swiper: SwiperClass}>} */
const swiperRef = useRef(null);
const [currentSlidesPerView, setCurrentSlidesPerView] = useState(swiperProps?.slidesPerView || 1);
const navigation = useMemo(() => {
let nav;
if (hasControls) {
nav = {
// Important to use dot notation (swiper uses it as selector internally)
nextEl: `.swiper-button-next.${buttonNext}`,
prevEl: `.swiper-button-prev.${buttonPrev}`
};
}
if (typeof controls === 'object') {
nav = controls;
}
return nav;
}, [controls, hasControls]);
const handleSlideChange = useCallback(swiper => {
if (typeof onSlideChange === 'function') {
onSlideChange(swiper.realIndex, swiper);
}
}, [onSlideChange]);
/**
* @type {SwiperCmpProps}
*/
const internalProps = useMemo(() => ({
modules: [A11y, Autoplay, FreeMode, Navigation, Pagination, Zoom].concat(Array.isArray(additionalModules) ? additionalModules : []),
className: cls(innerContainer, classNames.container, {
[zoomFix]: swiperProps?.zoom
}),
autoplay: autoPlay ? {
delay: interval
} : false,
navigation,
...(showPagination && {
pagination: {
el: undefined,
type: paginationTypeProp || paginationType,
bulletClass: classNames.bulletClass || 'swiper-pagination-bullet',
bulletActiveClass: classNames.bulletActiveClass || 'swiper-pagination-bullet-active',
dynamicBullets: true,
clickable: true,
enabled: indicators && children.length > 1
}
}),
allowSlidePrev: !disabled,
allowSlideNext: !disabled,
onSlideChange: handleSlideChange
}), [additionalModules, classNames.container, classNames.bulletClass, classNames.bulletActiveClass, swiperProps, autoPlay, interval, navigation, showPagination, paginationTypeProp, paginationType, indicators, children.length, disabled, handleSlideChange]);
useEffect(() => {
if (!internalProps.autoplay && !swiperProps.autoplay) {
if (swiperRef.current?.swiper?.autoplay) {
// When autoplay is disabled, ensure that the slider is really stopped. That tackles UI
// issues when e.g. autoplay and loop mode where disabled during one slide interval.
swiperRef.current.swiper.autoplay.stop();
}
return;
}
if (swiperRef.current?.swiper?.autoplay) {
if (reduceMotion) {
swiperRef.current.swiper.autoplay.stop();
} else {
swiperRef.current.swiper.autoplay.start();
}
}
}, [internalProps.autoplay, reduceMotion, swiperProps.autoplay]);
// The currently configured delay for autoplay.
const delay = internalProps.autoplay?.delay || swiperProps.autoplay?.delay;
// Whether the loop mode should be enabled.
const shouldLoop = loopProp && children?.length > currentSlidesPerView + 1;
useEffect(() => {
if (!swiperRef.current) return;
// Perform required steps when loop prop changes on runtime.
handleLoopPropUpdate(swiperRef.current.swiper, shouldLoop);
}, [shouldLoop]);
/**
* Handles the breakpoint change event.
* The Swiper has some issues when props are changed on runtime followed by a breakpoint change.
* This function is supposed to ensure the the Swiper behaves as expected in that case.
*/
const handleOnBreakpoint = useCallback(
/**
* @param {SwiperClass} swiper Swiper instance
* @param {Object} breakpoint Current breakpoint object
*/
(swiper, breakpoint) => {
let {
slidesPerView
} = breakpoint;
if (!slidesPerView) {
slidesPerView = 1;
}
const wasRunning = swiper?.autoplay?.running || false;
if (wasRunning) {
swiper.autoplay.stop();
}
const loopUpdate = loopProp && swiper.slides.length > slidesPerView + 1;
handleLoopPropUpdate(swiper, loopUpdate);
if (typeof delay === 'number' && swiper.params.autoplay) {
// eslint-disable-next-line no-param-reassign
swiper.params.autoplay.delay = delay;
}
if (wasRunning) {
swiper.autoplay.start();
}
setCurrentSlidesPerView(slidesPerView);
if (typeof onBreakpoint === 'function') {
onBreakpoint(swiper, breakpoint);
}
}, [delay, loopProp, onBreakpoint]);
return /*#__PURE__*/_jsx("div", {
className: cls(container, className, 'common__swiper'),
"aria-hidden": ariaHidden,
children: /*#__PURE__*/_jsxs(OriginalSwiper, {
"aria-live": "off",
a11y: {
enabled: false
},
...internalProps,
...swiperProps,
loop: shouldLoop,
onBreakpoint: handleOnBreakpoint,
ref: swiperRef,
children: [children, hasControls && /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx("div", {
className: `swiper-button-next ${buttonNext}`
}), /*#__PURE__*/_jsx("div", {
className: `swiper-button-prev ${buttonPrev}`
})]
})]
})
});
};
Swiper.Item = SwiperItem;
Swiper.defaultProps = {
'aria-hidden': false,
additionalModules: null,
autoPlay: false,
className: null,
classNames: {},
controls: false,
indicators: false,
interval: 3000,
loop: false,
maxIndicators: null,
disabled: false,
onSlideChange: null,
onBreakpoint: null,
paginationType: null
};
export default Swiper;