@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
JavaScript
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';