UNPKG

@syncfusion/react-navigations

Version:

Syncfusion React Navigations with Toolbar and Context Menu for seamless page navigation

288 lines (287 loc) 11.8 kB
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';