Skip to content

Commit

Permalink
feat(tabs): supports scrolling operations via wheel or touchpad (#2929)
Browse files Browse the repository at this point in the history
* feat(tabs): supports scrolling operations via wheel or touchpad

* fix(tabs): adjust logic to avoid side effects
  • Loading branch information
oljc authored Jun 5, 2024
1 parent 9485426 commit 16dea87
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 91 deletions.
2 changes: 1 addition & 1 deletion src/_common
170 changes: 80 additions & 90 deletions src/tabs/TabNav.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState, WheelEvent } from 'react';
import classNames from 'classnames';
import {
AddIcon as TdAddIcon,
Expand All @@ -12,13 +12,11 @@ import noop from '../_util/noop';
import { useTabClass } from './useTabClass';
import TabNavItem from './TabNavItem';
import TabBar from './TabBar';
import tabBase from '../_common/js/tabs/base';
import { calcMaxOffset, calcValidOffset, calculateOffset, calcPrevOrNextOffset } from '../_common/js/tabs/base';
import useGlobalIcon from '../hooks/useGlobalIcon';
import type { DragSortInnerProps } from '../hooks/useDragSorter';
import parseTNode from '../_util/parseTNode';

const { moveActiveTabIntoView, calcScrollLeft, scrollToLeft, scrollToRight, calculateCanToLeft, calculateCanToRight } =
tabBase;
export interface TabNavProps extends TdTabsProps, DragSortInnerProps {
itemList: TdTabPanelProps[];
tabClick: (s: TabValue) => void;
Expand Down Expand Up @@ -81,94 +79,101 @@ const TabNav: React.FC<TabNavProps> = (props) => {
const toRightBtnRef = useRef(null);

const [scrollLeft, setScrollLeft] = useState(0);
const [maxScrollLeft, setMaxScrollLeft] = useState(0);
const [activeTab, setActiveTab] = useState<HTMLElement>(null);

useEffect(() => {
const left = moveActiveTabIntoView(
{
activeTab,
navsContainer: navsContainerRef.current,
const setOffset = (offset: number) => {
setScrollLeft(calcValidOffset(offset, maxScrollLeft));
};

const getMaxScrollLeft = useCallback(() => {
if (['top', 'bottom'].includes(placement.toLowerCase())) {
const maxOffset = calcMaxOffset({
navsWrap: navsWrapRef.current,
toLeftBtn: toLeftBtnRef.current,
toRightBtn: toRightBtnRef.current,
leftOperations: leftOperationsRef.current,
navsContainer: navsContainerRef.current,
rightOperations: rightOperationsRef.current,
},
scrollLeft,
);
setScrollLeft(left);
}, [activeTab, scrollLeft]);
toRightBtn: toRightBtnRef.current,
});
setMaxScrollLeft(maxOffset);
}
}, [placement]);

// 调用检查函数,并设置左右滑动按钮的展示状态
const setScrollBtnVisibleHandler = useCallback(() => {
const canToleft = calculateCanToLeft(
const moveActiveTabIntoView = () => {
const offset = calculateOffset(
{
activeTab,
navsContainer: navsContainerRef.current,
navsWrap: navsWrapRef.current,
leftOperations: leftOperationsRef.current,
toLeftBtn: toLeftBtnRef.current,
rightOperations: rightOperationsRef.current,
},
scrollLeft,
placement,
'auto',
);
const canToRight = calculateCanToRight(
setOffset(offset);
};

// 当 activeTab 变化时,移动 activeTab 到可视区域
useEffect(() => {
const timeout = setTimeout(() => {
moveActiveTabIntoView();
}, 100);

return () => clearTimeout(timeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, maxScrollLeft]);

// 左右滑动按钮的展示状态
useEffect(() => {
if (['top', 'bottom'].includes(placement.toLowerCase())) {
// 这里的 1 是小数像素不精确误差修正
const canToLeft = scrollLeft > 1;
const canToRight = scrollLeft < maxScrollLeft - 1;

setToLeftBtnVisible(canToLeft);
setToRightBtnVisible(canToRight);
}
}, [placement, scrollLeft, maxScrollLeft]);

// 滚动条处理逻辑
const handleScroll = (action: 'prev' | 'next') => {
const offset = calcPrevOrNextOffset(
{
activeTab,
navsContainer: navsContainerRef.current,
navsWrap: navsWrapRef.current,
rightOperations: rightOperationsRef.current,
toRightBtn: toRightBtnRef.current,
},
scrollLeft,
placement,
action,
);
setOffset(offset);
};

setToLeftBtnVisible(canToleft);
setToRightBtnVisible(canToRight);
// children 发生变化也要触发一次切换箭头判断
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrollLeft, placement, children]);
// 滚轮和触摸板
useEffect(() => {
const scrollBar = scrollBarRef.current;
if (!scrollBar) return;

// 滚动条处理逻辑
const handleScroll = (position: 'left' | 'right') => {
const val =
position === 'left'
? scrollToLeft(
{
navsContainer: navsContainerRef.current,
leftOperations: leftOperationsRef.current,
toLeftBtn: toLeftBtnRef.current,
},
scrollLeft,
)
: scrollToRight(
{
navsWrap: navsWrapRef.current,
navsContainer: navsContainerRef.current,
rightOperations: rightOperationsRef.current,
toRightBtn: toRightBtnRef.current,
},
scrollLeft,
);

setScrollLeft(val);
};
const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
if (!canToLeft && !canToRight) return;
e.preventDefault();

const { deltaX, deltaY } = e;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
setOffset(scrollLeft + deltaX);
} else {
setOffset(scrollLeft + deltaY);
}
};

scrollBar.addEventListener('wheel', handleWheel, { passive: false });

return () => {
scrollBar?.removeEventListener('wheel', handleWheel);
};
});

// handle window resize
useEffect(() => {
const onResize = debounce(() => {
if (['top', 'bottom'].includes(placement.toLowerCase())) {
const left = calcScrollLeft(
{
navsContainer: navsContainerRef.current,
navsWrap: navsWrapRef.current,
rightOperations: rightOperationsRef.current,
},
scrollLeft,
);
setScrollLeft(left);
setScrollBtnVisibleHandler();
}
}, 300);
const onResize = debounce(getMaxScrollLeft, 300);

window.addEventListener('resize', onResize);

Expand All @@ -179,29 +184,14 @@ const TabNav: React.FC<TabNavProps> = (props) => {
});

useEffect(() => {
if (['top', 'bottom'].includes(placement.toLowerCase())) {
const left = calcScrollLeft(
{
navsContainer: navsContainerRef.current,
navsWrap: navsWrapRef.current,
rightOperations: rightOperationsRef.current,
},
scrollLeft,
);
setScrollLeft(left);
}
}, [itemList.length, scrollLeft, placement]);
getMaxScrollLeft();
}, [itemList.length, children, getMaxScrollLeft]);

// TabBar 组件逻辑层抽象,卡片类型时无需展示,故将逻辑整合到此处
const TabBarCom = isCard ? null : (
<TabBar tabPosition={placement} activeId={activeIndex} containerRef={navsWrapRef} />
);

// 组件初始化后判断当前是否需要展示滑动按钮
useEffect(() => {
setScrollBtnVisibleHandler();
}, [setScrollBtnVisibleHandler]);

const handleTabItemRemove = (removeItem) => {
const { value: removeValue, index: removeIndex } = removeItem;
if (removeValue === activeValue) {
Expand Down Expand Up @@ -231,7 +221,7 @@ const TabNav: React.FC<TabNavProps> = (props) => {
{canToLeft ? (
<div
onClick={() => {
handleScroll('left');
handleScroll('prev');
}}
className={classNames(
tdTabsClassGenerator('btn'),
Expand All @@ -251,7 +241,7 @@ const TabNav: React.FC<TabNavProps> = (props) => {
{canToRight ? (
<div
onClick={() => {
handleScroll('right');
handleScroll('next');
}}
className={classNames(
tdTabsClassGenerator('btn'),
Expand Down

0 comments on commit 16dea87

Please sign in to comment.