diff --git a/README.md b/README.md
index 37e66507..d54f1023 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
# rc-tabs
+
---
React Tabs
@@ -39,39 +40,38 @@ online example: http://react-component.github.io/tabs/
### Keyboard
-* left and up: switch to previous tab
-* right and down: switch to next tab
+- left and up: switch to previous tab
+- right and down: switch to next tab
## Usage
```js
-import Tabs, { TabPane } from 'rc-tabs';
-import TabContent from 'rc-tabs/lib/TabContent';
-import ScrollableInkTabBar from 'rc-tabs/lib/ScrollableInkTabBar';
-
-var callback = function(key){
+import Tabs, { TabPane } from "rc-tabs";
+import TabContent from "rc-tabs/lib/TabContent";
+import ScrollableInkTabBar from "rc-tabs/lib/ScrollableInkTabBar";
-}
+var callback = function(key) {};
React.render(
- (
- }
- renderTabContent={()=>}
- >
- first
- second
- third
-
- ),
- document.getElementById('t2'));
+ } renderTabContent={() => }>
+
+ first
+
+
+ second
+
+
+ third
+
+ ,
+ document.getElementById("t2")
+);
```
+
### Usage of navWrapper() function
navWrapper() prop allows to wrap tabs bar in a component to provide additional features.
-Eg with react-sortablejs to make tabs sortable by DnD :
+Eg with react-sortablejs to make tabs sortable by DnD :
```js
import Sortable from 'react-sortablejs';
@@ -149,6 +149,12 @@ navWrapper={(content) => {content}}
{},
+ saveRef: () => { },
};
diff --git a/src/ScrollableTabBarNode.js b/src/ScrollableTabBarNode.js
index 429f0114..fd4d5225 100755
--- a/src/ScrollableTabBarNode.js
+++ b/src/ScrollableTabBarNode.js
@@ -122,7 +122,7 @@ export default class ScrollableTabBarNode extends React.Component {
}
setOffset(offset, checkNextPrev = true) {
- const target = Math.min(0, offset);
+ let target = Math.min(0, offset);
if (this.offset !== target) {
this.offset = target;
let navOffset = {};
@@ -141,15 +141,18 @@ export default class ScrollableTabBarNode extends React.Component {
};
}
} else if (transformSupported) {
- navOffset = {
- value: `translate3d(${target}px,0,0)`,
- };
- } else {
- navOffset = {
- name: 'left',
- value: `${target}px`,
- };
+ if (this.props.direction === 'rtl') {
+ target = -target;
}
+ navOffset = {
+ value: `translate3d(${target}px,0,0)`,
+ };
+ } else {
+ navOffset = {
+ name: 'left',
+ value: `${target}px`,
+ };
+ }
if (transformSupported) {
setTransform(navStyle, navOffset.value);
} else {
@@ -324,6 +327,7 @@ ScrollableTabBarNode.propTypes = {
children: PropTypes.node,
prevIcon: PropTypes.node,
nextIcon: PropTypes.node,
+ direction: PropTypes.node,
};
ScrollableTabBarNode.defaultProps = {
diff --git a/src/SwipeableTabBarNode.js b/src/SwipeableTabBarNode.js
index e652b550..e6669dbd 100755
--- a/src/SwipeableTabBarNode.js
+++ b/src/SwipeableTabBarNode.js
@@ -49,7 +49,17 @@ export default class SwipeableTabBarNode extends React.Component {
// calculate distance dom need transform
let _nextDelta = nowDelta + totalDelta;
- if (_nextDelta >= 0) {
+
+ if (this.isRtl()) {
+ // calculate distance from right when direction is right-to-left
+ if (_nextDelta <= 0) {
+ _nextDelta = 0;
+ } else if (_nextDelta >= totalAvaliableDelta) {
+ _nextDelta = totalAvaliableDelta;
+ }
+ }
+ // calculate distance from left when direction is left-to-right
+ else if (_nextDelta >= 0) {
_nextDelta = 0;
} else if (_nextDelta <= -totalAvaliableDelta) {
_nextDelta = -totalAvaliableDelta;
@@ -89,7 +99,12 @@ export default class SwipeableTabBarNode extends React.Component {
const index = this.getIndexByKey(activeKey);
const centerTabCount = Math.floor(pageSize / 2);
const { tabWidth } = this.cache;
- const delta = (index - centerTabCount) * tabWidth * -1;
+ let delta = (index - centerTabCount) * tabWidth;
+ // in rtl direction tabs are ordered from right to left, so delta should be positive in order to
+ // push swiped element to righ side (start of view)
+ if (!this.isRtl()) {
+ delta *= -1;
+ }
return delta;
}
@@ -117,7 +132,7 @@ export default class SwipeableTabBarNode extends React.Component {
delta = 0;
} else if (!hasNextPage) {
// the last page
- delta = -totalAvaliableDelta;
+ delta = this.isRtl() ? totalAvaliableDelta : -totalAvaliableDelta;
} else if (hasNextPage) {
// the middle page
delta = this.getDeltaByKey(activeKey);
@@ -149,7 +164,9 @@ export default class SwipeableTabBarNode extends React.Component {
hasNextPage: -delta < totalAvaliableDelta,
};
}
-
+ isRtl() {
+ return this.props.direction === 'rtl';
+ }
render() {
const { prefixCls, hammerOptions, tabBarPosition } = this.props;
const { hasPrevPage, hasNextPage } = this.state;
@@ -201,6 +218,7 @@ SwipeableTabBarNode.propTypes = {
speed: PropTypes.number,
saveRef: PropTypes.func,
getRef: PropTypes.func,
+ direction: PropTypes.string,
};
SwipeableTabBarNode.defaultProps = {
@@ -211,6 +229,6 @@ SwipeableTabBarNode.defaultProps = {
hammerOptions: {},
pageSize: 5, // per page show how many tabs
speed: 7, // swipe speed, 1 to 10, more bigger more faster
- saveRef: () => {},
- getRef: () => {},
+ saveRef: () => { },
+ getRef: () => { },
};
diff --git a/src/TabBarTabsNode.js b/src/TabBarTabsNode.js
index e7cb58e9..e66689f5 100644
--- a/src/TabBarTabsNode.js
+++ b/src/TabBarTabsNode.js
@@ -13,6 +13,7 @@ export default class TabBarTabsNode extends React.Component {
saveRef,
tabBarPosition,
renderTabBarNode,
+ direction,
} = this.props;
const rst = [];
@@ -37,8 +38,10 @@ export default class TabBarTabsNode extends React.Component {
}
const gutter = tabBarGutter && index === children.length - 1 ? 0 : tabBarGutter;
+
+ const marginProperty = direction === 'rtl' ? 'marginLeft' : 'marginRight';
const style = {
- [isVertical(tabBarPosition) ? 'marginBottom' : 'marginRight']: gutter,
+ [isVertical(tabBarPosition) ? 'marginBottom' : marginProperty]: gutter,
};
warning('tab' in child.props, 'There must be `tab` property on children of Tabs.');
@@ -50,7 +53,7 @@ export default class TabBarTabsNode extends React.Component {
{...events}
className={cls}
key={key}
- style={ style }
+ style={style}
{...ref}
>
{child.props.tab}
@@ -81,12 +84,13 @@ TabBarTabsNode.propTypes = {
saveRef: PropTypes.func,
renderTabBarNode: PropTypes.func,
tabBarPosition: PropTypes.string,
+ direction: PropTypes.string,
};
TabBarTabsNode.defaultProps = {
panels: [],
prefixCls: [],
tabBarGutter: null,
- onTabClick: () => {},
- saveRef: () => {},
+ onTabClick: () => { },
+ saveRef: () => { },
};
diff --git a/src/TabContent.js b/src/TabContent.js
index 11f5d3fa..f1a21282 100755
--- a/src/TabContent.js
+++ b/src/TabContent.js
@@ -36,6 +36,7 @@ export default class TabContent extends React.Component {
const {
prefixCls, children, activeKey, className,
tabBarPosition, animated, animatedWithMargin,
+ direction,
} = props;
let { style } = props;
const classes = classnames({
@@ -48,8 +49,8 @@ export default class TabContent extends React.Component {
const activeIndex = getActiveIndex(children, activeKey);
if (activeIndex !== -1) {
const animatedStyle = animatedWithMargin ?
- getMarginStyle(activeIndex, tabBarPosition) :
- getTransformPropValue(getTransformByIndex(activeIndex, tabBarPosition));
+ getMarginStyle(activeIndex, tabBarPosition) :
+ getTransformPropValue(getTransformByIndex(activeIndex, tabBarPosition, direction));
style = {
...style,
...animatedStyle,
@@ -82,6 +83,7 @@ TabContent.propTypes = {
tabBarPosition: PropTypes.string,
className: PropTypes.string,
destroyInactiveTabPane: PropTypes.bool,
+ direction: PropTypes.string,
};
TabContent.defaultProps = {
diff --git a/src/Tabs.js b/src/Tabs.js
index f2ffbf42..002ff65d 100755
--- a/src/Tabs.js
+++ b/src/Tabs.js
@@ -167,12 +167,14 @@ class Tabs extends React.Component {
renderTabContent,
renderTabBar,
destroyInactiveTabPane,
+ direction,
...restProps
} = props;
const cls = classnames({
[prefixCls]: 1,
[`${prefixCls}-${tabBarPosition}`]: 1,
[className]: !!className,
+ [`${prefixCls}-rtl`]: direction === 'rtl',
});
this.tabBar = renderTabBar();
@@ -186,6 +188,7 @@ class Tabs extends React.Component {
onTabClick: this.onTabClick,
panels: props.children,
activeKey: this.state.activeKey,
+ direction: this.props.direction,
});
const tabContent = React.cloneElement(renderTabContent(), {
@@ -196,6 +199,7 @@ class Tabs extends React.Component {
children: props.children,
onChange: this.setActiveKey,
key: 'tabContent',
+ direction: this.props.direction,
});
const sentinelStart = (
@@ -255,6 +259,7 @@ Tabs.propTypes = {
style: PropTypes.object,
activeKey: PropTypes.string,
defaultActiveKey: PropTypes.string,
+ direction: PropTypes.string,
};
Tabs.defaultProps = {
@@ -265,6 +270,7 @@ Tabs.defaultProps = {
tabBarPosition: 'top',
children: null,
style: {},
+ direction: 'ltr',
};
Tabs.TabPane = TabPane;
diff --git a/src/utils.js b/src/utils.js
index e5ddbac9..9b3bd550 100755
--- a/src/utils.js
+++ b/src/utils.js
@@ -57,8 +57,12 @@ export function isVertical(tabBarPosition) {
return tabBarPosition === 'left' || tabBarPosition === 'right';
}
-export function getTransformByIndex(index, tabBarPosition) {
+export function getTransformByIndex(index, tabBarPosition, direction = 'ltr') {
const translate = isVertical(tabBarPosition) ? 'translateY' : 'translateX';
+
+ if (!isVertical(tabBarPosition) && direction === 'rtl') {
+ return `${translate}(${index * 100}%) translateZ(0)`;
+ }
return `${translate}(${-index * 100}%) translateZ(0)`;
}
diff --git a/tests/__snapshots__/rtl.spec.js.snap b/tests/__snapshots__/rtl.spec.js.snap
new file mode 100644
index 00000000..406dedef
--- /dev/null
+++ b/tests/__snapshots__/rtl.spec.js.snap
@@ -0,0 +1,663 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`rc-swipeable-tabs Should render scrollable tabbar with correct DOM structure 1`] = `
+
+
+
+
+
+
+
+ tab-0-content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`rc-swipeable-tabs Should render swipeable tabbar with correct DOM structure 1`] = `
+
+
+
+
+
+
+
+ tab-0
+
+
+ tab-1
+
+
+ tab-2
+
+
+ tab-3
+
+
+ tab-4
+
+
+ tab-5
+
+
+ tab-6
+
+
+ tab-7
+
+
+ tab-8
+
+
+ tab-9
+
+
+ tab-10
+
+
+
+
+
+
+
+
+
+
+
+
+ tab-0-content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`rc-swipeable-tabs should render Slider with correct DOM structure 1`] = `
+
+
+
+
+
+
+
+
+ tab-0
+
+
+ tab-1
+
+
+ tab-2
+
+
+ tab-3
+
+
+ tab-4
+
+
+ tab-5
+
+
+ tab-6
+
+
+ tab-7
+
+
+ tab-8
+
+
+ tab-9
+
+
+ tab-10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tab-8-content
+
+
+
+
+
+
+
+
+
+`;
diff --git a/tests/rtl.spec.js b/tests/rtl.spec.js
new file mode 100644
index 00000000..8d7ab3b9
--- /dev/null
+++ b/tests/rtl.spec.js
@@ -0,0 +1,215 @@
+/* eslint-disable no-undef */
+import React, { Component } from 'react';
+import { mount, render } from 'enzyme';
+import { renderToJson } from 'enzyme-to-json';
+import Tabs, { TabPane } from '../src';
+import SwipeableTabContent from '../src/SwipeableTabContent';
+import SwipeableInkTabBar from '../src/SwipeableInkTabBar';
+import ScrollableInkTabBar from '../src/ScrollableInkTabBar';
+import InkTabBar from '../src/InkTabBar';
+import TabContent from '../src/TabContent';
+
+const contentStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '5rem',
+ backgroundColor: '#fff',
+};
+
+const makeTabPane = key => (
+
+
+ {`tab-${key}-content`}
+
+
+);
+
+const makeMultiTabPane = (count) => {
+ const result = [];
+ for (let i = 0; i < count; i++) {
+ result.push(makeTabPane(i));
+ }
+ return result;
+};
+
+class RtlTabs extends Component {
+ render() {
+ return (
+
+
+ { this.root = root; }}
+ defaultActiveKey="8"
+ renderTabBar={() => { this.tabBar = tabBar; }} />}
+ renderTabContent={() => }
+ direction="rtl"
+ >
+ {makeMultiTabPane(11)}
+
+
+ );
+ }
+}
+describe('rc-swipeable-tabs', () => {
+ it('should render Slider with correct DOM structure', () => {
+ const wrapper = render(
);
+ expect(renderToJson(wrapper)).toMatchSnapshot();
+ });
+ it('create and nav should works', () => {
+ const wrapper = render(
);
+ expect(wrapper.find('.rc-tabs').length).toBe(1);
+ expect(wrapper.find('.rc-tabs-tab').length).toBe(11);
+ });
+
+ it('default active should works', () => {
+ const wrapper = mount(
);
+ expect(wrapper.find('.rc-tabs-tab').length).toBe(11);
+ expect(wrapper.instance().root.state.activeKey).toBe('8');
+ expect(wrapper.find('.rc-tabs-tab').at(8).hasClass('rc-tabs-tab-active')).toBe(true);
+ });
+
+ it('onChange and onTabClick should works', () => {
+ const handleChange = jest.fn();
+ const handleTabClick = jest.fn();
+ const wrapper = mount(
+
}
+ renderTabContent={() => }
+ onChange={handleChange}
+ direction="rtl"
+ >
+ {makeMultiTabPane(11)}
+
+ );
+ const targetTab = wrapper.find('.rc-tabs-tab').at(6);
+ targetTab.simulate('click');
+ expect(handleTabClick).toHaveBeenCalledWith('6', expect.anything());
+ expect(handleChange).toHaveBeenCalledWith('6');
+ });
+
+ it('onChange and onTabClick should works', () => {
+ const handleChange = jest.fn();
+ const handleTabClick = jest.fn();
+ const wrapper = mount(
+
}
+ renderTabContent={() => }
+ onChange={handleChange}
+ direction="rtl"
+ >
+ {makeMultiTabPane(11)}
+
+ );
+ const targetTab = wrapper.find('.rc-tabs-tab').at(6);
+ targetTab.simulate('click');
+ expect(handleTabClick).toHaveBeenCalledWith('6', expect.anything());
+ expect(handleChange).toHaveBeenCalledWith('6');
+ });
+
+ it('Should render swipeable tabbar with correct DOM structure', () => {
+ const wrapper = render(
+
}
+ renderTabContent={() => }
+ direction="rtl"
+ >
+ {makeMultiTabPane(11)}
+
+ );
+ expect(renderToJson(wrapper)).toMatchSnapshot();
+ });
+ it('Should render scrollable tabbar with correct DOM structure', () => {
+ const wrapper = render(
+
}
+ renderTabContent={() => }
+ direction="rtl"
+ >
+ {makeMultiTabPane(11)}
+
+ );
+ expect(renderToJson(wrapper)).toMatchSnapshot();
+ });
+ it('`onPrevClick` and `onNextClick` should work', (done) => {
+ const onPrevClick = jest.fn();
+ const onNextClick = jest.fn();
+ const wrapper = mount(
+
(
+
+ )}
+ renderTabContent={() => }
+ direction="rtl"
+ >
+ first
+ second
+ third
+
+ );
+
+ // To force Tabs show prev/next button
+ const scrollableTabBarNode = wrapper.find('ScrollableTabBarNode').instance();
+ scrollableTabBarNode.offset = -1;
+ jest.spyOn(scrollableTabBarNode, 'getScrollWH').mockImplementation(() => {
+ return 200;
+ });
+ jest.spyOn(scrollableTabBarNode, 'getOffsetWH').mockImplementation((node) => {
+ if (node.className.indexOf('rc-tabs-nav-container') !== -1) {
+ return 100;
+ }
+ if (node.className.indexOf('rc-tabs-nav-wrap') !== -1) {
+ return 100;
+ }
+ return 0;
+ });
+ wrapper.update();
+
+ setTimeout(() => {
+ wrapper.find('.rc-tabs-tab-next').simulate('click');
+ expect(onNextClick).toHaveBeenCalled();
+
+ wrapper.find('.rc-tabs-tab-prev').simulate('click');
+ expect(onPrevClick).toHaveBeenCalled();
+
+ done();
+ }, 50);
+ });
+ it('activate tab on click should show inkbar', () => {
+ const children = [1, 2]
+ .map(number =>
{number});
+ const wrapper = mount(
+
}
+ renderTabContent={() => }
+ direction="rtl"
+ >
+ {children}
+
+ );
+
+ wrapper.find('TabBarTabsNode').find('.rc-tabs-tab').at(1).simulate('click', {});
+ expect(wrapper.find('InkTabBarNode').html().indexOf('display: block;') !== -1).toBe(true);
+ });
+
+ it('activate tab on click should show inkbar', () => {
+ const children = [1, 2]
+ .map(number =>
{number});
+ const wrapper = mount(
+
}
+ renderTabContent={() => }
+ direction="rtl"
+ >
+ {children}
+
+ );
+
+ wrapper.find('SwipeableTabBarNode').find('.rc-tabs-tab').at(1).simulate('click', {});
+ expect(wrapper.find('InkTabBarNode').html().indexOf('display: block;') !== -1).toBe(true);
+ });
+});
diff --git a/tests/swipe.spec.js b/tests/swipe.spec.js
index eac5b84d..de9435ee 100644
--- a/tests/swipe.spec.js
+++ b/tests/swipe.spec.js
@@ -39,7 +39,7 @@ class NormoalTabs extends Component {
ref={root => { this.root = root; }}
defaultActiveKey="8"
renderTabBar={() =>
{ this.tabBar = tabBar; }} />}
- renderTabContent={() => }
+ renderTabContent={() => }
>
{makeMultiTabPane(11)}
@@ -47,21 +47,19 @@ class NormoalTabs extends Component {
);
}
}
-
describe('rc-swipeable-tabs', () => {
it('should render Slider with correct DOM structure', () => {
- const wrapper = render();
+ const wrapper = render();
expect(renderToJson(wrapper)).toMatchSnapshot();
});
-
it('create and nav should works', () => {
- const wrapper = render();
+ const wrapper = render();
expect(wrapper.find('.rc-tabs').length).toBe(1);
expect(wrapper.find('.rc-tabs-tab').length).toBe(11);
});
it('default active should works', () => {
- const wrapper = mount();
+ const wrapper = mount();
expect(wrapper.find('.rc-tabs-tab').length).toBe(11);
expect(wrapper.instance().root.state.activeKey).toBe('8');
expect(wrapper.find('.rc-tabs-tab').at(8).hasClass('rc-tabs-tab-active')).toBe(true);