├── .all-contributorsrc
├── .babelrc
├── .circleci
└── config.yml
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .gitbook.yml
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.md
│ ├── config.yml
│ └── feature_request.md
├── demo-full.gif
├── demo-icon.gif
├── demo-shifting.gif
└── logo.png
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierrc.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── __tests__
├── BackgroundDecorator.test.js
├── BackgroundRippleAnimation.test.js
├── Badge.test.js
├── BottomNavigation.test.js
├── FullTab.test.js
├── IconTab.test.js
├── PressFeedback.test.js
├── PressRippleAnimation.test.js
├── ShiftingTab.test.js
└── TabList.test.js
├── docs
├── README.md
├── Usage.md
└── api
│ ├── Badge.md
│ ├── BottomNavigation.md
│ ├── FullTab.md
│ ├── IconTab.md
│ ├── README.md
│ └── ShiftingTab.md
├── examples
├── Playground
│ ├── .babelrc
│ ├── .flowconfig
│ ├── .gitignore
│ ├── .watchmanconfig
│ ├── App.js
│ ├── README.md
│ ├── app.json
│ ├── cut.png
│ ├── package-lock.json
│ ├── package.json
│ └── rn-cli.config.js
└── with-react-navigation.js
├── flow-typed
└── react-native-material-bottom-navigation.js
├── index.d.ts
├── index.js
├── lib
├── BackgroundDecorator.js
├── BackgroundRippleAnimation.js
├── Badge.js
├── BottomNavigation.js
├── FullTab.js
├── IconTab.js
├── PressFeedback.js
├── PressRippleAnimation.js
├── ShiftingTab.js
├── TabList.js
└── utils
│ ├── device.js
│ └── easing.js
├── package.json
├── scripts
├── docgen
└── utils
│ ├── doc-template.hbs
│ └── docgen-markdown.js
└── setupTests.js
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "react-native-material-bottom-navigation",
3 | "projectOwner": "timomeh",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": [
7 | "README.md"
8 | ],
9 | "imageSize": 100,
10 | "commit": false,
11 | "contributors": [
12 | {
13 | "login": "timomeh",
14 | "name": "Timo Mämecke",
15 | "avatar_url": "https://avatars3.githubusercontent.com/u/4227520?v=4",
16 | "profile": "https://twitter.com/timomeh",
17 | "contributions": [
18 | "bug",
19 | "code",
20 | "design",
21 | "doc",
22 | "example",
23 | "infra",
24 | "ideas",
25 | "review"
26 | ]
27 | },
28 | {
29 | "login": "ShayanJavadi",
30 | "name": "Shayan Javadi",
31 | "avatar_url": "https://avatars3.githubusercontent.com/u/11575429?v=4",
32 | "profile": "https://www.shayanjavadi.com/",
33 | "contributions": [
34 | "code"
35 | ]
36 | },
37 | {
38 | "login": "davidmpr",
39 | "name": "David",
40 | "avatar_url": "https://avatars2.githubusercontent.com/u/14214500?v=4",
41 | "profile": "https://github.com/davidmpr",
42 | "contributions": [
43 | "code"
44 | ]
45 | },
46 | {
47 | "login": "jayserdny",
48 | "name": "Jayser Mendez",
49 | "avatar_url": "https://avatars2.githubusercontent.com/u/19354816?v=4",
50 | "profile": "http://steemia.io",
51 | "contributions": [
52 | "doc"
53 | ]
54 | },
55 | {
56 | "login": "PeterKottas",
57 | "name": "Peter Kottas",
58 | "avatar_url": "https://avatars0.githubusercontent.com/u/10601911?v=4",
59 | "profile": "https://www.facebook.com/tipsforholiday",
60 | "contributions": [
61 | "code"
62 | ]
63 | },
64 | {
65 | "login": "matt-oakes",
66 | "name": "Matt Oakes",
67 | "avatar_url": "https://avatars0.githubusercontent.com/u/97068?v=4",
68 | "profile": "https://mattoakes.net",
69 | "contributions": [
70 | "code"
71 | ]
72 | },
73 | {
74 | "login": "keeleycarrigan",
75 | "name": "Keeley Carrigan",
76 | "avatar_url": "https://avatars0.githubusercontent.com/u/1533112?v=4",
77 | "profile": "http://www.keeleycarrigan.com",
78 | "contributions": [
79 | "code"
80 | ]
81 | },
82 | {
83 | "login": "wildseansy",
84 | "name": "Sean Holbert",
85 | "avatar_url": "https://avatars1.githubusercontent.com/u/177857?v=4",
86 | "profile": "http://www.twitter.com/wildseansy",
87 | "contributions": [
88 | "code"
89 | ]
90 | },
91 | {
92 | "login": "aparolin",
93 | "name": "Alessandro Parolin",
94 | "avatar_url": "https://avatars0.githubusercontent.com/u/9802139?v=4",
95 | "profile": "https://github.com/aparolin",
96 | "contributions": [
97 | "doc"
98 | ]
99 | },
100 | {
101 | "login": "prashantham",
102 | "name": "Prashanth Acharya M",
103 | "avatar_url": "https://avatars0.githubusercontent.com/u/1837764?v=4",
104 | "profile": "https://github.com/prashantham",
105 | "contributions": [
106 | "doc"
107 | ]
108 | },
109 | {
110 | "login": "lemming",
111 | "name": "Alexey Tcherevatov",
112 | "avatar_url": "https://avatars1.githubusercontent.com/u/64609?v=4",
113 | "profile": "https://github.com/lemming",
114 | "contributions": [
115 | "code",
116 | "bug"
117 | ]
118 | },
119 | {
120 | "login": "trevor-atlas",
121 | "name": "Trevor Atlas",
122 | "avatar_url": "https://avatars2.githubusercontent.com/u/5009188?v=4",
123 | "profile": "https://blog.trevoratlas.com/",
124 | "contributions": [
125 | "bug"
126 | ]
127 | }
128 | ]
129 | }
130 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-native"]
3 | }
4 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | defaults: &defaults
4 | docker:
5 | - image: circleci/node:9.4
6 | working_directory: ~/project
7 | environment:
8 | CC_TEST_REPORTER_ID: 8c2520b4bcc18770be1ec6683f28035713007dfe505f7418b2f41a8e053ab6ea
9 |
10 | jobs:
11 | install_dependencies:
12 | <<: *defaults
13 |
14 | steps:
15 | - checkout
16 | - restore_cache:
17 | keys:
18 | - v1-react-native-mbn-{{ checksum "package.json" }}
19 | - v1-react-native-mbn-
20 | - run:
21 | name: Install dependencies
22 | command: npm install
23 | - run:
24 | name: Install Code Climate Reporter
25 | command: |
26 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
27 | chmod +x ./cc-test-reporter
28 | - save_cache:
29 | key: v1-react-native-mbn-{{ checksum "package.json" }}
30 | paths: node_modules
31 | - persist_to_workspace:
32 | root: .
33 | paths: .
34 |
35 | lint:
36 | <<: *defaults
37 |
38 | steps:
39 | - attach_workspace:
40 | at: ~/project
41 | - run: npm run lint
42 |
43 | unit_test:
44 | <<: *defaults
45 |
46 | steps:
47 | - attach_workspace:
48 | at: ~/project
49 | - run:
50 | name: Unit Tests with Coverage
51 | command: |
52 | ./cc-test-reporter before-build
53 | npm run test:ci
54 | ./cc-test-reporter after-build --exit-code $?
55 | environment:
56 | JEST_JUNIT_OUTPUT: 'coverage/junit/js-test-results.xml'
57 | - store_test_results:
58 | path: coverage/junit
59 | - store_artifacts:
60 | path: coverage
61 | destination: coverage
62 |
63 | workflows:
64 | version: 2
65 | ci_test:
66 | jobs:
67 | - install_dependencies
68 | - lint:
69 | requires:
70 | - install_dependencies
71 | - unit_test:
72 | requires:
73 | - install_dependencies
74 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.md]
11 | trim_trailing_whitespace = false
12 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | package*.json
2 | examples/**/node_modules
3 | coverage/
4 | /scripts/utils/
5 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["plugin:prettier/recommended"],
4 | "plugins": ["react", "react-native"],
5 | "env": {
6 | "node": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitbook.yml:
--------------------------------------------------------------------------------
1 | structure:
2 | readme: README.md
3 | summary: docs/README.md
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Expected behavior**
14 | A clear and concise description of what you expected to happen.
15 |
16 | **Screenshots**
17 | If applicable, add screenshots to help explain your problem.
18 |
19 | **To Reproduce**
20 | Provide an [Expo Snack](https://snack.expo.io) or a code snippet to reproduce the issue.
21 |
22 | **Environment**
23 | - React Native Version:
24 | - Expo Version (if applicable):
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is.
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/demo-full.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timomeh/react-native-material-bottom-navigation/ae325eb468cad26e8089e81f2b0ddfcc50858a33/.github/demo-full.gif
--------------------------------------------------------------------------------
/.github/demo-icon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timomeh/react-native-material-bottom-navigation/ae325eb468cad26e8089e81f2b0ddfcc50858a33/.github/demo-icon.gif
--------------------------------------------------------------------------------
/.github/demo-shifting.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timomeh/react-native-material-bottom-navigation/ae325eb468cad26e8089e81f2b0ddfcc50858a33/.github/demo-shifting.gif
--------------------------------------------------------------------------------
/.github/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timomeh/react-native-material-bottom-navigation/ae325eb468cad26e8089e81f2b0ddfcc50858a33/.github/logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | npm-debug.log*
3 | coverage
4 | _book
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 9
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/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, gender identity and expression, level of experience,
9 | education, socio-economic status, nationality, personal appearance, race,
10 | 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 maemecketimo@gmail.com. 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 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | You can contribute by:
4 |
5 | - Reporting Bugs.
6 | - Answering Issues and helping people.
7 | - Making the Documentation better.
8 | - Fixing issues or adding new features through Pull Requests.
9 |
10 | ## Local setup
11 |
12 | 1. Fork and clone it
13 | 2. `npm install && npm link`
14 | 3. `cd examples/Playground && npm install && npm link react-native-material-bottom-navigation`
15 |
16 | ## Run tests & lint files
17 |
18 | 1. `npm test`
19 | 2. `npm run lint`
20 |
21 | ## Run the example app
22 |
23 | 1. `cd examples/Playground && npm start`
24 | 2. Download the Expo App (iOS or Android)
25 | 3. Follow the instructions in your terminal
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017-present, Timo Mämecke
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | A beautiful, customizable and easy-to-use
Material Design Bottom Navigation for react-native.
17 |
18 |
19 |
20 |
21 | - **Pure JavaScript.** No native dependencies. No linking. No obstacles.
22 | - **Looks beautiful.** Stunning and fluid animations. You won't believe it's not a native view.
23 | - **Customize it.** You can adjust nearly everything to make it fit perfectly to your app.
24 | - **Easy to use.** Uses established React patterns for both simple and advanced usage.
25 | - **Pluggable.** Includes customizable Tabs and Badges. Not enough? Create and use your own!
26 |
27 |
28 |
29 | ## Installation
30 |
31 | ```sh
32 | npm install react-native-material-bottom-navigation
33 | ```
34 |
35 | ## Table of Contents
36 |
37 | - [Installation](#installation)
38 | - [Demo](#demo)
39 | - [Usage](#usage)
40 | - [Documentation](#documentation)
41 | - [Notes](#notes)
42 | - [Contribute](#contribute)
43 | - [Contributors](#contributors)
44 | - [License](#license)
45 |
46 | ## Demo
47 |
48 |
49 | 
50 | Shifting Tab
51 |
52 |
53 |
54 | 
55 | Full Tab
56 |
57 |
58 |
59 | 
60 | Icon Tab
61 |
62 |
63 | ## Usage
64 |
65 | This library uses ["render props"](https://reactjs.org/docs/render-props.html) as established pattern for component composition. The example below illustrates the basic usage of the Bottom Navigation. All available Props are listed in the [Documentation](#documentation).
66 |
67 | Dive into the example below, check out [the example app](/examples/Playground) and take a look at the [Usage Documentation](https://timomeh.gitbook.io/material-bottom-navigation/usage).
68 |
69 | ```js
70 | import BottomNavigation, {
71 | FullTab
72 | } from 'react-native-material-bottom-navigation'
73 |
74 | export default class App extends React.Component {
75 | tabs = [
76 | {
77 | key: 'games',
78 | icon: 'gamepad-variant',
79 | label: 'Games',
80 | barColor: '#388E3C',
81 | pressColor: 'rgba(255, 255, 255, 0.16)'
82 | },
83 | {
84 | key: 'movies-tv',
85 | icon: 'movie',
86 | label: 'Movies & TV',
87 | barColor: '#B71C1C',
88 | pressColor: 'rgba(255, 255, 255, 0.16)'
89 | },
90 | {
91 | key: 'music',
92 | icon: 'music-note',
93 | label: 'Music',
94 | barColor: '#E64A19',
95 | pressColor: 'rgba(255, 255, 255, 0.16)'
96 | }
97 | ]
98 |
99 | state = {
100 | activeTab: 'games'
101 | }
102 |
103 | renderIcon = icon => ({ isActive }) => (
104 |
105 | )
106 |
107 | renderTab = ({ tab, isActive }) => (
108 |
114 | )
115 |
116 | render() {
117 | return (
118 |
119 |
120 | {/* Your screen contents depending on current tab. */}
121 |
122 | this.setState({ activeTab: newTab.key })}
125 | renderTab={this.renderTab}
126 | tabs={this.tabs}
127 | />
128 |
129 | )
130 | }
131 | }
132 | ```
133 |
134 | **Note:** Out-of-the-box support for React Navigation (called `NavigationComponent` in earlier releases) was removed with v1. Check [this example](/examples/with-react-navigation.js) for a custom React Navigation integration. [Read more...](#react-navigation-support)
135 |
136 | ## Documentation
137 |
138 | - [Usage](/docs/Usage.md)
139 | - [API Reference](/docs/api)
140 | - [``](/docs/api/Badge.md)
141 | - [``](/docs/api/BottomNavigation.md)
142 | - [``](/docs/api/FullTab.md)
143 | - [``](/docs/api/IconTab.md)
144 | - [``](/docs/api/ShiftingTab.md)
145 |
146 | ## Notes
147 |
148 | ### React Navigation Support
149 |
150 | **Check [this example](/examples/with-react-navigation.js) for a custom React Navigation integration.**
151 |
152 | In contrary to earlier releases, this library does not support React Navigation _out of the box_. React Navigation now ships with its own Material Bottom Navigation: [`createMaterialBottomTabNavigator`](https://reactnavigation.org/docs/en/material-bottom-tab-navigator.html).
153 |
154 | You can still implement react-native-material-bottom-navigation manually by using React Navigation's [Custom Navigators](https://reactnavigation.org/docs/en/custom-navigators.html#api-for-building-custom-navigators). Check out [this example](/examples/with-react-navigation.js).
155 |
156 | ### Updated Material Design Specs
157 |
158 | Google updated the Material Guidelines on Google I/O 2018 with new specifications, including a slightly changed Bottom Navigation and a new "App Bar Bottom" with a FAB in a centered cutout. react-native-material-bottom-navigation uses the _older_ specs.
159 |
160 | ## Contribute
161 |
162 | Contributions are always welcome. Read more in the [Contribution Guides](CONTRIBUTING.md).
163 |
164 | Please note that this project is released with a Contributor [Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
165 |
166 | ## Contributors
167 |
168 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
169 |
170 |
171 |
172 | | [
Timo Mämecke](https://twitter.com/timomeh)
[🐛](https://github.com/timomeh/react-native-material-bottom-navigation/issues?q=author%3Atimomeh "Bug reports") [💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=timomeh "Code") [🎨](#design-timomeh "Design") [📖](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=timomeh "Documentation") [💡](#example-timomeh "Examples") [🚇](#infra-timomeh "Infrastructure (Hosting, Build-Tools, etc)") [🤔](#ideas-timomeh "Ideas, Planning, & Feedback") [👀](#review-timomeh "Reviewed Pull Requests") | [
Shayan Javadi](https://www.shayanjavadi.com/)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=ShayanJavadi "Code") | [
David](https://github.com/davidmpr)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=davidmpr "Code") | [
Jayser Mendez](http://steemia.io)
[📖](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=jayserdny "Documentation") | [
Peter Kottas](https://www.facebook.com/tipsforholiday)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=PeterKottas "Code") | [
Matt Oakes](https://mattoakes.net)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=matt-oakes "Code") | [
Keeley Carrigan](http://www.keeleycarrigan.com)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=keeleycarrigan "Code") |
173 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
174 | | [
Sean Holbert](http://www.twitter.com/wildseansy)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=wildseansy "Code") | [
Alessandro Parolin](https://github.com/aparolin)
[📖](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=aparolin "Documentation") | [
Prashanth Acharya M](https://github.com/prashantham)
[📖](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=prashantham "Documentation") | [
Alexey Tcherevatov](https://github.com/lemming)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=lemming "Code") [🐛](https://github.com/timomeh/react-native-material-bottom-navigation/issues?q=author%3Alemming "Bug reports") | [
Trevor Atlas](https://blog.trevoratlas.com/)
[🐛](https://github.com/timomeh/react-native-material-bottom-navigation/issues?q=author%3Atrevor-atlas "Bug reports") |
175 |
176 |
177 |
178 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
179 |
180 | ## License
181 |
182 | [MIT](LICENSE.md), © 2017 - present Timo Mämecke
183 |
--------------------------------------------------------------------------------
/__tests__/BackgroundDecorator.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View } from 'react-native'
3 | import { shallow } from 'enzyme'
4 |
5 | import BackgroundDecorator from '../lib/BackgroundDecorator'
6 | import BackgroundRippleAnimation from '../lib/BackgroundRippleAnimation'
7 |
8 | describe('BackgroundDecorator', () => {
9 | const consoleError = console.error
10 | let mountedDecorator
11 | let props
12 | const bgDecorator = () => {
13 | if (!mountedDecorator) {
14 | mountedDecorator = shallow()
15 | }
16 | return mountedDecorator
17 | }
18 |
19 | beforeEach(() => {
20 | mountedDecorator = null
21 | props = {
22 | children: jest.fn()
23 | }
24 |
25 | // prevent output of warnings because of native RN Components
26 | console.error = () => {}
27 | })
28 |
29 | afterEach(() => {
30 | console.error = consoleError
31 | })
32 |
33 | it('initially has no decorators', () => {
34 | expect(bgDecorator().state().decorators).toEqual([])
35 | })
36 |
37 | it('handles new decorator', () => {
38 | const decoratorData = { x: 0, y: 0, color: 'red' }
39 | bgDecorator()
40 | .instance()
41 | .addDecorator(decoratorData)
42 |
43 | expect(bgDecorator().state().decorators[0]).toHaveProperty('key')
44 | expect(bgDecorator().state().decorators[0]).toHaveProperty('x', 0)
45 | expect(bgDecorator().state().decorators[0]).toHaveProperty('y', 0)
46 | expect(bgDecorator().state().decorators[0]).toHaveProperty('color', 'red')
47 | })
48 |
49 | it('renders all current decorators', () => {
50 | // Add some decorators
51 | const decoratorData = { x: 0, y: 0, color: 'red' }
52 | bgDecorator()
53 | .instance()
54 | .addDecorator(decoratorData)
55 | bgDecorator()
56 | .instance()
57 | .addDecorator(decoratorData)
58 | bgDecorator()
59 | .instance()
60 | .addDecorator(decoratorData)
61 |
62 | bgDecorator().update()
63 |
64 | expect(bgDecorator().find(BackgroundRippleAnimation)).toHaveLength(3)
65 | })
66 |
67 | it('handles background color change', () => {
68 | bgDecorator()
69 | .instance()
70 | .setBackgroundColor('red')
71 |
72 | bgDecorator().update()
73 |
74 | expect(
75 | bgDecorator()
76 | .find(View)
77 | .first()
78 | .props().style
79 | ).toContainEqual({ backgroundColor: 'red' })
80 | })
81 | })
82 |
--------------------------------------------------------------------------------
/__tests__/BackgroundRippleAnimation.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Animated } from 'react-native'
3 | import { shallow } from 'enzyme'
4 |
5 | import BackgroundRippleAnimation from '../lib/BackgroundRippleAnimation'
6 |
7 | jest.useFakeTimers()
8 | jest.mock('Animated', () => {
9 | return {
10 | View: () => ,
11 | Value: jest.fn(num => ({
12 | interpolate: jest.fn(() => num)
13 | })),
14 | timing: jest.fn((value, options) => {
15 | return {
16 | start(cb) {
17 | setTimeout(cb, options.duration)
18 | }
19 | }
20 | })
21 | }
22 | })
23 |
24 | describe('BackgroundRippleAnimation', () => {
25 | const consoleError = console.error
26 | let mountedRipple
27 | let props
28 | const animationFn = jest.spyOn(Animated, 'timing')
29 | const ripple = () => {
30 | if (!mountedRipple) {
31 | mountedRipple = shallow()
32 | }
33 | return mountedRipple
34 | }
35 |
36 | beforeEach(() => {
37 | animationFn.mockClear()
38 | mountedRipple = null
39 | props = {
40 | containerWidth: 200,
41 | containerHeight: 50,
42 | x: 10,
43 | y: 10,
44 | onAnimationEnd: jest.fn(),
45 | color: 'purple'
46 | }
47 |
48 | // prevent output of warnings because of native RN Components
49 | console.error = () => {}
50 | })
51 |
52 | afterEach(() => {
53 | console.error = consoleError
54 | jest.clearAllTimers()
55 | })
56 |
57 | it('initially calculates radius', () => {
58 | expect(ripple().instance().radius).toBeCloseTo(194, 0)
59 | })
60 |
61 | it('initially runs through animation', () => {
62 | ripple()
63 | expect(animationFn).toHaveBeenCalled()
64 | expect(props.onAnimationEnd).not.toHaveBeenCalled()
65 |
66 | jest.advanceTimersByTime(400)
67 |
68 | expect(props.onAnimationEnd).toHaveBeenCalled()
69 | })
70 |
71 | it('renders an animated view', () => {
72 | expect(ripple().find(Animated.View)).toHaveLength(1)
73 | expect(
74 | ripple()
75 | .find(Animated.View)
76 | .props().style
77 | ).toHaveProperty('backgroundColor', 'purple')
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/__tests__/Badge.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View, Text } from 'react-native'
3 | import { shallow } from 'enzyme'
4 |
5 | import Badge from '../lib/Badge'
6 |
7 | const MockContent = () =>
8 |
9 | describe('Badge', () => {
10 | const consoleError = console.error
11 | let mountedBadge
12 | let props
13 | const badge = () => {
14 | if (!mountedBadge) {
15 | mountedBadge = shallow()
16 | }
17 | return mountedBadge
18 | }
19 |
20 | beforeEach(() => {
21 | mountedBadge = null
22 | })
23 |
24 | it('sets styles from props', () => {
25 | props = { style: { margin: 10 } }
26 | expect(
27 | badge()
28 | .find(View)
29 | .first()
30 | .props().style
31 | ).toContainEqual({ margin: 10 })
32 | })
33 |
34 | it('can use number as children', () => {
35 | props = { children: 1 }
36 | expect(
37 | badge()
38 | .find(Text)
39 | .dive()
40 | .text()
41 | ).toBe('1')
42 | })
43 |
44 | it('can use text as children', () => {
45 | props = { children: 'A' }
46 | expect(
47 | badge()
48 | .find(Text)
49 | .dive()
50 | .text()
51 | ).toBe('A')
52 | })
53 |
54 | it('can use component as children', () => {
55 | props = { children: }
56 | expect(badge().find(MockContent)).toHaveLength(1)
57 | })
58 |
59 | it('sets style of text from props', () => {
60 | props = { children: 'A', textStyle: { color: 'red' } }
61 | expect(
62 | badge()
63 | .find(Text)
64 | .props().style
65 | ).toContainEqual({ color: 'red' })
66 | })
67 | })
68 |
--------------------------------------------------------------------------------
/__tests__/BottomNavigation.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View } from 'react-native'
3 | import { mount } from 'enzyme'
4 |
5 | import BackgroundDecorator from '../lib/BackgroundDecorator'
6 | import PressFeedback from '../lib/PressFeedback'
7 | import TabList from '../lib/TabList'
8 | import BottomNavigation from '../lib/BottomNavigation'
9 |
10 | const MockTab = () =>
11 |
12 | describe('BottomNavigation', () => {
13 | const consoleError = console.error
14 | let mountedBN
15 | let props
16 | const bottomNavigation = () => {
17 | if (!mountedBN) {
18 | mountedBN = mount()
19 | }
20 | return mountedBN
21 | }
22 |
23 | beforeEach(() => {
24 | mountedBN = null
25 | props = {
26 | style: { position: 'absolute' },
27 | tabs: [{ key: 'up' }, { key: 'town' }, { key: 'funk' }],
28 | onTabPress: jest.fn(),
29 | renderTab: jest.fn(() => )
30 | }
31 |
32 | // prevent output of warnings because of native RN Components
33 | console.error = () => {}
34 | })
35 |
36 | afterEach(() => {
37 | console.error = consoleError
38 | })
39 |
40 | it('renders BackgroundDecorator', () => {
41 | expect(bottomNavigation().find(BackgroundDecorator)).toHaveLength(1)
42 | })
43 |
44 | it('renders PressFeedback', () => {
45 | expect(bottomNavigation().find(PressFeedback)).toHaveLength(1)
46 | })
47 |
48 | it('renders TabList', () => {
49 | expect(bottomNavigation().find(TabList)).toHaveLength(1)
50 | })
51 |
52 | it('applies styles from props', () => {
53 | expect(
54 | bottomNavigation()
55 | .find(View)
56 | .first()
57 | .props().style
58 | ).toContain(props.style)
59 | })
60 |
61 | it('passes correct props to TabList', () => {
62 | const listProps = bottomNavigation()
63 | .find(TabList)
64 | .props()
65 | const bg = bottomNavigation()
66 | .find(BackgroundDecorator)
67 | .instance()
68 | const press = bottomNavigation()
69 | .find(PressFeedback)
70 | .instance()
71 |
72 | // passed from root
73 | expect(listProps).toHaveProperty('tabs', props.tabs)
74 | expect(listProps).toHaveProperty('onTabPress', props.onTabPress)
75 | expect(listProps).toHaveProperty('renderTab', props.renderTab)
76 |
77 | // passed from effect components
78 | expect(listProps).toHaveProperty(
79 | 'setBackgroundColor',
80 | bg.setBackgroundColor
81 | )
82 | expect(listProps).toHaveProperty('addDecorator', bg.addDecorator)
83 | expect(listProps).toHaveProperty('addFeedbackIn', press.addFeedbackIn)
84 | expect(listProps).toHaveProperty(
85 | 'enqueueFeedbackOut',
86 | press.enqueueFeedbackOut
87 | )
88 | })
89 | })
90 |
--------------------------------------------------------------------------------
/__tests__/FullTab.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View, Text } from 'react-native'
3 | import { shallow } from 'enzyme'
4 |
5 | import FullTab from '../lib/FullTab'
6 |
7 | const MockIcon = () =>
8 | const MockBadge = () =>
9 |
10 | describe('FullTab', () => {
11 | const consoleError = console.error
12 | let mountedTab
13 | let props
14 | const tab = () => {
15 | if (!mountedTab) {
16 | mountedTab = shallow()
17 | }
18 | return mountedTab
19 | }
20 |
21 | beforeEach(() => {
22 | mountedTab = null
23 | })
24 |
25 | it('renders an icon component', () => {
26 | props = {
27 | isActive: false,
28 | renderIcon: jest.fn(() => ),
29 | label: 'Hit it!'
30 | }
31 |
32 | expect(tab().find(MockIcon)).toHaveLength(1)
33 | expect(props.renderIcon).toHaveBeenCalledWith({ isActive: false })
34 | })
35 |
36 | it('renders a label', () => {
37 | props = {
38 | isActive: false,
39 | renderIcon: jest.fn(() => ),
40 | label: 'Hit it!'
41 | }
42 |
43 | expect(
44 | tab()
45 | .find(Text)
46 | .dive()
47 | .text()
48 | ).toBe('Hit it!')
49 | })
50 |
51 | it('renders a badge component', () => {
52 | props = {
53 | isActive: false,
54 | showBadge: true,
55 | label: 'Hit it!',
56 | renderIcon: jest.fn(() => ),
57 | renderBadge: jest.fn(() => )
58 | }
59 |
60 | expect(tab().find(MockBadge)).toHaveLength(1)
61 | expect(props.renderBadge).toHaveBeenCalledWith({ isActive: false })
62 | })
63 |
64 | it('calls animation functions', () => {
65 | props = {
66 | isActive: false,
67 | label: 'Hit it!',
68 | renderIcon: jest.fn(() => ),
69 | iconAnimation: jest.fn(),
70 | labelAnimation: jest.fn(),
71 | badgeAnimation: jest.fn()
72 | }
73 |
74 | tab()
75 | expect(props.iconAnimation).toHaveBeenCalled()
76 | expect(props.labelAnimation).toHaveBeenCalled()
77 | expect(props.badgeAnimation).toHaveBeenCalled()
78 | })
79 |
80 | it('handles inactive to active', () => {
81 | props = {
82 | isActive: false,
83 | renderIcon: jest.fn(() => ),
84 | label: 'Hit it!'
85 | }
86 | const spy = jest.spyOn(tab().instance(), 'animateTo')
87 |
88 | tab().setProps({ isActive: true })
89 | expect(spy).toHaveBeenCalledWith(1)
90 | })
91 |
92 | it('handles active to inactive', () => {
93 | props = {
94 | isActive: true,
95 | label: 'Hit it!',
96 | renderIcon: jest.fn(() => )
97 | }
98 | const spy = jest.spyOn(tab().instance(), 'animateTo')
99 |
100 | tab().setProps({ isActive: false })
101 | expect(spy).toHaveBeenCalledWith(0)
102 | })
103 |
104 | it('passes label props', () => {
105 | props = {
106 | isActive: true,
107 | renderIcon: jest.fn(() => ),
108 | label: 'Hit it!',
109 | labelProps: {
110 | color: 'red'
111 | }
112 | }
113 |
114 | expect(
115 | tab()
116 | .find(Text)
117 | .first()
118 | .props()
119 | ).toHaveProperty('color', 'red')
120 | })
121 |
122 | it('passes responder props', () => {
123 | props = {
124 | isActive: true,
125 | renderIcon: jest.fn(() => ),
126 | label: 'Hit it!',
127 | onResponderMove: () => {}
128 | }
129 |
130 | expect(
131 | tab()
132 | .find(View)
133 | .first()
134 | .props()
135 | ).toHaveProperty('onResponderMove', props.onResponderMove)
136 | })
137 | })
138 |
--------------------------------------------------------------------------------
/__tests__/IconTab.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View } from 'react-native'
3 | import { shallow } from 'enzyme'
4 |
5 | import IconTab from '../lib/IconTab'
6 |
7 | const MockIcon = () =>
8 | const MockBadge = () =>
9 |
10 | describe('IconTab', () => {
11 | const consoleError = console.error
12 | let mountedTab
13 | let props
14 | const tab = () => {
15 | if (!mountedTab) {
16 | mountedTab = shallow()
17 | }
18 | return mountedTab
19 | }
20 |
21 | beforeEach(() => {
22 | mountedTab = null
23 | })
24 |
25 | it('renders an icon component', () => {
26 | props = {
27 | isActive: false,
28 | renderIcon: jest.fn(() => )
29 | }
30 |
31 | expect(tab().find(MockIcon)).toHaveLength(1)
32 | expect(props.renderIcon).toHaveBeenCalledWith({ isActive: false })
33 | })
34 |
35 | it('renders a badge component', () => {
36 | props = {
37 | isActive: false,
38 | showBadge: true,
39 | renderIcon: jest.fn(() => ),
40 | renderBadge: jest.fn(() => )
41 | }
42 |
43 | expect(tab().find(MockBadge)).toHaveLength(1)
44 | expect(props.renderBadge).toHaveBeenCalledWith({ isActive: false })
45 | })
46 |
47 | it('calls animation functions', () => {
48 | props = {
49 | isActive: false,
50 | renderIcon: jest.fn(() => ),
51 | iconAnimation: jest.fn(),
52 | badgeAnimation: jest.fn()
53 | }
54 |
55 | tab()
56 | expect(props.iconAnimation).toHaveBeenCalled()
57 | expect(props.badgeAnimation).toHaveBeenCalled()
58 | })
59 |
60 | it('handles inactive to active', () => {
61 | props = {
62 | isActive: false,
63 | renderIcon: jest.fn(() => )
64 | }
65 | const spy = jest.spyOn(tab().instance(), 'animateTo')
66 |
67 | tab().setProps({ isActive: true })
68 | expect(spy).toHaveBeenCalledWith(1)
69 | })
70 |
71 | it('handles active to inactive', () => {
72 | props = {
73 | isActive: true,
74 | renderIcon: jest.fn(() => )
75 | }
76 | const spy = jest.spyOn(tab().instance(), 'animateTo')
77 |
78 | tab().setProps({ isActive: false })
79 | expect(spy).toHaveBeenCalledWith(0)
80 | })
81 |
82 | it('passes responder props', () => {
83 | props = {
84 | isActive: true,
85 | renderIcon: jest.fn(() => ),
86 | onResponderMove: () => {}
87 | }
88 |
89 | expect(
90 | tab()
91 | .find(View)
92 | .first()
93 | .props()
94 | ).toHaveProperty('onResponderMove', props.onResponderMove)
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/__tests__/PressFeedback.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View } from 'react-native'
3 | import { shallow } from 'enzyme'
4 |
5 | import PressFeedback from '../lib/PressFeedback'
6 | import PressRippleAnimation from '../lib/PressRippleAnimation'
7 |
8 | describe('PressFeedback', () => {
9 | const consoleError = console.error
10 | let mountedFeedback
11 | let props
12 | const pressFeedback = () => {
13 | if (!mountedFeedback) {
14 | mountedFeedback = shallow()
15 | }
16 | return mountedFeedback
17 | }
18 |
19 | beforeEach(() => {
20 | mountedFeedback = null
21 | props = {
22 | children: jest.fn()
23 | }
24 |
25 | // prevent output of warnings because of native RN Components
26 | console.error = () => {}
27 | })
28 |
29 | afterEach(() => {
30 | console.error = consoleError
31 | })
32 |
33 | it('initially has no presses', () => {
34 | expect(pressFeedback().state().presses).toEqual([])
35 | })
36 |
37 | it('handles new presses', () => {
38 | const pressData = { key: 13, x: 3, y: 7, color: 'red' }
39 | pressFeedback()
40 | .instance()
41 | .addFeedbackIn(pressData)
42 |
43 | expect(pressFeedback().state().presses[0]).toHaveProperty('key', 13)
44 | expect(pressFeedback().state().presses[0]).toHaveProperty('x', 3)
45 | expect(pressFeedback().state().presses[0]).toHaveProperty('y', 7)
46 | expect(pressFeedback().state().presses[0]).toHaveProperty('color', 'red')
47 | })
48 |
49 | it('renders all current presses', () => {
50 | // Add some presses
51 | const pressData = { key: 13, x: 3, y: 7, color: 'red', size: 80 }
52 | pressFeedback()
53 | .instance()
54 | .addFeedbackIn(pressData)
55 | pressFeedback()
56 | .instance()
57 | .addFeedbackIn(pressData)
58 | pressFeedback()
59 | .instance()
60 | .addFeedbackIn(pressData)
61 |
62 | pressFeedback().update()
63 |
64 | expect(pressFeedback().find(PressRippleAnimation)).toHaveLength(3)
65 | })
66 |
67 | it('enqueues the removal of a press', () => {
68 | // Add some presses...
69 | pressFeedback()
70 | .instance()
71 | .addFeedbackIn({ key: 'meh', x: 1, y: 2, color: 'white' })
72 | pressFeedback()
73 | .instance()
74 | .addFeedbackIn({ key: 'this-out', x: 3, y: 4, color: 'green' })
75 | pressFeedback()
76 | .instance()
77 | .addFeedbackIn({ key: 'huh', x: 5, y: 6, color: 'black' })
78 |
79 | // ...and enqueue a removal.
80 | pressFeedback()
81 | .instance()
82 | .enqueueFeedbackOut('this-out')
83 |
84 | expect(pressFeedback().state().presses).toContainEqual({
85 | key: 'this-out',
86 | x: 3,
87 | y: 4,
88 | color: 'green',
89 | animateOut: true
90 | })
91 | })
92 |
93 | it('removes a press when finished', () => {
94 | // Add some presses...
95 | pressFeedback()
96 | .instance()
97 | .addFeedbackIn({ key: 'meh', x: 1, y: 2, color: 'white' })
98 | pressFeedback()
99 | .instance()
100 | .addFeedbackIn({ key: 'this-out', x: 3, y: 4, color: 'green' })
101 |
102 | pressFeedback().update()
103 |
104 | pressFeedback()
105 | .find(PressRippleAnimation)
106 | .last()
107 | .props()
108 | .onOutEnd()
109 |
110 | expect(pressFeedback().state().presses).toHaveLength(1)
111 | expect(pressFeedback().state().presses).not.toContainEqual({
112 | key: 'this-out',
113 | x: 3,
114 | y: 4,
115 | color: 'green'
116 | })
117 | })
118 | })
119 |
--------------------------------------------------------------------------------
/__tests__/PressRippleAnimation.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Animated } from 'react-native'
3 | import { shallow } from 'enzyme'
4 |
5 | import PressRippleAnimation from '../lib/PressRippleAnimation'
6 |
7 | jest.useFakeTimers()
8 | jest.mock('Animated', () => {
9 | return {
10 | View: () => ,
11 | Value: jest.fn(num => ({
12 | interpolate: jest.fn(() => num)
13 | })),
14 | timing: jest.fn((value, options) => {
15 | return {
16 | start(cb) {
17 | setTimeout(cb, options.duration)
18 | }
19 | }
20 | })
21 | }
22 | })
23 |
24 | describe('PressRippleAnimation', () => {
25 | const consoleError = console.error
26 | let mountedRipple
27 | let props
28 | const animationFn = jest.spyOn(Animated, 'timing')
29 | const ripple = () => {
30 | if (!mountedRipple) {
31 | mountedRipple = shallow()
32 | }
33 | return mountedRipple
34 | }
35 |
36 | beforeEach(() => {
37 | animationFn.mockClear()
38 | mountedRipple = null
39 | props = {
40 | x: 10,
41 | y: 10,
42 | onInEnd: jest.fn(),
43 | onOutEnd: jest.fn(),
44 | color: 'purple',
45 | size: 100
46 | }
47 |
48 | // prevent output of warnings because of native RN Components
49 | console.error = () => {}
50 | })
51 |
52 | afterEach(() => {
53 | console.error = consoleError
54 | jest.clearAllTimers()
55 | })
56 |
57 | it('initially runs through long press animation', () => {
58 | ripple()
59 | expect(animationFn).toHaveBeenCalledTimes(1)
60 | expect(props.onInEnd).not.toHaveBeenCalled()
61 |
62 | jest.advanceTimersByTime(400)
63 |
64 | expect(props.onInEnd).toHaveBeenCalled()
65 |
66 | ripple().setProps({ animateOut: true })
67 | expect(animationFn).toHaveBeenCalledTimes(2)
68 |
69 | jest.advanceTimersByTime(300)
70 | expect(props.onOutEnd).toHaveBeenCalled()
71 | })
72 |
73 | it('initially runs through short press animation', () => {
74 | ripple()
75 | expect(animationFn).toHaveBeenCalledTimes(1)
76 | expect(props.onInEnd).not.toHaveBeenCalled()
77 |
78 | ripple().setProps({ animateOut: true })
79 |
80 | jest.advanceTimersByTime(400)
81 |
82 | expect(props.onInEnd).toHaveBeenCalled()
83 | expect(animationFn).toHaveBeenCalledTimes(2)
84 |
85 | jest.advanceTimersByTime(400)
86 | expect(props.onOutEnd).toHaveBeenCalled()
87 | })
88 |
89 | it('renders an animated view', () => {
90 | expect(ripple().find(Animated.View)).toHaveLength(1)
91 | const { style } = ripple()
92 | .find(Animated.View)
93 | .props()
94 | expect(style).toHaveProperty('backgroundColor', 'purple')
95 | expect(style).toHaveProperty('width', 100)
96 | expect(style).toHaveProperty('height', 100)
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/__tests__/ShiftingTab.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View, Text } from 'react-native'
3 | import { shallow } from 'enzyme'
4 |
5 | import ShiftingTab from '../lib/ShiftingTab'
6 | import FullTab from '../lib/FullTab'
7 |
8 | const MockIcon = () =>
9 | const MockBadge = () =>
10 |
11 | describe('FullTab', () => {
12 | const consoleError = console.error
13 | let mountedTab
14 | let props
15 | const tab = () => {
16 | if (!mountedTab) {
17 | mountedTab = shallow()
18 | }
19 | return mountedTab
20 | }
21 |
22 | beforeEach(() => {
23 | mountedTab = null
24 | })
25 |
26 | it('renders a FullTab', () => {
27 | props = {
28 | isActive: false,
29 | renderIcon: jest.fn(() => ),
30 | label: 'Hit it!'
31 | }
32 |
33 | expect(tab().find(FullTab)).toHaveLength(1)
34 | })
35 |
36 | it('sets inactive styles', () => {
37 | props = {
38 | isActive: false,
39 | renderIcon: jest.fn(() => ),
40 | label: 'Hit it!'
41 | }
42 |
43 | expect(
44 | tab()
45 | .find(FullTab)
46 | .props().style[0]
47 | ).toHaveProperty('flex', 1)
48 | })
49 |
50 | it('sets active styles', () => {
51 | props = {
52 | isActive: true,
53 | renderIcon: jest.fn(() => ),
54 | label: 'Hit it!'
55 | }
56 |
57 | expect(
58 | tab()
59 | .find(FullTab)
60 | .props().style[0]
61 | ).toHaveProperty('flex', 1.75)
62 | })
63 |
64 | it('passes other props to FullTab', () => {
65 | props = {
66 | isActive: false,
67 | renderIcon: jest.fn(() => ),
68 | label: 'Hit it!'
69 | }
70 |
71 | const tabProps = tab()
72 | .find(FullTab)
73 | .props()
74 |
75 | expect(tabProps.isActive).toBe(false)
76 | expect(tabProps.renderIcon).toBe(props.renderIcon)
77 | expect(tabProps.label).toBe('Hit it!')
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/__tests__/TabList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | View,
4 | TouchableWithoutFeedback,
5 | UIManager,
6 | LayoutAnimation
7 | } from 'react-native'
8 | import { mount } from 'enzyme'
9 |
10 | import TabList from '../lib/TabList'
11 |
12 | const MockTab = () =>
13 |
14 | UIManager.configureNextLayoutAnimation = jest.fn()
15 | jest.useFakeTimers()
16 |
17 | describe('TabList', () => {
18 | const consoleError = console.error
19 | let mountedList
20 | let props
21 | const tabList = () => {
22 | if (!mountedList) {
23 | mountedList = mount()
24 | }
25 | return mountedList
26 | }
27 |
28 | beforeEach(() => {
29 | // prevent output of warnings because of native RN Components
30 | console.error = () => {}
31 | })
32 |
33 | afterEach(() => {
34 | console.error = consoleError
35 | })
36 |
37 | describe('uncontrolled with three mock tabs', () => {
38 | beforeEach(() => {
39 | mountedList = null
40 | props = {
41 | tabs: [
42 | { key: 'up', text: 'Up!', barColor: 'green', pressColor: 'green' },
43 | { key: 'town', text: 'Town!', barColor: 'blue', pressColor: 'blue' },
44 | { key: 'funk', text: 'Funk!', barColor: 'red', pressColor: 'red' }
45 | ],
46 | onTabPress: jest.fn(),
47 | renderTab: jest.fn(() => ),
48 | setBackgroundColor: jest.fn(),
49 | addDecorator: jest.fn(),
50 | addFeedbackIn: jest.fn(),
51 | enqueueFeedbackOut: jest.fn()
52 | }
53 | })
54 |
55 | it('is uncontrolled', () => {
56 | expect(tabList().instance().isControlled).toBe(false)
57 | })
58 |
59 | it('initially sets first tab active', () => {
60 | expect(tabList().state().activeTab).toBe('up')
61 | })
62 |
63 | it('renders three tabs', () => {
64 | expect(tabList().find(MockTab)).toHaveLength(3)
65 | })
66 |
67 | it('calls renderTab three times', () => {
68 | tabList()
69 | expect(props.renderTab).toHaveBeenCalledTimes(3)
70 | })
71 |
72 | it('calls renderTab with correct arguments', () => {
73 | tabList()
74 | expect(props.renderTab.mock.calls).toEqual([
75 | [{ isActive: true, tab: props.tabs[0] }],
76 | [{ isActive: false, tab: props.tabs[1] }],
77 | [{ isActive: false, tab: props.tabs[2] }]
78 | ])
79 | })
80 |
81 | it('passes touchable props to tab component', () => {
82 | const tabProps = tabList()
83 | .find(MockTab)
84 | .first()
85 | .props()
86 |
87 | // Just test a few. If they are passed, we can be pretty sure all
88 | // are passed.
89 | expect(tabProps).toHaveProperty('onResponderGrant')
90 | expect(tabProps).toHaveProperty('onResponderMove')
91 | expect(tabProps).toHaveProperty('onResponderRelease')
92 | })
93 |
94 | it('handles tab press', () => {
95 | const fakeEvent = { nativeEvent: {} }
96 | tabList()
97 | props.setBackgroundColor.mockClear()
98 |
99 | tabList()
100 | .find(TouchableWithoutFeedback)
101 | .at(1)
102 | .props()
103 | .onPress(fakeEvent)
104 |
105 | expect(props.onTabPress).toHaveBeenCalledWith(
106 | props.tabs[1],
107 | props.tabs[0]
108 | )
109 |
110 | expect(tabList().state().activeTab).toBe('town')
111 | expect(props.setBackgroundColor).not.toHaveBeenCalled()
112 | expect(props.addDecorator).toHaveBeenCalled()
113 | })
114 |
115 | it('handles tab press in', () => {
116 | const fakeEvent = { nativeEvent: { pageX: 13, locationY: 37 } }
117 | tabList()
118 | .find(TouchableWithoutFeedback)
119 | .at(0)
120 | .props()
121 | .onPressIn(fakeEvent)
122 |
123 | const [[call]] = props.addFeedbackIn.mock.calls
124 |
125 | expect(call).toHaveProperty('x', 13)
126 | expect(call).toHaveProperty('y', 37)
127 | expect(call).toHaveProperty('color', 'green')
128 | })
129 |
130 | it('handles tab press out', () => {
131 | tabList()
132 | .find(TouchableWithoutFeedback)
133 | .at(0)
134 | .props()
135 | .onPressOut()
136 |
137 | expect(props.enqueueFeedbackOut).toHaveBeenCalled()
138 | })
139 | })
140 |
141 | describe('controlled with three mock tabs', () => {
142 | beforeEach(() => {
143 | mountedList = null
144 | props = {
145 | tabs: [
146 | { key: 'up', text: 'Up!', barColor: 'green', pressColor: 'green' },
147 | { key: 'town', text: 'Town!', barColor: 'blue', pressColor: 'blue' },
148 | { key: 'funk', text: 'Funk!', barColor: 'red', pressColor: 'red' }
149 | ],
150 | activeTab: 'funk',
151 | onTabPress: jest.fn(),
152 | renderTab: jest.fn(() => ),
153 | setBackgroundColor: jest.fn(),
154 | addDecorator: jest.fn(),
155 | addFeedbackIn: jest.fn(),
156 | enqueueFeedbackOut: jest.fn()
157 | }
158 | })
159 |
160 | it('is controlled', () => {
161 | expect(tabList().instance().isControlled).toBe(true)
162 | })
163 |
164 | it('initially sets tab active', () => {
165 | expect(tabList().state().activeTab).toBe('funk')
166 | })
167 |
168 | it('calls renderTab with correct arguments', () => {
169 | tabList()
170 | expect(props.renderTab.mock.calls).toEqual([
171 | [{ isActive: false, tab: props.tabs[0] }],
172 | [{ isActive: false, tab: props.tabs[1] }],
173 | [{ isActive: true, tab: props.tabs[2] }]
174 | ])
175 | })
176 |
177 | it('handles tab press', () => {
178 | const fakeEvent = { nativeEvent: {} }
179 | tabList()
180 | .find(TouchableWithoutFeedback)
181 | .at(1)
182 | .props()
183 | .onPress(fakeEvent)
184 |
185 | expect(props.onTabPress).toHaveBeenCalledWith(
186 | props.tabs[1],
187 | props.tabs[2]
188 | )
189 |
190 | expect(tabList().state().activeTab).not.toBe('town')
191 | })
192 |
193 | it('updates active tab when prop changes after press', () => {
194 | // Setup
195 | const fakeEvent = { nativeEvent: { pageX: 13, locationY: 37 } }
196 | tabList()
197 | props.setBackgroundColor.mockClear()
198 |
199 | // Trigger tab press
200 | tabList()
201 | .find(TouchableWithoutFeedback)
202 | .first()
203 | .props()
204 | .onPress(fakeEvent)
205 |
206 | // activeTab should not be updated
207 | expect(tabList().state().activeTab).toBe('funk')
208 |
209 | // Update activeTab
210 | tabList().setProps({ activeTab: 'up' })
211 |
212 | expect(tabList().state().activeTab).toBe('up')
213 | expect(props.addDecorator).toHaveBeenCalled()
214 | expect(props.setBackgroundColor).not.toHaveBeenCalled()
215 | })
216 |
217 | it('updates active tab when prop changes without press', () => {
218 | tabList()
219 | props.setBackgroundColor.mockClear()
220 |
221 | tabList().setProps({ activeTab: 'up' })
222 |
223 | expect(tabList().state().activeTab).toBe('up')
224 | expect(props.setBackgroundColor).toHaveBeenCalledTimes(1)
225 | })
226 | })
227 |
228 | describe('with layout animations', () => {
229 | beforeEach(() => {
230 | mountedList = null
231 | props = {
232 | tabs: [
233 | { key: 'up', text: 'Up!', barColor: 'green', pressColor: 'green' },
234 | { key: 'town', text: 'Town!', barColor: 'blue', pressColor: 'blue' },
235 | { key: 'funk', text: 'Funk!', barColor: 'red', pressColor: 'red' }
236 | ],
237 | useLayoutAnimation: true,
238 | onTabPress: jest.fn(),
239 | renderTab: jest.fn(() => ),
240 | setBackgroundColor: jest.fn(),
241 | addDecorator: jest.fn(),
242 | addFeedbackIn: jest.fn(),
243 | enqueueFeedbackOut: jest.fn()
244 | }
245 | })
246 |
247 | it('handles tab press', () => {
248 | const spy = jest.spyOn(LayoutAnimation, 'configureNext')
249 | const fakeEvent = { nativeEvent: {} }
250 | tabList()
251 | props.setBackgroundColor.mockClear()
252 |
253 | tabList()
254 | .find(TouchableWithoutFeedback)
255 | .at(1)
256 | .props()
257 | .onPress(fakeEvent)
258 |
259 | jest.runAllTimers()
260 |
261 | expect(tabList().state().activeTab).toBe('town')
262 | expect(spy).toHaveBeenCalled()
263 | })
264 | })
265 | })
266 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Table of Contents
2 |
3 | * [Introduction](../README.md)
4 | * [Usage](Usage.md)
5 | * [API Reference](api/README.md)
6 | * [Badge](api/Badge.md)
7 | * [BottomNavigation](api/BottomNavigation.md)
8 | * [FullTab](api/FullTab.md)
9 | * [IconTab](api/IconTab.md)
10 | * [ShiftingTab](api/ShiftingTab.md)
11 |
--------------------------------------------------------------------------------
/docs/Usage.md:
--------------------------------------------------------------------------------
1 | # Usage
2 |
3 | The Material Bottom Navigation is designed to be very customizable and pluggable. Instead of a big configuration object, it uses React's [Render Props](https://reactjs.org/docs/render-props.html) to render smaller, customizable Components. If you're new to React and/or Render Props, read the [article on render props](https://reactjs.org/docs/render-props.html) first – or simply dive into the Bottom Navigation and learn it by doing.
4 |
5 | You will use render props for
6 |
7 | * rendering a Tab (FullTab, IconTab or ShiftingTab).
8 | * rendering an Icon.
9 | * rendering a Badge.
10 |
11 | ## Step by Step Guide
12 |
13 | ### The basics
14 |
15 | The `BottomNavigation` receives two main props:
16 |
17 | * `tabs` is an array of tab objects. Read more about required keys in this object in the [documentation for the tabs array](./api/BottomNavigation.md#tabs)
18 | * `renderTab` is a Render Prop which will be called for each tab, and you have to return a Component.
19 |
20 | ```js
21 | import { View } from 'react-native'
22 | import BottomNavigation from 'react-native-material-bottom-navigation'
23 |
24 | export default class App extends React.Component {
25 | tabs = [
26 | {
27 | key: 'games',
28 | icon: 'gamepad-variant',
29 | label: 'Games',
30 | barColor: '#388E3C',
31 | pressColor: 'rgba(255, 255, 255, 0.16)'
32 | },
33 | {
34 | key: 'movies-tv',
35 | icon: 'movie',
36 | label: 'Movies & TV',
37 | barColor: '#B71C1C',
38 | pressColor: 'rgba(255, 255, 255, 0.16)'
39 | },
40 | {
41 | key: 'music',
42 | icon: 'music-note',
43 | label: 'Music',
44 | barColor: '#E64A19',
45 | pressColor: 'rgba(255, 255, 255, 0.16)'
46 | }
47 | ]
48 |
49 | render() {
50 | return (
51 |
52 |
56 |
57 | )
58 | }
59 |
60 | renderTab = () => {
61 | return
62 | }
63 | }
64 | ```
65 |
66 | This will render just the Bottom Navigation without any Tabs – because we're just returning a blank View Component.
67 |
68 | Some notes on the `tabs` Array:
69 |
70 | * `key` is required and should be a unique identifier for this tab.
71 | * `barColor` defines the background color of the Bottom Navigation when this Tab is active.
72 | * `pressColor` defines the color of the press feedback.
73 | * The other keys (`icon` and `label`) are just payload, defined by you. You will use those two keys in the next section.
74 |
75 | ### Rendering a Tab
76 |
77 | Now we will render a Tab instead of a blank View:
78 |
79 | ```js
80 | import { View } from 'react-native'
81 | import BottomNavigation, {
82 | FullTab
83 | } from 'react-native-material-bottom-navigation'
84 |
85 | export default class App extends React.Component {
86 | tabs = [
87 | {
88 | key: 'games',
89 | icon: 'gamepad-variant',
90 | label: 'Games',
91 | barColor: '#388E3C',
92 | pressColor: 'rgba(255, 255, 255, 0.16)'
93 | },
94 | {
95 | key: 'movies-tv',
96 | icon: 'movie',
97 | label: 'Movies & TV',
98 | barColor: '#B71C1C',
99 | pressColor: 'rgba(255, 255, 255, 0.16)'
100 | },
101 | {
102 | key: 'music',
103 | icon: 'music-note',
104 | label: 'Music',
105 | barColor: '#E64A19',
106 | pressColor: 'rgba(255, 255, 255, 0.16)'
107 | }
108 | ]
109 |
110 | render() {
111 | return (
112 |
113 |
117 |
118 | )
119 | }
120 |
121 | renderTab = ({ tab, isActive }) => {
122 | return (
123 |
129 | )
130 | }
131 |
132 | renderIcon = ({ isActive }) => {
133 | return
134 | }
135 | }
136 | ```
137 |
138 | The `renderTab` method will be called for each object in our `tabs` array. The method contains an object as parameter, with `tab` and `isActive`.
139 |
140 | * `tab` is the tab object, which is currently being rendered. This is the exact same object from our `tabs` array.
141 | * `isActive` tells us if the Tab is currently active.
142 |
143 | We use those informations to return a [`FullTab`](./api/FullTab.md), which displays a label and an Icon. The Icon is once again a render prop, similar to the `renderTab` prop. For now we just return a blank `View`, as we did earlier for the Tab.
144 |
145 | Instead of a [`FullTab`](./api/FullTab.md), you can also use:
146 |
147 | * an [`IconTab`](./api/IconTab.md) which displays just an Icon.
148 | * a [`ShiftingTab`](./api/ShiftingTab.md), which is a FullTab, but the active tab is bigger than the other tabs.
149 | * a Component you created yourself! As a starting point, take a look at the [implementation of the IconTab](https://github.com/timomeh/react-native-material-bottom-navigation/blob/rewrite-cleanup/lib/IconTab.js).
150 |
151 | ### Rendering an Icon
152 |
153 | The Icon can be any Component you want to use, for example [@expo/vector-icons](https://github.com/expo/vector-icons). In fact, the Material Bottom Navigation doesn't include an own Icon Component.
154 |
155 | As you see above, the `renderIcon` prop only contains an object as argument with the `isActive` key. We pass more arguments to `renderIcon` using something called a _Thunk_ (or _Curry_, _Higher-order Function_, _Closure_): a function returning a function. This sounds complicated, but are just two small changes:
156 |
157 | ```js
158 | // ...
159 |
160 | renderTab = ({ tab, isActive }) => {
161 | return (
162 |
168 | )
169 | }
170 |
171 | renderIcon = iconName => ({ isActive }) => {
172 | return
173 | }
174 |
175 | // ...
176 | ```
177 |
178 | And you're finished! You now should have a fully functional Bottom Navigation with nice animations.
179 |
180 | ### Rendering a Badge
181 |
182 | The [`Badge`](./api/Badge.md) is a render prop on the Tab Component. You can render _anything_ inside a badge: e.g. a white dot, text, or simply nothing.
183 |
184 | ```js
185 | import BottomNavigation, {
186 | FullTab,
187 | Badge
188 | } from 'react-native-material-bottom-navigation'
189 | // ...
190 |
191 | renderBadge = badgeCount => {
192 | return {badgeCount}
193 | }
194 |
195 | renderTab = ({ tab, isActive }) => {
196 | return (
197 | 0}
199 | renderBadge={this.renderBadge(tab.badgeCount)}
200 | key={tab.key}
201 | isActive={isActive}
202 | label={tab.label}
203 | renderIcon={this.renderIcon(tab.icon)}
204 | />
205 | )
206 | }
207 | ```
208 |
209 | You have two new props to render a badge:
210 |
211 | * `renderBadge`, the render prop to render a badge
212 | * `showBadge` which defines a condition if the badge should be rendered.
213 |
214 | Check out the API Documentations of the different Tabs and for the [`Badge`](./api/Badge.md) to see more informations and additional props for Badges.
215 |
216 | ## Controlled Component
217 |
218 | To use the Bottom Navigation as [Controlled Component](https://reactjs.org/docs/forms.html#controlled-components), you can use two additional props:
219 |
220 | * `activeTab`: the key of the currently active tab.
221 | * `onTabPress`: event handler when a Tab is being pressed. Parameters are the tab object of the new and old tab.
222 |
223 | ```js
224 | import BottomNavigation from 'react-native-material-bottom-navigation'
225 |
226 | export default class App extends React.Component {
227 | state = {
228 | activeTab: 'games'
229 | }
230 |
231 | handleTabPress = (newTab, oldTab) => {
232 | this.setState({ activeTab: newTab.key })
233 | }
234 |
235 | render() {
236 | return (
237 |
238 |
244 |
245 | )
246 | }
247 | }
248 | ```
249 |
250 | ## Changing your screen
251 |
252 | If you want to display different contents on your screen depending on the active tab, you can use the `onTabPress` prop and save the active tab in your state. Check out the Chapter [Controlled Component](#controlled-component).
253 |
254 | You most likely want to use this together with a navigation library, e.g. [React Navigation](https://github.com/react-navigation/react-navigation/) or [React Native Navigation](https://github.com/wix/react-native-navigation).
255 |
--------------------------------------------------------------------------------
/docs/api/Badge.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # Badge
8 |
9 | A Badge which can be rendered on top of a Tab.
10 |
11 | ## Props
12 |
13 | ### children
14 |
15 | Type: `ReactNode`
16 |
17 | Content of the Badge. String and Number will be wrapped in a `Text`.
18 |
19 |
20 |
21 | ### style
22 |
23 | Type: `ViewPropTypes.style`
24 |
25 | Extends the style of the badge's view.
26 |
27 |
28 |
29 | ### textStyle
30 |
31 | Type: `Text.propTypes.style`
32 |
33 | Extends the style of wrapped `Text` component.
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/docs/api/BottomNavigation.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # BottomNavigation
8 |
9 | The BottomNavigation renders all tabs and takes care of running animations.
10 |
11 | It uses a [render prop](https://reactjs.org/docs/render-props.html) to
12 | render the tabs, so you can easily customize them without clunky
13 | configurations.
14 |
15 | This library includes multiple configurable Tabs which you can use inside
16 | the `renderTab` prop. You can also build and use your own tabs.
17 |
18 | You can use the BottomNavigation as an uncontrolled or a controlled
19 | component by using the prop `activeTab`. If you set `activeTab`, the
20 | BottomNavigation will switch to controlled mode. If a tab is pressed, it
21 | will only become active if you update the value for `activeTab`.
22 | You receive tab presses through the prop `onTabPress={(newTab) => ...}`.
23 | `newTab` is the tab object, you can get its key with `newTab.key`.
24 | See also: https://reactjs.org/docs/forms.html#controlled-components
25 |
26 | If you use it as an uncontrolled component, the tab will automatically
27 | become active once it's pressed. `onTabPress` will also be called, so you
28 | can change to another screen.
29 |
30 | ## Props
31 |
32 | ### activeTab
33 |
34 | Type: `Union`
35 |
36 | The identifier of the currently active tab. If you set this, the
37 | Bottom navigation will become a controlled component.
38 |
39 |
40 |
41 | ### onTabPress
42 |
43 | Type: `Function`
44 |
45 | The called function when a tab was pressed. Useful to change the active
46 | tab when you use the Bottom navigation as controlled component. Has
47 | the tab object of the pressed tab and the currently active tab as
48 | as parameters.
49 | Arguments: `(newTab, oldTab)`
50 |
51 |
52 |
53 | ### renderTab
54 | **Required.**
55 | Type: `Function`
56 |
57 | The render prop to render a tab. Arguments: `({ isActive, tab })`
58 |
59 |
60 |
61 | ### style
62 |
63 | Type: `ViewPropTypes.style`
64 |
65 | Extends the style of the root view.
66 |
67 |
68 |
69 | ### tabs
70 | **Required.**
71 | Type: `Array[]`
72 |
73 | The config of all tabs. Each item will be called in `renderTab`.
74 |
75 |
76 |
77 | ### tabs[].barColor
78 |
79 | Type: `String`
80 |
81 | The background color of the bottom navigation bar.
82 |
83 |
84 |
85 | ### tabs[].key
86 | **Required.**
87 | Type: `Union`
88 |
89 | A unique identifier for a tab.
90 |
91 |
92 |
93 | ### tabs[].pressColor
94 |
95 | Type: `String`
96 |
97 | The color of the touch feedback.
98 |
99 |
100 |
101 | ### tabs[].pressSize
102 |
103 | Type: `Number`
104 |
105 | The diameter of the expanded touch feedback.
106 |
107 |
108 |
109 | ### useLayoutAnimation
110 |
111 | Type: `Boolean`
112 |
113 | If `true`, a LayoutAnimation will be triggered when the active tab
114 | changes. Necessary to get nice animations when using
115 | [ShiftingTab](ShiftingTab.md).
116 |
117 |
118 |
119 | ### viewportHeight
120 |
121 | Type: `Number`
122 |
123 | (experimental, android only) If you pass the height of the viewport, it
124 | will check if android soft navigation is enabled and configure the
125 | BottomNavigation so it looks nice behind the navigation bar.
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/docs/api/FullTab.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # FullTab
8 |
9 | A Tab with a label and an icon.
10 |
11 | ## Props
12 |
13 | ### animationDuration
14 |
15 | Type: `Number`
16 |
17 | The duration of the animation between active and inactive.
18 |
19 |
20 | Default: `160`
21 |
22 | ### animationEasing
23 |
24 | Type: `Function`
25 |
26 | The easing function of the animation between active and inactive.
27 |
28 |
29 | Default: `easings.easeInOut`
30 |
31 | ### badgeAnimation
32 |
33 | Type: `Function`
34 |
35 | Defines the animation of the badge from active to inactive. Receives the
36 | animation progress (`AnimatedValue` between 0 and 1), needs to return a
37 | style object.
38 | See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
39 |
40 |
41 | Default:
42 | ```js
43 | progress => ({
44 | transform: [
45 | {
46 | scale: progress.interpolate({
47 | inputRange: [0, 1],
48 | outputRange: [0.9, 1]
49 | })
50 | }
51 | ]
52 | })
53 | ```
54 |
55 | ### badgeSlotStyle
56 |
57 | Type: `ViewPropTypes.style`
58 |
59 | Extends the style of the badge's wrapping View.
60 |
61 |
62 |
63 | ### iconAnimation
64 |
65 | Type: `Function`
66 |
67 | Defines the animation of the icon from active to inactive. Receives the
68 | animation progress (`AnimatedValue` between 0 and 1), needs to return a
69 | style object.
70 | See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
71 |
72 |
73 | Default:
74 | ```js
75 | progress => ({
76 | transform: [
77 | {
78 | translateY: progress.interpolate({
79 | inputRange: [0, 1],
80 | outputRange: [0, -2]
81 | })
82 | }
83 | ],
84 | opacity: progress.interpolate({
85 | inputRange: [0, 1],
86 | outputRange: [0.8, 1]
87 | })
88 | })
89 | ```
90 |
91 | ### isActive
92 | **Required.**
93 | Type: `Boolean`
94 |
95 | If `true`, the tab is visually active.
96 |
97 |
98 |
99 | ### label
100 | **Required.**
101 | Type: `String`
102 |
103 | The text of the label.
104 |
105 |
106 |
107 | ### labelAnimation
108 |
109 | Type: `Function`
110 |
111 | Defines the animation of the label from active to inactive. Receives the
112 | animation progress (`AnimatedValue` between 0 and 1), needs to return a
113 | style object.
114 | See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
115 |
116 |
117 | Default:
118 | ```js
119 | progress => ({
120 | transform: [
121 | {
122 | scale: progress.interpolate({
123 | inputRange: [0, 1],
124 | outputRange: [1, 1.12]
125 | })
126 | },
127 | {
128 | translateY: progress.interpolate({
129 | inputRange: [0, 1],
130 | outputRange: [0, -1]
131 | })
132 | }
133 | ],
134 | opacity: progress.interpolate({
135 | inputRange: [0, 1],
136 | outputRange: [0.8, 1]
137 | })
138 | })
139 | ```
140 |
141 | ### labelProps
142 |
143 | Type: `Object`
144 |
145 | Useful to add more props to the Text component of the label.
146 |
147 |
148 | Default: `{ numberOfLines: 1 }`
149 |
150 | ### labelStyle
151 |
152 | Type: `Text.propTypes.style`
153 |
154 | Extends the style of the label.
155 |
156 |
157 |
158 | ### renderBadge
159 |
160 | Type: `Function`
161 |
162 | The render prop to render the badge. Arguments: `({ isActive })`
163 |
164 |
165 |
166 | ### renderIcon
167 | **Required.**
168 | Type: `Function`
169 |
170 | The render prop to render the icon. Arguments: `({ isActive })`
171 |
172 |
173 |
174 | ### showBadge
175 |
176 | Type: `Boolean`
177 |
178 | If `true`, the badge will be rendered.
179 |
180 |
181 | Default: `false`
182 |
183 | ### style
184 |
185 | Type: `ViewPropTypes.style`
186 |
187 | Extends the style of the tab's view.
188 |
189 |
190 |
191 |
--------------------------------------------------------------------------------
/docs/api/IconTab.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # IconTab
8 |
9 | A Tab with an icon.
10 |
11 | ## Props
12 |
13 | ### animationDuration
14 |
15 | Type: `Number`
16 |
17 | The duration of the animation between active and inactive.
18 |
19 |
20 | Default: `160`
21 |
22 | ### animationEasing
23 |
24 | Type: `Function`
25 |
26 | The easing function of the animation between active and inactive.
27 |
28 |
29 | Default: `easings.easeInOut`
30 |
31 | ### badgeAnimation
32 |
33 | Type: `Function`
34 |
35 | Defines the animation of the badge from active to inactive. Receives the
36 | animation progress (`AnimatedValue` between 0 and 1), needs to return a
37 | style object.
38 | See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
39 |
40 |
41 | Default:
42 | ```js
43 | progress => ({
44 | transform: [
45 | {
46 | scale: progress.interpolate({
47 | inputRange: [0, 1],
48 | outputRange: [0.9, 1]
49 | })
50 | }
51 | ]
52 | })
53 | ```
54 |
55 | ### badgeSlotStyle
56 |
57 | Type: `ViewPropTypes.style`
58 |
59 | Extends the style of the badge's wrapping View.
60 |
61 |
62 |
63 | ### iconAnimation
64 |
65 | Type: `Function`
66 |
67 | Defines the animation of the icon from active to inactive. Receives the
68 | animation progress (`AnimatedValue` between 0 and 1), needs to return a
69 | style object.
70 | See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
71 |
72 |
73 | Default:
74 | ```js
75 | progress => ({
76 | transform: [
77 | {
78 | scale: progress.interpolate({
79 | inputRange: [0, 1],
80 | outputRange: [1, 1.2]
81 | })
82 | }
83 | ],
84 | opacity: progress.interpolate({
85 | inputRange: [0, 1],
86 | outputRange: [0.8, 1]
87 | })
88 | })
89 | ```
90 |
91 | ### isActive
92 | **Required.**
93 | Type: `Boolean`
94 |
95 | If `true`, the tab is visually active.
96 |
97 |
98 |
99 | ### renderBadge
100 |
101 | Type: `Function`
102 |
103 | The render prop to render the badge. Arguments: `({ isActive })`
104 |
105 |
106 |
107 | ### renderIcon
108 | **Required.**
109 | Type: `Function`
110 |
111 | The render prop to render the icon. Arguments: `({ isActive })`
112 |
113 |
114 |
115 | ### showBadge
116 |
117 | Type: `Boolean`
118 |
119 | If `true`, the badge will be rendered.
120 |
121 |
122 | Default: `false`
123 |
124 | ### style
125 |
126 | Type: `ViewPropTypes.style`
127 |
128 | Extends the style of the tab's view.
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/docs/api/README.md:
--------------------------------------------------------------------------------
1 | # API Reference
2 |
3 | * [Badge](./Badge.md)
4 | * [BottomNavigation](./BottomNavigation.md)
5 | * [FullTab](./FullTab.md)
6 | * [IconTab](./IconTab.md)
7 | * [ShiftingTab](./ShiftingTab.md)
8 |
--------------------------------------------------------------------------------
/docs/api/ShiftingTab.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # ShiftingTab
8 |
9 | A Tab for the shifting bottom navigation bar, implemented according to the
10 | Bottom navigation specs.
11 | In its inactive state, only the icon is visible.
12 | In its active state, the tab's label is also visible, and the tab is wider.
13 |
14 | **To enable a nice transition between both states, the `BottomNavigation`
15 | needs to have the `useLayoutAnimation` prop set to `true`.**
16 |
17 | The ShiftingTab is basically a [FullTab](./FullTab.md) with
18 | predefined style- and animation-props.
19 |
20 | ## Props
21 |
22 | ### animationDuration
23 |
24 | Type: `Number`
25 |
26 | The duration of the animation between active and inactive.
27 |
28 |
29 | Default: `160`
30 |
31 | ### animationEasing
32 |
33 | Type: `Function`
34 |
35 | The easing function of the animation between active and inactive.
36 |
37 |
38 | Default: `easings.easeInOut`
39 |
40 | ### badgeAnimation
41 |
42 | Type: `Function`
43 |
44 | Defines the animation of the badge from active to inactive. Receives the
45 | animation progress (`AnimatedValue` between 0 and 1), needs to return a
46 | style object.
47 | See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
48 |
49 |
50 | Default:
51 | ```js
52 | progress => ({
53 | transform: [
54 | {
55 | scale: progress.interpolate({
56 | inputRange: [0, 1],
57 | outputRange: [0.9, 1]
58 | })
59 | },
60 | {
61 | translateY: progress.interpolate({
62 | inputRange: [0, 1],
63 | outputRange: Platform.select({ ios: [9, 4], android: [6, 0] })
64 | })
65 | }
66 | ]
67 | })
68 | ```
69 |
70 | ### badgeSlotStyle
71 |
72 | Type: `ViewPropTypes.style`
73 |
74 | Extends the style of the badge's wrapping View.
75 |
76 |
77 |
78 | ### iconAnimation
79 |
80 | Type: `Function`
81 |
82 | Defines the animation of the icon from active to inactive. Receives the
83 | animation progress (0-1), needs to return a style object.
84 | See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
85 |
86 |
87 | Default:
88 | ```js
89 | progress => ({
90 | transform: [
91 | {
92 | translateY: progress.interpolate({
93 | inputRange: [0, 1],
94 | outputRange: [7, 0]
95 | })
96 | }
97 | ],
98 | opacity: progress.interpolate({
99 | inputRange: [0, 1],
100 | outputRange: [0.8, 1]
101 | })
102 | })
103 | ```
104 |
105 | ### isActive
106 | **Required.**
107 | Type: `Boolean`
108 |
109 | If `true`, the tab is visually active.
110 |
111 |
112 |
113 | ### label
114 | **Required.**
115 | Type: `String`
116 |
117 | The text of the label.
118 |
119 |
120 |
121 | ### labelAnimation
122 |
123 | Type: `Function`
124 |
125 | Defines the animation of the label from active to inactive. Receives the
126 | animation progress (`AnimatedValue` between 0 and 1), needs to return a
127 | style object.
128 | See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
129 |
130 |
131 | Default:
132 | ```js
133 | progress => ({
134 | opacity: progress.interpolate({
135 | inputRange: [0, 1],
136 | outputRange: [0, 1]
137 | })
138 | })
139 | ```
140 |
141 | ### labelProps
142 |
143 | Type: `Object`
144 |
145 | Useful to add more props to the Text component of the label.
146 |
147 |
148 | Default: `{ numberOfLines: 1 }`
149 |
150 | ### labelStyle
151 |
152 | Type: `Text.propTypes.style`
153 |
154 | Extends the style of the label.
155 |
156 |
157 |
158 | ### renderBadge
159 |
160 | Type: `Function`
161 |
162 | The render prop to render the badge. Arguments: `({ isActive })`
163 |
164 |
165 |
166 | ### renderIcon
167 | **Required.**
168 | Type: `Function`
169 |
170 | The render prop to render the icon. Arguments: `({ isActive })`
171 |
172 |
173 |
174 | ### showBadge
175 |
176 | Type: `Boolean`
177 |
178 | If `true`, the badge will be rendered.
179 |
180 |
181 | Default: `false`
182 |
183 | ### style
184 |
185 | Type: `ViewPropTypes.style`
186 |
187 | Extends the style of the tab's view.
188 |
189 |
190 |
191 |
--------------------------------------------------------------------------------
/examples/Playground/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["babel-preset-expo"],
3 | "env": {
4 | "development": {
5 | "plugins": ["transform-react-jsx-source"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/Playground/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | ; We fork some components by platform
3 | .*/*[.]android.js
4 |
5 | ; Ignore templates for 'react-native init'
6 | /node_modules/react-native/local-cli/templates/.*
7 |
8 | ; Ignore RN jest
9 | /node_modules/react-native/jest/.*
10 |
11 | ; Ignore RNTester
12 | /node_modules/react-native/RNTester/.*
13 |
14 | ; Ignore the website subdir
15 | /node_modules/react-native/website/.*
16 |
17 | ; Ignore the Dangerfile
18 | /node_modules/react-native/danger/dangerfile.js
19 |
20 | ; Ignore Fbemitter
21 | /node_modules/fbemitter/.*
22 |
23 | ; Ignore "BUCK" generated dirs
24 | /node_modules/react-native/\.buckd/
25 |
26 | ; Ignore unexpected extra "@providesModule"
27 | .*/node_modules/.*/node_modules/fbjs/.*
28 |
29 | ; Ignore polyfills
30 | /node_modules/react-native/Libraries/polyfills/.*
31 |
32 | ; Ignore various node_modules
33 | /node_modules/react-native-gesture-handler/.*
34 | /node_modules/expo/.*
35 | /node_modules/react-navigation/.*
36 | /node_modules/xdl/.*
37 | /node_modules/reqwest/.*
38 | /node_modules/metro-bundler/.*
39 |
40 | [include]
41 |
42 | [libs]
43 | node_modules/react-native/Libraries/react-native/react-native-interface.js
44 | node_modules/react-native/flow/
45 | node_modules/expo/flow/
46 |
47 | [options]
48 | emoji=true
49 |
50 | module.system=haste
51 |
52 | module.file_ext=.js
53 | module.file_ext=.jsx
54 | module.file_ext=.json
55 | module.file_ext=.ios.js
56 |
57 | munge_underscores=true
58 |
59 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
60 |
61 | suppress_type=$FlowIssue
62 | suppress_type=$FlowFixMe
63 | suppress_type=$FlowFixMeProps
64 | suppress_type=$FlowFixMeState
65 | suppress_type=$FixMe
66 |
67 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)
68 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)?:? #[0-9]+
69 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
70 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
71 |
72 | unsafe.enable_getters_and_setters=true
73 |
74 | [version]
75 | ^0.56.0
76 |
--------------------------------------------------------------------------------
/examples/Playground/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # expo
4 | .expo/
5 |
6 | # dependencies
7 | /node_modules
8 |
9 | # misc
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
--------------------------------------------------------------------------------
/examples/Playground/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/examples/Playground/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View, StyleSheet, Image } from 'react-native'
3 | import BottomNavigation, {
4 | IconTab,
5 | Badge
6 | } from 'react-native-material-bottom-navigation'
7 | import Icon from '@expo/vector-icons/MaterialCommunityIcons'
8 |
9 | export default class App extends React.Component {
10 | state = {
11 | activeTab: 'games'
12 | }
13 |
14 | tabs = [
15 | {
16 | key: 'games',
17 | label: 'Games',
18 | barColor: '#388E3C',
19 | pressColor: 'rgba(255, 255, 255, 0.16)',
20 | icon: 'gamepad-variant'
21 | },
22 | {
23 | key: 'movies-tv',
24 | label: 'Movies & TV',
25 | barColor: '#00695C',
26 | pressColor: 'rgba(255, 255, 255, 0.16)',
27 | icon: 'movie'
28 | },
29 | {
30 | key: 'music',
31 | label: 'Music',
32 | barColor: '#6A1B9A',
33 | pressColor: 'rgba(255, 255, 255, 0.16)',
34 | icon: 'music-note'
35 | },
36 | {
37 | key: 'books',
38 | label: 'Books',
39 | barColor: '#1565C0',
40 | pressColor: 'rgba(255, 255, 255, 0.16)',
41 | icon: 'book'
42 | }
43 | ]
44 |
45 | state = {
46 | activeTab: this.tabs[0].key
47 | }
48 |
49 | renderIcon = icon => ({ isActive }) => (
50 |
51 | )
52 |
53 | renderTab = ({ tab, isActive }) => (
54 | 2}
58 | key={tab.key}
59 | label={tab.label}
60 | renderIcon={this.renderIcon(tab.icon)}
61 | />
62 | )
63 |
64 | render() {
65 | return (
66 |
67 |
68 |
77 |
78 | this.setState({ activeTab: newTab.key })}
82 | renderTab={this.renderTab}
83 | useLayoutAnimation
84 | />
85 |
86 | )
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/examples/Playground/README.md:
--------------------------------------------------------------------------------
1 | # Bottom Navigation Playground 👩🔬
2 |
3 | A playground (with CRNA) to play around and test the Bottom Navigation.
4 |
5 | ## Setup
6 |
7 | 1. In the root directory of this Repo, run `npm install`
8 | 2. `cd examples/Playground`
9 | 3. `npm install`
10 | 4. `npm start`
11 |
--------------------------------------------------------------------------------
/examples/Playground/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "sdkVersion": "25.0.0"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/examples/Playground/cut.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timomeh/react-native-material-bottom-navigation/ae325eb468cad26e8089e81f2b0ddfcc50858a33/examples/Playground/cut.png
--------------------------------------------------------------------------------
/examples/Playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Playground",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "glob-to-regexp": "^0.4.0",
7 | "react-native-scripts": "1.11.1",
8 | "react-test-renderer": "16.2.0"
9 | },
10 | "main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
11 | "scripts": {
12 | "start": "react-native-scripts start",
13 | "eject": "react-native-scripts eject",
14 | "android": "react-native-scripts android",
15 | "ios": "react-native-scripts ios"
16 | },
17 | "dependencies": {
18 | "@expo/vector-icons": "^6.3.1",
19 | "expo": "^25.0.0",
20 | "react": "16.2.0",
21 | "react-native": "0.52.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/Playground/rn-cli.config.js:
--------------------------------------------------------------------------------
1 | // rn-cli config from react-native-tab-view/example/package.json
2 |
3 | const path = require('path')
4 | const glob = require('glob-to-regexp')
5 | const blacklist = require('metro/src/blacklist')
6 | const rootPackage = require('../../package.json')
7 |
8 | const dependencies = Object.keys(rootPackage.dependencies)
9 | const peerDependencies = Object.keys(rootPackage.peerDependencies)
10 |
11 | module.exports = {
12 | getProjectRoots() {
13 | return [__dirname, path.resolve(__dirname, '../..')]
14 | },
15 | getProvidesModuleNodeModules() {
16 | return [...dependencies, ...peerDependencies]
17 | },
18 | getBlacklistRE() {
19 | return blacklist([
20 | glob(`${path.resolve(__dirname, '../..')}/node_modules/*`),
21 | glob(`${__dirname}/node_modules/*/{${dependencies.join(',')}}`, {
22 | extended: true
23 | })
24 | ])
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/examples/with-react-navigation.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 | import { View, Text } from 'react-native'
3 | import { createNavigator, TabRouter } from 'react-navigation'
4 | import BottomNavigation, {
5 | FullTab
6 | } from 'react-native-material-bottom-navigation'
7 | import Icon from '@expo/vector-icons/MaterialCommunityIcons'
8 |
9 | // Screens. Normally you would put these in separate files.
10 | const Movies = () => (
11 |
12 | Movies
13 |
14 | )
15 | const Music = () => (
16 |
17 | Music
18 |
19 | )
20 | const Books = () => (
21 |
22 | Books
23 |
24 | )
25 |
26 | function AppTabView(props) {
27 | const tabs = [
28 | { key: 'Movies', label: 'Movies', barColor: '#00695C', icon: 'movie' },
29 | { key: 'Music', label: 'Music', barColor: '#6A1B9A', icon: 'music-note' },
30 | { key: 'Books', label: 'Books', barColor: '#1565C0', icon: 'book' }
31 | ]
32 |
33 | const { navigation, descriptors } = props
34 | const { routes, index } = navigation.state
35 | const activeScreenName = routes[index].key
36 | const descriptor = descriptors[activeScreenName]
37 | const ActiveScreen = descriptor.getComponent()
38 |
39 | const handleTabPress = useCallback(
40 | newTab => navigation.navigate(newTab.key),
41 | [navigation]
42 | )
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
50 | (
55 | }
60 | />
61 | )}
62 | />
63 |
64 | )
65 | }
66 |
67 | const AppTabRouter = TabRouter({
68 | Movies: { screen: Movies },
69 | Music: { screen: Music },
70 | Books: { screen: Books }
71 | })
72 |
73 | const AppNavigator = createNavigator(AppTabView, AppTabRouter, {})
74 |
75 | export default AppNavigator
76 |
--------------------------------------------------------------------------------
/flow-typed/react-native-material-bottom-navigation.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | declare module 'react-native-material-bottom-navigation' {
4 | // A few type definitions.
5 | // This is adoped from react-navigation/flow/react-navigation.js
6 | declare type StyleObj =
7 | | null
8 | | void
9 | | number
10 | | false
11 | | ''
12 | | $ReadOnlyArray
13 | | { [name: string]: any }
14 | declare type ViewStyleProp = StyleObj
15 | declare type TextStyleProp = StyleObj
16 | declare type AnimatedViewStyleProp = StyleObj
17 | declare type AnimatedTextStyleProp = StyleObj
18 | declare type AnimatedValue = Object
19 | declare type EasingFunction = (t: number) => number
20 | declare type AnimationDefinition = (
21 | progress: AnimatedValue
22 | ) => AnimatedViewStyleProp
23 |
24 | declare export type TabConfig = {
25 | key: number | string,
26 | barColor?: string,
27 | pressColor?: string
28 | }
29 |
30 | declare export type BottomNavigationProps = {
31 | tabs: Array,
32 | renderTab: ({ isActive: boolean }) => React$Element<*>,
33 | activeTab?: number | string,
34 | onTabPress?: (newTab: TabConfig, oldTab: TabConfig) => void,
35 | useLayoutAnimation?: boolean,
36 | style?: ViewStyleProp,
37 | viewport?: number
38 | }
39 |
40 | declare export type IconTabProps = {
41 | isActive: boolean,
42 | style?: ViewStyleProp,
43 | renderIcon: ({ isActive: boolean }) => React$Element<*>,
44 | renderBadge?: ({ isActive: boolean }) => React$Element<*>,
45 | showBadge?: boolean,
46 | badgeSlotStyle?: ViewStyleProp,
47 | animationDuration?: number,
48 | animationEasing?: EasingFunction,
49 | iconAnimation?: AnimationDefinition,
50 | badgeAnimation?: AnimationDefinition
51 | }
52 |
53 | declare export type FullTabProps = {
54 | isActive: boolean,
55 | style?: ViewStyleProp,
56 | renderIcon: ({ isActive: boolean }) => React$Element<*>,
57 | renderBadge?: ({ isActive: boolean }) => React$Element<*>,
58 | showBadge?: boolean,
59 | badgeSlotStyle?: ViewStyleProp,
60 | label: string,
61 | labelStyle?: TextStyleProp,
62 | animationDuration?: number,
63 | animationEasing?: EasingFunction,
64 | iconAnimation?: AnimationDefinition,
65 | labelAnimation?: AnimationDefinition,
66 | badgeAnimation?: AnimationDefinition
67 | }
68 |
69 | declare export type BadgeProps = {
70 | children?: React$Node,
71 | style?: ViewStyleProp,
72 | textStyle: TextStyleProp
73 | }
74 |
75 | declare export default React$ComponentType
76 | declare export var IconTab: React$ComponentType
77 | declare export var FullTab: React$ComponentType
78 | declare export var ShiftingTab: React$ComponentType
79 | declare export var Badge: React$ComponentType
80 | }
81 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ViewStyle, TextStyle, StyleProp, Animated } from 'react-native'
3 |
4 | declare module 'react-native-material-bottom-navigation' {
5 | export interface TabConfig {
6 | key: number | string
7 | barColor?: string
8 | pressColor?: string
9 | }
10 |
11 | export type AnimationDefinition = (progress: Animated.Value) => any
12 | export type EasingFunction = (t: number) => number
13 |
14 | export interface BottomNavigationProps {
15 | tabs: { [index: number]: TabConfig }
16 | renderTab: ({ tab: TabConfig, isActive: boolean }) => JSX.Element
17 | activeTab?: number | string
18 | onTabPress?: (newTab: TabConfig, oldTab: TabConfig) => void
19 | useLayoutAnimation?: boolean
20 | style?: StyleProp
21 | viewportHeight?: number
22 | }
23 |
24 | export interface IconTabProps {
25 | isActive: boolean
26 | style?: StyleProp
27 | renderIcon: ({ isActive: boolean }) => JSX.Element
28 | renderBadge?: ({ isActive: boolean }) => JSX.Element
29 | showBadge?: boolean
30 | badgeSlotStyle?: StyleProp
31 | animationDuration?: number
32 | animationEasing?: EasingFunction
33 | iconAnimation?: AnimationDefinition
34 | badgeAnimation?: AnimationDefinition
35 | }
36 |
37 | export interface FullTabProps {
38 | isActive: boolean
39 | style?: StyleProp
40 | renderIcon: ({ isActive: boolean }) => JSX.Element
41 | renderBadge?: ({ isActive: boolean }) => JSX.Element
42 | showBadge?: boolean
43 | badgeSlotStyle?: StyleProp
44 | label: string
45 | labelStyle?: StyleProp
46 | animationDuration?: number
47 | animationEasing?: EasingFunction
48 | iconAnimation?: AnimationDefinition
49 | labelAnimation?: AnimationDefinition
50 | badgeAnimation?: AnimationDefinition
51 | }
52 |
53 | export interface BadgeProps {
54 | children?: JSX.Element | string | number
55 | style?: StyleProp
56 | textStyle?: StyleProp
57 | }
58 |
59 | export default class BottomNavigation extends React.Component<
60 | BottomNavigationProps
61 | > {}
62 | export class IconTab extends React.Component {}
63 | export class FullTab extends React.Component {}
64 | export class ShiftingTab extends React.Component {}
65 | export class Badge extends React.Component {}
66 | }
67 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Timo Mämecke.
3 | * This project is released under the MIT License.
4 | */
5 |
6 | export { default } from './lib/BottomNavigation'
7 | export { default as IconTab } from './lib/IconTab'
8 | export { default as FullTab } from './lib/FullTab'
9 | export { default as ShiftingTab } from './lib/ShiftingTab'
10 | export { default as Badge } from './lib/Badge'
11 |
--------------------------------------------------------------------------------
/lib/BackgroundDecorator.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { View, StyleSheet } from 'react-native'
4 |
5 | import BackgroundRippleAnimation from './BackgroundRippleAnimation'
6 |
7 | export default class BackgroundDecorator extends React.Component {
8 | static propTypes = {
9 | children: PropTypes.func.isRequired
10 | }
11 |
12 | state = {
13 | decorators: [],
14 | backgroundColor: 'transparent'
15 | }
16 |
17 | layout = { width: 0, height: 0 }
18 |
19 | addDecorator = decoratorData => {
20 | this.setState(({ decorators }) => ({
21 | decorators: [...decorators, { ...decoratorData, key: Date.now() }]
22 | }))
23 | }
24 |
25 | setBackgroundColor = backgroundColor => {
26 | this.setState({ backgroundColor })
27 | }
28 |
29 | handleLayout = ({ nativeEvent }) => {
30 | const { width, height } = nativeEvent.layout
31 | this.layout = { width, height }
32 | }
33 |
34 | handleAnimationEnd = decorator => () => {
35 | this.setState(({ decorators }) => ({
36 | decorators: decorators.filter(d => d.key !== decorator.key),
37 | backgroundColor: decorator.barColor
38 | }))
39 | }
40 |
41 | render() {
42 | const { backgroundColor, decorators } = this.state
43 |
44 | return (
45 |
46 |
50 | {decorators.map(decorator => (
51 |
60 | ))}
61 |
62 | {this.props.children(this.addDecorator, this.setBackgroundColor)}
63 |
64 | )
65 | }
66 | }
67 |
68 | const styles = StyleSheet.create({
69 | decorators: {
70 | ...StyleSheet.absoluteFillObject,
71 | overflow: 'hidden'
72 | }
73 | })
74 |
--------------------------------------------------------------------------------
/lib/BackgroundRippleAnimation.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Animated, Dimensions, Platform } from 'react-native'
4 |
5 | import * as easings from './utils/easing'
6 |
7 | export default class BackgroundRippleAnimation extends React.PureComponent {
8 | static propTypes = {
9 | containerWidth: PropTypes.number.isRequired,
10 | containerHeight: PropTypes.number.isRequired,
11 | x: PropTypes.number.isRequired,
12 | y: PropTypes.number.isRequired,
13 | color: PropTypes.string.isRequired,
14 | onAnimationEnd: PropTypes.func.isRequired
15 | }
16 |
17 | state = {
18 | animation: new Animated.Value(0)
19 | }
20 |
21 | UNSAFE_componentWillMount() {
22 | this.radius = this.calcRadius()
23 | }
24 |
25 | componentDidMount() {
26 | this.startAnimation()
27 | }
28 |
29 | calcRadius = () => {
30 | const { containerWidth, containerHeight, x, y } = this.props
31 | const testVetices = [
32 | { x: 0, y: 0 }, // top left
33 | { x: containerWidth, y: 0 }, // top right
34 | { x: 0, y: containerHeight }, // bottom left
35 | { x: containerWidth, y: containerHeight } // bottom right
36 | ]
37 |
38 | const possibleRadii = testVetices.map(vertex => {
39 | const dX = vertex.x - x
40 | const dY = vertex.y - y
41 |
42 | const radiusSquared = Math.pow(dX, 2) + Math.pow(dY, 2)
43 | return Math.sqrt(radiusSquared)
44 | })
45 |
46 | return Math.max(...possibleRadii)
47 | }
48 |
49 | startAnimation = () => {
50 | Animated.timing(this.state.animation, {
51 | toValue: 1,
52 | duration: 400,
53 | easing: easings.easeOut,
54 | useNativeDriver: Platform.OS === 'android'
55 | }).start(() => {
56 | this.props.onAnimationEnd()
57 | })
58 | }
59 |
60 | render() {
61 | const { x, y, color } = this.props
62 | const { radius } = this
63 | const diameter = radius * 2
64 |
65 | const scale = this.state.animation.interpolate({
66 | inputRange: [0, 1],
67 | outputRange: [0.01, 1]
68 | })
69 | const opacity = this.state.animation.interpolate({
70 | inputRange: [0, 0.3, 1],
71 | outputRange: [0, 1, 1]
72 | })
73 |
74 | return (
75 |
88 | )
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/lib/Badge.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View, StyleSheet, Text, ViewPropTypes } from 'react-native'
3 | import PropTypes from 'prop-types'
4 |
5 | /**
6 | * A Badge which can be rendered on top of a Tab.
7 | */
8 | export default class Badge extends React.Component {
9 | static propTypes = {
10 | /** Content of the Badge. String and Number will be wrapped in a `Text`. */
11 | children: PropTypes.node,
12 | /** Extends the style of the badge's view. */
13 | style: ViewPropTypes.style,
14 | /** Extends the style of wrapped `Text` component. */
15 | textStyle: Text.propTypes.style
16 | }
17 |
18 | render() {
19 | const { children, style, textStyle } = this.props
20 |
21 | return (
22 |
23 | {typeof children === 'string' || typeof children === 'number' ? (
24 | {children}
25 | ) : (
26 | children
27 | )}
28 |
29 | )
30 | }
31 | }
32 |
33 | const styles = StyleSheet.create({
34 | badge: {
35 | display: 'flex',
36 | alignItems: 'center',
37 | justifyContent: 'center',
38 | paddingHorizontal: 3,
39 | height: 18,
40 | minWidth: 18,
41 | borderRadius: 10,
42 | backgroundColor: 'red'
43 | },
44 | text: {
45 | textAlign: 'center',
46 | fontSize: 10,
47 | fontWeight: 'bold',
48 | color: 'white',
49 | lineHeight: 12
50 | }
51 | })
52 |
--------------------------------------------------------------------------------
/lib/BottomNavigation.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | View,
5 | ViewPropTypes,
6 | StyleSheet,
7 | Platform,
8 | Dimensions
9 | } from 'react-native'
10 |
11 | import Device from './utils/device'
12 | import TabList from './TabList'
13 | import BackgroundDecorator from './BackgroundDecorator'
14 | import PressFeedback from './PressFeedback'
15 |
16 | export const BAR_HEIGHT_ANDROID = 56
17 | export const BAR_HEIGHT_IOS = 49
18 |
19 | /**
20 | * The BottomNavigation renders all tabs and takes care of running animations.
21 | *
22 | * It uses a [render prop](https://reactjs.org/docs/render-props.html) to
23 | * render the tabs, so you can easily customize them without clunky
24 | * configurations.
25 | *
26 | * This library includes multiple configurable Tabs which you can use inside
27 | * the `renderTab` prop. You can also build and use your own tabs.
28 | *
29 | * You can use the BottomNavigation as an uncontrolled or a controlled
30 | * component by using the prop `activeTab`. If you set `activeTab`, the
31 | * BottomNavigation will switch to controlled mode. If a tab is pressed, it
32 | * will only become active if you update the value for `activeTab`.
33 | * You receive tab presses through the prop `onTabPress={(newTab) => ...}`.
34 | * `newTab` is the tab object, you can get its key with `newTab.key`.
35 | * See also: https://reactjs.org/docs/forms.html#controlled-components
36 | *
37 | * If you use it as an uncontrolled component, the tab will automatically
38 | * become active once it's pressed. `onTabPress` will also be called, so you
39 | * can change to another screen.
40 | */
41 | export default class BottomNavigation extends React.Component {
42 | static propTypes = {
43 | /** The config of all tabs. Each item will be called in `renderTab`. */
44 | tabs: PropTypes.arrayOf(
45 | PropTypes.shape({
46 | /** A unique identifier for a tab. */
47 | key: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
48 | .isRequired,
49 | /** The background color of the bottom navigation bar. */
50 | barColor: PropTypes.string,
51 | /** The color of the touch feedback. */
52 | pressColor: PropTypes.string,
53 | /** The diameter of the expanded touch feedback. */
54 | pressSize: PropTypes.number
55 | })
56 | ).isRequired,
57 | /** The render prop to render a tab. Arguments: `({ isActive, tab })` */
58 | renderTab: PropTypes.func.isRequired,
59 | /**
60 | * The identifier of the currently active tab. If you set this, the
61 | * Bottom navigation will become a controlled component.
62 | */
63 | activeTab: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
64 | /**
65 | * The called function when a tab was pressed. Useful to change the active
66 | * tab when you use the Bottom navigation as controlled component. Has
67 | * the tab object of the pressed tab and the currently active tab as
68 | * as parameters.
69 | * Arguments: `(newTab, oldTab)`
70 | */
71 | onTabPress: PropTypes.func,
72 | /**
73 | * If `true`, a LayoutAnimation will be triggered when the active tab
74 | * changes. Necessary to get nice animations when using
75 | * [ShiftingTab](ShiftingTab.md).
76 | */
77 | useLayoutAnimation: PropTypes.bool,
78 | /** Extends the style of the root view. */
79 | style: ViewPropTypes.style,
80 | /**
81 | * (experimental, android only) If you pass the height of the viewport, it
82 | * will check if android soft navigation is enabled and configure the
83 | * BottomNavigation so it looks nice behind the navigation bar.
84 | */
85 | viewportHeight: PropTypes.number
86 | }
87 |
88 | constructor(props) {
89 | super(props)
90 |
91 | this.state = {
92 | isLandscape: Device.isLandscape(),
93 | hasSoftKeysAndroid: Device.hasSoftKeysAndroid(props.viewportHeight)
94 | }
95 | }
96 |
97 | componentDidMount() {
98 | Dimensions.addEventListener('change', this.handleDimensionChange)
99 | }
100 |
101 | UNSAFE_componentWillReceiveProps(nextProps) {
102 | if (Platform.OS !== 'android') return
103 |
104 | if (nextProps.viewportHeight !== this.props.viewportHeight) {
105 | this.setState({
106 | hasSoftKeysAndroid: Device.hasSoftKeysAndroid(nextProps.viewportHeight)
107 | })
108 | }
109 | }
110 |
111 | componentWillUnmount() {
112 | Dimensions.removeEventListener('change', this.handleDimensionChange)
113 | }
114 |
115 | handleDimensionChange = () => {
116 | if (Device.isLandscape() && !this.state.isLandscape) {
117 | this.setState({ isLandscape: true })
118 | } else if (Device.isPortrait() && this.state.isLandscape) {
119 | this.setState({ isLandscape: false })
120 | }
121 | }
122 |
123 | render() {
124 | const { isLandscape, hasSoftKeysAndroid } = this.state
125 | const { style, ...tabProps } = this.props
126 | const extraStyle = [
127 | isLandscape ? orientationStyle.portrait : orientationStyle.landscape,
128 | hasSoftKeysAndroid ? androidStyle.softKeyBar : null
129 | ]
130 |
131 | return (
132 |
133 |
134 | {(addDecorator, setBackgroundColor) => (
135 |
136 | {(addFeedbackIn, enqueueFeedbackOut) => (
137 |
144 | )}
145 |
146 | )}
147 |
148 |
149 | )
150 | }
151 | }
152 |
153 | const androidStyle = StyleSheet.create({
154 | softKeyBar: {
155 | height: BAR_HEIGHT_ANDROID + Device.ANDROID_SOFTKEY_HEIGHT,
156 | paddingBottom: Device.ANDROID_SOFTKEY_HEIGHT
157 | }
158 | })
159 |
160 | const orientationStyle = StyleSheet.create({
161 | landscape: {
162 | ...Device.select({
163 | iPhoneX: {
164 | height: BAR_HEIGHT_IOS + Device.IPHONE_X_BOTTOM_LANDSCAPE,
165 | paddingBottom: Device.IPHONE_X_BOTTOM_LANDSCAPE
166 | }
167 | })
168 | },
169 | portrait: {
170 | ...Device.select({
171 | iPhoneX: {
172 | height: BAR_HEIGHT_IOS + Device.IPHONE_X_BOTTOM_PORTRAIT,
173 | paddingBottom: Device.IPHONE_X_BOTTOM_PORTRAIT
174 | }
175 | })
176 | }
177 | })
178 |
179 | const styles = StyleSheet.create({
180 | bar: {
181 | backgroundColor: 'white',
182 | ...Platform.select({
183 | ios: {
184 | height: BAR_HEIGHT_IOS,
185 | shadowColor: '#000',
186 | shadowOffset: { width: 0, height: 0 },
187 | shadowOpacity: 0.1,
188 | shadowRadius: 3
189 | },
190 | android: {
191 | height: BAR_HEIGHT_ANDROID,
192 | elevation: 8
193 | }
194 | })
195 | }
196 | })
197 |
--------------------------------------------------------------------------------
/lib/FullTab.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | View,
5 | Text,
6 | ViewPropTypes,
7 | Animated,
8 | StyleSheet,
9 | Platform,
10 | TouchableWithoutFeedback
11 | } from 'react-native'
12 |
13 | import * as easings from './utils/easing'
14 |
15 | /**
16 | * A Tab with a label and an icon.
17 | */
18 | export default class FullTab extends React.Component {
19 | static propTypes = {
20 | /** If `true`, the tab is visually active. */
21 | isActive: PropTypes.bool.isRequired,
22 | /** Extends the style of the tab's view. */
23 | style: ViewPropTypes.style,
24 | /** The render prop to render the icon. Arguments: `({ isActive })` */
25 | renderIcon: PropTypes.func.isRequired,
26 | /** The render prop to render the badge. Arguments: `({ isActive })` */
27 | renderBadge: PropTypes.func,
28 | /** If `true`, the badge will be rendered. */
29 | showBadge: PropTypes.bool,
30 | /** Extends the style of the badge's wrapping View. */
31 | badgeSlotStyle: ViewPropTypes.style,
32 | /** The text of the label. */
33 | label: PropTypes.string.isRequired,
34 | /** Extends the style of the label. */
35 | labelStyle: Text.propTypes.style,
36 | /** Useful to add more props to the Text component of the label. */
37 | labelProps: PropTypes.object,
38 | /** The duration of the animation between active and inactive. */
39 | animationDuration: PropTypes.number,
40 | /** The easing function of the animation between active and inactive. */
41 | animationEasing: PropTypes.func,
42 | /**
43 | * Defines the animation of the icon from active to inactive. Receives the
44 | * animation progress (`AnimatedValue` between 0 and 1), needs to return a
45 | * style object.
46 | * See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
47 | */
48 | iconAnimation: PropTypes.func,
49 | /**
50 | * Defines the animation of the label from active to inactive. Receives the
51 | * animation progress (`AnimatedValue` between 0 and 1), needs to return a
52 | * style object.
53 | * See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
54 | */
55 | labelAnimation: PropTypes.func,
56 | /**
57 | * Defines the animation of the badge from active to inactive. Receives the
58 | * animation progress (`AnimatedValue` between 0 and 1), needs to return a
59 | * style object.
60 | * See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
61 | */
62 | badgeAnimation: PropTypes.func
63 | }
64 |
65 | static defaultProps = {
66 | animationDuration: 160,
67 | animationEasing: easings.easeInOut,
68 | showBadge: false,
69 | labelProps: { numberOfLines: 1 },
70 | iconAnimation: progress => ({
71 | transform: [
72 | {
73 | translateY: progress.interpolate({
74 | inputRange: [0, 1],
75 | outputRange: [0, -2]
76 | })
77 | }
78 | ],
79 | opacity: progress.interpolate({
80 | inputRange: [0, 1],
81 | outputRange: [0.8, 1]
82 | })
83 | }),
84 | labelAnimation: progress => ({
85 | transform: [
86 | {
87 | scale: progress.interpolate({
88 | inputRange: [0, 1],
89 | outputRange: [1, 1.12]
90 | })
91 | },
92 | {
93 | translateY: progress.interpolate({
94 | inputRange: [0, 1],
95 | outputRange: [0, -1]
96 | })
97 | }
98 | ],
99 | opacity: progress.interpolate({
100 | inputRange: [0, 1],
101 | outputRange: [0.8, 1]
102 | })
103 | }),
104 | badgeAnimation: progress => ({
105 | transform: [
106 | {
107 | scale: progress.interpolate({
108 | inputRange: [0, 1],
109 | outputRange: [0.9, 1]
110 | })
111 | }
112 | ]
113 | })
114 | }
115 |
116 | constructor(props) {
117 | super(props)
118 |
119 | this.state = {
120 | activeStateTransition: new Animated.Value(props.isActive ? 1 : 0)
121 | }
122 | }
123 |
124 | UNSAFE_componentWillReceiveProps(nextProps) {
125 | if (!this.props.isActive && nextProps.isActive) {
126 | this.animateIn()
127 | }
128 |
129 | if (this.props.isActive && !nextProps.isActive) {
130 | this.animateOut()
131 | }
132 | }
133 |
134 | animateIn = () => this.animateTo(1)
135 | animateOut = () => this.animateTo(0)
136 |
137 | animateTo = value => {
138 | Animated.timing(this.state.activeStateTransition, {
139 | toValue: value,
140 | duration: this.props.animationDuration,
141 | easing: this.props.animationEasing,
142 | useNativeDriver: Platform.OS === 'android'
143 | }).start()
144 | }
145 |
146 | render() {
147 | const {
148 | isActive,
149 | style,
150 | label,
151 | labelStyle,
152 | labelProps,
153 | renderIcon,
154 | renderBadge,
155 | showBadge,
156 | badgeSlotStyle,
157 | animationDuration,
158 | animationEasing,
159 | iconAnimation,
160 | badgeAnimation,
161 |
162 | // `rest` includes the Responder Props from TouchableWithoutFeedback,
163 | // which need to be spreaded to the first `View`.
164 | ...rest
165 | } = this.props
166 | const { activeStateTransition } = this.state
167 | const iconTransitions = this.props.iconAnimation(activeStateTransition)
168 | const labelTransitions = this.props.labelAnimation(activeStateTransition)
169 | const badgeTransitions = this.props.badgeAnimation(activeStateTransition)
170 |
171 | return (
172 |
173 |
174 | {renderIcon({ isActive })}
175 |
176 |
177 |
178 | {label}
179 |
180 |
181 |
182 |
185 | {showBadge && renderBadge({ isActive })}
186 |
187 |
188 |
189 | )
190 | }
191 | }
192 |
193 | const styles = StyleSheet.create({
194 | tab: {
195 | position: 'relative',
196 | flex: 1,
197 | minWidth: 80,
198 | maxWidth: 168,
199 | alignItems: 'center',
200 | justifyContent: 'space-between',
201 | paddingTop: 8,
202 | paddingBottom: 10,
203 | overflow: 'hidden'
204 | },
205 | label: {
206 | color: 'white',
207 | fontSize: 12,
208 | textAlign: 'center'
209 | },
210 | overlay: {
211 | alignItems: 'center',
212 | justifyContent: 'center',
213 | ...StyleSheet.absoluteFillObject
214 | },
215 | badgeSlot: {
216 | flex: -1,
217 | right: -11,
218 | ...Platform.select({
219 | ios: { top: -11 },
220 | android: { top: -14 }
221 | })
222 | }
223 | })
224 |
--------------------------------------------------------------------------------
/lib/IconTab.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | View,
5 | ViewPropTypes,
6 | Animated,
7 | StyleSheet,
8 | Platform,
9 | TouchableWithoutFeedback
10 | } from 'react-native'
11 |
12 | import * as easings from './utils/easing'
13 |
14 | /**
15 | * A Tab with an icon.
16 | */
17 | export default class IconTab extends React.Component {
18 | static propTypes = {
19 | /** If `true`, the tab is visually active. */
20 | isActive: PropTypes.bool.isRequired,
21 | /** Extends the style of the tab's view. */
22 | style: ViewPropTypes.style,
23 | /** The render prop to render the icon. Arguments: `({ isActive })` */
24 | renderIcon: PropTypes.func.isRequired,
25 | /** The render prop to render the badge. Arguments: `({ isActive })` */
26 | renderBadge: PropTypes.func,
27 | /** If `true`, the badge will be rendered. */
28 | showBadge: PropTypes.bool,
29 | /** Extends the style of the badge's wrapping View. */
30 | badgeSlotStyle: ViewPropTypes.style,
31 | /** The duration of the animation between active and inactive. */
32 | animationDuration: PropTypes.number,
33 | /** The easing function of the animation between active and inactive. */
34 | animationEasing: PropTypes.func,
35 | /**
36 | * Defines the animation of the icon from active to inactive. Receives the
37 | * animation progress (`AnimatedValue` between 0 and 1), needs to return a
38 | * style object.
39 | * See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
40 | */
41 | iconAnimation: PropTypes.func,
42 | /**
43 | * Defines the animation of the badge from active to inactive. Receives the
44 | * animation progress (`AnimatedValue` between 0 and 1), needs to return a
45 | * style object.
46 | * See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
47 | */
48 | badgeAnimation: PropTypes.func
49 | }
50 |
51 | static defaultProps = {
52 | animationDuration: 160,
53 | animationEasing: easings.easeInOut,
54 | showBadge: false,
55 | iconAnimation: progress => ({
56 | transform: [
57 | {
58 | scale: progress.interpolate({
59 | inputRange: [0, 1],
60 | outputRange: [1, 1.2]
61 | })
62 | }
63 | ],
64 | opacity: progress.interpolate({
65 | inputRange: [0, 1],
66 | outputRange: [0.8, 1]
67 | })
68 | }),
69 | badgeAnimation: progress => ({
70 | transform: [
71 | {
72 | scale: progress.interpolate({
73 | inputRange: [0, 1],
74 | outputRange: [0.9, 1]
75 | })
76 | }
77 | ]
78 | })
79 | }
80 |
81 | constructor(props) {
82 | super(props)
83 |
84 | this.state = {
85 | activeStateTransition: new Animated.Value(props.isActive ? 1 : 0)
86 | }
87 | }
88 |
89 | UNSAFE_componentWillReceiveProps(nextProps) {
90 | if (!this.props.isActive && nextProps.isActive) {
91 | this.animateIn()
92 | }
93 |
94 | if (this.props.isActive && !nextProps.isActive) {
95 | this.animateOut()
96 | }
97 | }
98 |
99 | animateIn = () => this.animateTo(1)
100 | animateOut = () => this.animateTo(0)
101 |
102 | animateTo = value => {
103 | Animated.timing(this.state.activeStateTransition, {
104 | toValue: value,
105 | duration: this.props.animationDuration,
106 | easing: this.props.animationEasing,
107 | useNativeDriver: Platform.OS === 'android'
108 | }).start()
109 | }
110 |
111 | render() {
112 | const {
113 | renderIcon,
114 | renderBadge,
115 | isActive,
116 | style,
117 | showBadge,
118 | badgeSlotStyle,
119 | animationDuration,
120 | animationEasing,
121 | iconAnimation,
122 | badgeAnimation,
123 |
124 | // Includes the Responder Props from TouchableWithoutFeedback, which
125 | // need to be spreaded to the first `View`.
126 | ...rest
127 | } = this.props
128 | const { activeStateTransition } = this.state
129 | const iconTransitions = this.props.iconAnimation(activeStateTransition)
130 | const badgeTransitions = this.props.badgeAnimation(activeStateTransition)
131 |
132 | return (
133 |
134 |
135 | {renderIcon({ isActive })}
136 |
137 |
138 |
141 | {showBadge && renderBadge({ isActive })}
142 |
143 |
144 |
145 | )
146 | }
147 | }
148 |
149 | const styles = StyleSheet.create({
150 | tab: {
151 | position: 'relative',
152 | flex: 1,
153 | minWidth: 80,
154 | maxWidth: 168,
155 | alignItems: 'center',
156 | justifyContent: 'center',
157 | overflow: 'hidden'
158 | },
159 | overlay: {
160 | position: 'absolute'
161 | },
162 | badgeSlot: {
163 | right: -11,
164 | top: -11
165 | }
166 | })
167 |
--------------------------------------------------------------------------------
/lib/PressFeedback.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { View, StyleSheet } from 'react-native'
4 |
5 | import PressRippleAnimation from './PressRippleAnimation'
6 |
7 | export default class PressFeedback extends React.Component {
8 | static propTypes = {
9 | children: PropTypes.func.isRequired
10 | }
11 |
12 | state = {
13 | presses: []
14 | }
15 |
16 | addFeedbackIn = pressData => {
17 | this.setState(({ presses }) => ({
18 | presses: [...presses, pressData]
19 | }))
20 | }
21 |
22 | enqueueFeedbackOut = pressKey => {
23 | this.setState(({ presses }) => ({
24 | presses: presses.map(press =>
25 | press.key === pressKey ? { ...press, animateOut: true } : press
26 | )
27 | }))
28 | }
29 |
30 | handleOutEnd = outPress => () => {
31 | this.setState(({ presses }) => ({
32 | presses: presses.filter(press => press.key !== outPress.key)
33 | }))
34 | }
35 |
36 | render() {
37 | return (
38 |
39 |
40 | {this.state.presses.map(press => (
41 |
50 | ))}
51 |
52 | {this.props.children(this.addFeedbackIn, this.enqueueFeedbackOut)}
53 |
54 | )
55 | }
56 | }
57 |
58 | const styles = StyleSheet.create({
59 | pressFeedbacks: {
60 | ...StyleSheet.absoluteFillObject,
61 | overflow: 'hidden'
62 | }
63 | })
64 |
--------------------------------------------------------------------------------
/lib/PressRippleAnimation.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Animated, Dimensions, Platform } from 'react-native'
4 |
5 | import * as easings from './utils/easing'
6 |
7 | export default class PressRippleAnimation extends React.PureComponent {
8 | static propTypes = {
9 | x: PropTypes.number.isRequired,
10 | y: PropTypes.number.isRequired,
11 | color: PropTypes.string,
12 | onInEnd: PropTypes.func,
13 | onOutEnd: PropTypes.func,
14 | size: PropTypes.number
15 | }
16 |
17 | static defaultProps = {
18 | color: 'rgba(0, 0, 0, 0.18)',
19 | size: 110,
20 | onInEnd: () => {},
21 | onOutEnd: () => {}
22 | }
23 |
24 | state = {
25 | animation: new Animated.Value(-1)
26 | }
27 |
28 | inAnimationFinished = false
29 | outAnimationRunning = false
30 |
31 | componentDidMount() {
32 | this.runInAnimation()
33 | }
34 |
35 | UNSAFE_componentWillReceiveProps({ animateOut }) {
36 | if (animateOut && !this.props.animateOut) {
37 | this.runOutAnimation()
38 | }
39 | }
40 |
41 | runInAnimation = () => {
42 | Animated.timing(this.state.animation, {
43 | toValue: 0,
44 | duration: 400,
45 | useNativeDriver: Platform.OS === 'android'
46 | }).start(() => {
47 | if (this.props.animateOut && !this.outAnimationRunning) {
48 | this.runOutAnimation()
49 | }
50 |
51 | this.inAnimationFinished = true
52 | this.props.onInEnd()
53 | })
54 | }
55 |
56 | runOutAnimation = () => {
57 | this.outAnimationRunning = true
58 |
59 | Animated.timing(this.state.animation, {
60 | toValue: 1,
61 | duration: this.inAnimationFinished ? 300 : 400,
62 | easing: easings.easeInOut,
63 | useNativeDriver: Platform.OS === 'android'
64 | }).start(() => {
65 | this.props.onOutEnd()
66 | })
67 | }
68 |
69 | render() {
70 | const { x, y, color, size } = this.props
71 |
72 | const scale = this.state.animation.interpolate({
73 | inputRange: [-1, 0, 1],
74 | outputRange: [0.01, 1, 1.2]
75 | })
76 | const opacity = this.state.animation.interpolate({
77 | inputRange: [-1, 0, 1],
78 | outputRange: [1, 1, 0]
79 | })
80 |
81 | return (
82 |
95 | )
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/lib/ShiftingTab.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { StyleSheet, Platform, ViewPropTypes, View } from 'react-native'
4 | import FullTab from './FullTab'
5 |
6 | /**
7 | * A Tab for the shifting bottom navigation bar, implemented according to the
8 | * Bottom navigation specs.
9 | * In its inactive state, only the icon is visible.
10 | * In its active state, the tab's label is also visible, and the tab is wider.
11 | *
12 | * **To enable a nice transition between both states, the `BottomNavigation`
13 | * needs to have the `useLayoutAnimation` prop set to `true`.**
14 | *
15 | * The ShiftingTab is basically a [FullTab](./FullTab.md) with
16 | * predefined style- and animation-props.
17 | */
18 | export default class ShiftingTab extends React.Component {
19 | static propTypes = {
20 | ...FullTab.propTypes,
21 | /** If `true`, the tab is visually active. */
22 | isActive: PropTypes.bool.isRequired,
23 | /** Extends the style of the tab's view. */
24 | style: ViewPropTypes.style,
25 | /**
26 | * Defines the animation of the icon from active to inactive. Receives the
27 | * animation progress (0-1), needs to return a style object.
28 | * See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
29 | */
30 | iconAnimation: PropTypes.func,
31 | /**
32 | * Defines the animation of the label from active to inactive. Receives the
33 | * animation progress (`AnimatedValue` between 0 and 1), needs to return a
34 | * style object.
35 | * See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
36 | */
37 | labelAnimation: PropTypes.func,
38 | /**
39 | * Defines the animation of the badge from active to inactive. Receives the
40 | * animation progress (`AnimatedValue` between 0 and 1), needs to return a
41 | * style object.
42 | * See also: https://facebook.github.io/react-native/docs/animations.html#interpolation
43 | */
44 | badgeAnimation: PropTypes.func
45 | }
46 |
47 | static defaultProps = {
48 | iconAnimation: progress => ({
49 | transform: [
50 | {
51 | translateY: progress.interpolate({
52 | inputRange: [0, 1],
53 | outputRange: [7, 0]
54 | })
55 | }
56 | ],
57 | opacity: progress.interpolate({
58 | inputRange: [0, 1],
59 | outputRange: [0.8, 1]
60 | })
61 | }),
62 | labelAnimation: progress => ({
63 | opacity: progress.interpolate({
64 | inputRange: [0, 1],
65 | outputRange: [0, 1]
66 | })
67 | }),
68 | badgeAnimation: progress => ({
69 | transform: [
70 | {
71 | scale: progress.interpolate({
72 | inputRange: [0, 1],
73 | outputRange: [0.9, 1]
74 | })
75 | },
76 | {
77 | translateY: progress.interpolate({
78 | inputRange: [0, 1],
79 | outputRange: Platform.select({ ios: [9, 4], android: [6, 0] })
80 | })
81 | }
82 | ]
83 | })
84 | }
85 |
86 | render() {
87 | const { isActive, style } = this.props
88 |
89 | return (
90 |
94 | )
95 | }
96 | }
97 |
98 | const styles = StyleSheet.create({
99 | activeTab: {
100 | minWidth: 96,
101 | maxWidth: 168,
102 | flex: 1.75
103 | },
104 | inactiveTab: {
105 | minWidth: 56,
106 | maxWidth: 96,
107 | flex: 1
108 | }
109 | })
110 |
--------------------------------------------------------------------------------
/lib/TabList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | View,
5 | StyleSheet,
6 | TouchableWithoutFeedback,
7 | LayoutAnimation,
8 | Platform,
9 | UIManager
10 | } from 'react-native'
11 |
12 | const SHIFTING_DURATION = 250
13 | const SHIFTING_EASING = LayoutAnimation.Types.easeInEaseOut
14 | const LAYOUT_ANIMATION_CONFIG = LayoutAnimation.create(
15 | SHIFTING_DURATION,
16 | SHIFTING_EASING,
17 | LayoutAnimation.Properties.opacity
18 | )
19 |
20 | export default class TabList extends React.PureComponent {
21 | static propTypes = {
22 | tabs: PropTypes.array,
23 | renderTab: PropTypes.func.isRequired,
24 | activeTab: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
25 | onTabPress: PropTypes.func,
26 | useLayoutAnimation: PropTypes.bool,
27 | setBackgroundColor: PropTypes.func.isRequired,
28 | addDecorator: PropTypes.func.isRequired,
29 | addFeedbackIn: PropTypes.func.isRequired,
30 | enqueueFeedbackOut: PropTypes.func.isRequired
31 | }
32 |
33 | static defaultProps = {
34 | tabs: [],
35 | onTabPress: (newTab, oldTab) => {},
36 |
37 | // Using LayoutAnimation is an opt-in, because it could potentially cause
38 | // unintentional glitches in the App's UI. If it's enabled, the developer
39 | // should be aware of that.
40 | useLayoutAnimation: false
41 | }
42 |
43 | constructor(props) {
44 | super(props)
45 |
46 | // When the user presses a Tab, the Decorator data is stored in here and
47 | // retrieved when the Tab visually becomes active.
48 | // We need to temporarily store it for the controlled component because we
49 | // can't pass the data between the onPress-callbacks without it being too
50 | // much of an hassle for the developer.
51 | this.nextDecorator = null
52 |
53 | this.isControlled = props.activeTab != null
54 |
55 | this.state = {
56 | activeTab: this.isControlled ? props.activeTab : props.tabs[0].key
57 | }
58 | }
59 |
60 | UNSAFE_componentWillMount() {
61 | // Initially set background color
62 | this.props.setBackgroundColor(this.getActiveTab().barColor)
63 |
64 | if (Platform.OS === 'android' && this.props.useLayoutAnimation) {
65 | UIManager.setLayoutAnimationEnabledExperimental &&
66 | UIManager.setLayoutAnimationEnabledExperimental(true)
67 | }
68 | }
69 |
70 | UNSAFE_componentWillReceiveProps({ activeTab: nextActiveTab }) {
71 | if (this.isControlled && nextActiveTab !== this.props.activeTab) {
72 | const { barColor } = this.getTab(nextActiveTab)
73 | this.setActiveTab(nextActiveTab)
74 | if (barColor) this.runDecorator(nextActiveTab)
75 | }
76 | }
77 |
78 | getTab = tabKey => {
79 | return this.props.tabs.find(tab => tab.key === tabKey)
80 | }
81 |
82 | getActiveTab = () => {
83 | return this.getTab(this.state.activeTab)
84 | }
85 |
86 | setActiveTab = activeTab => {
87 | if (this.props.useLayoutAnimation && Platform.OS !== 'web') {
88 | // Delay activeTab update to next frame, so LayoutAnimation won't screw
89 | // up other changes on the screen.
90 | requestAnimationFrame(() => {
91 | LayoutAnimation.configureNext(LAYOUT_ANIMATION_CONFIG)
92 | this.setState({ activeTab })
93 | })
94 | } else {
95 | this.setState({ activeTab })
96 | }
97 | }
98 |
99 | /**
100 | * Sends the Decorator data to the BackgroundDecorator Component.
101 | */
102 | runDecorator = newTabKey => {
103 | // `nextDecorator` could be null if the Tab was changed without a user
104 | // interaction. In this case we just update the backgroundColor.
105 | if (!this.nextDecorator) {
106 | const { barColor } = this.getTab(newTabKey)
107 | this.props.setBackgroundColor(barColor)
108 | return
109 | }
110 |
111 | // Cloning the decorator data prevents mutated data while the Animation is
112 | // being rendered. In theory this could happen, but tbh I don't know if
113 | // it can happen in practice. Just to be safe...
114 | const decorator = { ...this.nextDecorator }
115 | this.nextDecorator = null
116 | this.props.addDecorator(decorator)
117 | }
118 |
119 | /**
120 | * Called when a Tab is pressed.
121 | */
122 | handleTabPress = tab => event => {
123 | const { pageX: x, locationY: y } = event.nativeEvent
124 | const { barColor } = tab
125 | if (barColor) this.nextDecorator = { x, y, barColor }
126 | this.props.onTabPress(tab, this.getActiveTab())
127 |
128 | if (!this.isControlled) {
129 | this.setActiveTab(tab.key)
130 | if (barColor) this.runDecorator(tab.key)
131 | }
132 | }
133 |
134 | /**
135 | * Called at the start of a Tab press.
136 | * Show press feedback.
137 | */
138 | handleTabPressIn = tab => event => {
139 | const { pageX: x, locationY: y } = event.nativeEvent
140 | this.tabPressKey = Date.now()
141 | this.props.addFeedbackIn({
142 | x,
143 | y,
144 | key: this.tabPressKey,
145 | color: tab.pressColor,
146 | size: tab.pressSize
147 | })
148 | }
149 |
150 | /**
151 | * Called at the end of a Tab press.
152 | * Hide press feedback.
153 | */
154 | handleTabPressOut = tab => event => {
155 | this.props.enqueueFeedbackOut(this.tabPressKey)
156 | this.tabPressKey = null
157 | }
158 |
159 | render() {
160 | const { tabs, renderTab } = this.props
161 |
162 | return (
163 |
164 | {tabs.map((tab, i) => (
165 |
171 | {renderTab({
172 | tab,
173 | isActive: tab.key === this.getActiveTab().key
174 | })}
175 |
176 | ))}
177 |
178 | )
179 | }
180 | }
181 |
182 | const styles = StyleSheet.create({
183 | tabs: {
184 | flex: 1,
185 | flexDirection: 'row',
186 | justifyContent: 'center',
187 | alignItems: 'stretch'
188 | }
189 | })
190 |
--------------------------------------------------------------------------------
/lib/utils/device.js:
--------------------------------------------------------------------------------
1 | import { Dimensions, Platform } from 'react-native'
2 |
3 | const IPHONE_X_WIDTH = 375
4 | const IPHONE_X_HEIGHT = 812
5 | const IPHONE_XR_XSMAX_WIDTH = 414
6 | const IPHONE_XR_XSMAX_HEIGHT = 896
7 | const IPHONE_X_BOTTOM_PORTRAIT = 34
8 | const IPHONE_X_BOTTOM_LANDSCAPE = 24
9 | export const ANDROID_SOFTKEY_HEIGHT = 48
10 | export const LANDSCAPE = 'LANDSCAPE'
11 | export const PORTRAIT = 'PORTRAIT'
12 |
13 | export const isIPhoneX = () => {
14 | if (Platform.OS === 'web' || Platform.OS === 'android') return false
15 |
16 | const { width, height } = Dimensions.get('window')
17 | return (
18 | Platform.OS === 'ios' &&
19 | !Platform.isPad &&
20 | !Platform.isTVOS &&
21 | ((height === IPHONE_X_HEIGHT && width === IPHONE_X_WIDTH) ||
22 | (height === IPHONE_X_WIDTH && width === IPHONE_X_HEIGHT) ||
23 | (height === IPHONE_XR_XSMAX_HEIGHT && width === IPHONE_XR_XSMAX_WIDTH) ||
24 | (height === IPHONE_XR_XSMAX_WIDTH && width === IPHONE_XR_XSMAX_HEIGHT))
25 | )
26 | }
27 |
28 | export const getOrientation = () => {
29 | const { width, height } = Dimensions.get('screen')
30 | return width > height ? LANDSCAPE : PORTRAIT
31 | }
32 |
33 | export const isLandscape = () => {
34 | return getOrientation() === LANDSCAPE
35 | }
36 |
37 | export const isPortrait = () => {
38 | return getOrientation() === PORTRAIT
39 | }
40 |
41 | export const hasSoftKeysAndroid = viewportHeight => {
42 | if (Platform.OS === 'android' && isPortrait()) {
43 | const { height: screenHeight } = Dimensions.get('screen')
44 | return screenHeight === viewportHeight
45 | }
46 |
47 | return false
48 | }
49 |
50 | export default {
51 | select({ iPhoneX, androidSoftKeys }) {
52 | return {
53 | ...(isIPhoneX() ? iPhoneX : {})
54 | }
55 | },
56 | isIPhoneX,
57 | hasSoftKeysAndroid,
58 | getOrientation,
59 | isLandscape,
60 | isPortrait,
61 | ANDROID_SOFTKEY_HEIGHT,
62 | IPHONE_X_BOTTOM_LANDSCAPE,
63 | IPHONE_X_BOTTOM_PORTRAIT
64 | }
65 |
--------------------------------------------------------------------------------
/lib/utils/easing.js:
--------------------------------------------------------------------------------
1 | import { Easing } from 'react-native'
2 |
3 | export const easeInOut = new Easing.bezier(0.4, 0.0, 0.2, 1)
4 | export const easeOut = new Easing.bezier(0, 0, 0.2, 1)
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-material-bottom-navigation",
3 | "version": "1.0.5",
4 | "description": "A beautiful, customizable and easy-to-use Material Design Bottom Navigation for react-native.",
5 | "main": "index.js",
6 | "files": [
7 | "index.js",
8 | "index.d.ts",
9 | "flow-typed/",
10 | "lib/",
11 | "docs/"
12 | ],
13 | "scripts": {
14 | "docs:gen:api": "./scripts/docgen",
15 | "test": "jest --all --env=jsdom",
16 | "test:watch": "jest --watch --env=jsdom",
17 | "test:ci": "export JEST_JUNIT_OUTPUT=\"coverage/junit/js-test-results.xml\" && jest --env=jsdom --runInBand --ci --coverage --testResultsProcessor=\"jest-junit\"",
18 | "lint": "eslint .",
19 | "contributors:add": "all-contributors add",
20 | "contributors:generate": "all-contributors generate",
21 | "contributors:check": "all-contributors check"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/timomeh/react-native-material-bottom-navigation.git"
26 | },
27 | "bugs": {
28 | "url": "https://github.com/timomeh/react-native-material-bottom-navigation/issues"
29 | },
30 | "author": "Timo Mämecke ",
31 | "license": "MIT",
32 | "dependencies": {
33 | "prop-types": "^15.6.1"
34 | },
35 | "peerDependencies": {
36 | "react": "*",
37 | "react-native": "*"
38 | },
39 | "devDependencies": {
40 | "all-contributors-cli": "^5.4.1",
41 | "babel-eslint": "^8.2.2",
42 | "babel-jest": "18.0.0",
43 | "babel-preset-react-native": "1.9.1",
44 | "enzyme": "^3.3.0",
45 | "enzyme-adapter-react-16": "^1.1.1",
46 | "eslint": "^4.18.1",
47 | "eslint-config-prettier": "^2.9.0",
48 | "eslint-plugin-prettier": "^2.6.0",
49 | "eslint-plugin-react": "^7.7.0",
50 | "eslint-plugin-react-native": "^3.2.1",
51 | "jest": "22.4.0",
52 | "jest-junit": "^3.6.0",
53 | "prettier": "^1.10.2",
54 | "react": "^16.2.0",
55 | "react-docgen": "^2.20.1",
56 | "react-docgen-markdown-renderer": "^1.0.2",
57 | "react-dom": "^16.2.0",
58 | "react-native": "^0.54.2",
59 | "react-native-mock": "^0.3.1",
60 | "react-native-mock-render": "0.0.22",
61 | "react-test-renderer": "~15.4.0"
62 | },
63 | "types": "./index.d.ts",
64 | "jest": {
65 | "preset": "react-native",
66 | "testURL": "http://localhost",
67 | "setupFiles": [
68 | "/setupTests.js"
69 | ],
70 | "transform": {
71 | "^.+\\.js$": "/node_modules/react-native/jest/preprocessor.js"
72 | },
73 | "modulePathIgnorePatterns": [
74 | "/examples/Playground/node_modules"
75 | ]
76 | },
77 | "keywords": [
78 | "react-native",
79 | "material",
80 | "bottomnavigation",
81 | "bottom navigation",
82 | "ios",
83 | "android",
84 | "react-component",
85 | "react-navigation"
86 | ]
87 | }
88 |
--------------------------------------------------------------------------------
/scripts/docgen:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require('fs')
4 | const path = require('path')
5 | const reactDocs = require('react-docgen')
6 | const ReactDocGenMarkdownRenderer = require('./utils/docgen-markdown')
7 |
8 | const docTemplate = fs.readFileSync(
9 | path.join(__dirname, 'utils', 'doc-template.hbs'),
10 | 'utf-8'
11 | )
12 |
13 | const renderer = new ReactDocGenMarkdownRenderer({
14 | template: docTemplate,
15 | componentsBasePath: path.resolve(__dirname, '..')
16 | })
17 |
18 | const components = [
19 | { input: './Badge.js', output: './docs/api/Badge.md' },
20 | {
21 | input: './BottomNavigation.js',
22 | output: './docs/api/BottomNavigation.md'
23 | },
24 | { input: './FullTab.js', output: './docs/api/FullTab.md' },
25 | { input: './IconTab.js', output: './docs/api/IconTab.md' },
26 | { input: './ShiftingTab.js', output: './docs/api/ShiftingTab.md' }
27 | ]
28 |
29 | // Generate all docs
30 | const allDocgen = components.map(component => {
31 | const componentPath = path.resolve('lib', component.input)
32 | const fileContents = fs.readFileSync(componentPath, 'utf-8')
33 | const doc = reactDocs.parse(fileContents)
34 |
35 | return {
36 | ...component,
37 | doc: {
38 | ...doc,
39 | file: path.join('/', 'lib', component.input)
40 | }
41 | }
42 | })
43 |
44 | // Generate markdown for each doc
45 | allDocgen.forEach(({ input, output, doc }) => {
46 | const composes = doc.composes
47 | ? doc.composes.map(
48 | composesName =>
49 | allDocgen.find(({ input }) => input.includes(composesName)).doc
50 | )
51 | : []
52 |
53 | const docMarkdown = renderer.render(
54 | path.join('/', 'lib', input),
55 | doc,
56 | composes
57 | )
58 | fs.writeFileSync(output, docMarkdown, 'utf-8')
59 | })
60 |
--------------------------------------------------------------------------------
/scripts/utils/doc-template.hbs:
--------------------------------------------------------------------------------
1 |
6 |
7 | # {{componentName}}
8 |
9 | {{#if description}}{{{description}}}{{/if}}
10 |
11 | ## Props
12 |
13 | {{#each this.props}}
14 | ### {{@key}}
15 | {{#if this.required}}**Required.** {{/if}}
16 | Type: `{{> (typePartial this) this}}`
17 |
18 | {{#if this.description}}{{{this.description}}}{{/if}}
19 |
20 |
21 | {{#if this.defaultValue}}
22 | Default: {{ defaultPropBody this.defaultValue }}
23 | {{/if}}
24 |
25 | {{/each}}
26 |
--------------------------------------------------------------------------------
/scripts/utils/docgen-markdown.js:
--------------------------------------------------------------------------------
1 | // Customized version of
2 | // https://github.com/OriR/react-docgen-markdown-renderer
3 |
4 | const path = require('path')
5 | const os = require('os')
6 | const process = require('process')
7 | const handlebars = require('handlebars')
8 |
9 | const getType = obj => {
10 | if ((obj.type || {}).name === 'custom') {
11 | return { name: obj.type.raw }
12 | }
13 |
14 | return obj.type && typeof obj.type.name === 'string'
15 | ? obj.type
16 | : typeof obj.name === 'string' ? obj : undefined
17 | }
18 |
19 | const defaultPropBody = text => {
20 | const lines = text.split(/\r\n|\r|\n/g)
21 | return new handlebars.SafeString(
22 | lines.length > 1 ? ` \n\`\`\`js\n${text}\n\`\`\`` : `\`${text}\``
23 | )
24 | }
25 |
26 | handlebars.registerPartial('Unknown', 'Unknown')
27 |
28 | handlebars.registerPartial('func', 'Function')
29 | handlebars.registerPartial('array', 'Array')
30 | handlebars.registerPartial('object', 'Object')
31 | handlebars.registerPartial('string', 'String')
32 | handlebars.registerPartial('number', 'Number')
33 | handlebars.registerPartial('bool', 'Boolean')
34 | handlebars.registerPartial('node', 'ReactNode')
35 | handlebars.registerPartial('element', 'ReactElement')
36 | handlebars.registerPartial('any', '*')
37 | handlebars.registerPartial('custom', 'custom')
38 | handlebars.registerPartial('shape', 'Shape')
39 | handlebars.registerPartial('ViewPropTypes.style', 'ViewPropTypes.style')
40 | handlebars.registerPartial('Text.propTypes.style', 'Text.propTypes.style')
41 |
42 | handlebars.registerPartial(
43 | 'arrayOf',
44 | 'Array[]<{{#with (typeObject this)}}{{> (typePartial value) value}}{{/with}}>'
45 | )
46 | handlebars.registerPartial(
47 | 'objectOf',
48 | 'Object[#]<{{#with (typeObject this)}}{{> (typePartial value) value}}{{/with}}>'
49 | )
50 | handlebars.registerPartial(
51 | 'instanceOf',
52 | '{{#with (typeObject this)}}{{value}}{{/with}}'
53 | )
54 | handlebars.registerPartial(
55 | 'enum',
56 | 'Enum({{#with (typeObject this)}}{{#each value}}{{{this.value}}}{{#unless @last}},{{/unless}}{{/each}}{{/with}})'
57 | )
58 | handlebars.registerPartial(
59 | 'union',
60 | 'Union<{{#with (typeObject this)}}{{#each value}}{{> (typePartial this) this}}{{#unless @last}} | {{/unless}}{{/each}}{{/with}}>'
61 | )
62 |
63 | handlebars.registerHelper('typeObject', getType)
64 | handlebars.registerHelper('defaultPropBody', defaultPropBody)
65 |
66 | handlebars.registerHelper('typePartial', function(type) {
67 | const partials = [
68 | 'any',
69 | 'array',
70 | 'arrayOf',
71 | 'bool',
72 | 'custom',
73 | 'element',
74 | 'enum',
75 | 'func',
76 | 'node',
77 | 'number',
78 | 'object',
79 | 'string',
80 | 'union',
81 | 'instanceOf',
82 | 'objectOf',
83 | 'shape',
84 | 'ViewPropTypes.style',
85 | 'Text.propTypes.style'
86 | ]
87 | const typeObj = getType(type)
88 | return typeObj && partials.includes(typeObj.name) ? typeObj.name : 'Unknown'
89 | })
90 |
91 | const defaultTemplate = `
92 | ## {{componentName}}
93 |
94 | {{#if srcLink }}From [\`{{srcLink}}\`]({{srcLink}}){{/if}}
95 |
96 | {{#if description}}{{{description}}}{{/if}}
97 |
98 | prop | type | default | required | description
99 | ---- | :----: | :-------: | :--------: | -----------
100 | {{#each props}}
101 | **{{@key}}** | \`{{> (typePartial this) this}}\` | {{#if this.defaultValue}}\`{{{this.defaultValue}}}\`{{/if}} | {{#if this.required}}:white_check_mark:{{else}}:x:{{/if}} | {{#if this.description}}{{{this.description}}}{{/if}}
102 | {{/each}}
103 |
104 | {{#if isMissingComposes}}
105 | *Some or all of the composed components are missing from the list below because a documentation couldn't be generated for them.
106 | See the source code of the component for more information.*
107 | {{/if}}
108 |
109 | {{#if composes.length}}
110 | {{componentName}} gets more \`propTypes\` from these composed components
111 | {{/if}}
112 |
113 | {{#each composes}}
114 | #### {{this.componentName}}
115 |
116 | prop | type | default | required | description
117 | ---- | :----: | :-------: | :--------: | -----------
118 | {{#each this.props}}
119 | **{{@key}}** | \`{{> (typePartial this) this}}\` | {{#if this.defaultValue}}\`{{{this.defaultValue}}}\`{{/if}} | {{#if this.required}}:white_check_mark:{{else}}:x:{{/if}} | {{#if this.description}}{{{this.description}}}{{/if}}
120 | {{/each}}
121 |
122 | {{/each}}
123 | `
124 |
125 | let typeFlatteners = {}
126 |
127 | const replaceNewLine = value => value.replace(new RegExp(os.EOL, 'g'), ' ')
128 | const normalizeValue = (value, hasInnerValue) =>
129 | value ? (hasInnerValue ? value.value : value) : value
130 |
131 | const flattenProp = (seed, currentObj, name, isImmediateNesting) => {
132 | const typeObject = getType(currentObj)
133 |
134 | if (typeObject) {
135 | const flattener = typeFlatteners[typeObject.name] || (() => {})
136 | flattener(seed, typeObject, name)
137 | }
138 |
139 | if (!isImmediateNesting) {
140 | seed[name] = Object.assign({}, currentObj, {
141 | description: normalizeValue(currentObj.description, false),
142 | defaultValue: normalizeValue(currentObj.defaultValue, true)
143 | })
144 | }
145 | }
146 |
147 | typeFlatteners = {
148 | arrayOf(seed, arrayType, name) {
149 | flattenProp(seed, arrayType.value, name + '[]', true)
150 | },
151 | shape(seed, shapeType, name) {
152 | Object.keys(shapeType.value).forEach(inner => {
153 | flattenProp(seed, shapeType.value[inner], name + '.' + inner)
154 | })
155 | },
156 | objectOf(seed, objectOfType, name) {
157 | flattenProp(seed, objectOfType.value, name + '[#]', true)
158 | }
159 | }
160 |
161 | const flattenProps = props => {
162 | const sortedProps = {}
163 | if (props) {
164 | const flattenedProps = Object.keys(props).reduce((seed, prop) => {
165 | flattenProp(seed, props[prop], prop)
166 | return seed
167 | }, {})
168 |
169 | Object.keys(flattenedProps)
170 | .sort()
171 | .forEach(key => {
172 | sortedProps[key] = flattenedProps[key]
173 | })
174 | }
175 |
176 | return sortedProps
177 | }
178 |
179 | class ReactDocGenMarkdownRenderer {
180 | constructor(options) {
181 | this.options = Object.assign(
182 | {
183 | componentsBasePath: process.cwd(),
184 | template: defaultTemplate
185 | },
186 | options
187 | )
188 |
189 | this.template = handlebars.compile(this.options.template)
190 | this.extension = '.md'
191 | }
192 |
193 | render(file, docs, composes) {
194 | const componentName = path.basename(file, path.extname(file))
195 |
196 | const sortedProps = flattenProps(docs.props)
197 |
198 | const composesFlattened = []
199 | if (composes.length !== 0) {
200 | composes.forEach(compose => {
201 | composesFlattened.push({
202 | srcLink: compose.file.replace(
203 | this.options.componentsBasePath + '/',
204 | ''
205 | ),
206 | componentName: compose.displayName,
207 | props: flattenProps(compose.props)
208 | })
209 | })
210 | }
211 |
212 | const mergedProps = Object.assign(
213 | {},
214 | ...composesFlattened.map(({ props }) => props),
215 | sortedProps
216 | )
217 |
218 | const mergedSortedProps = Object.keys(mergedProps)
219 | .sort()
220 | .reduce((acc, val) => {
221 | acc[val] = mergedProps[val]
222 | return acc
223 | }, {})
224 |
225 | return this.template({
226 | componentName,
227 | srcLink: file.replace(this.options.componentsBasePath + '/', ''),
228 | description: docs.description,
229 | isMissingComposes: (docs.composes || []).length !== composes.length,
230 | props: mergedSortedProps,
231 | composes: composesFlattened
232 | })
233 | }
234 | }
235 |
236 | module.exports = ReactDocGenMarkdownRenderer
237 |
--------------------------------------------------------------------------------
/setupTests.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme'
2 | import Adapter from 'enzyme-adapter-react-16'
3 |
4 | Enzyme.configure({ adapter: new Adapter() })
5 |
--------------------------------------------------------------------------------