├── .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 | react-native-material-bottom-navigation 3 |

4 | 5 |
6 | 7 |

8 | 9 | npm version 10 | 11 | 12 | downloads 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
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
Shayan Javadi](https://www.shayanjavadi.com/)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=ShayanJavadi "Code") | [David
David](https://github.com/davidmpr)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=davidmpr "Code") | [Jayser Mendez
Jayser Mendez](http://steemia.io)
[📖](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=jayserdny "Documentation") | [Peter Kottas
Peter Kottas](https://www.facebook.com/tipsforholiday)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=PeterKottas "Code") | [Matt Oakes
Matt Oakes](https://mattoakes.net)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=matt-oakes "Code") | [Keeley Carrigan
Keeley Carrigan](http://www.keeleycarrigan.com)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=keeleycarrigan "Code") | 173 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 174 | | [Sean Holbert
Sean Holbert](http://www.twitter.com/wildseansy)
[💻](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=wildseansy "Code") | [Alessandro Parolin
Alessandro Parolin](https://github.com/aparolin)
[📖](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=aparolin "Documentation") | [Prashanth Acharya M
Prashanth Acharya M](https://github.com/prashantham)
[📖](https://github.com/timomeh/react-native-material-bottom-navigation/commits?author=prashantham "Documentation") | [Alexey Tcherevatov
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
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 | --------------------------------------------------------------------------------