diff --git a/README.md b/README.md
index d984b10b..d55d8f94 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ React.render(
document.getElementById('t2'));
```
-## API
+## API
### Tabs
@@ -237,6 +237,41 @@ tab bar with ink indicator, in addition to tab bar props, extra props:
scrollable tab bar with ink indicator, same with tab bar/ink bar props.
+### lib/SwipeableInkTabBar (Use for Mobile)
+
+swipeable tab bar with ink indicator, same with tab bar/ink bar props, and below is the additional props.
+
+
+
+
+ name |
+ type |
+ default |
+ description |
+
+
+
+
+ pageSize |
+ number |
+ 5 |
+ show how many tabs at one page |
+
+
+ speed |
+ number |
+ 5 |
+ swipe speed, 1 to 10, more bigger more faster |
+
+
+ hammerOptions |
+ Object |
+ |
+ options for react-hammerjs |
+
+
+
+
### lib/TabContent
diff --git a/assets/index/bottom.less b/assets/index/bottom.less
index 5d4d0434..b5dd77e4 100644
--- a/assets/index/bottom.less
+++ b/assets/index/bottom.less
@@ -21,6 +21,20 @@
width: 99999px;
}
+ &-bottom &-nav-swipe {
+ position: relative;
+ left: 0;
+ .@{tabs-prefix-cls}-nav {
+ display: flex;
+ flex: 1;
+ width: 100%;
+ .@{tabs-prefix-cls}-tab {
+ margin-right: 0;
+ padding: 8px 0;
+ justify-content: center;
+ }
+ }
+ }
&-bottom &-nav-wrap {
width: 100%;
}
diff --git a/assets/index/left.less b/assets/index/left.less
index 7b82a417..c5d401f2 100644
--- a/assets/index/left.less
+++ b/assets/index/left.less
@@ -34,6 +34,20 @@
height: 99999px;
}
+ &-left &-nav-swipe {
+ position: relative;
+ top: 0;
+ .@{tabs-prefix-cls}-nav {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ height: 100%;
+ .@{tabs-prefix-cls}-tab {
+ justify-content: center;
+ }
+ }
+ }
+
&-left &-tab-prev, &-left &-tab-next {
margin-top: -2px;
height: 32px;
@@ -73,4 +87,4 @@
&-left &-tab {
padding: 16px 24px;
}
-}
\ No newline at end of file
+}
diff --git a/assets/index/right.less b/assets/index/right.less
index a2221ea3..84ba8c47 100644
--- a/assets/index/right.less
+++ b/assets/index/right.less
@@ -26,6 +26,19 @@
height: 99999px;
}
+ &-right &-nav-swipe {
+ position: relative;
+ .@{tabs-prefix-cls}-nav {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ height: 100%;
+ .@{tabs-prefix-cls}-tab {
+ justify-content: center;
+ }
+ }
+ }
+
&-right &-tab-prev, &-right &-tab-next {
margin-top: -2px;
height: 32px;
@@ -73,4 +86,4 @@
&-right &-tab {
padding: 16px 24px;
}
-}
\ No newline at end of file
+}
diff --git a/assets/index/top.less b/assets/index/top.less
index d7daac23..bba7a155 100644
--- a/assets/index/top.less
+++ b/assets/index/top.less
@@ -21,6 +21,21 @@
width: 99999px;
}
+ &-top &-nav-swipe {
+ position: relative;
+ left: 0;
+ .@{tabs-prefix-cls}-nav {
+ display: flex;
+ flex: 1;
+ width: 100%;
+ .@{tabs-prefix-cls}-tab {
+ margin-right: 0;
+ padding: 8px 0;
+ justify-content: center;
+ }
+ }
+ }
+
&-top &-nav-wrap {
width: 100%;
}
diff --git a/examples/swipeInkTabBar.html b/examples/swipeInkTabBar.html
new file mode 100644
index 00000000..b3a42524
--- /dev/null
+++ b/examples/swipeInkTabBar.html
@@ -0,0 +1 @@
+placeholder
\ No newline at end of file
diff --git a/examples/swipeInkTabBar.js b/examples/swipeInkTabBar.js
new file mode 100644
index 00000000..9cf94ee2
--- /dev/null
+++ b/examples/swipeInkTabBar.js
@@ -0,0 +1,86 @@
+/* eslint react/no-multi-comp:0, no-console:0 */
+
+import 'rc-tabs/assets/index.less';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import Tabs, { TabPane } from 'rc-tabs';
+import TabContent from '../src/SwipeableTabContent';
+import SwipeableInkTabBar from '../src/SwipeableInkTabBar';
+
+const contentStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '100px',
+ backgroundColor: '#fff',
+};
+
+
+const makeTabPane = key => (
+
+
+ {`选项${key}内容`}
+
+
+);
+
+const makeMultiTabPane = (count) => {
+ const result = [];
+ for (let i = 0; i < count; i++) {
+ result.push(makeTabPane(i));
+ }
+ return result;
+};
+
+const Component = () => (
+
+
pageSize = 5, speed = 5
+
+
+
+ }
+ renderTabContent={() => }
+ defaultActiveKey="8"
+ >
+ {makeMultiTabPane(11)}
+
+
+
pageSize = 3, speed = 10
+
+
+
+ }
+ renderTabContent={() => }
+ defaultActiveKey="2"
+ >
+ {makeMultiTabPane(7)}
+
+
+
pageSize = 3, speed = 10, tabBarPosition='bottom'
+
+
+
+ }
+ renderTabContent={() => }
+ defaultActiveKey="2"
+ >
+ {makeMultiTabPane(7)}
+
+
+
+);
+
+ReactDOM.render(, document.getElementById('__react-content'));
diff --git a/package.json b/package.json
index 5263fa94..40398440 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"start": "rc-tools run server",
"pub": "rc-tools run pub --babel-runtime",
"lint": "rc-tools run lint",
+ "watch": "rc-tools run watch",
"karma": "rc-tools run karma",
"saucelabs": "rc-tools run saucelabs",
"test": "jest",
diff --git a/src/SwipeableInkTabBar.js b/src/SwipeableInkTabBar.js
new file mode 100644
index 00000000..f7d5e5c7
--- /dev/null
+++ b/src/SwipeableInkTabBar.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import InkTabBarMixin from './InkTabBarMixin';
+import SwipeableTabBarMixin from './SwipeableTabBarMixin';
+import TabBarMixin from './TabBarMixin';
+
+const SwipeableInkTabBar = React.createClass({
+ mixins: [TabBarMixin, InkTabBarMixin, SwipeableTabBarMixin],
+
+ getSwipeableTabs() {
+ const props = this.props;
+ const children = props.panels;
+ const activeKey = props.activeKey;
+ const rst = [];
+ const prefixCls = props.prefixCls;
+
+ const tabStyle = {
+ display: 'flex',
+ flex: `0 0 ${1 / props.pageSize * 100}%`,
+ };
+
+ React.Children.forEach(children, (child) => {
+ if (!child) {
+ return;
+ }
+ const key = child.key;
+ let cls = activeKey === key ? `${prefixCls}-tab-active` : '';
+ cls += ` ${prefixCls}-tab`;
+ let events = {};
+ if (child.props.disabled) {
+ cls += ` ${prefixCls}-tab-disabled`;
+ } else {
+ events = {
+ onClick: this.onTabClick.bind(this, key),
+ };
+ }
+ const ref = {};
+ if (activeKey === key) {
+ ref.ref = 'activeTab';
+ }
+ rst.push(
+ {child.props.tab}
+
);
+ });
+
+ return rst;
+ },
+
+ render() {
+ const inkBarNode = this.getInkBarNode();
+ const tabs = this.getSwipeableTabs();
+ const scrollbarNode = this.getSwipeBarNode([inkBarNode, tabs]);
+ return this.getRootNode(scrollbarNode);
+ },
+});
+
+export default SwipeableInkTabBar;
diff --git a/src/SwipeableTabBarMixin.js b/src/SwipeableTabBarMixin.js
new file mode 100644
index 00000000..17c373c6
--- /dev/null
+++ b/src/SwipeableTabBarMixin.js
@@ -0,0 +1,162 @@
+import React from 'react';
+import classnames from 'classnames';
+import Hammer from 'react-hammerjs';
+import ReactDOM from 'react-dom';
+import {
+ isVertical,
+ getStyle,
+ setPxStyle,
+} from './utils';
+
+export default {
+ getInitialState() {
+ const { hasPrevPage, hasNextPage } = this.checkPaginationByKey(this.props.activeKey);
+ return {
+ hasPrevPage,
+ hasNextPage,
+ };
+ },
+ getDefaultProps() {
+ return {
+ hammerOptions: {},
+ pageSize: 5, // per page show how many tabs
+ speed: 5, // swipe speed, 1 to 10, more bigger more faster
+ };
+ },
+ checkPaginationByKey(activeKey) {
+ const { panels, pageSize } = this.props;
+ const index = this.getIndexByKey(activeKey);
+ const centerTabCount = Math.floor(pageSize / 2);
+ // the basic rule is to make activeTab be shown in the center of TabBar viewport
+ return {
+ hasPrevPage: index - centerTabCount > 0,
+ hasNextPage: index + centerTabCount < panels.length,
+ };
+ },
+ /**
+ * used for props.activeKey setting, not for swipe callback
+ */
+ getDeltaByKey(activeKey) {
+ const { pageSize } = this.props;
+ const index = this.getIndexByKey(activeKey);
+ const centerTabCount = Math.floor(pageSize / 2);
+ const { tabWidth } = this.cache;
+ const delta = (index - centerTabCount) * tabWidth * -1;
+ return delta;
+ },
+ getIndexByKey(activeKey) {
+ return this.props.panels.findIndex(panel => panel.key === activeKey);
+ },
+ checkPaginationByDelta(delta) {
+ const { totalAvaliableDelta } = this.cache;
+ return {
+ hasPrevPage: delta < 0,
+ hasNextPage: -delta < totalAvaliableDelta,
+ };
+ },
+ setSwipePositionByKey(activeKey) {
+ const { hasPrevPage, hasNextPage } = this.checkPaginationByKey(activeKey);
+ const { totalAvaliableDelta } = this.cache;
+ this.setState({
+ hasPrevPage,
+ hasNextPage,
+ });
+ let delta;
+ if (!hasPrevPage) {
+ // the first page
+ delta = 0;
+ } else if (!hasNextPage) {
+ // the last page
+ delta = -totalAvaliableDelta;
+ } else if (hasNextPage) {
+ // the middle page
+ delta = this.getDeltaByKey(activeKey);
+ }
+ this.setSwipePositionByDelta(delta);
+ },
+ setSwipePositionByDelta(value) {
+ const { relativeDirection } = this.cache;
+ setPxStyle(this.swipeNode, relativeDirection, value);
+ },
+ componentDidMount() {
+ const { swipe, nav } = this.refs;
+ const { tabBarPosition, pageSize, panels, activeKey } = this.props;
+ this.swipeNode = ReactDOM.findDOMNode(swipe); // dom which scroll (9999px)
+ this.realNode = ReactDOM.findDOMNode(nav); // dom which visiable in screen (viewport)
+ const _isVertical = isVertical(tabBarPosition);
+ const _viewSize = getStyle(this.realNode, _isVertical ? 'height' : 'width');
+ const _tabWidth = _viewSize / pageSize;
+ this.cache = {
+ vertical: _isVertical,
+ relativeDirection: _isVertical ? 'top' : 'left',
+ totalAvaliableDelta: _tabWidth * panels.length - _viewSize,
+ tabWidth: _tabWidth,
+ };
+ this.setSwipePositionByKey(activeKey);
+ },
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.activeKey && nextProps.activeKey !== this.props.activeKey) {
+ this.setSwipePositionByKey(nextProps.activeKey);
+ }
+ },
+ onPan(e) {
+ const { vertical, relativeDirection } = this.cache;
+ const { speed } = this.props;
+ let nowDelta = vertical ? e.deltaY : e.deltaX;
+ nowDelta = nowDelta * (speed / 10);
+ const preDelta = getStyle(this.swipeNode, relativeDirection);
+ const nextTotalDelta = nowDelta + preDelta;
+ const { hasPrevPage, hasNextPage } = this.checkPaginationByDelta(nextTotalDelta);
+ this.setState({
+ hasPrevPage,
+ hasNextPage,
+ });
+ if (hasPrevPage && hasNextPage) {
+ this.setSwipePositionByDelta(nextTotalDelta);
+ }
+ },
+ getSwipeBarNode(tabs) {
+ const { prefixCls, hammerOptions, tabBarPosition } = this.props;
+ const { hasPrevPage, hasNextPage } = this.state;
+ const navClassName = `${prefixCls}-nav`;
+ const navClasses = classnames({
+ [navClassName]: true,
+ });
+ let direction = {};
+ if (isVertical(tabBarPosition)) {
+ direction = {
+ vertical: true,
+ };
+ }
+ const events = {
+ onPan: this.onPan,
+ };
+ return (
+
+ );
+ },
+};
diff --git a/src/utils.js b/src/utils.js
index b868642e..a0248d29 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -66,3 +66,11 @@ export function getMarginStyle(index, tabBarPosition) {
[marginDirection]: `${-index * 100}%`,
};
}
+
+export function getStyle(el, property) {
+ return +getComputedStyle(el).getPropertyValue(property).replace('px', '');
+}
+
+export function setPxStyle(el, property, value) {
+ el.style[property] = `${value}px`;
+}
diff --git a/tests/__snapshots__/swipe.spec.js.snap b/tests/__snapshots__/swipe.spec.js.snap
new file mode 100644
index 00000000..a8496048
--- /dev/null
+++ b/tests/__snapshots__/swipe.spec.js.snap
@@ -0,0 +1,202 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`rc-swipeable-tabs should render Slider with correct DOM structure 1`] = `
+
+
+
+
+
+
+
+
+
+ 选项0
+
+
+ 选项1
+
+
+ 选项2
+
+
+ 选项3
+
+
+ 选项4
+
+
+ 选项5
+
+
+ 选项6
+
+
+ 选项7
+
+
+ 选项8
+
+
+ 选项9
+
+
+ 选项10
+
+
+
+
+
+
+
+
+
+`;
diff --git a/tests/swipe.spec.js b/tests/swipe.spec.js
new file mode 100644
index 00000000..4ca17b9f
--- /dev/null
+++ b/tests/swipe.spec.js
@@ -0,0 +1,88 @@
+/* eslint-disable no-undef */
+import React, { Component } from 'react';
+import { mount, render } from 'enzyme';
+import { renderToJson } from 'enzyme-to-json';
+import Tabs, { TabPane } from '../index';
+import SwipeableTabContent from '../src/SwipeableTabContent';
+import SwipeableInkTabBar from '../src/SwipeableInkTabBar';
+
+const contentStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '5rem',
+ backgroundColor: '#fff',
+};
+
+const makeTabPane = key => (
+
+
+ {`选项${key}内容`}
+
+
+);
+
+const makeMultiTabPane = (count) => {
+ const result = [];
+ for (let i = 0; i < count; i++) {
+ result.push(makeTabPane(i));
+ }
+ return result;
+};
+
+
+class NormoalTabs extends Component {
+ render() {
+ return (
+
+ this.root = root}
+ defaultActiveKey="8"
+ renderTabBar={() => this.tabBar = tabBar} />}
+ renderTabContent={() => }
+ >
+ {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}
+ >
+ {makeMultiTabPane(11)}
+
+ );
+ const targetTab = wrapper.find('.rc-tabs-tab').at(6);
+ targetTab.simulate('click');
+ expect(handleTabClick).toHaveBeenCalledWith('6');
+ expect(handleChange).toHaveBeenCalledWith('6');
+ });
+});