diff --git a/docs/tab/demo/excess-mode.md b/docs/tab/demo/excess-mode.md index 148219f2e7..77ed6fe366 100644 --- a/docs/tab/demo/excess-mode.md +++ b/docs/tab/demo/excess-mode.md @@ -43,7 +43,7 @@ function onClick(key) { ReactDOM.render(
Dropdown mode
- + { tabs.map(item => {item.tab} content, content, content) } diff --git a/docs/tab/demo/extra.md b/docs/tab/demo/extra.md index 78a2b21d0c..05d706b88d 100644 --- a/docs/tab/demo/extra.md +++ b/docs/tab/demo/extra.md @@ -2,14 +2,14 @@ - order: 11 -通过 `extra` 属性添加附加内容,请确保只在有限选项卡的情况下才使用附加内容。 +通过 `extra` 属性添加附加内容,请确保只在有限选项卡的情况下才使用附加内容, 该功能在选项卡溢出时会和溢出导航的按钮冲突。 :::lang=en-us # Extra - order: 11 -Pass your custom contents to `extra`. +Pass your custom contents to `extra`, please consider using this when the tab-items are limited, since it is not designed to be used in combination with excess-mode. ::: diff --git a/docs/tab/index.en-us.md b/docs/tab/index.en-us.md index b5180dbf1e..ac361bb63f 100644 --- a/docs/tab/index.en-us.md +++ b/docs/tab/index.en-us.md @@ -35,7 +35,7 @@ Disable animation with `animation={false}` | navClassName | Custom className of nav | String | - | | contentStyle | Custom style of content | Object | - | | contentClassName | Custom className of content | String | - | -| extra | Extra content of tab | ReactNode | - | +| extra | Extra content of tab, ensure the item won't excess when using this | ReactNode | - | | onClick | Callback when click tab | Function | () => {} | | onChange | Callback when active tab changes

**signature**:
Function(key: String/Number)) => void
**parameter**:
_key_: {String/Number)} theActiveKey | Function | () => {} | | onClose | Callback when close the tab

