├── .editorconfig
├── .gitignore
├── .stylelintrc
├── .travis.yml
├── .vscode
└── settings.json
├── HISTORY.md
├── README.md
├── assets
├── common
│ ├── TabBar.less
│ ├── Tabs.less
│ └── index.less
└── index.less
├── examples
├── basic.html
├── basic.tsx
├── react-native
│ ├── basic.tsx
│ └── scrolltabbar.tsx
├── scroll-tab.html
├── scroll-tab.tsx
├── single-content.html
├── single-content.tsx
├── sticky.html
├── sticky.tsx
├── vertical.html
└── vertical.tsx
├── index.android.js
├── index.ios.js
├── package.json
├── src
├── DefaultTabBar.native.tsx
├── DefaultTabBar.tsx
├── Models.ts
├── PropsType.ts
├── Styles.native.tsx
├── TabPane.native.tsx
├── TabPane.tsx
├── Tabs.base.tsx
├── Tabs.native.tsx
├── Tabs.tsx
├── index.native.tsx
├── index.tsx
└── util
│ └── index.ts
├── tests
├── Tabs.spec.tsx
└── __snapshots__
│ └── Tabs.spec.tsx.snap
├── tsconfig.json
├── tslint.json
└── typings
└── models.d.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | # Unix-style newlines with a newline ending every file
5 | [*.{js,css}]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.log
3 | *.log.*
4 | .idea
5 | .ipr
6 | .iws
7 | *~
8 | ~*
9 | *.diff
10 | *.patch
11 | *.bak
12 | .DS_Store
13 | Thumbs.db
14 | .project
15 | .*proj
16 | .svn
17 | *.swp
18 | *.swo
19 | *.pyc
20 | *.pyo
21 | node_modules
22 | .cache
23 | *.css
24 | build
25 | lib
26 | es
27 | coverage
28 | *.js
29 | *.jsx
30 | *.map
31 | !tests/index.js
32 | !/index*.js
33 | ios/
34 | android/
35 | xcuserdata
36 | yarn.lock
37 | _ts2js
38 | package-lock.json
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard",
3 | "rules": {
4 | at-rule-empty-line-before: null,
5 | at-rule-name-space-after: null,
6 | at-rule-no-unknown: null,
7 | comment-empty-line-before: null,
8 | declaration-bang-space-before: null,
9 | declaration-empty-line-before: null,
10 | function-comma-newline-after: null,
11 | function-name-case: null,
12 | function-parentheses-newline-inside: null,
13 | function-max-empty-lines: null,
14 | function-whitespace-after: null,
15 | indentation: null,
16 | number-leading-zero: null,
17 | number-no-trailing-zeros: null,
18 | rule-empty-line-before: null,
19 | selector-combinator-space-after: null,
20 | selector-list-comma-newline-after: null,
21 | selector-pseudo-element-colon-notation: null,
22 | unit-no-unknown: null,
23 | value-list-max-empty-lines: null,
24 | unit-case: null,
25 | color-hex-case: null,
26 | declaration-colon-newline-after: null,
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | sudo: false
4 |
5 | notifications:
6 | email:
7 | - zhang740@qq.com
8 |
9 | node_js:
10 | - 6.9.1
11 |
12 | before_install:
13 | - |
14 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/'
15 | then
16 | echo "Only docs were updated, stopping build process."
17 | exit
18 | fi
19 |
20 | script:
21 | - |
22 | if [ "$TEST_TYPE" = test ]; then
23 | npm test
24 | else
25 | npm run $TEST_TYPE
26 | fi
27 | env:
28 | matrix:
29 | - TEST_TYPE=lint
30 | - TEST_TYPE=test
31 | - TEST_TYPE=coverage:upload
32 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "components/**/*.js*": {
4 | "when": "$(basename).tsx"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # History
2 |
3 | ---
4 |
5 | ## 1.2.29 / 2019-01-24
6 |
7 | - `fix` 修复 componentWillReceiveProps 时,page 使用 nextProps, tabs 使用 currentProps 导致逻辑逻辑判断出错
8 |
9 | ## 1.2.27 / 2018-07-11
10 |
11 | - `feat` 无障碍优化
12 |
13 | ## 1.2.26 / 2018-06-04
14 |
15 | - `feat` 修复滚动时触发 Tab 切换的问题
16 |
17 | ## 0.0.1 / 2017-08-10
18 |
19 | - TODO
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rmc-tabs
2 | ---
3 |
4 | React Mobile Tabs Component (web & react-native), inspired by [react-native-scrollable-tab-view](https://github.com/skv-headless/react-native-scrollable-tab-view)
5 |
6 | [![NPM version][npm-image]][npm-url]
7 | 
8 | [![build status][travis-image]][travis-url]
9 | [![Test coverage][coveralls-image]][coveralls-url]
10 | [![gemnasium deps][gemnasium-image]][gemnasium-url]
11 | [![npm download][download-image]][download-url]
12 |
13 | [npm-image]: http://img.shields.io/npm/v/rmc-tabs.svg?style=flat-square
14 | [npm-url]: http://npmjs.org/package/rmc-tabs
15 | [travis-image]: https://img.shields.io/travis/react-component/m-tabs.svg?style=flat-square
16 | [travis-url]: https://travis-ci.org/react-component/m-tabs
17 | [coveralls-image]: https://img.shields.io/coveralls/react-component/m-tabs.svg?style=flat-square
18 | [coveralls-url]: https://coveralls.io/r/react-component/m-tabs?branch=master
19 | [gemnasium-image]: http://img.shields.io/gemnasium/react-component/m-tabs.svg?style=flat-square
20 | [gemnasium-url]: https://gemnasium.com/react-component/m-tabs
21 | [node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square
22 | [node-url]: http://nodejs.org/download/
23 | [download-image]: https://img.shields.io/npm/dm/rmc-tabs.svg?style=flat-square
24 | [download-url]: https://npmjs.org/package/rmc-tabs
25 |
26 | ## Screenshots
27 |
28 | ## Development
29 |
30 | ```
31 | npm i
32 | npm start
33 | ```
34 |
35 | ## Example
36 |
37 | http://localhost:8000/examples/
38 |
39 | online example: http://react-component.github.io/m-tabs/
40 |
41 |
42 | ## install
43 |
44 | [](https://npmjs.org/package/rmc-tabs)
45 |
46 |
47 | # docs
48 |
49 | ## Usage
50 | ```jsx
51 | // normal
52 | content1 content2 content3 content4 content5 single content {this.state.scData} single content {JSON.stringify({ index: index + Math.random(), tab })}
tab 1 1
40 |tab 1 2
41 |tab 1 3
42 |tab 1 4
43 |tab 3 1
49 |tab 3 2
50 |{JSON.stringify(data)}
51 |tab 4 1
54 |tab 5 1
57 |single content
49 |{this.state.scData}
50 |single content
51 |single content
52 |single content
53 |single content
64 |{this.state.scData2}
65 |single content
66 |single content
67 |single content
68 |single content
91 |{this.state.scData2}
92 |single content
93 |single content
94 |single content
95 |single content
29 |{JSON.stringify({ index: index + Math.random(), tab })}
30 |single content
49 |{this.state.scData}
50 |tab 1 1
28 |tab 1 2
29 |tab 1 3
30 |tab 1 4
31 |tab 2 1
34 |tab 2 2
35 |tab 2 3
36 |tab 2 4
37 |tab 2 5
38 |tab 2 6
39 |tab 2 7
40 |tab 2 8
41 |tab 2 9
42 |tab 1 1
22 |tab 1 2
23 |tab 1 3
24 |tab 1 4
25 |tab 2 1
28 |tab 2 2
29 |tab 2 3
30 |tab 2 4
31 |tab 2 5
32 |tab 2 6
33 |tab 2 7
34 |tab 2 8
35 |tab 2 9
36 | {
15 | static defaultProps = {
16 | tabBarPosition: 'top',
17 | initialPage: 0,
18 | swipeable: true,
19 | animated: true,
20 | prerenderingSiblingsNumber: 1,
21 | tabs: [],
22 | destroyInactiveTab: false,
23 | usePaged: true,
24 | tabDirection: 'horizontal',
25 | distanceToChangeTab: .3,
26 | } as PropsType;
27 |
28 | protected instanceId: number;
29 | protected prevCurrentTab: number;
30 | protected tabCache: { [index: number]: React.ReactNode } = {};
31 |
32 | /** compatible for different between react and preact in `setState`. */
33 | private nextCurrentTab: number;
34 |
35 | constructor(props: P) {
36 | super(props);
37 |
38 | this.state = {
39 | currentTab: this.getTabIndex(props),
40 | } as any;
41 | this.nextCurrentTab = this.state.currentTab;
42 | this.instanceId = instanceId++;
43 | }
44 |
45 | getTabIndex(props: P) {
46 | const { page, initialPage, tabs } = props;
47 | const param = (page !== undefined ? page : initialPage) || 0;
48 |
49 | let index = 0;
50 | if (typeof (param as any) === 'string') {
51 | tabs.forEach((t, i) => {
52 | if (t.key === param) {
53 | index = i;
54 | }
55 | });
56 | } else {
57 | index = param as number || 0;
58 | }
59 | return index < 0 ? 0 : index;
60 | }
61 |
62 | isTabVertical = (direction = (this.props as PropsType).tabDirection) => direction === 'vertical';
63 |
64 | shouldRenderTab = (idx: number) => {
65 | const { prerenderingSiblingsNumber = 0 } = this.props as PropsType;
66 | const { currentTab = 0 } = this.state as any as StateType;
67 |
68 | return currentTab - prerenderingSiblingsNumber <= idx && idx <= currentTab + prerenderingSiblingsNumber;
69 | }
70 |
71 | componentWillReceiveProps(nextProps: P) {
72 | if (this.props.page !== nextProps.page && nextProps.page !== undefined) {
73 | this.goToTab(this.getTabIndex(nextProps), true, {}, nextProps);
74 | }
75 | }
76 |
77 | componentDidMount() {
78 | this.prevCurrentTab = this.state.currentTab;
79 | }
80 |
81 | componentDidUpdate() {
82 | this.prevCurrentTab = this.state.currentTab;
83 | }
84 |
85 | getOffsetIndex = (current: number, width: number, threshold = this.props.distanceToChangeTab || 0) => {
86 | const ratio = Math.abs(current / width);
87 | const direction = ratio > this.state.currentTab ? '<' : '>';
88 | const index = Math.floor(ratio);
89 | switch (direction) {
90 | case '<':
91 | return ratio - index > threshold ? index + 1 : index;
92 | case '>':
93 | return 1 - ratio + index > threshold ? index : index + 1;
94 | default:
95 | return Math.round(ratio);
96 | }
97 | }
98 |
99 | goToTab(index: number, force = false, newState: any = {}, props: P = this.props) {
100 | if (!force && this.nextCurrentTab === index) {
101 | return false;
102 | }
103 | this.nextCurrentTab = index;
104 | const { tabs, onChange } = props as P;
105 | if (index >= 0 && index < tabs.length) {
106 | if (!force) {
107 | onChange && onChange(tabs[index], index);
108 | if (props.page !== undefined) {
109 | return false;
110 | }
111 | }
112 |
113 | this.setState({
114 | currentTab: index,
115 | ...newState,
116 | });
117 | }
118 | return true;
119 | }
120 |
121 | tabClickGoToTab(index: number) {
122 | this.goToTab(index);
123 | }
124 |
125 | getTabBarBaseProps() {
126 | const { currentTab } = this.state;
127 |
128 | const {
129 | animated,
130 | onTabClick,
131 | tabBarActiveTextColor,
132 | tabBarBackgroundColor,
133 | tabBarInactiveTextColor,
134 | tabBarPosition,
135 | tabBarTextStyle,
136 | tabBarUnderlineStyle,
137 | tabs,
138 | } = this.props;
139 | return {
140 | activeTab: currentTab,
141 | animated: !!animated,
142 | goToTab: this.tabClickGoToTab.bind(this),
143 | onTabClick,
144 | tabBarActiveTextColor,
145 | tabBarBackgroundColor,
146 | tabBarInactiveTextColor,
147 | tabBarPosition,
148 | tabBarTextStyle,
149 | tabBarUnderlineStyle,
150 | tabs,
151 | instanceId: this.instanceId,
152 | };
153 | }
154 |
155 | renderTabBar(tabBarProps: any, DefaultTabBar: React.ComponentClass) {
156 | const { renderTabBar } = this.props as P;
157 | if (renderTabBar === false) {
158 | return null;
159 | } else if (renderTabBar) {
160 | // return React.cloneElement(this.props.renderTabBar(props), props);
161 | return renderTabBar(tabBarProps);
162 | } else {
163 | return tab 1 1 tab 1 2 tab 1 3 tab 1 4 tab 2 1 tab 2 2 tab 2 3 tab 2 4 tab 2 5 single content {JSON.stringify({ index, tab })} single content
87 | tab 1 1
88 |
92 | tab 1 2
93 |
97 | tab 1 3
98 |
102 | tab 1 4
103 |
391 | tab 2 1
392 |
396 | tab 2 2
397 |
401 | tab 2 3
402 |
406 | tab 2 4
407 |
411 | tab 2 5
412 |
527 | tab 1 1
528 |
532 | tab 1 2
533 |
537 | tab 1 3
538 |
542 | tab 1 4
543 |
688 | tab 1 1
689 |
693 | tab 1 2
694 |
698 | tab 1 3
699 |
703 | tab 1 4
704 |
864 | tab 1 1
865 |
869 | tab 1 2
870 |
874 | tab 1 3
875 |
879 | tab 1 4
880 |
961 | tab 1 1
962 |
966 | tab 1 2
967 |
971 | tab 1 3
972 |
976 | tab 1 4
977 |
1186 | tab 1 1
1187 |
1191 | tab 1 2
1192 |
1196 | tab 1 3
1197 |
1201 | tab 1 4
1202 |
1343 | single content
1344 |
1461 | single content
1462 |
1464 | {"index":0,"tab":{"title":"t1"}}
1465 |