From d15d5ef5fcfff8418904e7a1706316389cb43e20 Mon Sep 17 00:00:00 2001 From: Saeed Rahimi Date: Wed, 13 Nov 2019 05:21:25 +0330 Subject: [PATCH] Implemented rtl layout support (#230) * Implemented rtl layout support * update readme * added rtl test and snapshot * fixed issues mentioned by zombieJ * fixed issues mentioned by zombieJ * fix lint errors and added rtl tests * update tests * update test * update tests * update tests * change global rtl style to prefixed class * pass direction to tabbars from parent * update snapshots * added comment on getDeltaByKey * clean up * fix utils.js issue --- README.md | 56 ++- assets/index/bottom.less | 21 + assets/index/common.less | 8 +- assets/index/top.less | 21 + examples/rtl.html | 1 + examples/rtl.js | 262 +++++++++++ src/InkTabBarNode.js | 13 +- src/ScrollableTabBarNode.js | 22 +- src/SwipeableTabBarNode.js | 30 +- src/TabBarTabsNode.js | 12 +- src/TabContent.js | 6 +- src/Tabs.js | 6 + src/utils.js | 6 +- tests/__snapshots__/rtl.spec.js.snap | 663 +++++++++++++++++++++++++++ tests/rtl.spec.js | 215 +++++++++ tests/swipe.spec.js | 10 +- 16 files changed, 1293 insertions(+), 59 deletions(-) create mode 100644 examples/rtl.html create mode 100644 examples/rtl.js create mode 100644 tests/__snapshots__/rtl.spec.js.snap create mode 100644 tests/rtl.spec.js 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}} rc-tabs prefix class name, use to custom style + + direction + String + ltr + Layout direction of tabs component, it supports RTL direction too. + @@ -306,9 +312,9 @@ scrollable tab bar, in addition to tab bar props, extra props: scrollable tab bar with ink indicator, same with tab bar and ink bar and scrollable bar props. -| name | type | default | description | -|------|------|---------|-------------| -| children | (node) => node | - | Customize tab bar node | +| name | type | default | description | +| -------- | -------------- | ------- | ---------------------- | +| children | (node) => node | - | Customize tab bar node | ### lib/SwipeableInkTabBar (Use for Mobile) diff --git a/assets/index/bottom.less b/assets/index/bottom.less index d4561d62..15c86fc8 100644 --- a/assets/index/bottom.less +++ b/assets/index/bottom.less @@ -56,12 +56,23 @@ } } + &-bottom&-rtl &-tab-next { + left: 2px; + right:auto; + } + &-bottom &-tab-prev { left: 0; &-icon:before { content: "<"; } } + + &-bottom&-rtl &-tab-prev { + right: 0; + left:auto; + } + &-bottom &-tab-prev, &-bottom &-tab-next { margin-right: -2px; @@ -77,12 +88,22 @@ left: 0; } + &-bottom&-rtl &-ink-bar { + right: 0; + left:auto; + } + &-bottom &-tab { float: left; height: 100%; margin-right: 30px; } + &-bottom&-rtl &-tab { + float: right; + margin-left: 30px; + margin-right: 0; + } &-bottom &-tabpane-inactive { height: 0; overflow: visible; diff --git a/assets/index/common.less b/assets/index/common.less index 888a8ac6..6239205c 100644 --- a/assets/index/common.less +++ b/assets/index/common.less @@ -83,7 +83,6 @@ list-style: none; display: inline-block; transform-origin: 0 0; - &-animated { transition: transform 0.5s @easing-in-out; } @@ -97,6 +96,10 @@ clear: both; } } + + &-rtl &-nav { + float: right; + } &-tab { box-sizing: border-box; @@ -159,4 +162,7 @@ &-content-no-animated &-tabpane-inactive { display: none; } + &-rtl{ + direction: rtl; + } } diff --git a/assets/index/top.less b/assets/index/top.less index ebb69226..f051d0a9 100644 --- a/assets/index/top.less +++ b/assets/index/top.less @@ -56,6 +56,11 @@ } } + &-top&-rtl &-tab-next { + left: 2px; + right:auto; + } + &-top &-tab-prev { left: 0; &-icon:before { @@ -63,6 +68,11 @@ } } + &-top&-rtl &-tab-prev { + right: 0; + left:auto; + } + &-top &-tab-prev, &-top &-tab-next { margin-right: -2px; width: 0; @@ -85,12 +95,23 @@ left: 0; } + &-top&-rtl &-ink-bar { + right: 0; + left:auto; + } + &-top &-tab { float: left; height: 100%; margin-right: 30px; } + &-top&-rtl &-tab { + float: right; + margin-left: 30px; + margin-right: 0; + } + &-top &-tabpane-inactive { height: 0; overflow: visible; diff --git a/examples/rtl.html b/examples/rtl.html new file mode 100644 index 00000000..b3a42524 --- /dev/null +++ b/examples/rtl.html @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/examples/rtl.js b/examples/rtl.js new file mode 100644 index 00000000..d0ddc718 --- /dev/null +++ b/examples/rtl.js @@ -0,0 +1,262 @@ +/* eslint react/no-multi-comp:0, no-console:0, react/prop-types: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 'rc-tabs/lib/TabContent'; +import SwipeableInkTabBar from 'rc-tabs/lib/SwipeableInkTabBar'; +import ScrollableTabBar from 'rc-tabs/lib/ScrollableTabBar'; +import InkTabBar from 'rc-tabs/lib/InkTabBar'; + +const arrowPath = 'M869 487.8L491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h' + + '-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v' + + '60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91' + + '.5c1.9 0 3.8-0.7 5.2-2L869 536.2c14.7-12.8 14.7-35.6 0-48.4z'; + +const getSvg = (path, style = {}, svgStyle = {}) => { + return ( + + + + + + ); +}; + +const next = getSvg(arrowPath, {}, { + transform: 'scaleX(-1)', +}); +const prev = getSvg(arrowPath); + +const contentStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100px', + backgroundColor: '#fff', +}; +const tabTitle = (key) => (
{`تب ${key}`}
); +const makeTabPane = key => ( + +
+ {`مطالب داخل تب ${key}`} +
+
+); + +const makeMultiTabPane = (count) => { + const result = []; + for (let i = 0; i < count; i++) { + result.push(makeTabPane(i)); + } + return result; +}; +class PanelContent extends React.Component { + constructor(props) { + super(props); + console.log(this.props.id, 'constructor'); + } + + componentWillReceiveProps(nextProps) { + console.log(nextProps.id, 'componentWillReceiveProps'); + } + + render() { + const length = Math.round(10 * (Math.random()) + 4); + const count = new Array(length);// new Array(4) skip forEach .... + for (let i = 0; i < length; i++) { + count[i] = 1; + } + const content = new Array(Math.round(100 * (Math.random())) + 4).join(` ${this.props.id}`); + const els = count.map((c, i) => { + return

{content}

; + }); + return
{els}
; + } +} + +function construct(start, num) { + const ends = []; + let index = 1; + for (let i = start; i < start + num; i++) { + ends.push( + + ); + index++; + } + return ends; +} + +class Demo extends React.Component { + state = { + tabBarPosition: 'top', + activeKey: '3', + start: 0, + useIcon: false, + }; + + onChange = (key) => { + console.log(`onChange ${key}`); + } + + onChange2 = (activeKey) => { + this.setState({ activeKey }); + } + + onTabClick = (key) => { + console.log(`onTabClick ${key}`); + } + + tick = () => { + this.setState({ + start: this.state.start + 10, + }); + } + + toggleCustomIcon = () => { + this.setState({ + useIcon: !this.state.useIcon, + }); + } + + changeTabPosition = (e) => { + this.setState({ + tabBarPosition: e.target.value, + }); + } + + scrollToActive = () => { + this.bar.scrollToActiveTab(); + } + + switchToLast = (ends) => { + if (this.state.activeKey !== ends[ends.length - 1].key) { + this.setState({ activeKey: ends[ends.length - 1].key }, this.scrollToActive); + } else { + this.scrollToActive(); + } + } + + saveBar = (bar) => { + this.bar = bar; + } + + render() { + const start = this.state.start; + const ends = construct(start, 9); + const ends2 = construct(start, 3); + const tabBarPosition = this.state.tabBarPosition; + let style; + const contentStyleSwipeable = { + height: 400, + }; + if (tabBarPosition === 'left' || tabBarPosition === 'right') { + style = { + height: 400, + }; + } else { + style = { + width: 500, + }; + } + + const cls = this.state.useIcon && 'rc-tabs-custom-icon' || undefined; + + const iconProps = this.state.useIcon ? { + nextIcon: next, + prevIcon: prev, + } : {}; + + return ( +
+

Basic Tabs With Ink Bar and tabBarGutter

+

+ tabBarPosition: + +

+
+ ( + + )} + renderTabContent={() => } + onChange={this.onChange} + direction='rtl' + > + {ends2} + +
+

Scroll Tabs

+
+ +
+ + is using icon: {this.state.useIcon && 'true' || 'false'} + } + renderTabContent={() => } + onChange={this.onChange2} + direction='rtl' + > + {ends} + +
+ +

Swipeable Tabs with inkBar

+
+ + + } + renderTabContent={() => } + defaultActiveKey="2" + > + {makeMultiTabPane(11)} + +
+ +
+ ); + } +} + +ReactDOM.render(, document.getElementById('__react-content')); diff --git a/src/InkTabBarNode.js b/src/InkTabBarNode.js index 2e200201..85553ff3 100644 --- a/src/InkTabBarNode.js +++ b/src/InkTabBarNode.js @@ -1,10 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { setTransform, isTransform3dSupported, getLeft, getTop, getActiveIndex } from './utils'; +import { setTransform, isTransform3dSupported, getLeft, getStyle, getTop, getActiveIndex } from './utils'; function componentDidUpdate(component, init) { - const { styles, panels, activeKey } = component.props; + const { styles, panels, activeKey, direction } = component.props; const rootNode = component.props.getRef('root'); const wrapNode = component.props.getRef('nav') || rootNode; const inkBarNode = component.props.getRef('inkBar'); @@ -44,7 +44,9 @@ function componentDidUpdate(component, init) { left += (tabNode.offsetWidth - width) / 2; } } - + if (direction === 'rtl') { + left = getStyle(tabNode, 'margin-left') - left; + } // use 3d gpu to optimize render if (transformSupported) { setTransform(inkBarNodeStyle, `translate3d(${left}px,0,0)`); @@ -103,7 +105,7 @@ export default class InkTabBarNode extends React.Component { inkBarAnimated ? `${className}-animated` : `${className}-no-animated` - ]: true, + ]: true, }); return (
{}, + 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 +
+
+
+