Skip to content

Commit

Permalink
fix(Tab): scroll by active correctly, close #4227 (#4505)
Browse files Browse the repository at this point in the history
  • Loading branch information
YSMJ1994 committed Oct 31, 2023
1 parent 64396da commit 1f0a4f4
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 67 deletions.
50 changes: 36 additions & 14 deletions src/tab/tabs/nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,18 @@ class Nav extends React.Component {
showBtn: false,
dropdownTabs: [],
};
this.offset = 0;
}

/**
* 实时获取滚动位置
*/
get offset() {
const scroller = this.scroller;
if (!scroller) {
return 0;
}
const scrollLeft = scroller.scrollLeft;
return scrollLeft > 0 ? -scrollLeft : scrollLeft;
}

componentDidMount() {
Expand All @@ -63,12 +74,8 @@ class Nav extends React.Component {
clearTimeout(this.scrollTimer);
this.scrollTimer = setTimeout(() => {
this.scrollToActiveTab();
}, 410); // transition-duration is set to be .4s, wait for the transition finishes before re-calc

clearTimeout(this.slideTimer);
this.slideTimer = setTimeout(() => {
this.setSlideBtn();
}, 410);
}, 410); // transition-duration is set to be .4s, wait for the transition finishes before re-calc

// 更改tabs后如果有dropdown属性,应该重新执行getDropdownItems函数更新dropdown数据
if (this.props.excessMode === 'dropdown') {
Expand Down Expand Up @@ -128,8 +135,6 @@ class Nav extends React.Component {
const offsetValue = isNaN(_ov) ? target : _ov;

if (this.offset !== target && this.nav) {
// needs move
this.offset = target;
const divScroll = this.nav.parentElement;

if (tabPosition === 'left' || tabPosition === 'right') {
Expand Down Expand Up @@ -199,18 +204,19 @@ class Nav extends React.Component {
// TEMP: 这里会受 Animate 影响,re-render 过程中 this.nav 实际上指向的是上次的 tabList 元素,建议暂时关闭 animation 解决
const navWH = getOffsetWH(this.nav, tabPosition);
const wrapperWH = getOffsetWH(this.wrapper, tabPosition);
const minOffset = wrapperWH - navWH;

// 这里统一向下取整再做比较,否则会因为小数点精度问题导致无法对齐
const minOffset = Math.floor(wrapperWH - navWH);
const offset = Math.floor(this.offset);
let next;
let prev;
if (minOffset >= 0 || navWH <= wrapperWH) {
next = false;
prev = false;
this.setOffset(0, false); // no need to check slide again since this call is invoked from inside setSlideBtn
} else if (this.offset < 0 && this.offset <= minOffset) {
} else if (offset < 0 && offset <= minOffset) {
prev = true;
next = false;
} else if (this.offset >= 0) {
} else if (offset >= 0) {
prev = false;
next = true;
} else {
Expand Down Expand Up @@ -260,6 +266,18 @@ class Nav extends React.Component {
this.props.onClose(key);
};

debounceSetSideBtn = () => {
clearTimeout(this.slideTimer);
this.slideTimer = setTimeout(() => {
this.setSlideBtn();
}, 100);
};

onScroll = () => {
// 每次滚动时更新btn状态
this.debounceSetSideBtn();
};

onCloseKeyDown = (key, e) => {
if (e.keyCode === KEYCODE.ENTER) {
e.stopPropagation();
Expand Down Expand Up @@ -354,7 +372,7 @@ class Nav extends React.Component {
const wrapperOffset = getOffsetLT(this.wrapper);
const target = this.offset;

if (activeTabOffset + activeTabWH >= wrapperOffset + wrapperWH || activeTabOffset < wrapperOffset) {
if (activeTabOffset + activeTabWH > wrapperOffset + wrapperWH + 1 || activeTabOffset < wrapperOffset) {
this.setOffset(this.offset + wrapperOffset - activeTabOffset, true, true);
return;
}
Expand Down Expand Up @@ -469,6 +487,10 @@ class Nav extends React.Component {
this.wrapper = ref;
};

scrollerRefHandler = ref => {
this.scroller = ref;
};

navbarRefHandler = ref => {
this.navbar = ref;
};
Expand Down Expand Up @@ -562,7 +584,7 @@ class Nav extends React.Component {
const container = (
<div className={containerCls} onKeyDown={onKeyDown} key="nav-container">
<div className={`${prefix}tabs-nav-wrap`} ref={this.wrapperRefHandler}>
<div className={`${prefix}tabs-nav-scroll`}>
<div className={`${prefix}tabs-nav-scroll`} ref={this.scrollerRefHandler} onScroll={this.onScroll}>
{animation ? (
<Animate
role="tablist"
Expand Down
127 changes: 74 additions & 53 deletions test/tab/index-spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import Enzyme, { mount, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import sinon from 'sinon';
Expand All @@ -10,6 +11,7 @@ import TabNav from '../../src/tab/tabs/nav';
import '../../src/tab/style.js';

Enzyme.configure({ adapter: new Adapter() });
const delay = time => new Promise(resolve => setTimeout(resolve, time));

describe('Tab', () => {
describe('simple', () => {
Expand Down Expand Up @@ -399,7 +401,7 @@ describe('Tab', () => {
});

describe('excess mode', () => {
let wrapper, target;
let target;
const panes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item, index) => (
<Tab.Item title={`tab item ${item}`} key={index} />
));
Expand All @@ -412,90 +414,85 @@ describe('Tab', () => {

afterEach(() => {
document.body.removeChild(target);
wrapper.unmount();
wrapper = null;
target = null;
});

it('should render excess tabs with slides', done => {
wrapper = mount(
it('should render excess tabs with slides', async () => {
const wrapper = mount(
<div style={boxStyle}>
<Tab animation={false}>{panes}</Tab>
</div>,
{ attachTo: target }
);
setTimeout(() => {
assert(
dom.hasClass(
wrapper.find('.next-tabs-btn-prev').getDOMNode(),
'disabled'
)
);
assert(wrapper.find('.next-tabs-btn-next').length === 1);
done();
}, 420);
await delay(600);
assert(
dom.hasClass(
wrapper.getDOMNode().querySelector('.next-tabs-btn-prev'),
'disabled'
)
);
assert(wrapper.getDOMNode().querySelector('.next-tabs-btn-next'));
});

it('should click prev/next to slide', done => {
wrapper = mount(
it('should click prev/next to slide', async () => {
const wrapper = mount(
<div style={boxStyle}>
<Tab animation={false}>{panes}</Tab>
</div>,
{ attachTo: target }
);
setTimeout(() => {
assert(
dom.hasClass(
wrapper.find('.next-tabs-btn-prev').getDOMNode(),
'disabled'
)
);
wrapper.find('.next-tabs-btn-next').simulate('click');
assert(
!dom.hasClass(
wrapper.find('.next-tabs-btn-prev').getDOMNode(),
'disabled'
)
);
wrapper.find('.next-tabs-btn-prev').simulate('click');
assert(
dom.hasClass(
wrapper.find('.next-tabs-btn-prev').getDOMNode(),
'disabled'
)
);
done();
}, 420);
await delay(600);
const domNode = wrapper.getDOMNode();
assert(domNode);
assert(
dom.hasClass(
domNode.querySelector('.next-tabs-btn-prev'),
'disabled'
)
);
ReactTestUtils.Simulate.click(domNode.querySelector('.next-tabs-btn-next'));
await delay(800);
assert(
!dom.hasClass(
domNode.querySelector('.next-tabs-btn-prev'),
'disabled'
)
);
ReactTestUtils.Simulate.click(domNode.querySelector('.next-tabs-btn-prev'));
await delay(800);
assert(
dom.hasClass(
domNode.querySelector('.next-tabs-btn-prev'),
'disabled'
)
);
});

it('should not render dropdown if not excessed', () => {
wrapper = mount(
const wrapper = mount(
<div style={boxStyle}>
<Tab excessMode="dropdown" shape="wrapped">
<Tab.Item title="item" key={1} />
</Tab>
</div>,
{ attachTo: target }
);
assert(wrapper.find('.next-tabs-btn-down').length === 0);
assert(!wrapper.getDOMNode().querySelector('.next-tabs-btn-down'));
});

it('should work fine without animation', () => {
wrapper = mount(<Tab animation={false}>{panes}</Tab>, { attachTo: target });
const wrapper = mount(<Tab animation={false}>{panes}</Tab>, { attachTo: target });
assert(
wrapper
.find('.next-tabs-tab')
.at(0)
.hasClass('active')
wrapper.getDOMNode()
.querySelectorAll('.next-tabs-tab').item(0).classList.contains('active')
);
});

it('should scrollToActiveTab', () => {
wrapper = mount(
const wrapper = mount(
<div style={boxStyle}>
<Tab defaultActiveKey="9">{panes}</Tab>
</div>,
{ attachTo: target }
</div>
);
wrapper
.find('.next-tabs-tab')
Expand All @@ -508,10 +505,27 @@ describe('Tab', () => {
.hasClass('active')
);
});

it('should scroll into the viewport while click the edge item', async () => {
const wrapper = mount(<Tab animation={false} style={{width: 380}}>{panes}</Tab>, {attachTo: target});
await delay(600);
const scroller = wrapper.getDOMNode().querySelector('.next-tabs-nav-scroll');
assert(scroller);
const rightEdgeItem = scroller.querySelectorAll('.next-tabs-tab').item(3);
assert(rightEdgeItem);
ReactTestUtils.Simulate.click(rightEdgeItem);
await delay(1000);
assert(isInScrollBoxViewport(scroller, rightEdgeItem));
scroller.scrollLeft = 2000;
const leftEdgeItem = scroller.querySelectorAll('.next-tabs-tab').item(6);
assert(leftEdgeItem);
ReactTestUtils.Simulate.click(leftEdgeItem);
await delay(1000);
assert(isInScrollBoxViewport(scroller, leftEdgeItem));
});
});
describe('animation sensitive tests', () => {
let target;
const delay = time => new Promise(resolve => setTimeout(resolve, time));
const panes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item, index) => (
<Tab.Item title={`tab item ${item}`} key={index} />
));
Expand Down Expand Up @@ -549,7 +563,7 @@ describe('Tab', () => {
{panes}
</Tab>
</div>, target);
await delay(800);
await delay(1000);
assert(target.querySelector('.next-tabs-btn-prev').classList.contains("disabled"));
});
it('should slide', async () => {
Expand All @@ -566,6 +580,7 @@ describe('Tab', () => {
await delay(200);
newpos = target.querySelector(".next-tabs-nav").getBoundingClientRect().left;
assert(newpos > prev);
await delay(800);
});

it('should adjust scroll length so that tab not partially in view', async () => {
Expand Down Expand Up @@ -713,8 +728,14 @@ describe('Tab', () => {
</Tab>,
{ attachTo: target }
);
const el = wrapper.find('#test-extra').getDOMNode().parentElement;
const el = wrapper.getDOMNode().querySelector('#test-extra').parentElement;
assert(el.style.getPropertyValue('float') === 'left');
});
});
});

function isInScrollBoxViewport(scroller, item) {
const scrollerRect = scroller.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
return itemRect.left >= scrollerRect.left - 1 && itemRect.width + itemRect.left <= scrollerRect.width + scrollerRect.left + 1;
}

0 comments on commit 1f0a4f4

Please sign in to comment.