Skip to content

Commit

Permalink
feat(tabs): supports scrolling operations via wheel or touchpad (#3187)
Browse files Browse the repository at this point in the history
  • Loading branch information
oljc authored Jun 5, 2024
1 parent 8227a8b commit 0c8106f
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 206 deletions.
2 changes: 1 addition & 1 deletion src/_common
262 changes: 57 additions & 205 deletions src/tabs/tab-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -79,8 +41,7 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({
data() {
return {
scrollLeft: 0,
canToLeft: false,
canToRight: false,
maxScrollLeft: 0,
navBarStyle: {},
};
},
Expand Down Expand Up @@ -108,9 +69,6 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({
dataCanUpdateNavBarStyle(): Array<any> {
return [this.scrollLeft, this.placement, this.theme, this.navs, this.value];
},
dataCanUpdateArrow(): Array<any> {
return [this.scrollLeft, this.placement, this.navs];
},
iconBaseClass(): { [key: string]: boolean } {
return {
[`${this.classPrefix}-tabs__btn`]: true,
Expand Down Expand Up @@ -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();
Expand All @@ -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 {};
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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',
),
);
},

Expand Down Expand Up @@ -455,7 +307,7 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({
>
<transition name="fade" mode="out-in" appear>
{this.canToLeft ? (
<div ref="leftIcon" class={this.leftIconClass} onClick={this.handleScrollToLeft}>
<div ref="leftIcon" class={this.leftIconClass} onClick={() => this.handleScroll('prev')}>
<ChevronLeftIcon />
</div>
) : null}
Expand All @@ -467,7 +319,7 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({
>
<transition name="fade" mode="out-in" appear>
{this.canToRight ? (
<div ref="rightIcon" class={this.rightIconClass} onClick={this.handleScrollToRight}>
<div ref="rightIcon" class={this.rightIconClass} onClick={() => this.handleScroll('next')}>
<ChevronRightIcon />
</div>
) : null}
Expand All @@ -483,7 +335,7 @@ export default mixins(classPrefixMixins, getGlobalIconMixins()).extend({
renderNavs() {
return (
<div class={this.navContainerClass}>
<div class={this.navScrollContainerClass}>
<div class={this.navScrollContainerClass} onWheel={this.handleWheel}>
<div ref="navsWrap" class={this.navsWrapClass} style={this.wrapTransformStyle}>
{this.renderNavBar()}
{this.renderPanelContent()}
Expand All @@ -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() {
Expand Down

0 comments on commit 0c8106f

Please sign in to comment.