From 0c8106f91e4916fa5b053caf90b72f9b42e2709e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=B1=9F=E8=BE=B0?= Date: Thu, 6 Jun 2024 00:31:39 +0800 Subject: [PATCH] feat(tabs): supports scrolling operations via wheel or touchpad (#3187) --- src/_common | 2 +- src/tabs/tab-nav.tsx | 262 ++++++++++--------------------------------- 2 files changed, 58 insertions(+), 206 deletions(-) diff --git a/src/_common b/src/_common index d148b55b4..913eb70a6 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit d148b55b4d0c9e52d18cd49f16a7db3b2107ee34 +Subproject commit 913eb70a6378989d376231b1ab64a74090c54735 diff --git a/src/tabs/tab-nav.tsx b/src/tabs/tab-nav.tsx index 76f7c2a2e..8eae16b77 100644 --- a/src/tabs/tab-nav.tsx +++ b/src/tabs/tab-nav.tsx @@ -13,52 +13,14 @@ import tabProps from './props'; import { renderTNodeJSX } from '../utils/render-tnode'; import { getClassPrefixMixins, getGlobalIconMixins } from '../config-provider/config-receiver'; import mixins from '../utils/mixins'; +import { + calcMaxOffset, calcValidOffset, calculateOffset, calcPrevOrNextOffset, +} from '../_common/js/tabs/base'; import type { TdTabsProps } from './type'; const classPrefixMixins = getClassPrefixMixins('tab__nav'); -const getDomWidth = (dom: HTMLElement): number => dom?.offsetWidth || 0; - -interface GetLeftCoverWidth { - leftZone: HTMLElement; - leftIcon: HTMLElement; - totalWidthBeforeActiveTab: number; -} - -interface GetRightCoverWidth { - rightZone: HTMLElement; - rightIcon: HTMLElement; - wrap: HTMLElement; - totalWidthBeforeActiveTab: number; - activeTabWidth: number; -} - -// 如果要当前tab左边对齐左操作栏的右边以展示完整的tab,需要获取左边操作栏的宽度 -const getLeftCoverWidth = (o: GetLeftCoverWidth) => { - const leftOperationsZoneWidth = getDomWidth(o.leftZone); - const leftIconWidth = getDomWidth(o.leftIcon); - // 判断当前tab是不是第一个tab - if (o.totalWidthBeforeActiveTab === 0) { - // 如果是第一个tab要移动到最左边,则要减去左箭头的宽度,因为此时左箭头会被隐藏起来 - return leftOperationsZoneWidth - leftIconWidth; - } - return leftOperationsZoneWidth; -}; - -// 如果要当前tab右边对齐右操作栏的左边以展示完整的tab,需要获取右边操作栏的宽度 -const getRightCoverWidth = (o: GetRightCoverWidth) => { - const rightOperationsZoneWidth = getDomWidth(o.rightZone); - const rightIconWidth = getDomWidth(o.rightIcon as HTMLElement); - const wrapWidth = getDomWidth(o.wrap); - // 判断当前tab是不是最后一个tab,小于1是防止小数像素导致值不相等的情况 - if (Math.abs(o.totalWidthBeforeActiveTab + o.activeTabWidth - wrapWidth) < 1) { - // 如果是最后一个tab,则要减去右箭头的宽度,因为此时右箭头会被隐藏 - return rightOperationsZoneWidth - rightIconWidth; - } - return rightOperationsZoneWidth; -}; - export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ name: 'TTabNav', components: { @@ -79,8 +41,7 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ data() { return { scrollLeft: 0, - canToLeft: false, - canToRight: false, + maxScrollLeft: 0, navBarStyle: {}, }; }, @@ -108,9 +69,6 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ dataCanUpdateNavBarStyle(): Array { return [this.scrollLeft, this.placement, this.theme, this.navs, this.value]; }, - dataCanUpdateArrow(): Array { - return [this.scrollLeft, this.placement, this.navs]; - }, iconBaseClass(): { [key: string]: boolean } { return { [`${this.classPrefix}-tabs__btn`]: true, @@ -170,13 +128,14 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ } return null; }, + canToLeft(): boolean { + return this.scrollLeft > 1; + }, + canToRight(): boolean { + return this.scrollLeft < this.maxScrollLeft - 1; + }, }, watch: { - dataCanUpdateArrow() { - this.$nextTick(() => { - this.calculateCanShowArrow(); - }); - }, dataCanUpdateNavBarStyle() { this.$nextTick(() => { this.calculateNavBarStyle(); @@ -189,44 +148,12 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ }, navs() { this.$nextTick(() => { - this.fixScrollLeft(); + this.getMaxScrollLeft(); + this.moveActiveTabIntoView(); }); }, }, methods: { - calculateCanShowArrow() { - this.calculateCanToLeft(); - this.calculateCanToRight(); - }, - - calculateCanToLeft() { - if (['left', 'right'].includes(this.placement.toLowerCase())) { - this.canToLeft = false; - } - const container = this.$refs.navsContainer as HTMLElement; - const wrap = this.$refs.navsWrap as HTMLElement; - if (!wrap || !container) { - this.canToLeft = false; - } - const leftOperationsZoneWidth = getDomWidth(this.$refs.leftOperationsZone as HTMLElement); - const leftIconWidth = getDomWidth(this.$refs.leftIcon as HTMLElement); - this.canToLeft = this.scrollLeft + Math.round(leftOperationsZoneWidth - leftIconWidth) > 0; - }, - - calculateCanToRight() { - if (['left', 'right'].includes(this.placement.toLowerCase())) { - this.canToRight = false; - } - const container = this.$refs.navsContainer as HTMLElement; - const wrap = this.$refs.navsWrap as HTMLElement; - if (!wrap || !container) { - this.canToRight = false; - } - const rightOperationsZoneWidth = getDomWidth(this.$refs.rightOperationsZone as HTMLElement); - const rightIconWidth = getDomWidth(this.$refs.rightIcon as HTMLElement); - this.canToRight = this.scrollLeft + getDomWidth(container) - (rightOperationsZoneWidth - rightIconWidth) - getDomWidth(wrap) < -1; // 小数像素不精确,所以这里判断小于-1 - }, - calculateNavBarStyle() { const getNavBarStyle = () => { if (this.theme === 'card') return {}; @@ -260,25 +187,9 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ this.navBarStyle = getNavBarStyle(); }, - calculateMountedScrollLeft() { - if (['left', 'right'].includes(this.placement.toLowerCase())) return; - - const container = this.$refs.navsContainer as HTMLElement; - const activeTabEl: HTMLElement = this.activeElement; - const containerWidth = getDomWidth(container); - const activeTabWidth = activeTabEl?.offsetWidth || 0; - // index of the active tab - const activeElIndex = Array.prototype.indexOf.call((this.$refs.navsWrap as HTMLElement).children, activeTabEl); - // calculate whether the right btn is display or not - const isRightBtnShow = this.navs?.length - activeElIndex >= Math.round((containerWidth - activeTabWidth) / activeTabWidth) ? 1 : 0; - - const totalWidthBeforeActiveTab = activeTabEl?.offsetLeft; - if (totalWidthBeforeActiveTab > containerWidth - activeTabWidth) this.scrollLeft = totalWidthBeforeActiveTab - isRightBtnShow * activeTabWidth; - }, - watchDomChange() { const onResize = debounce(() => { - this.resetScrollPosition(); + this.getMaxScrollLeft(); }, 300); window.addEventListener('resize', onResize); this.$once('beforeDestroy', () => { @@ -293,118 +204,59 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ }); }, - resetScrollPosition() { - this.fixScrollLeft(); - this.calculateCanShowArrow(); + setOffset(offset: number) { + this.scrollLeft = calcValidOffset(offset, this.maxScrollLeft); }, - handleScrollToLeft() { - const container = this.$refs.navsContainer as HTMLElement; - if (!container) return; - const leftOperationsZoneWidth = getDomWidth(this.$refs.leftOperationsZone as HTMLElement); - const leftIconWidth = getDomWidth(this.$refs.leftIcon as HTMLElement); - const containerWidth = getDomWidth(container); - this.scrollLeft = Math.max(-(leftOperationsZoneWidth - leftIconWidth), this.scrollLeft - containerWidth); + getMaxScrollLeft() { + this.maxScrollLeft = calcMaxOffset({ + navsWrap: this.$refs.navsWrap as HTMLElement, + navsContainer: this.$refs.navsContainer as HTMLElement, + rightOperations: this.$refs.rightOperationsZone as HTMLElement, + toRightBtn: this.$refs.rightIcon as HTMLElement, + }); }, - handleScrollToRight() { - const container = this.$refs.navsContainer as HTMLElement; - const wrap = this.$refs.navsWrap as HTMLElement; - const rightOperationsZoneWidth = getDomWidth(this.$refs.rightOperationsZone as HTMLElement); - const rightIconWidth = getDomWidth(this.$refs.rightIcon as HTMLElement); - const containerWidth = getDomWidth(container); - const wrapWidth = getDomWidth(wrap); - this.scrollLeft = Math.min( - this.scrollLeft + containerWidth, - wrapWidth - containerWidth + rightOperationsZoneWidth - rightIconWidth, + handleScroll(action: 'next' | 'prev') { + const offset = calcPrevOrNextOffset( + { + navsContainer: this.$refs.navsContainer as HTMLElement, + activeTab: this.activeElement, + }, + this.scrollLeft, + action, ); - }, - shouldMoveToLeftSide(activeTabEl: HTMLElement) { - const totalWidthBeforeActiveTab = activeTabEl.offsetLeft; - const container = this.$refs.navsContainer as HTMLElement; - if (!container) return; - const leftCoverWidth = getLeftCoverWidth({ - leftZone: this.$refs.leftOperationsZone as HTMLElement, - leftIcon: this.$refs.leftIcon as HTMLElement, - totalWidthBeforeActiveTab, - }); - // 判断当前tab是不是在左边被隐藏 - const isCurrentTabHiddenInLeftZone = () => this.scrollLeft + leftCoverWidth > totalWidthBeforeActiveTab; - if (isCurrentTabHiddenInLeftZone()) { - this.scrollLeft = totalWidthBeforeActiveTab - leftCoverWidth; - return true; - } - return false; + this.setOffset(offset); }, - shouldMoveToRightSide(activeTabEl: HTMLElement) { - const totalWidthBeforeActiveTab = activeTabEl.offsetLeft; - const activeTabWidth = activeTabEl.offsetWidth; - const container = this.$refs.navsContainer as HTMLElement; - const wrap = this.$refs.navsWrap as HTMLElement; - if (!container || !wrap) return; - const containerWidth = getDomWidth(container); - const rightCoverWidth = getRightCoverWidth({ - rightZone: this.$refs.rightOperationsZone as HTMLElement, - rightIcon: this.$refs.rightIcon as HTMLElement, - wrap, - totalWidthBeforeActiveTab, - activeTabWidth, - }); - // 判断当前tab是不是在右边被隐藏 - const isCurrentTabHiddenInRightZone = () => this.scrollLeft + containerWidth - rightCoverWidth < totalWidthBeforeActiveTab + activeTabWidth; - if (isCurrentTabHiddenInRightZone()) { - this.scrollLeft = totalWidthBeforeActiveTab + activeTabWidth - containerWidth + rightCoverWidth; - return true; - } - return false; - }, + handleWheel(e: WheelEvent) { + if (!this.canToLeft && !this.canToRight) return; - moveActiveTabIntoView({ needCheckUpdate } = { needCheckUpdate: true }) { - if (['left', 'right'].includes(this.placement)) { - return false; - } - const activeTabEl: HTMLElement = this.activeElement; - if (!activeTabEl) { - // 如果没有当前 value 对应的tab,一种情况是真没有;一种情况是在修改value的同时,新增了一个值为该value的tab。后者因为navs的更新在$nextTick之后,所以得等下一个updated才能拿到新的tab - if (needCheckUpdate) { - this.$once('hook:updated', () => { - this.moveActiveTabIntoView({ needCheckUpdate: false }); - }); - } - return false; - } - return this.shouldMoveToLeftSide(activeTabEl) || this.shouldMoveToRightSide(activeTabEl); - }, + e.preventDefault(); + const { deltaX, deltaY } = e; - fixIfLastItemNotTouchRightSide(containerWidth: number, wrapWidth: number) { - const rightOperationsZoneWidth = getDomWidth(this.$refs.rightOperationsZone as HTMLElement); - if (this.scrollLeft + containerWidth - rightOperationsZoneWidth > wrapWidth) { - this.scrollLeft = wrapWidth + rightOperationsZoneWidth - containerWidth; - return true; + if (Math.abs(deltaX) > Math.abs(deltaY)) { + this.setOffset(this.scrollLeft + deltaX); + } else { + this.setOffset(this.scrollLeft + deltaY); } - return false; }, - fixIfItemTotalWidthIsLessThenContainerWidth(containerWidth: number, wrapWidth: number) { - if (wrapWidth <= containerWidth) { - this.scrollLeft = 0; - return true; - } - return false; - }, - - fixScrollLeft() { + moveActiveTabIntoView() { if (['left', 'right'].includes(this.placement.toLowerCase())) return; - const container = this.$refs.navsContainer as HTMLElement; - const wrap = this.$refs.navsWrap as HTMLElement; - if (!wrap || !container) return false; - const containerWidth = getDomWidth(container); - const wrapWidth = getDomWidth(wrap); - return ( - this.fixIfItemTotalWidthIsLessThenContainerWidth(containerWidth, wrapWidth) - || this.fixIfLastItemNotTouchRightSide(containerWidth, wrapWidth) + + this.setOffset( + calculateOffset( + { + activeTab: this.activeElement, + navsContainer: this.$refs.navsContainer as HTMLElement, + leftOperations: this.$refs.leftOperationsZone as HTMLElement, + rightOperations: this.$refs.rightOperationsZone as HTMLElement, + }, + this.scrollLeft, + 'auto', + ), ); }, @@ -455,7 +307,7 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ > {this.canToLeft ? ( -
+
this.handleScroll('prev')}>
) : null} @@ -467,7 +319,7 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ > {this.canToRight ? ( -
+
this.handleScroll('next')}>
) : null} @@ -483,7 +335,7 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ renderNavs() { return (
-
+
{this.renderNavBar()} {this.renderPanelContent()} @@ -501,10 +353,10 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({ this.$nextTick(() => { this.watchDomChange(); this.calculateNavBarStyle(); - this.calculateCanShowArrow(); + this.getMaxScrollLeft(); }); setTimeout(() => { - this.calculateMountedScrollLeft(); + this.moveActiveTabIntoView(); }); }, render() {