@syncfusion/react-navigations
Version:
Syncfusion React Navigations with Toolbar and Context Menu for seamless page navigation
288 lines (287 loc) • 11.8 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useRef, useState, forwardRef, useImperativeHandle, useEffect, useMemo, memo, useCallback } from 'react';
import { ChevronLeftIcon, ChevronRightIcon } from '@syncfusion/react-icons';
import { Touch, getUniqueID, Browser, useProviderContext } from '@syncfusion/react-base';
const CLS_ROOT = 'sf-hscroll';
const CLS_RTL = 'sf-rtl';
const CLS_DISABLE = 'sf-overlay';
const CLS_HSCROLLBAR = 'sf-hscroll-bar';
const CLS_HSCROLLCON = 'sf-hscroll-content';
const CLS_NAVARROW = 'sf-hscroll-arrow';
const CLS_NAVRIGHTARROW = 'sf-nav-right-arrow';
const CLS_NAVLEFTARROW = 'sf-nav-left-arrow';
const CLS_HSCROLLNAV = 'sf-hscroll-nav';
const CLS_HSCROLLNAVRIGHT = 'sf-scroll-right-nav';
const CLS_HSCROLLNAVLEFT = 'sf-scroll-left-nav';
const CLS_DEVICE = 'sf-scroll-device';
/**
* HScroll component introduces horizontal scroller when content exceeds the current viewing area.
* It can be useful for components like Toolbar, Tab which need horizontal scrolling.
* Hidden content can be viewed by touch moving or navigation icon click.
*
* ```typescript
* <HScroll scrollStep={10} className="hscroll-container">
* <div>Item 1</div>
* <div>Item 2</div>
* <div>Item 3</div>
* </HScroll>
* ```
*/
export const HScroll = memo(forwardRef((props, ref) => {
const { className = '', scrollStep: scrollStepProp = undefined, isDisabled = false, children, ...restProps } = props;
const scrollStep = useRef(scrollStepProp);
const uniqueId = useRef(getUniqueID('hscroll'));
const [leftNavDisabled, setLeftNavDisabled] = useState(true);
const [rightNavDisabled, setRightNavDisabled] = useState(false);
const { dir } = useProviderContext();
const elementRef = useRef(null);
const scrollEleRef = useRef(null);
const touchModuleRef = useRef(null);
const timeoutRef = useRef(null);
const keyTimeoutRef = useRef(false);
const keyTimerRef = useRef(null);
const publicAPI = {
scrollStep: scrollStepProp,
isDisabled
};
const browserInfo = useMemo(() => ({
name: Browser.info.name,
isDevice: Browser.isDevice,
isMozilla: Browser.info.name === 'mozilla'
}), []);
useEffect(() => {
const setScrollState = () => {
if (typeof scrollStepProp !== 'number' || scrollStepProp < 0) {
scrollStep.current = scrollEleRef.current?.offsetWidth || undefined;
}
else {
scrollStep.current = scrollStepProp;
}
};
setScrollState();
}, [scrollStepProp]);
const scrollUpdating = useCallback((scrollVal, action) => {
if (scrollEleRef.current) {
if (action === 'add') {
scrollEleRef.current.scrollLeft += scrollVal;
}
else {
scrollEleRef.current.scrollLeft -= scrollVal;
}
if (dir === 'rtl' && scrollEleRef.current.scrollLeft > 0) {
scrollEleRef.current.scrollLeft = 0;
}
}
}, [dir]);
const frameScrollRequest = useCallback((scrollVal, action, isContinuous) => {
const step = 10;
if (isContinuous) {
scrollUpdating(scrollVal, action);
return;
}
const animate = () => {
let scrollValue = scrollVal;
let scrollStep = step;
if (elementRef.current?.classList.contains(CLS_RTL) && browserInfo.isMozilla) {
scrollValue = -scrollVal;
scrollStep = -step;
}
if (scrollValue < step) {
cancelAnimationFrame(scrollStep);
}
else {
scrollUpdating(scrollStep, action);
scrollVal -= scrollStep;
requestAnimationFrame(animate);
}
};
animate();
}, [browserInfo.isMozilla, scrollUpdating]);
const eleScrolling = useCallback((scrollDis, trgt, isContinuous) => {
const rootEle = elementRef.current;
let classList = trgt.classList;
if (!classList.contains(CLS_HSCROLLNAV)) {
const sctollNav = trgt.closest('.' + CLS_HSCROLLNAV);
if (sctollNav) {
trgt = sctollNav;
}
}
classList = trgt.querySelector(`.${CLS_NAVARROW}`).classList;
if (rootEle.classList.contains(CLS_RTL) && browserInfo.isMozilla) {
scrollDis = -scrollDis;
}
if ((!rootEle.classList.contains(CLS_RTL) || browserInfo.isMozilla)) {
if (classList.contains(CLS_NAVRIGHTARROW)) {
frameScrollRequest(scrollDis, 'add', isContinuous);
}
else {
frameScrollRequest(scrollDis, '', isContinuous);
}
}
else {
if (classList.contains(CLS_NAVLEFTARROW)) {
frameScrollRequest(scrollDis, 'add', isContinuous);
}
else {
frameScrollRequest(scrollDis, '', isContinuous);
}
}
}, [browserInfo.isMozilla, frameScrollRequest]);
const tapHoldHandler = useCallback((e) => {
if (e) {
const trgt = e.originalEvent.target;
const scrollDis = 10;
const timeoutFun = () => {
eleScrolling(scrollDis, trgt, true);
};
timeoutRef.current = window.setInterval(() => {
timeoutFun();
}, 50);
}
}, [eleScrolling]);
const onKeyPress = useCallback((e) => {
if (e.key === 'Enter') {
const timeoutFun = () => {
keyTimeoutRef.current = true;
eleScrolling(10, e.target, true);
};
keyTimerRef.current = window.setTimeout(() => {
timeoutFun();
}, 100);
}
}, [eleScrolling]);
const onKeyUp = useCallback((e) => {
if (e.key !== 'Enter') {
return;
}
if (keyTimeoutRef.current) {
keyTimeoutRef.current = false;
}
else {
e.target.click();
}
clearTimeout(keyTimerRef.current);
}, []);
const repeatScroll = useCallback(() => {
clearInterval(timeoutRef.current);
}, []);
const clickEventHandler = useCallback((e) => {
eleScrolling(scrollStep.current, e.target, false);
}, [eleScrolling]);
const swipeHandler = useCallback((e) => {
if (e) {
const swipeEle = scrollEleRef.current;
let distance;
if (typeof e.velocity === 'number' && typeof e.distanceX === 'number') {
if (e.velocity <= 1) {
distance = e.distanceX / (e.velocity * 10);
}
else {
distance = e.distanceX / e.velocity;
}
}
let start = 0.5;
const animate = () => {
const step = Math.sin(start);
if (step <= 0) {
cancelAnimationFrame(step);
}
else {
if (e.swipeDirection === 'Left') {
swipeEle.scrollLeft += distance * step;
}
else if (e.swipeDirection === 'Right') {
swipeEle.scrollLeft -= distance * step;
}
start -= 0.5;
requestAnimationFrame(animate);
}
};
animate();
}
}, []);
const touchHandler = useCallback((e) => {
if (e) {
const ele = scrollEleRef.current;
if (ele && typeof e.distanceX === 'number') {
const distance = e.distanceX;
if (e.scrollDirection === 'Left') {
ele.scrollLeft = ele.scrollLeft + distance;
}
else if (e.scrollDirection === 'Right') {
ele.scrollLeft = ele.scrollLeft - distance;
}
}
}
}, []);
const scrollHandler = (e) => {
const target = e.target;
const width = target.offsetWidth;
let scrollLeft = target.scrollLeft;
if (scrollLeft <= 0) {
scrollLeft = -scrollLeft;
}
if (scrollLeft === 0) {
setLeftNavDisabled(true);
setRightNavDisabled(false);
repeatScroll();
}
else if (Math.ceil(width + scrollLeft + .1) >= target.scrollWidth) {
setLeftNavDisabled(false);
setRightNavDisabled(true);
repeatScroll();
}
else {
setLeftNavDisabled(false);
setRightNavDisabled(false);
}
};
useImperativeHandle(ref, () => {
return {
...publicAPI,
element: elementRef.current
};
});
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearInterval(timeoutRef.current);
}
if (keyTimerRef.current) {
clearTimeout(keyTimerRef.current);
}
};
}, []);
const touchProps = useMemo(() => ({
swipe: swipeHandler,
scroll: touchHandler
}), [swipeHandler, touchHandler]);
touchModuleRef.current = Touch(elementRef, touchProps);
const classNames = useMemo(() => {
return [
'sf-control',
'sf-lib',
CLS_ROOT,
dir === 'rtl' && CLS_RTL,
browserInfo.isDevice && CLS_DEVICE,
isDisabled && CLS_DISABLE,
className
].filter(Boolean).join(' ');
}, [className, dir, browserInfo.isDevice, isDisabled]);
return (_jsxs("div", { ref: elementRef, id: uniqueId.current, className: classNames, ...restProps, children: [_jsx(NavIcon, { direction: 'left', id: uniqueId.current, isDisabled: isDisabled || leftNavDisabled, onKeyPress: onKeyPress, onKeyUp: onKeyUp, onMouseUp: repeatScroll, onClick: clickEventHandler, onTapHold: tapHoldHandler }), _jsx("div", { ref: scrollEleRef, className: `${CLS_HSCROLLBAR} sf-display-flex`, style: { overflowX: 'hidden' }, onScroll: scrollHandler, children: _jsx("div", { className: CLS_HSCROLLCON, children: children }) }), _jsx(NavIcon, { direction: 'right', id: uniqueId.current, isDisabled: isDisabled || rightNavDisabled, onKeyPress: onKeyPress, onKeyUp: onKeyUp, onMouseUp: repeatScroll, onClick: clickEventHandler, onTapHold: tapHoldHandler })] }));
}));
HScroll.displayName = 'HScrollComponent';
export default HScroll;
const NavIcon = memo(({ direction, id, isDisabled, onKeyPress, onKeyUp, onMouseUp, onClick, onTapHold }) => {
const isRight = direction === 'right';
const className = `sf-${id}_nav ${CLS_HSCROLLNAV} ${isRight ? CLS_HSCROLLNAVRIGHT : CLS_HSCROLLNAVLEFT}`;
const arrowClass = `${isRight ? CLS_NAVRIGHTARROW : CLS_NAVLEFTARROW} ${CLS_NAVARROW} sf-icons`;
const navRef = useRef(null);
const touchProps = useMemo(() => ({
tapHold: onTapHold,
tapHoldThreshold: 500
}), [onTapHold]);
Touch(navRef, touchProps);
return (_jsx("div", { ref: navRef, className: `${className} ${isDisabled ? CLS_DISABLE : ''}`, role: 'button', id: `${id}_nav_${direction}`, "aria-label": `Scroll ${direction}`, "aria-disabled": isDisabled ? 'true' : 'false', tabIndex: isDisabled ? -1 : 0, onKeyDown: onKeyPress, onKeyUp: onKeyUp, onMouseUp: onMouseUp, onTouchEnd: onMouseUp, onContextMenu: (e) => e.preventDefault(), onClick: onClick, children: _jsx("div", { className: arrowClass, children: isRight ? _jsx(ChevronRightIcon, {}) : _jsx(ChevronLeftIcon, {}) }) }));
});
NavIcon.displayName = 'NavIcon';