├── .eslintrc.js
├── .gitignore
├── .prettierrc.js
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── examples
├── ScrollableTabStringDemo.js
└── ScrollableTabStringParentDemo.js
├── index.d.ts
├── package.json
└── src
├── ScrollableTabString.js
└── index.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parserOptions: {
3 | ecmaVersion: 7,
4 | ecmaFeatures: {
5 | impliedStrict: true,
6 | jsx: true,
7 | experimentalObjectRestSpread: true
8 | },
9 | sourceType: 'module'
10 | },
11 | parser: 'babel-eslint',
12 | env: {
13 | node: true,
14 | es6: true
15 | },
16 | extends: [
17 | // "@react-native-community",
18 | 'airbnb',
19 | 'plugin:react/recommended',
20 | ],
21 | plugins: [
22 | 'react',
23 | 'react-native',
24 | // "@react-native-community"
25 | ],
26 | rules: {
27 | 'max-len': ["error", { "code": 200 }],
28 | 'comma-dangle': 0,
29 | 'radix': 0,
30 | 'global-require': 1,
31 | 'no-nested-ternary': 0,
32 | 'no-return-assign': 0,
33 | 'react/require-default-props': 0,
34 | 'react/prefer-stateless-function': 0,
35 | 'function-paren-newline': 0,
36 | 'import/prefer-default-export': 0,
37 | 'react/forbid-prop-types': 0,
38 | 'react/jsx-filename-extension': 0,
39 | 'arrow-body-style': 'warn',
40 | 'no-console': 0,
41 | 'react/prop-types': 0,
42 | 'react/no-string-refs': 0,
43 | 'no-undef': 'error',
44 | 'linebreak-style': 0,
45 | 'class-methods-use-this': 0,
46 | 'no-underscore-dangle': 0,
47 | 'import/no-extraneous-dependencies': ["error", { devDependencies: true }],
48 | 'react/no-did-mount-set-state': 0,
49 | 'react/sort-comp': 0,
50 | 'camelcase': 0,
51 | // Indent with 4 spaces∂
52 | "indent": ["error", 4],
53 | // Indent JSX with 4 spaces
54 | "react/jsx-indent": ["error", 4],
55 | // Indent props with 4 spaces
56 | "react/jsx-indent-props": ["error", 4],
57 | // "import/no-unresolved": ["error", {commonjs: true, caseSensitive: true}],
58 | "no-use-before-define": [2, { "functions": false, "classes": false, "variables": false }],
59 | "react/jsx-props-no-spreading": [0, {}],
60 | "consistent-return": [0],
61 | "import/no-unresolved": [0],
62 | "no-mixed-operators": [0],
63 | "no-unused-expressions": [0],
64 | "no-useless-constructor": [0],
65 | "no-plusplus": [0]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSpacing: false,
3 | jsxBracketSameLine: true,
4 | singleQuote: true,
5 | trailingComma: 'all',
6 | "requirePragma": true,
7 | };
8 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at . All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2020 Thong Bui
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | [](https://travis-ci.org/joemccann/dillinger)
8 |
9 |
10 |
11 | A ScrollView-like component with animated horizontal tab when scrolling
12 |
13 | # Get started
14 |
15 | ## Installation
16 | Install the dependency.
17 | ```sh
18 | $ npm install react-native-scrollable-tabstring
19 | ```
20 | ```sh
21 | $ yarn add react-native-scrollable-tabstring
22 | ```
23 |
24 | ## Usage
25 |
26 | Start using the components or try it on Snack
27 | [here](https://snack.expo.io/@thongbui/rn-scrollable-tabstring).
28 |
29 | ```js
30 | import ScrollableTabString from 'react-native-scrollable-tabstring';
31 | //Standard scrollable tab
32 | yourCustomOnPressIfNeeded}
34 | dataTabs={yourTabNamesList}
35 | dataSections={yourDataSectionList}
36 | renderSection={(item) => yourCustomSectionItemRender}
37 | renderTabName={(item) => yourCustomSectionTabName}
38 | selectedTabStyle={{
39 | ...your custom styles when a Tab is scrolled to or tapped
40 | }}
41 | unselectedTabStyle={{
42 | ...your custom styles when a Tab is normal
43 | }}
44 | />
45 | ```
46 |
47 | ## Component Detail
48 | This component currently support tab list for **horizontal** side and vertical section list. Both of which are __**[Flatlist](https://facebook.github.io/react-native/docs/flatlist)**__
49 |
50 | | Property | Type | Required | Default | Description |
51 | | -------- | ---- | -------- | ------- | ----------- |
52 | | dataTabs | Array | Yes | [] | A tab list to represent |
53 | | dataSections | Array | Yes | [] | A Section list to represent |
54 | | isParent | Boolean | No | false | Switch to `true` if you want to support more sections following by a parent tab, see detail [here](https://github.com/hoangthongbui/react-native-scrollable-tabstring#scrollable-tab-with-parent-tab) |
55 | | headerTransitionWhenScroll | Boolean | No | true | Animation at tab header when section scrolling |
56 | | tabPosition | String | No | top | Tab list position arrangement, `top` and `bottom` |
57 | | renderSectionItem | Func | Yes | | Function to render Section Item |
58 | | renderTabNameItem | Func | Yes | | Function to render Tab Item, equal to [renderItem](https://reactnative.dev/docs/flatlist#renderitem) in `Flatlist` |
59 | | customTabNamesProps | Object | No | | [Flatlist](https://reactnative.dev/docs/flatlist) Props, avoid props like `renderItem`, `data`, `ref`, `onScroll` as may result some issues |
60 | | customSectionProps | Object | No | | [ScrollView](https://reactnative.dev/docs/scrollview) Props |
61 | | onPressTab | Func | No | | Custom function when pressing on a tab |
62 | | onScrollSection | Func | No | | Custom function when section scrolling |
63 | | selectedTabStyle | Object | No | `{ borderBottomColor: 'black', borderBottomWidth: 1, }` | Custom style when a tab is selected |
64 | | unselectedTabStyle | Object | No | `{ backgroundColor: 'white', alignItems: 'center', justifyContent: 'center', }` | Custom style when a tab is unselected |
65 |
66 | ## Example
67 | ### Scrollable tab
68 |
69 | Display a basic scrollable tab
70 |
71 | ```text
72 | Note: Length of `dataTabs` and `dataSections` must equal, otherwise may result in incorrect scrolling order
73 | ```
74 |
75 |
76 |
77 | ```js
78 | const tabNames = [{
79 | title: 'Tab 1',
80 | },
81 | ...................
82 | {
83 | title: 'Tab 6',
84 | }];
85 |
86 | const dataSections = [
87 | {
88 | name: 'Section 1',
89 | data: [..........]
90 | },
91 | ...............
92 | {
93 | name: 'Section 6',
94 | data: [..........]
95 |
96 | },
97 | ];
98 |
99 | render () {
100 | return (
101 | (
105 |
106 | {item.name}
107 | {
108 | item.data.map((i) => (
109 | {i.name}
110 | ))
111 | }
112 |
113 | )}
114 | renderTabName={(item) => (
115 |
116 |
117 | {item.title}
118 |
119 |
120 | )}
121 | selectedTabStyle={{
122 | borderColor: Colors.brown_grey,
123 | borderRadius: 10,
124 | borderWidth: 1,
125 | margin: 10
126 | }}
127 | unselectedTabStyle={{
128 | backgroundColor: Colors.white,
129 | alignItems: 'center',
130 | justifyContent: 'center',
131 | }}
132 | />
133 | )
134 | };
135 | ```
136 |
137 | ### Scrollable tab with parent tab
138 |
139 | Scrollable tab with parent tab and children section follow
140 |
141 | Use this if you want to support more sections following on a tab.
142 |
143 | Add `index` key to parent tab and sections (start from 0). For example Tab 1 has 2 children section follow. They are Section 1 and Section 2 -> index of Tab 1, Section 1 and 2 are 0
144 |
145 | ```text
146 | Note: Index of both parent and children section must equivalent and those sections must be adjacent.
147 | ```
148 |
149 |
150 |
151 | ```js
152 | const tabNames = [{
153 | title: 'Tab 1',
154 | index: 0
155 | }
156 | .....
157 | , {
158 | title: 'Tab 6',
159 | index: 5
160 | }];
161 |
162 | const dataSections = [
163 | {
164 | name: 'Section 1',
165 | index: 0,
166 | data: [..........]
167 | },
168 | {
169 | name: 'Section 2',
170 | index: 0,
171 | data: [..........]
172 | },
173 | {
174 | name: 'Section 3',
175 | index: 1,
176 | data: [..........]
177 | },
178 | {
179 | name: 'Section 4',
180 | index: 1,
181 | data: [..........]
182 | },
183 | {
184 | name: 'Section 5',
185 | index: 2,
186 | data: [..........]
187 | },
188 | {
189 | name: 'Section 6',
190 | index: 2,
191 | data: [..........]
192 | },
193 | {
194 | name: 'Section 7',
195 | index: 3,
196 | data: [..........]
197 | },
198 | {
199 | name: 'Section 8',
200 | index: 4,
201 | data: [..........]
202 | },
203 | ];
204 |
205 | const ScrollableTabStringDemo = () => (
206 | (
211 |
212 | {item.name}
213 | {
214 | item.data.map((i) => (
215 | {i.name}
216 | ))
217 | }
218 |
219 | )}
220 | renderTabName={(item) => (
221 |
222 |
223 | {item.title}
224 |
225 |
226 | )}
227 | selectedTabStyle={{
228 | borderColor: Colors.brown_grey,
229 | borderRadius: 10,
230 | borderWidth: 1,
231 | margin: 10
232 | }}
233 | unselectedTabStyle={{
234 | backgroundColor: Colors.white,
235 | alignItems: 'center',
236 | justifyContent: 'center',
237 | }}
238 | />
239 | );
240 | ```
241 |
242 | ## Limitation
243 | This component allows you to customize some Flatlist props on Tab Name and ScrollView props as well. However, you should avoid some of properties like `onScroll`, `renderItem`, `CellRendererComponent`, `horizontal` as may result some issues.
244 |
245 | Furthermore, this component doesn't support on load more yet due to heavily calculated, still working on this :p
246 |
247 | ## Contributing
248 | All contributions are welcome! Please open an issue if you get stuck and bugs, or a PR if you have any feature idea, improvements and bug fixing. I'm very appreciate !
249 |
250 | ## License
251 | MIT
252 |
253 |
254 |
255 |
256 |
--------------------------------------------------------------------------------
/examples/ScrollableTabStringDemo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TouchableOpacity, View, Text } from 'react-native';
3 | import ScrollableTabString from '../src';
4 |
5 | const tabNames = [{
6 | title: 'Tab 1',
7 | }, {
8 | title: 'Tab 2',
9 | }, {
10 | title: 'Tab 3',
11 | }, {
12 | title: 'Tab 4',
13 | }, {
14 | title: 'Tab 5',
15 | }, {
16 | title: 'Tab 6',
17 | }];
18 |
19 | const dataMain = [
20 | {
21 | name: 'Section 1',
22 | data: [
23 | {
24 | id: '0',
25 | name: 'Section 1 - 1',
26 | },
27 | {
28 | id: '1',
29 | name: 'Section 1 - 2',
30 | },
31 | {
32 | id: '2',
33 | name: 'Section 1 - 3',
34 | },
35 | {
36 | id: '3',
37 | name: 'Section 1 - 4',
38 | },
39 | {
40 | id: '4',
41 | name: 'Section 1 - 5',
42 | },
43 | ]
44 | },
45 | {
46 | name: 'Section 2',
47 | data: [
48 | {
49 | id: '5',
50 | name: 'Section 2 - 1',
51 | },
52 | {
53 | id: '6',
54 | name: 'Section 2 - 2',
55 | },
56 | {
57 | id: '7',
58 | name: 'Section 2 - 3',
59 | },
60 | {
61 | id: '8',
62 | name: 'Section 2 - 4',
63 | },
64 | {
65 | id: '9',
66 | name: 'Section 2 - 5',
67 | },
68 | ]
69 | },
70 | {
71 | name: 'Section 3',
72 | data: [
73 | {
74 | id: '10',
75 | name: 'Section 3 - 1',
76 | },
77 | {
78 | id: '11',
79 | name: 'Section 3 - 2',
80 | },
81 | ]
82 | },
83 | {
84 | name: 'Section 4',
85 | data: [
86 | {
87 | id: '15',
88 | name: 'Section 4 - 1',
89 | },
90 | {
91 | id: '16',
92 | name: 'Section 4 - 2',
93 | },
94 | {
95 | id: '17',
96 | name: 'Section 4 - 3',
97 | },
98 | ]
99 | },
100 | {
101 | name: 'Section 5',
102 | data: [
103 | {
104 | id: '18',
105 | name: 'Section 5 - 1',
106 | },
107 | {
108 | id: '19',
109 | name: 'Section 5 - 2',
110 | },
111 | {
112 | id: '20',
113 | name: 'Section 5 - 3',
114 | },
115 | {
116 | id: '21',
117 | name: 'Section 5 - 2',
118 | },
119 | {
120 | id: '22',
121 | name: 'Section 5 - 3',
122 | },
123 | ]
124 | },
125 | {
126 | name: 'Section 6',
127 | data: [
128 | {
129 | id: '18',
130 | name: 'Section 5 - 1',
131 | },
132 | {
133 | id: '19',
134 | name: 'Section 5 - 2',
135 | },
136 | {
137 | id: '20',
138 | name: 'Section 5 - 3',
139 | },
140 | {
141 | id: '21',
142 | name: 'Section 5 - 2',
143 | },
144 | {
145 | id: '22',
146 | name: 'Section 5 - 3',
147 | },
148 | ]
149 | },
150 | ];
151 |
152 | const ScrollableTabStringDemo = () => (
153 | (
157 |
158 | {item.name}
159 | {
160 | item.data.map((i) => (
161 | {i.name}
162 | ))
163 | }
164 |
165 | )}
166 | renderTabName={(item) => (
167 |
168 |
169 | {item.title}
170 |
171 |
172 | )}
173 | selectedTabStyle={{
174 | borderColor: 'black',
175 | borderRadius: 10,
176 | borderWidth: 1,
177 | margin: 10
178 | }}
179 | unselectedTabStyle={{
180 | backgroundColor: 'black',
181 | alignItems: 'center',
182 | justifyContent: 'center',
183 | }}
184 | />
185 | );
186 |
187 | export default ScrollableTabStringDemo;
188 |
--------------------------------------------------------------------------------
/examples/ScrollableTabStringParentDemo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, TouchableOpacity, View } from 'react-native';
3 | import { Colors } from '../..';
4 | import ScrollableTabString from './ScrollableTabString';
5 |
6 | const tabNames = [{
7 | title: 'Tab 1',
8 | index: 0
9 | }, {
10 | title: 'Tab 2',
11 | index: 1
12 | }, {
13 | title: 'Tab 3',
14 | index: 2
15 | }, {
16 | title: 'Tab 4',
17 | index: 3
18 | }, {
19 | title: 'Tab 5',
20 | index: 4
21 | }, {
22 | title: 'Tab 6',
23 | index: 5
24 | }];
25 |
26 | const dataMain = [
27 | {
28 | name: 'Section 1 - Tab 1',
29 | index: 0,
30 | data: [
31 | {
32 | id: '0',
33 | name: 'Section 1 - 1',
34 | },
35 | {
36 | id: '1',
37 | name: 'Section 1 - 2',
38 | },
39 | {
40 | id: '2',
41 | name: 'Section 1 - 3',
42 | },
43 | {
44 | id: '3',
45 | name: 'Section 1 - 4',
46 | },
47 | {
48 | id: '4',
49 | name: 'Section 1 - 5',
50 | },
51 | ]
52 | },
53 | {
54 | name: 'Section 2 - Tab 1',
55 | index: 0,
56 | data: [
57 | {
58 | id: '5',
59 | name: 'Section 2 - 1',
60 | },
61 | {
62 | id: '6',
63 | name: 'Section 2 - 2',
64 | },
65 | {
66 | id: '7',
67 | name: 'Section 2 - 3',
68 | },
69 | {
70 | id: '8',
71 | name: 'Section 2 - 4',
72 | },
73 | {
74 | id: '9',
75 | name: 'Section 2 - 5',
76 | },
77 | ]
78 | },
79 | {
80 | name: 'Section 3 - Tab 2',
81 | index: 1,
82 | data: [
83 | {
84 | id: '10',
85 | name: 'Section 3 - 1',
86 | },
87 | {
88 | id: '11',
89 | name: 'Section 3 - 2',
90 | },
91 | ]
92 | },
93 | {
94 | name: 'Section 4 - Tab 2',
95 | index: 1,
96 | data: [
97 | {
98 | id: '15',
99 | name: 'Section 4 - 1',
100 | },
101 | {
102 | id: '16',
103 | name: 'Section 4 - 2',
104 | },
105 | {
106 | id: '17',
107 | name: 'Section 4 - 3',
108 | },
109 | ]
110 | },
111 | {
112 | name: 'Section 5 - Tab 3',
113 | index: 2,
114 | data: [
115 | {
116 | id: '18',
117 | name: 'Section 5 - 1',
118 | },
119 | {
120 | id: '19',
121 | name: 'Section 5 - 2',
122 | },
123 | {
124 | id: '20',
125 | name: 'Section 5 - 3',
126 | },
127 | {
128 | id: '21',
129 | name: 'Section 5 - 2',
130 | },
131 | {
132 | id: '22',
133 | name: 'Section 5 - 3',
134 | },
135 | ]
136 | },
137 | {
138 | name: 'Section 6 - Tab 3',
139 | index: 2,
140 | data: [
141 | {
142 | id: '18',
143 | name: 'Section 5 - 1',
144 | },
145 | {
146 | id: '19',
147 | name: 'Section 5 - 2',
148 | },
149 | {
150 | id: '20',
151 | name: 'Section 5 - 3',
152 | },
153 | {
154 | id: '21',
155 | name: 'Section 5 - 2',
156 | },
157 | {
158 | id: '22',
159 | name: 'Section 5 - 3',
160 | },
161 | ]
162 | },
163 | {
164 | name: 'Section 7 - Tab 4',
165 | index: 3,
166 | data: [
167 | {
168 | id: '18',
169 | name: 'Section 5 - 1',
170 | },
171 | {
172 | id: '19',
173 | name: 'Section 5 - 2',
174 | },
175 | ]
176 | },
177 | {
178 | name: 'Section 8 - Tab 4',
179 | index: 3,
180 | data: [
181 | {
182 | id: '18',
183 | name: 'Section 5 - 1',
184 | },
185 | {
186 | id: '19',
187 | name: 'Section 5 - 2',
188 | },
189 | {
190 | id: '20',
191 | name: 'Section 5 - 3',
192 | },
193 | {
194 | id: '21',
195 | name: 'Section 5 - 2',
196 | },
197 | {
198 | id: '22',
199 | name: 'Section 5 - 3',
200 | },
201 | ]
202 | },
203 | {
204 | name: 'Section 9 - Tab 5',
205 | index: 4,
206 | data: [
207 | {
208 | id: '18',
209 | name: 'Text',
210 | },
211 | {
212 | id: '19',
213 | name: 'Text',
214 | },
215 | {
216 | id: '20',
217 | name: 'Text',
218 | },
219 | {
220 | id: '21',
221 | name: 'Text',
222 | },
223 | {
224 | id: '22',
225 | name: 'Text',
226 | },
227 | ]
228 | },
229 | {
230 | name: 'Section 10 - Tab 5',
231 | index: 4,
232 | data: [
233 | {
234 | id: '18',
235 | name: 'Text',
236 | },
237 | {
238 | id: '19',
239 | name: 'Text',
240 | },
241 | ]
242 | },
243 | {
244 | name: 'Section 11 - Tab 6',
245 | index: 5,
246 | data: [
247 | {
248 | id: '18',
249 | name: 'Text',
250 | },
251 | {
252 | id: '19',
253 | name: 'Text',
254 | },
255 | {
256 | id: '20',
257 | name: 'Text',
258 | },
259 | {
260 | id: '21',
261 | name: 'Text',
262 | },
263 | {
264 | id: '22',
265 | name: 'Text',
266 | },
267 | ]
268 | },
269 | {
270 | name: 'Section 12 - Tab 6',
271 | index: 5,
272 | data: [
273 | {
274 | id: '18',
275 | name: 'Text',
276 | },
277 | {
278 | id: '19',
279 | name: 'Text',
280 | },
281 | ]
282 | },
283 | ];
284 |
285 | const ScrollableTabStringDemo = () => (
286 | (
291 |
292 | {item.name}
293 | {
294 | item.data.map((i) => (
295 | {i.name}
296 | ))
297 | }
298 |
299 | )}
300 | renderTabName={(item) => (
301 |
302 |
303 | {item.title}
304 |
305 |
306 | )}
307 | selectedTabStyle={{
308 | borderColor: Colors.brown_grey,
309 | borderRadius: 10,
310 | borderWidth: 1,
311 | margin: 10
312 | }}
313 | unselectedTabStyle={{
314 | contentContainerStyle: {
315 | backgroundColor: Colors.white,
316 | alignItems: 'center',
317 | justifyContent: 'center',
318 | }
319 | }}
320 | />
321 | );
322 |
323 | export default ScrollableTabStringDemo;
324 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { StyleProp, ViewStyle, FlatListProps, StyleProp, FlatListProps } from 'react-native';
4 |
5 | interface ScrollableTabStringProps {
6 | dataTabs: Array,
7 | dataSections: Array,
8 | isParent: boolean,
9 | headerTransitionWhenScroll: boolean,
10 | tabPosition?: 'top' | 'bottom',
11 | customTabNamesProps: FlatListProps,
12 | customSectionProps: FlatListProps,
13 | selectedTabStyle: StyleProp,
14 | unselectedTabStyle: StyleProp,
15 | onPressTab: Function,
16 | onScrollSection: Function,
17 | renderSectionItem: Function,
18 | renderTabNameItem: Function,
19 | }
20 |
21 | export class ScrollableTabString extends React.Component { }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-scrollable-tabstring",
3 | "version": "0.0.8",
4 | "private": false,
5 | "description": "a scrollable list with animated horizontal tab when scrolling",
6 | "main": "src/index.js",
7 | "directories": {
8 | "example": "examples"
9 | },
10 | "scripts": {
11 | "test": "echo \"Error: no test specified\" && exit 1"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/hoangthongbui/react-native-scrollable-tabstring.git"
16 | },
17 | "keywords": [
18 | "react",
19 | "native",
20 | "tab",
21 | "scroll",
22 | "string",
23 | "navigator",
24 | "tabstring",
25 | "stringtab",
26 | "scrollable",
27 | "horizontal",
28 | "animated",
29 | "component",
30 | "scrollview",
31 | "ios",
32 | "android"
33 | ],
34 | "author": "Thong Bui",
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/hoangthongbui/react-native-scrollable-tabstring/issues"
38 | },
39 | "homepage": "https://github.com/hoangthongbui/react-native-scrollable-tabstring/blob/master/README.md",
40 | "dependencies": {
41 | "prop-types": "^15.7.2",
42 | "eslint": "7.10.0",
43 | "prettier": "2.1.2"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/ScrollableTabString.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import {
3 | Animated,
4 | Platform,
5 | View
6 | } from 'react-native';
7 |
8 | import PropTypes from 'prop-types';
9 |
10 | const binarySearch = (arr, element) => {
11 | let right = arr.length - 1;
12 | let left = 0;
13 | let mid;
14 | while (left <= right) {
15 | mid = Math.floor((left + right) / 2);
16 | if (arr[mid].y <= element) {
17 | left = mid + 1;
18 | } else {
19 | right = mid - 1;
20 | }
21 | }
22 | return [left, right];
23 | };
24 |
25 | const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }) => {
26 | const paddingToBottom = 20;
27 | return layoutMeasurement.height + contentOffset.y
28 | >= contentSize.height - paddingToBottom;
29 | };
30 |
31 | const listViews = [];
32 |
33 | class ScrollableTabString extends Component {
34 |
35 | static TAB_POSITION_TOP = 'top'
36 | static TAB_POSITION_BOTTOM = 'bottom'
37 |
38 | constructor(props) {
39 | super(props);
40 | this.state = {
41 | selectedScrollIndex: 0,
42 | isPressToScroll: false,
43 | };
44 | this.heightTabNames = 0;
45 |
46 | this.goToIndex = this.goToIndex.bind(this);
47 | this.dataTabNameChildren = this.dataTabNameChildren.bind(this);
48 | this.dataSectionsChildren = this.dataSectionsChildren.bind(this);
49 | this.onScroll = this.onScroll.bind(this);
50 | }
51 |
52 | componentDidMount() {
53 | const { dataSections, dataTabs, isParent, tabPosition } = this.props;
54 |
55 | if (dataSections.length !== dataTabs.length && !isParent) {
56 | console.warn('The \'dataSections\' and \'dataTabs\''
57 | + ' length are not equal. This will cause some issues, especially when the section list is scrolling.'
58 | + ' Consider number of items of those lists to be equal, or add \'isParent\''
59 | + ' param if you are supporting parent tab - children sections');
60 | }
61 |
62 | if (tabPosition && (tabPosition !== ScrollableTabString.TAB_POSITION_BOTTOM) && (tabPosition !== ScrollableTabString.TAB_POSITION_TOP)) {
63 | console.warn('The tabPosition only accept \'top\' or \'bottom\' only !')
64 | }
65 | }
66 |
67 | componentDidUpdate(prevProps) {
68 | const { dataSections } = this.props;
69 |
70 | if (dataSections.length > prevProps.dataSections.length) {
71 | console.warn('Are you loading more items on the dataSections ? This component does not support on load more yet !');
72 | }
73 | }
74 |
75 | goToIndex(item) {
76 | const { onPressTab } = this.props;
77 |
78 | this.setState({ isPressToScroll: true });
79 |
80 | const findMinYAxis = Math.min(...listViews.filter((i) => i.item.index === item.index).map((ii) => ii.y));
81 | const res = findMinYAxis && listViews.find((i) => i.y === findMinYAxis);
82 |
83 | this.tabScrollMainRef?.scrollTo({ animated: true, y: res?.y || 0 });
84 | this.setState({
85 | selectedScrollIndex: res?.item?.index || 0
86 | });
87 |
88 | onPressTab && onPressTab(item);
89 | }
90 |
91 | // map tab item
92 | dataTabNameChildren({ item, index }) {
93 | const { renderTabName, selectedTabStyle, unselectedTabStyle } = this.props;
94 | const { heightTabNames } = this;
95 | const { selectedScrollIndex } = this.state;
96 |
97 | return React.Children.map(
98 | React.Children.toArray(renderTabName(item, index)),
99 | (children) => React.cloneElement(children, {
100 | style: { ...(index === selectedScrollIndex) ? selectedTabStyle : unselectedTabStyle },
101 | onPress: () => this.goToIndex(item),
102 | onLayout: (e) => {
103 | if (heightTabNames === 0) {
104 | this.heightTabNames = e.nativeEvent.layout.height;
105 | }
106 | }
107 | })
108 | );
109 | }
110 |
111 | // map section item
112 | dataSectionsChildren(item, index) {
113 | const { renderSection, dataSections } = this.props;
114 |
115 | return React.Children.map(
116 | React.Children.toArray(renderSection(item, index)),
117 | (children) => React.cloneElement(children, {
118 | onLayout: (e) => {
119 | listViews.push({
120 | item: { ...item },
121 | y: e.nativeEvent.layout.y,
122 | });
123 | if (listViews.length >= dataSections.length) {
124 | listViews.sort((a, b) => a.y - b.y);
125 | }
126 | }
127 | })
128 | );
129 | }
130 |
131 | onScroll(e) {
132 | const { onScrollSection, dataTabs, headerTransitionWhenScroll } = this.props;
133 | const { selectedScrollIndex, isPressToScroll } = this.state;
134 |
135 | onScrollSection && onScrollSection(e);
136 |
137 | if (!isPressToScroll && headerTransitionWhenScroll) {
138 | try {
139 | if (e.nativeEvent.contentOffset.y === 0) {
140 | this.tabNamesRef?.scrollToOffset({
141 | offset: 0,
142 | animated: Platform.OS === 'ios',
143 | viewPosition: 0.5,
144 | });
145 |
146 | this.setState({
147 | selectedScrollIndex: 0,
148 | });
149 | } else if (isCloseToBottom(e.nativeEvent)) {
150 | const lastIndex = dataTabs.length - 1;
151 |
152 | this.tabNamesRef?.scrollToIndex({
153 | animated: Platform.OS === 'ios',
154 | index: lastIndex,
155 | viewPosition: 0.5,
156 | });
157 |
158 | this.setState({
159 | selectedScrollIndex: lastIndex
160 | });
161 | } else {
162 | const res = binarySearch(listViews, e.nativeEvent.contentOffset.y);
163 |
164 | const indexToScrollTo = res.includes(-1)
165 | ? listViews[Math.max(...res)]?.item?.index
166 | : Math.max(
167 | listViews[res[0]]?.item?.index,
168 | listViews[res[1]]?.item?.index
169 | );
170 |
171 | if (
172 | indexToScrollTo
173 | && indexToScrollTo !== -1
174 | && indexToScrollTo !== selectedScrollIndex) {
175 | this.tabNamesRef?.scrollToIndex({
176 | animated: Platform.OS === 'ios',
177 | index: indexToScrollTo,
178 | viewPosition: 0.5,
179 | });
180 |
181 | this.setState({
182 | selectedScrollIndex: indexToScrollTo
183 | });
184 | }
185 | }
186 | } catch (err) {
187 | console.warn('err: ', err);
188 | }
189 | }
190 | }
191 |
192 | render() {
193 | const {
194 | dataTabs,
195 | dataSections,
196 | isParent,
197 | tabPosition,
198 | customSectionProps,
199 | customTabNamesProps,
200 | } = this.props;
201 | return (
202 | <>
203 | { this.tabScrollMainRef = ref; }}
207 | bounces={false}
208 | onScrollBeginDrag={() => this.setState({ isPressToScroll: false })}
209 | nestedScrollEnabled
210 | showsVerticalScrollIndicator={false}
211 | scrollEnabled
212 | onScroll={this.onScroll}
213 | stickyHeaderIndices={tabPosition === 'top' ? [0] : null}
214 | >
215 | {
216 | (tabPosition === 'top' ? (
217 |
218 | ({ ...i, index }))}
220 | nestedScrollEnabled
221 | keyboardShouldPersistTaps="always"
222 | {...customTabNamesProps}
223 | ref={(ref) => { this.tabNamesRef = ref; }}
224 | keyExtractor={(item) => item.index}
225 | contentContainerStyle={{
226 | backgroundColor: 'white',
227 | }}
228 | showsHorizontalScrollIndicator={false}
229 | bounces={false}
230 | horizontal
231 | renderItem={this.dataTabNameChildren}
232 | />
233 |
234 | ) : null)
235 | }
236 |
237 | { (isParent ? dataSections : dataSections.map((i, index) => ({ ...i, index }))).map(this.dataSectionsChildren) }
238 |
239 |
240 | {
241 | (tabPosition === 'bottom' ? (
242 |
243 | ({ ...i, index }))}
248 | {...customTabNamesProps}
249 | contentContainerStyle={{
250 | backgroundColor: 'white',
251 | }}
252 | ref={(ref) => { this.tabNamesRef = ref; }}
253 | keyExtractor={(item) => item.index.toString()}
254 | showsHorizontalScrollIndicator={false}
255 | bounces={false}
256 | horizontal
257 | renderItem={this.dataTabNameChildren}
258 | />
259 |
260 |
261 | ) : null)
262 | }
263 | >
264 | );
265 | }
266 | }
267 |
268 | ScrollableTabString.propTypes = {
269 | dataTabs: PropTypes.array,
270 | dataSections: PropTypes.array,
271 | isParent: PropTypes.bool,
272 | headerTransitionWhenScroll: PropTypes.bool,
273 | tabPosition: PropTypes.oneOf(['top', 'bottom']),
274 | renderSection: PropTypes.func,
275 | renderTabName: PropTypes.func,
276 | customTabNamesProps: PropTypes.object,
277 | customSectionProps: PropTypes.object,
278 | onPressTab: PropTypes.func,
279 | onScrollSection: PropTypes.func,
280 | selectedTabStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
281 | unselectedTabStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
282 | };
283 |
284 | ScrollableTabString.defaultProps = {
285 | dataSections: [],
286 | dataTabs: [],
287 | isParent: false,
288 | headerTransitionWhenScroll: true,
289 | tabPosition: 'top',
290 | selectedTabStyle: {
291 | borderBottomColor: 'black',
292 | borderBottomWidth: 1,
293 | },
294 | unselectedTabStyle: {
295 | backgroundColor: 'transparent',
296 | alignItems: 'center',
297 | justifyContent: 'center',
298 | }
299 | };
300 |
301 | export default ScrollableTabString;
302 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import ScrollableTabString from './ScrollableTabString';
2 |
3 | export default ScrollableTabString;
4 |
--------------------------------------------------------------------------------