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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
pageSizenumber5show how many tabs at one page
speednumber5swipe speed, 1 to 10, more bigger more faster
hammerOptionsObjectoptions 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 ( +
+
+ +
+
+ {tabs} +
+
+
+
+
+ ); + }, +}; 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`] = ` +
+
+
+
+
+
+
+
+ + + + + + + + + + + +
+
+
+
+
+
+