UNPKG

@syncfusion/react-navigations

Version:

A package of Pure React navigation components such as Toolbar and Context-menu which is used to navigate from one page to another

270 lines (269 loc) 11.1 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { forwardRef, useImperativeHandle, useRef, useEffect, useState, useMemo, memo, useCallback } from 'react'; import { ChevronUp, ChevronDown } from '@syncfusion/react-icons'; import { Touch, getUniqueID, useProviderContext, Browser } from '@syncfusion/react-base'; const CLS_ROOT = 'sf-vscroll'; const CLS_RTL = 'sf-rtl'; const CLS_DISABLE = 'sf-overlay'; const CLS_VSCROLLBAR = 'sf-vscroll-bar'; const CLS_VSCROLLCON = 'sf-vscroll-content'; const CLS_NAVARROW = 'sf-nav-arrow'; const CLS_NAVUPARROW = 'sf-nav-up-arrow'; const CLS_NAVDOWNARROW = 'sf-nav-down-arrow'; const CLS_VSCROLLNAV = 'sf-scroll-nav'; const CLS_VSCROLLNAVUP = 'sf-scroll-up-nav'; const CLS_VSCROLLNAVDOWN = 'sf-scroll-down-nav'; const CLS_DEVICE = 'sf-scroll-device'; /** * VScroll component introduces vertical scroller when content exceeds the current viewing area. * It can be useful for components like Toolbar, Tab which need vertical scrolling. * Hidden content can be viewed by touch moving or navigation icon click. * * ```typescript * <VScroll scrollStep={10} className="vscroll-container"> * <div>Item 1</div> * <div>Item 2</div> * <div>Item 3</div> * </VScroll> * ``` */ export const VScroll = memo(forwardRef((props, ref) => { const { className = '', scrollStep: scrollStepProp = undefined, isDisabled = false, children, ...restProps } = props; const scrollStep = useRef(scrollStepProp); const uniqueId = useRef(getUniqueID('vscroll')); const [upNavDisabled, setUpNavDisabled] = useState(true); const [downNavDisabled, setDownNavDisabled] = 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 browserInfo = useMemo(() => ({ isDevice: Browser.isDevice }), []); const publicAPI = { scrollStep: scrollStepProp, isDisabled }; useEffect(() => { const setScrollState = () => { if (typeof scrollStepProp !== 'number' || scrollStepProp < 0) { scrollStep.current = scrollEleRef.current?.offsetHeight || undefined; } else { scrollStep.current = scrollStepProp; } }; setScrollState(); }, [scrollStepProp]); const scrollUpdating = useCallback((scrollVal, action) => { if (scrollEleRef.current) { if (action === 'add') { scrollEleRef.current.scrollTop += scrollVal; } else { scrollEleRef.current.scrollTop -= scrollVal; } } }, []); const frameScrollRequest = useCallback((scrollVal, action, isContinuous) => { const step = 10; if (isContinuous) { scrollUpdating(scrollVal, action); return; } const animate = () => { if (scrollVal < step) { cancelAnimationFrame(step); } else { scrollUpdating(step, action); scrollVal -= step; requestAnimationFrame(animate); } }; animate(); }, [scrollUpdating]); const eleScrolling = useCallback((scrollDis, trgt, isContinuous) => { let elementClassList = trgt.classList; if (!elementClassList.contains(CLS_VSCROLLNAV)) { const sctollNav = trgt.closest('.' + CLS_VSCROLLNAV); if (sctollNav) { trgt = sctollNav; } } const arrowElement = trgt.querySelector(`.${CLS_NAVARROW}`); if (arrowElement) { elementClassList = arrowElement.classList; } if (elementClassList.contains(CLS_NAVDOWNARROW)) { frameScrollRequest(scrollDis, 'add', isContinuous); } else if (elementClassList.contains(CLS_NAVUPARROW)) { frameScrollRequest(scrollDis, '', isContinuous); } }, [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(); } if (keyTimerRef.current) { clearTimeout(keyTimerRef.current); } }, []); const repeatScroll = useCallback(() => { clearInterval(timeoutRef?.current); }, []); const clickEventHandler = useCallback((e) => { eleScrolling(scrollStep.current, e.target, false); }, [eleScrolling, scrollStep]); const wheelEventHandler = useCallback((e) => { e.preventDefault(); frameScrollRequest(scrollStep.current, (e.deltaY > 0 ? 'add' : ''), false); }, [frameScrollRequest, scrollStep]); const swipeHandler = useCallback((e) => { if (e && scrollEleRef.current) { const swipeElement = scrollEleRef.current; let distance = 0; if (typeof e.velocity === 'number' && typeof e.distanceY === 'number') { if (e.velocity <= 1) { distance = e.distanceY / (e.velocity * 10); } else { distance = e.distanceY / e.velocity; } } let start = 0.5; const animate = () => { const step = Math.sin(start); if (step <= 0) { cancelAnimationFrame(step); } else { if (e.swipeDirection === 'Up') { swipeElement.scrollTop += distance * step; } else if (e.swipeDirection === 'Down') { swipeElement.scrollTop -= distance * step; } start -= 0.02; requestAnimationFrame(animate); } }; animate(); } }, []); const touchHandler = useCallback((e) => { if (e) { const ele = scrollEleRef.current; if (ele && typeof e.distanceY === 'number') { const distance = e.distanceY; if (e.scrollDirection === 'Up') { ele.scrollTop = ele.scrollTop + distance; } else if (e.scrollDirection === 'Down') { ele.scrollTop = ele.scrollTop - distance; } } } }, []); const scrollHandler = useCallback((e) => { const target = e.target; const height = target.offsetHeight; let scrollTop = target.scrollTop; if (scrollTop <= 0) { scrollTop = -scrollTop; } if (scrollTop === 0) { setUpNavDisabled(true); setDownNavDisabled(false); repeatScroll(); } else if (Math.ceil(height + scrollTop + .1) >= target.scrollHeight) { setUpNavDisabled(false); setDownNavDisabled(true); repeatScroll(); } else { setUpNavDisabled(false); setDownNavDisabled(false); } }, [repeatScroll]); useImperativeHandle(ref, () => { return { ...publicAPI, element: elementRef.current }; }); useEffect(() => { return () => { if (timeoutRef.current) { clearInterval(timeoutRef.current); } if (keyTimerRef.current) { clearTimeout(keyTimerRef.current); } }; }, []); const touchProps = useMemo(() => ({ scroll: touchHandler, swipe: swipeHandler }), [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: restProps.id || uniqueId.current, className: classNames, ...restProps, onWheel: wheelEventHandler, children: [_jsx(NavIcon, { direction: "up", id: uniqueId.current, isDisabled: isDisabled || upNavDisabled, onKeyPress: onKeyPress, onKeyUp: onKeyUp, onMouseUp: repeatScroll, onClick: clickEventHandler, onTapHold: tapHoldHandler }), _jsx("div", { ref: scrollEleRef, className: CLS_VSCROLLBAR, tabIndex: -1, style: { overflow: 'hidden' }, onScroll: scrollHandler, children: _jsx("div", { className: CLS_VSCROLLCON, children: children }) }), _jsx(NavIcon, { direction: "down", id: uniqueId.current, isDisabled: isDisabled || downNavDisabled, onKeyPress: onKeyPress, onKeyUp: onKeyUp, onMouseUp: repeatScroll, onClick: clickEventHandler, onTapHold: tapHoldHandler })] })); })); VScroll.displayName = 'VScrollComponent'; export default VScroll; const NavIcon = memo(({ direction, id, isDisabled, onKeyPress, onKeyUp, onMouseUp, onClick, onTapHold }) => { const isDown = direction === 'down'; const className = `sf-${id}_nav ${CLS_VSCROLLNAV} ${isDown ? CLS_VSCROLLNAVDOWN : CLS_VSCROLLNAVUP}`; const arrowClass = `${isDown ? CLS_NAVDOWNARROW : CLS_NAVUPARROW} ${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: isDown ? _jsx(ChevronDown, {}) : _jsx(ChevronUp, {}) }) })); }); NavIcon.displayName = 'NavIcon';