**signature**:
Function(key: String/Number)) => void
**parameter**:
_key_: {String/Number)} theClosedKey | Function | () => {} | diff --git a/src/tab/rtl.scss b/src/tab/rtl.scss index 8d875b33e4..56ae6efc54 100644 --- a/src/tab/rtl.scss +++ b/src/tab/rtl.scss @@ -1,7 +1,7 @@ @import "../core/index-noreset.scss"; @import "scss/variable"; -#{$tab-prefix}[dir="rtl"] { +#{$tab-prefix}[dir='rtl'] { &.#{$css-prefix}medium { & #{$tab-prefix}-nav-container-scrolling { padding-left: $tab-nav-scroll-padding-right-m; @@ -56,4 +56,14 @@ right: auto; } } +#{$tab-prefix}-text[dir='rtl'] > #{$tab-prefix}-bar { + #{$tab-prefix}-tab { + &:not(:last-child):after { + content: ' '; + position: absolute; + left: 0; + right: auto; + } + } +} diff --git a/src/tab/tabs/nav.jsx b/src/tab/tabs/nav.jsx index d012903e66..3437147f6e 100644 --- a/src/tab/tabs/nav.jsx +++ b/src/tab/tabs/nav.jsx @@ -6,7 +6,7 @@ import Icon from '../../icon'; import Overlay from '../../overlay'; import Menu from '../../menu'; import Animate from '../../animate'; -import { events, KEYCODE } from '../../util'; +import { events, KEYCODE, dom } from '../../util'; import { triggerEvents, getOffsetLT, @@ -42,8 +42,7 @@ class Nav extends React.Component { constructor(props, context) { super(props, context); this.state = { - next: false, - prev: false, + showBtn: false, dropdownTabs: [], }; this.offset = 0; @@ -54,22 +53,17 @@ class Nav extends React.Component { ctx.setSlideBtn(); ctx.getDropdownItems(this.props); - // 此处通过延时处理,屏蔽动画带来的定位不准确问题(由于要支持ie9,因此无法使用transitionend) - clearTimeout(ctx.scrollTimer); - ctx.scrollTimer = setTimeout(() => { - ctx.scrollToActiveTab(); - }, 400); - events.on(window, 'resize', this.onWindowResized); } componentDidUpdate() { const ctx = this; - - clearTimeout(ctx.slideTimer); - ctx.slideTimer = setTimeout(() => { - ctx.setSlideBtn(); - }, 200); + // 此处通过延时处理,屏蔽动画带来的定位不准确问题(由于要支持ie9,因此无法使用transitionend) + clearTimeout(ctx.scrollTimer); + ctx.scrollTimer = setTimeout(() => { + ctx.scrollToActiveTab(); + }, 410); // transition-duration is set to be .4s, wait for the transition finishes before re-calc + ctx.setSlideBtn(); if ( this.activeTab && findDOMNode(this).contains(document.activeElement) @@ -89,7 +83,7 @@ class Nav extends React.Component { * @param {bool} setActive need to check the active status or not */ setOffset(target, checkSlideBtn = true, setActive = true) { - const { tabPosition } = this.props; + const { tabPosition, rtl } = this.props; const navWH = getOffsetWH(this.nav, tabPosition); const wrapperWH = getOffsetWH(this.wrapper); @@ -107,15 +101,14 @@ class Nav extends React.Component { const activeTabOffset = getOffsetLT(this.activeTab) + relativeOffset; const wrapperOffset = getOffsetLT(this.wrapper); - - if ( - // active tab partially in visible zone - wrapperOffset + wrapperWH < activeTabOffset + activeTabWH && - activeTabOffset < wrapperOffset + wrapperWH - ) { - target -= // Move more to make active tab totally in visible zone - activeTabOffset + activeTabWH - (wrapperOffset + wrapperWH); - } + target = this._adjustTarget( + wrapperOffset, + wrapperWH, + activeTabWH, + activeTabOffset, + rtl, + target + ); } if (this.offset !== target) { @@ -171,6 +164,65 @@ class Nav extends React.Component { } } + _adjustTarget( + wrapperOffset, + wrapperWH, + activeTabWH, + activeTabOffset, + rtl, + target + ) { + if ( + // active tab covers wrapper right edge + wrapperOffset + wrapperWH < activeTabOffset + activeTabWH && + activeTabOffset < wrapperOffset + wrapperWH + ) { + if (rtl) { + target += // Move more to make active tab totally in visible zone + activeTabOffset + activeTabWH - (wrapperOffset + wrapperWH); + } else { + target -= + activeTabOffset + + activeTabWH - + (wrapperOffset + wrapperWH) + + 1; + } + + return target; + } + if ( + // active tab covers wrapper left edge + wrapperOffset < activeTabOffset + activeTabWH && + activeTabOffset < wrapperOffset + ) { + if (rtl) { + target -= wrapperOffset - activeTabOffset + 1; + } else { + target += wrapperOffset - activeTabOffset; + } + return target; + } + return target; + } + + _setBtnStyle(prev, next) { + if (this.prevBtn && this.nextBtn) { + const cls = 'disabled'; + this.prevBtn.disabled = !prev; + this.nextBtn.disabled = !next; + if (prev) { + dom.removeClass(this.prevBtn, cls); + } else { + dom.addClass(this.prevBtn, cls); + } + if (next) { + dom.removeClass(this.nextBtn, cls); + } else { + dom.addClass(this.nextBtn, cls); + } + } + } + setSlideBtn() { const { tabPosition } = this.props; @@ -185,7 +237,6 @@ class Nav extends React.Component { if (minOffset >= 0 || navWH <= navbarWH) { 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) { prev = true; next = false; @@ -196,12 +247,12 @@ class Nav extends React.Component { prev = true; next = true; } - - if (next !== this.state.next || prev !== this.state.prev) { + if ((prev || next) !== this.state.showBtn) { this.setState({ - next, - prev, + showBtn: prev || next, }); + } else { + this._setBtnStyle(prev, next); } } @@ -343,15 +394,7 @@ class Nav extends React.Component { ) { return; } - // if (activeTabOffset < wrapperOffset) { - // target += wrapperOffset - activeTabOffset; - // this.setOffset(target); - // } - if (wrapperOffset + wrapperWH < activeTabOffset + activeTabWH) { - target -= - activeTabOffset + activeTabWH - (wrapperOffset + wrapperWH); - this.setOffset(target, true, false); - } + this.setOffset(target, true, true); }; onPrevClick = () => { @@ -480,6 +523,14 @@ class Nav extends React.Component { this.activeTab = ref; }; + prevBtnHandler = ref => { + this.prevBtn = findDOMNode(ref); + }; + + nextBtnHandler = ref => { + this.nextBtn = findDOMNode(ref); + }; + render() { const { prefix, @@ -498,30 +549,22 @@ class Nav extends React.Component { let prevButton; let restButton; - const showNextPrev = state.prev || state.next; + const showNextPrev = state.showBtn; if ( excessMode === 'dropdown' && - state.next && + showNextPrev && state.dropdownTabs.length ) { restButton = this.renderDropdownTabs(state.dropdownTabs); prevButton = null; nextButton = null; } else if (showNextPrev) { - const prevBtnCls = classnames({ - [`${prefix}tabs-btn-prev`]: 1, - disabled: !state.prev, - }); - const nextBtnCls = classnames({ - [`${prefix}tabs-btn-next`]: 1, - disabled: !state.next, - }); - prevButton = ( @@ -529,8 +572,9 @@ class Nav extends React.Component { nextButton = ( diff --git a/test/tab/index-spec.js b/test/tab/index-spec.js index 09c6b73517..d70b8f48b4 100644 --- a/test/tab/index-spec.js +++ b/test/tab/index-spec.js @@ -3,7 +3,7 @@ import Enzyme, { mount, render } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import sinon from 'sinon'; import assert from 'power-assert'; -import { KEYCODE } from '../../src/util'; +import { KEYCODE, dom } from '../../src/util'; import Tab from '../../src/tab/index'; import Nav from '../../src/tab/tabs/nav'; import '../../src/tab/style.js'; @@ -372,29 +372,55 @@ describe('Tab', () => { target = null; }); - it('should render excess tabs with slides', () => { + it('should render excess tabs with slides', done => { wrapper = mount(
{panes}
, { attachTo: target } ); - assert(wrapper.find('.next-tabs-btn-prev').hasClass('disabled')); - assert(wrapper.find('.next-tabs-btn-next').length === 1); + setTimeout(() => { + assert( + dom.hasClass( + wrapper.find('.next-tabs-btn-prev').getDOMNode(), + 'disabled' + ) + ); + assert(wrapper.find('.next-tabs-btn-next').length === 1); + done(); + }, 300); }); - it('should click prev/next to slide', () => { + it('should click prev/next to slide', done => { wrapper = mount(
{panes}
, { attachTo: target } ); - assert(wrapper.find('.next-tabs-btn-prev').hasClass('disabled')); - wrapper.find('.next-tabs-btn-next').simulate('click'); - assert(!wrapper.find('.next-tabs-btn-prev').hasClass('disabled')); - wrapper.find('.next-tabs-btn-prev').simulate('click'); - assert(wrapper.find('.next-tabs-btn-prev').hasClass('disabled')); + 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(); + }, 300); }); it('should render excess tabs with dropdown', () => { @@ -451,6 +477,54 @@ describe('Tab', () => { .hasClass('active') ); }); + + it('should adjust scroll length so that tab not partially in view', done => { + const panes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13].map( + (item, index) => ( + + content + + ) + ); + wrapper = mount( +
+ + {panes} + +
, + { + attachTo: target, + } + ); + wrapper.update(); + setTimeout(() => { + const transStr = wrapper + .find('.next-tabs-nav') + .at(0) + .getDOMNode().style.transform; + const rst = transStr.match( + /translate3d\((\-?\d+\.\d+|\-?\d+)px,.*\)/i + ); + assert(rst[1].startsWith('-')); + wrapper.find('.next-tabs-btn-prev').simulate('click'); + }, 500); + setTimeout(() => { + const transStr = wrapper + .find('.next-tabs-nav') + .at(0) + .getDOMNode().style.transform; + const rst = transStr.match( + /translate3d\((\-?\d+\.\d+|\-?\d+)px,.*\)/i + ); + assert(rst[1] === '0'); + done(); + }, 1000); + }); }); describe('rtl mode', () => { let wrapper, target; @@ -488,7 +562,7 @@ describe('Tab', () => { const el = wrapper.find('#test-extra').getDOMNode().parentElement; assert(el.style.getPropertyValue('float') === 'left'); }); - it('should slide', () => { + it('should show slide buttons', done => { const boxStyle = { width: '200px' }; wrapper = mount(
@@ -498,10 +572,21 @@ describe('Tab', () => {
, { attachTo: target } ); - assert(wrapper.find('.next-tabs-btn-prev').hasClass('disabled')); - assert(wrapper.find('.next-tabs-btn-next').length === 1); + setTimeout(() => { + assert( + dom.hasClass( + wrapper + .find('.next-tabs-btn-prev') + .at(0) + .getDOMNode(), + 'disabled' + ) + ); + assert(wrapper.find('.next-tabs-btn-next').length === 1); + done(); + }, 500); }); - it('should slide', (done) => { + it('should slide', done => { const boxStyle = { width: '200px' }; wrapper = mount(
@@ -511,13 +596,72 @@ describe('Tab', () => {
, { attachTo: target } ); - const prev = wrapper.find(".next-tabs-nav").at(0).getDOMNode().getBoundingClientRect().left; - wrapper.find('.next-tabs-btn-next').simulate('click'); - setTimeout(()=>{ - const newpos = wrapper.find(".next-tabs-nav").at(0).getDOMNode().getBoundingClientRect().left; - assert(newpos>prev); + let prev, newpos; + setTimeout(() => { + prev = wrapper + .find('.next-tabs-nav') + .at(0) + .getDOMNode() + .getBoundingClientRect().left; + wrapper.find('.next-tabs-btn-next').simulate('click'); + }, 500); + setTimeout(() => { + newpos = wrapper + .find('.next-tabs-nav') + .at(0) + .getDOMNode() + .getBoundingClientRect().left; + assert(newpos > prev); done(); + }, 1000); + }); + it('should adjust scroll length', done => { + const panes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13].map( + (item, index) => ( + + content + + ) + ); + wrapper = mount( +
+ + {panes} + +
, + { + attachTo: target, + } + ); + wrapper.update(); + setTimeout(() => { + const transStr = wrapper + .find('.next-tabs-nav') + .at(0) + .getDOMNode().style.transform; + const rst = transStr.match( + /translate3d\((\-?\d+\.\d+|\-?\d+)px,.*\)/i + ); + assert(parseInt(rst[1])>0); + wrapper.find('.next-tabs-btn-prev').simulate('click'); }, 500); + setTimeout(() => { + const transStr = wrapper + .find('.next-tabs-nav') + .at(0) + .getDOMNode().style.transform; + const rst = transStr.match( + /translate3d\((\-?\d+\.\d+|\-?\d+)px,.*\)/i + ); + assert(rst[1] === '0'); + done(); + }, 1000); }); }); });