├── jest.setup.js ├── .gitignore ├── .npmignore ├── babel.config.js ├── src ├── utils │ ├── arr.js │ ├── platform.js │ ├── prop-types.js │ └── obj.js ├── assets │ ├── icons │ │ ├── info.png │ │ ├── close.png │ │ ├── error.png │ │ ├── close@2x.png │ │ ├── close@3x.png │ │ ├── error@2x.png │ │ ├── error@3x.png │ │ ├── info@2x.png │ │ ├── info@3x.png │ │ ├── success.png │ │ ├── success@2x.png │ │ └── success@3x.png │ └── index.js ├── components │ ├── icon │ │ ├── styles.js │ │ └── index.js │ ├── info.js │ ├── error.js │ ├── success.js │ ├── __tests__ │ │ ├── Icon.test.js │ │ ├── InfoToast.test.js │ │ ├── ErrorToast.test.js │ │ ├── SuccessToast.test.js │ │ └── Base.test.js │ └── base │ │ ├── styles.js │ │ └── index.js ├── colors │ └── index.js ├── styles.js ├── __tests__ │ └── Toast.test.js └── index.js ├── success-toast.gif ├── index.js ├── jest.config.js ├── .prettierrc.js ├── .eslintrc.js ├── .github ├── workflows │ ├── tag.yml │ ├── publish.yml │ └── tests.yml └── dependabot.yml ├── LICENSE ├── package.json ├── index.d.ts ├── CHANGELOG.md └── README.md /jest.setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .git 3 | node_modules 4 | .idea 5 | coverage 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | coverage 3 | .eslintrc.js 4 | .prettierrc.js 5 | success-toast.gif 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'] 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/arr.js: -------------------------------------------------------------------------------- 1 | const complement = (arr) => arr.map((i) => -i); 2 | 3 | export { complement }; 4 | -------------------------------------------------------------------------------- /success-toast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/success-toast.gif -------------------------------------------------------------------------------- /src/assets/icons/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/info.png -------------------------------------------------------------------------------- /src/assets/icons/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/close.png -------------------------------------------------------------------------------- /src/assets/icons/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/error.png -------------------------------------------------------------------------------- /src/assets/icons/close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/close@2x.png -------------------------------------------------------------------------------- /src/assets/icons/close@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/close@3x.png -------------------------------------------------------------------------------- /src/assets/icons/error@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/error@2x.png -------------------------------------------------------------------------------- /src/assets/icons/error@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/error@3x.png -------------------------------------------------------------------------------- /src/assets/icons/info@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/info@2x.png -------------------------------------------------------------------------------- /src/assets/icons/info@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/info@3x.png -------------------------------------------------------------------------------- /src/assets/icons/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/success.png -------------------------------------------------------------------------------- /src/assets/icons/success@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/success@2x.png -------------------------------------------------------------------------------- /src/assets/icons/success@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/react-native-toast-message/master/src/assets/icons/success@3x.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Toast from './src'; 2 | 3 | export { default as BaseToast } from './src/components/base'; 4 | export default Toast; 5 | -------------------------------------------------------------------------------- /src/utils/platform.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | 3 | const isIOS = Platform.OS === 'ios'; 4 | 5 | export { isIOS }; 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | setupFiles: ['./jest.setup.js'], 4 | setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'] 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/icon/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | base: { 5 | height: 16, 6 | width: 16 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /src/utils/prop-types.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const stylePropType = PropTypes.oneOfType([PropTypes.object, PropTypes.number]); 4 | 5 | export { stylePropType }; 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'none', 3 | tabWidth: 2, 4 | singleQuote: true, 5 | jsxSingleQuote: true, 6 | jsxBracketSameLine: true, 7 | arrowParens: 'always' 8 | }; 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['backpacker-react'], 3 | rules: { 4 | 'import/no-extraneous-dependencies': 'off', 5 | 'react/sort-comp': 'off', 6 | 'no-underscore-dangle': 'warn' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/colors/index.js: -------------------------------------------------------------------------------- 1 | const colors = { 2 | white: '#FFF', 3 | blazeOrange: '#FE6301', 4 | mantis: '#69C779', 5 | alto: '#D8D8D8', 6 | dustyGray: '#979797', 7 | lightSkyBlue: '#87CEFA' 8 | }; 9 | 10 | export default colors; 11 | -------------------------------------------------------------------------------- /src/utils/obj.js: -------------------------------------------------------------------------------- 1 | const includeKeys = ({ obj = {}, keys = [] }) => 2 | Object.keys(obj).reduce((acc, key) => { 3 | if (keys.includes(key)) { 4 | acc[key] = obj[key]; 5 | } 6 | return acc; 7 | }, {}); 8 | 9 | export { includeKeys }; 10 | -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | base: { 5 | position: 'absolute', 6 | alignItems: 'center', 7 | justifyContent: 'center', 8 | left: 0, 9 | right: 0 10 | }, 11 | top: { 12 | top: 0 13 | }, 14 | bottom: { 15 | bottom: 0 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Create Tag 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: Klemensas/action-autotag@stable 14 | with: 15 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 16 | tag_prefix: 'v' 17 | -------------------------------------------------------------------------------- /src/assets/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | const assets = { 4 | icons: { 5 | success: require('./icons/success.png'), 6 | error: require('./icons/error.png'), 7 | info: require('./icons/info.png'), 8 | close: require('./icons/close.png') 9 | } 10 | }; 11 | 12 | const { icons } = assets; 13 | 14 | export { icons }; 15 | export default assets; 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "03:00" 8 | open-pull-requests-limit: 10 9 | reviewers: 10 | - calintamas 11 | ignore: 12 | - dependency-name: husky 13 | versions: 14 | - "> 3.1.0" 15 | - dependency-name: husky 16 | versions: 17 | - ">= 4.a, < 5" 18 | -------------------------------------------------------------------------------- /src/components/info.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import BaseToast from './base'; 4 | import { icons } from '../assets'; 5 | import colors from '../colors'; 6 | 7 | function InfoToast(props) { 8 | return ( 9 | 14 | ); 15 | } 16 | 17 | InfoToast.propTypes = BaseToast.propTypes; 18 | 19 | export default InfoToast; 20 | -------------------------------------------------------------------------------- /src/components/error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import BaseToast from './base'; 4 | import { icons } from '../assets'; 5 | import colors from '../colors'; 6 | 7 | function ErrorToast(props) { 8 | return ( 9 | 14 | ); 15 | } 16 | 17 | ErrorToast.propTypes = BaseToast.propTypes; 18 | 19 | export default ErrorToast; 20 | -------------------------------------------------------------------------------- /src/components/success.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import BaseToast from './base'; 4 | import { icons } from '../assets'; 5 | import colors from '../colors'; 6 | 7 | function SuccessToast(props) { 8 | return ( 9 | 14 | ); 15 | } 16 | 17 | SuccessToast.propTypes = BaseToast.propTypes; 18 | 19 | export default SuccessToast; 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | - run: npm install -g yarn 17 | - name: Install dependencies 18 | run: yarn 19 | - id: publish 20 | uses: JS-DevTools/npm-publish@v1 21 | with: 22 | token: ${{ secrets.NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /src/components/__tests__/Icon.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import { render } from '@testing-library/react-native'; 4 | import React from 'react'; 5 | 6 | import Icon from '../icon'; 7 | 8 | describe('test Icon component', () => { 9 | it('does not render anything', () => { 10 | const { queryByTestId } = render(); 11 | const icon = queryByTestId('icon'); 12 | expect(icon).toBeFalsy(); 13 | }); 14 | 15 | it('renders an icon', () => { 16 | const mockIcon = { uri: 'mock' }; 17 | const { queryByTestId } = render(); 18 | const icon = queryByTestId('icon'); 19 | expect(icon.props.source).toBe(mockIcon); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/__tests__/InfoToast.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import { render } from '@testing-library/react-native'; 4 | import React from 'react'; 5 | 6 | import colors from '../../colors'; 7 | import { icons } from '../../assets'; 8 | import InfoToast from '../info'; 9 | 10 | describe('test InfoToast component', () => { 11 | it('renders style correctly', () => { 12 | const { queryByTestId } = render(); 13 | const rootView = queryByTestId('rootView'); 14 | const leadingIcon = queryByTestId('leadingIcon'); 15 | 16 | expect(rootView).toHaveStyle({ 17 | borderLeftColor: colors.lightSkyBlue 18 | }); 19 | expect(leadingIcon.children[0].props.source).toBe(icons.info); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/__tests__/ErrorToast.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import { render } from '@testing-library/react-native'; 4 | import React from 'react'; 5 | 6 | import colors from '../../colors'; 7 | import { icons } from '../../assets'; 8 | import ErrorToast from '../error'; 9 | 10 | describe('test ErrorToast component', () => { 11 | it('renders style correctly', () => { 12 | const { queryByTestId } = render(); 13 | const rootView = queryByTestId('rootView'); 14 | const leadingIcon = queryByTestId('leadingIcon'); 15 | 16 | expect(rootView).toHaveStyle({ 17 | borderLeftColor: colors.blazeOrange 18 | }); 19 | expect(leadingIcon.children[0].props.source).toBe(icons.error); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/__tests__/SuccessToast.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import { render } from '@testing-library/react-native'; 4 | import React from 'react'; 5 | 6 | import colors from '../../colors'; 7 | import { icons } from '../../assets'; 8 | import SuccessToast from '../success'; 9 | 10 | describe('test SuccessToast component', () => { 11 | it('renders style correctly', () => { 12 | const { queryByTestId } = render(); 13 | const rootView = queryByTestId('rootView'); 14 | const leadingIcon = queryByTestId('leadingIcon'); 15 | 16 | expect(rootView).toHaveStyle({ 17 | borderLeftColor: colors.mantis 18 | }); 19 | expect(leadingIcon.children[0].props.source).toBe(icons.success); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/icon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import { stylePropType } from '../../utils/prop-types'; 6 | import styles from './styles'; 7 | 8 | function Icon({ source, style }) { 9 | if (!source) { 10 | return null; 11 | } 12 | 13 | return ( 14 | 20 | ); 21 | } 22 | 23 | const imageSourcePropType = PropTypes.oneOfType([ 24 | PropTypes.number, 25 | PropTypes.shape({ 26 | uri: PropTypes.string 27 | }) 28 | ]); 29 | 30 | Icon.propTypes = { 31 | source: imageSourcePropType, 32 | style: stylePropType 33 | }; 34 | 35 | Icon.defaultProps = { 36 | source: undefined, 37 | style: undefined 38 | }; 39 | 40 | export default Icon; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Calin Tamas 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 | -------------------------------------------------------------------------------- /src/components/base/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import colors from '../../colors'; 3 | 4 | export const HEIGHT = 60; 5 | 6 | export default StyleSheet.create({ 7 | base: { 8 | flexDirection: 'row', 9 | height: HEIGHT, 10 | width: '90%', 11 | borderRadius: 6, 12 | backgroundColor: colors.white, 13 | shadowOffset: { width: 0, height: 0 }, 14 | shadowOpacity: 0.1, 15 | shadowRadius: 6, 16 | elevation: 2 17 | }, 18 | borderLeft: { 19 | borderLeftWidth: 5, 20 | borderLeftColor: colors.alto 21 | }, 22 | leadingIconContainer: { 23 | width: 50, 24 | justifyContent: 'center', 25 | alignItems: 'center' 26 | }, 27 | contentContainer: { 28 | flex: 1, 29 | justifyContent: 'center', 30 | alignItems: 'flex-start' // in case of rtl the text will start from the right 31 | }, 32 | trailingIconContainer: { 33 | width: 40, 34 | justifyContent: 'center', 35 | alignItems: 'center' 36 | }, 37 | leadingIcon: { 38 | width: 20, 39 | height: 20 40 | }, 41 | trailingIcon: { 42 | width: 9, 43 | height: 9 44 | }, 45 | text1: { 46 | fontSize: 12, 47 | fontWeight: 'bold', 48 | marginBottom: 3 49 | }, 50 | text2: { 51 | fontSize: 10, 52 | color: colors.dustyGray 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v1 11 | 12 | - name: Setup Node.js 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | 17 | - name: Get yarn cache directory path 18 | id: yarn-cache-dir-path 19 | run: echo "::set-output name=dir::$(yarn cache dir)" 20 | 21 | - name: Restore yarn cache 22 | uses: actions/cache@v2 23 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 24 | with: 25 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-yarn- 29 | 30 | - name: Install dependencies 31 | run: yarn --prefer-offline 32 | 33 | - name: Setup React Native environment 34 | run: | 35 | yarn add react-native@0.63.4 36 | yarn add react@16.13.1 37 | 38 | - name: Run tests 39 | run: yarn test --coverage 40 | 41 | - name: Coveralls 42 | uses: coverallsapp/github-action@master 43 | with: 44 | github-token: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-toast-message", 3 | "version": "1.4.9", 4 | "description": "Toast message component for React Native", 5 | "main": "index.js", 6 | "scripts": { 7 | "hooks:install": "node ./node_modules/husky/bin/install", 8 | "lint": "./node_modules/.bin/eslint . --ext=jsx --ext=js", 9 | "lint-staged": "./node_modules/.bin/lint-staged", 10 | "prettier": "./node_modules/.bin/prettier . --write", 11 | "test": "./node_modules/.bin/jest" 12 | }, 13 | "lint-staged": { 14 | "**/*.js": [ 15 | "prettier --write", 16 | "eslint" 17 | ] 18 | }, 19 | "husky": { 20 | "hooks": { 21 | "pre-commit": "lint-staged" 22 | } 23 | }, 24 | "keywords": [ 25 | "react-native", 26 | "toast" 27 | ], 28 | "author": "Calin Tamas ", 29 | "license": "MIT", 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/calintamas/react-native-toast-message.git" 33 | }, 34 | "devDependencies": { 35 | "@testing-library/jest-native": "^4.0.1", 36 | "@testing-library/react-native": "^7.1.0", 37 | "@types/jest": "^26.0.20", 38 | "eslint-config-backpacker-react": "^0.3.0", 39 | "husky": "^3.1.0", 40 | "jest": "^26.6.3", 41 | "lint-staged": "^10.2.2", 42 | "metro-react-native-babel-preset": "^0.66.0", 43 | "prettier": "2.2.1", 44 | "react-test-renderer": "^17.0.1" 45 | }, 46 | "dependencies": { 47 | "prop-types": "^15.7.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ViewStyle, TextStyle, ImageSourcePropType } from 'react-native' 3 | 4 | declare module 'react-native-toast-message' { 5 | interface AnyObject { 6 | [key: string]: any; 7 | } 8 | 9 | export type ToastPosition = 'top' | 'bottom' 10 | 11 | export interface BaseToastProps { 12 | leadingIcon?: ImageSourcePropType, 13 | trailingIcon?: ImageSourcePropType, 14 | text1?: string, 15 | text2?: string, 16 | onPress?: () => void, 17 | onTrailingIconPress?: () => void, 18 | onLeadingIconPress?: () => void, 19 | style?: ViewStyle, 20 | leadingIconContainerStyle?: ViewStyle, 21 | trailingIconContainerStyle?: ViewStyle, 22 | leadingIconStyle?: ViewStyle, 23 | trailingIconStyle?: ViewStyle, 24 | contentContainerStyle?: ViewStyle, 25 | text1Style?: TextStyle, 26 | text2Style?: TextStyle, 27 | activeOpacity?: number, 28 | text1NumberOfLines: number, 29 | text2NumberOfLines: number, 30 | } 31 | export const BaseToast: React.FC 32 | 33 | export interface ToastProps { 34 | ref: (ref: any) => any; 35 | config?: AnyObject, 36 | style?: ViewStyle, 37 | topOffset?: number, 38 | bottomOffset?: number, 39 | keyboardOffset?: number, 40 | visibilityTime?: number, 41 | autoHide?: boolean, 42 | height?: number, 43 | position?: ToastPosition, 44 | type?: string 45 | } 46 | 47 | export default class Toast extends React.Component { 48 | static show(options: { 49 | type: string, 50 | position?: ToastPosition, 51 | text1?: string, 52 | text2?: string, 53 | visibilityTime?: number, 54 | autoHide?: boolean, 55 | topOffset?: number, 56 | bottomOffset?: number, 57 | props?: AnyObject, 58 | onShow?: () => void, 59 | onHide?: () => void, 60 | onPress?: () => void 61 | }): void; 62 | 63 | static hide(): void; 64 | 65 | static setRef(ref: any): any; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/base/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, TouchableOpacity, Text } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import Icon from '../icon'; 6 | import { icons } from '../../assets'; 7 | import { stylePropType } from '../../utils/prop-types'; 8 | import styles, { HEIGHT } from './styles'; 9 | 10 | function BaseToast({ 11 | leadingIcon, 12 | trailingIcon, 13 | text1, 14 | text2, 15 | onPress, 16 | onLeadingIconPress, 17 | onTrailingIconPress, 18 | style, 19 | leadingIconContainerStyle, 20 | trailingIconContainerStyle, 21 | leadingIconStyle, 22 | trailingIconStyle, 23 | contentContainerStyle, 24 | text1Style, 25 | text2Style, 26 | activeOpacity, 27 | text1NumberOfLines, 28 | text2NumberOfLines 29 | }) { 30 | return ( 31 | 36 | {leadingIcon && ( 37 | 42 | 46 | 47 | )} 48 | 49 | 52 | {text1?.length > 0 && ( 53 | 54 | 58 | {text1} 59 | 60 | 61 | )} 62 | {text2?.length > 0 && ( 63 | 64 | 68 | {text2} 69 | 70 | 71 | )} 72 | 73 | 74 | {trailingIcon && ( 75 | 80 | 84 | 85 | )} 86 | 87 | ); 88 | } 89 | 90 | BaseToast.HEIGHT = HEIGHT; 91 | 92 | BaseToast.propTypes = { 93 | leadingIcon: Icon.propTypes.source, 94 | trailingIcon: Icon.propTypes.source, 95 | text1: PropTypes.string, 96 | text2: PropTypes.string, 97 | onPress: PropTypes.func, 98 | onTrailingIconPress: PropTypes.func, 99 | onLeadingIconPress: PropTypes.func, 100 | style: stylePropType, 101 | leadingIconContainerStyle: stylePropType, 102 | trailingIconContainerStyle: stylePropType, 103 | leadingIconStyle: stylePropType, 104 | trailingIconStyle: stylePropType, 105 | contentContainerStyle: stylePropType, 106 | text1Style: stylePropType, 107 | text2Style: stylePropType, 108 | activeOpacity: PropTypes.number, 109 | text1NumberOfLines: PropTypes.number, 110 | text2NumberOfLines: PropTypes.number 111 | }; 112 | 113 | BaseToast.defaultProps = { 114 | leadingIcon: undefined, 115 | trailingIcon: icons.close, 116 | text1: undefined, 117 | text2: undefined, 118 | onPress: undefined, 119 | onLeadingIconPress: undefined, 120 | onTrailingIconPress: undefined, 121 | style: undefined, 122 | leadingIconContainerStyle: undefined, 123 | trailingIconContainerStyle: undefined, 124 | leadingIconStyle: undefined, 125 | trailingIconStyle: undefined, 126 | contentContainerStyle: undefined, 127 | text1Style: undefined, 128 | text2Style: undefined, 129 | activeOpacity: 0.8, 130 | text1NumberOfLines: 1, 131 | text2NumberOfLines: 2 132 | }; 133 | 134 | export default BaseToast; 135 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 4 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 5 | 6 | ⚠️ The changelog should be **human-readable**, so everything that's added here should be easy to understand without additional lookups and checks 7 | 8 | Headers are one of: 9 | 10 | - `Added`, `Changed`, `Removed` or `Fixed`. 11 | 12 | ## [1.4.9] 13 | 14 | ### Fixed 15 | 16 | - Fix Keyboard pushing Toast too much on Android when displayed with position `bottom` ([#161](https://github.com/calintamas/react-native-toast-message/pull/161)) 17 | 18 | ## [1.4.8] 19 | 20 | ### Added 21 | 22 | - Add types for `text1NumberOfLines` and `text2NumberOfLines` ([#152](https://github.com/calintamas/react-native-toast-message/pull/152)) 23 | 24 | ## [1.4.7] 25 | 26 | ### Fixed 27 | 28 | - Fix proptype regression ([#151](https://github.com/calintamas/react-native-toast-message/pull/151)) 29 | 30 | ## [1.4.6] 31 | 32 | ### Fixed 33 | 34 | - Fix type declaration file ([#148](https://github.com/calintamas/react-native-toast-message/pull/148)) 35 | 36 | ## [1.4.5] 37 | 38 | ### Fixed 39 | 40 | - Remove dependency on ViewPropTypes ([#147](https://github.com/calintamas/react-native-toast-message/pull/147)) 41 | 42 | ## [1.4.4] 43 | 44 | ### Changed 45 | 46 | - Move eslint-plugin-prettier to dev dependencies ([#135](https://github.com/calintamas/react-native-toast-message/pull/135)) 47 | - Increase the threshold to register a swipe on the toast container ([#144](https://github.com/calintamas/react-native-toast-message/pull/144)) 48 | 49 | ## [1.4.3] 50 | 51 | ### Fixed 52 | 53 | - Fix type definitions ([#127](https://github.com/calintamas/react-native-toast-message/pull/127)) 54 | - Reset customProps every time show is called ([#128](https://github.com/calintamas/react-native-toast-message/pull/128)) 55 | 56 | ## [1.4.2] 57 | 58 | ### Fixed 59 | 60 | - Fix `onPress` handler for custom components 61 | 62 | ## [1.4.1] 63 | 64 | ### Fixed 65 | 66 | - Fix type declaration file 67 | 68 | ## [1.4.0] 69 | 70 | ### Added 71 | 72 | - Add `onPress` to `Toast.show` method 73 | - Export `BaseToast` component to allow styling 74 | - Add `topOffset`, `bottomOffset` and `visibilityTime` as instance props 75 | - When shown with `position: bottom`, Toast is now Keyboard aware 76 | 77 | ## [1.3.7] 78 | 79 | ### Added 80 | 81 | - Add Typescript declaration file ([#94](https://github.com/calintamas/react-native-toast-message/pull/94)) 82 | 83 | ### Fixed 84 | 85 | - Allow style prop to style the base component ([#93](https://github.com/calintamas/react-native-toast-message/pull/93)) 86 | 87 | ## [1.3.6] 88 | 89 | ### Fixed 90 | 91 | - Custom render props are now part of the initial state. This removes the need to use optional chaining when defining a custom toast `config` 92 | 93 | ## [1.3.5] 94 | 95 | ### Added 96 | 97 | - Allow arbitrary data to be passed into Toasts ([#81](https://github.com/calintamas/react-native-toast-message/pull/81)) 98 | 99 | ### Fixed 100 | 101 | - In case of RTL the text will start from the right ([#84](https://github.com/calintamas/react-native-toast-message/pull/84)) 102 | - null is not an object (evaluating 'this.\_ref.show') ([#90](https://github.com/calintamas/react-native-toast-message/pull/90)) 103 | 104 | ## [1.3.4] 105 | 106 | ### Fixed 107 | 108 | - Shadows not visible on Android ([#51](https://github.com/calintamas/react-native-toast-message/pull/51)) 109 | 110 | ## [1.3.3] 111 | 112 | ### Fixed 113 | 114 | - `position: bottom`, damping value must be grater than 0 error ([#48](https://github.com/calintamas/react-native-toast-message/pull/48)) 115 | 116 | ## [1.3.2] 117 | 118 | ### Changed 119 | 120 | - Given texts `text1` and `text2` are rendered conditionally now ([#40](https://github.com/calintamas/react-native-toast-message/pull/40)) 121 | 122 | ### Fixed 123 | 124 | - Custom toast does not hide completely if its `height` is greater than the default 60 125 | 126 | ## [1.3.1] 127 | 128 | ### Fixed 129 | 130 | - Fix typescript import err 131 | 132 | ## [1.3.0] 133 | 134 | ### Added 135 | 136 | - Render custom toast types using a `config` prop 137 | - A default `info` type toast 138 | - `onShow` and `onHide` callbacks when using `Toast.show({ onShow, onHide })` 139 | 140 | ### Changed 141 | 142 | - `autoHide` is now `true` by default 143 | 144 | ### Removed 145 | 146 | - `renderSuccessToast` and `renderErrorToast` props are no longer relevant, so they were removed 147 | 148 | ### Fixed 149 | 150 | - `onHide` is called when the toast is dismissed by a swipe gesture 151 | 152 | ## [1.2.3] 153 | 154 | ### Fixed 155 | 156 | - Android status bar has bottom shadow 157 | 158 | ## [1.2.2] 159 | 160 | ### Added 161 | 162 | - Swipe to dismiss gesture 163 | -------------------------------------------------------------------------------- /src/__tests__/Toast.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import { act, fireEvent, render, waitFor } from '@testing-library/react-native'; 4 | import React from 'react'; 5 | import { View } from 'react-native'; 6 | 7 | import Toast from '..'; 8 | import colors from '../colors'; 9 | 10 | function setup(props) { 11 | const ref = { 12 | current: undefined 13 | }; 14 | const utils = render(); 15 | const getAnimatedView = () => utils.queryByTestId('animatedView'); 16 | const getText1 = () => utils.queryByTestId('text1'); 17 | const getText2 = () => utils.queryByTestId('text2'); 18 | const getRootView = () => utils.queryByTestId('rootView'); 19 | 20 | return { 21 | ref, 22 | getRootView, 23 | getText1, 24 | getText2, 25 | getAnimatedView, 26 | ...utils 27 | }; 28 | } 29 | 30 | function getVerticalOffset(reactElement) { 31 | return reactElement.props.style.transform[0].translateY; 32 | } 33 | 34 | describe('test Toast behavior', () => { 35 | describe('test API', () => { 36 | it('becomes visible when show() is called, hides when hide() is called', async () => { 37 | const { ref, getText1, getText2, getAnimatedView } = setup(); 38 | let animatedView = getAnimatedView(); 39 | 40 | expect(getText1()).toBeFalsy(); 41 | // Toast View is pushed off screen, on the Y axis 42 | expect(getVerticalOffset(animatedView) < 0).toBe(true); 43 | 44 | await act(async () => { 45 | await ref.current.show({ 46 | text1: 'Hello', 47 | text2: 'Test' 48 | }); 49 | }); 50 | await waitFor(() => getText1()); 51 | await waitFor(() => getText2()); 52 | animatedView = getAnimatedView(); 53 | expect(getVerticalOffset(animatedView) < 0).toBe(false); 54 | 55 | await act(async () => { 56 | await ref.current.hide(); 57 | }); 58 | animatedView = getAnimatedView(); 59 | expect(getVerticalOffset(animatedView) < 0).toBe(true); 60 | }); 61 | 62 | it('shows success type', async () => { 63 | const { ref, getRootView } = setup(); 64 | 65 | await act(async () => { 66 | await ref.current.show({ 67 | type: 'success' 68 | }); 69 | }); 70 | 71 | expect(getRootView()).toHaveStyle({ 72 | borderLeftColor: colors.mantis 73 | }); 74 | }); 75 | 76 | it('shows info type', async () => { 77 | const { ref, getRootView } = setup(); 78 | 79 | await act(async () => { 80 | await ref.current.show({ 81 | type: 'info' 82 | }); 83 | }); 84 | 85 | expect(getRootView()).toHaveStyle({ 86 | borderLeftColor: colors.lightSkyBlue 87 | }); 88 | }); 89 | 90 | it('shows error type', async () => { 91 | const { ref, getRootView } = setup(); 92 | 93 | await act(async () => { 94 | await ref.current.show({ 95 | type: 'error' 96 | }); 97 | }); 98 | 99 | expect(getRootView()).toHaveStyle({ 100 | borderLeftColor: colors.blazeOrange 101 | }); 102 | }); 103 | 104 | it('calls onShow', async () => { 105 | const onShow = jest.fn(); 106 | const { ref } = setup(); 107 | 108 | await act(async () => { 109 | await ref.current.show({ 110 | type: 'info', 111 | onShow 112 | }); 113 | }); 114 | 115 | expect(onShow).toHaveBeenCalled(); 116 | }); 117 | 118 | it('calls onHide', async () => { 119 | const onHide = jest.fn(); 120 | const { ref } = setup(); 121 | 122 | await act(async () => { 123 | await ref.current.show({ 124 | type: 'info', 125 | onHide 126 | }); 127 | await ref.current.hide(); 128 | }); 129 | 130 | expect(onHide).toHaveBeenCalled(); 131 | }); 132 | 133 | it('fires onPress', async () => { 134 | const onPress = jest.fn(); 135 | const { ref, getRootView } = setup(); 136 | 137 | await act(async () => { 138 | await ref.current.show({ 139 | type: 'info', 140 | onPress 141 | }); 142 | }); 143 | 144 | fireEvent.press(getRootView()); 145 | expect(onPress).toHaveBeenCalled(); 146 | }); 147 | 148 | it('shows at the bottom', async () => { 149 | const { ref, getAnimatedView } = setup(); 150 | 151 | await act(async () => { 152 | await ref.current.show({ 153 | position: 'bottom' 154 | }); 155 | }); 156 | expect(getAnimatedView()).toHaveStyle({ 157 | bottom: 0 158 | }); 159 | }); 160 | 161 | it('shows with custom top offset', async () => { 162 | const { ref, getAnimatedView } = setup(); 163 | const offset = 60; 164 | const height = 65; 165 | 166 | let animatedView = getAnimatedView(); 167 | expect(getVerticalOffset(animatedView)).toBe(-height); 168 | 169 | await act(async () => { 170 | await ref.current.show({ 171 | topOffset: offset 172 | }); 173 | }); 174 | 175 | animatedView = getAnimatedView(); 176 | expect(getVerticalOffset(animatedView)).toBe(offset); 177 | }); 178 | 179 | it('shows with custom bottom offset', async () => { 180 | const { ref, getAnimatedView } = setup(); 181 | const offset = 60; 182 | const height = 65; 183 | 184 | let animatedView = getAnimatedView(); 185 | expect(getVerticalOffset(animatedView)).toBe(-height); 186 | 187 | await act(async () => { 188 | await ref.current.show({ 189 | position: 'bottom', 190 | bottomOffset: offset 191 | }); 192 | }); 193 | 194 | animatedView = getAnimatedView(); 195 | expect(getVerticalOffset(animatedView)).toBe(-offset); 196 | }); 197 | }); 198 | 199 | describe('test props', () => { 200 | it('shows Toast from custom config', async () => { 201 | const testID = 'testView'; 202 | const { ref, queryByTestId } = setup({ 203 | config: { 204 | test: () => 205 | } 206 | }); 207 | 208 | await act(async () => { 209 | await ref.current.show({ 210 | type: 'test' 211 | }); 212 | }); 213 | 214 | const testView = queryByTestId(testID); 215 | expect(testView).toBeTruthy(); 216 | }); 217 | 218 | it('tries to show Toast type that does not exist', async () => { 219 | const { ref, getRootView } = setup(); 220 | 221 | await act(async () => { 222 | await ref.current.show({ 223 | type: 'test' 224 | }); 225 | }); 226 | 227 | const rootView = getRootView(); 228 | expect(rootView).toBeFalsy(); 229 | }); 230 | }); 231 | }); 232 | -------------------------------------------------------------------------------- /src/components/__tests__/Base.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import { fireEvent, render } from '@testing-library/react-native'; 4 | import React from 'react'; 5 | 6 | import colors from '../../colors'; 7 | import { icons } from '../../assets'; 8 | import Base from '../base'; 9 | 10 | describe('test Base component', () => { 11 | it('renders default Views', () => { 12 | const { queryByTestId } = render(); 13 | const rootView = queryByTestId('rootView'); 14 | const leadingIcon = queryByTestId('leadingIcon'); 15 | const trailingIcon = queryByTestId('trailingIcon'); 16 | const text1 = queryByTestId('text1'); 17 | const text2 = queryByTestId('text2'); 18 | 19 | expect(rootView).toBeTruthy(); 20 | expect(rootView).toHaveStyle({ 21 | height: 60, 22 | width: '90%', 23 | borderLeftWidth: 5, 24 | borderLeftColor: colors.alto 25 | }); 26 | expect(text1).toBeFalsy(); 27 | expect(text2).toBeFalsy(); 28 | expect(leadingIcon).toBeFalsy(); 29 | expect(trailingIcon).toBeTruthy(); 30 | expect(trailingIcon.children[0].props.source).toBe(icons.close); 31 | }); 32 | 33 | it('renders custom leadingIcon and trailingIcon', () => { 34 | const mockIcon = { uri: 'iconSource' }; 35 | 36 | const { queryByTestId } = render( 37 | 38 | ); 39 | const leadingIcon = queryByTestId('leadingIcon'); 40 | const trailingIcon = queryByTestId('trailingIcon'); 41 | 42 | expect(leadingIcon.children[0].props.source).toBe(mockIcon); 43 | expect(trailingIcon.children[0].props.source).toBe(mockIcon); 44 | }); 45 | 46 | it('renders text1 and text2', () => { 47 | const t1 = 'foo'; 48 | const t2 = 'bar'; 49 | const { queryByTestId } = render(); 50 | const text1 = queryByTestId('text1'); 51 | const text2 = queryByTestId('text2'); 52 | 53 | expect(text1.children[0]).toBe(t1); 54 | expect(text2.children[0]).toBe(t2); 55 | }); 56 | 57 | it('fires onPress', () => { 58 | const onPress = jest.fn(); 59 | const { queryByTestId } = render(); 60 | const rootView = queryByTestId('rootView'); 61 | 62 | fireEvent.press(rootView); 63 | 64 | expect(onPress).toHaveBeenCalled(); 65 | }); 66 | 67 | it('fires onLeadingIconPress and onTrailingIconPress', () => { 68 | const onLeadingIconPress = jest.fn(); 69 | const onTrailingIconPress = jest.fn(); 70 | const mockIcon = { uri: 'mock' }; 71 | 72 | const { queryByTestId } = render( 73 | 79 | ); 80 | const leadingIcon = queryByTestId('leadingIcon'); 81 | const trailingIcon = queryByTestId('trailingIcon'); 82 | 83 | fireEvent.press(leadingIcon); 84 | expect(onLeadingIconPress).toHaveBeenCalledTimes(1); 85 | expect(onTrailingIconPress).toHaveBeenCalledTimes(0); 86 | 87 | fireEvent.press(trailingIcon); 88 | expect(onLeadingIconPress).toHaveBeenCalledTimes(1); 89 | expect(onTrailingIconPress).toHaveBeenCalledTimes(1); 90 | }); 91 | 92 | it('sets custom style on root View', () => { 93 | const mockStyle = { 94 | height: 20 95 | }; 96 | const { queryByTestId } = render(); 97 | const rootView = queryByTestId('rootView'); 98 | 99 | expect(rootView).toHaveStyle(mockStyle); 100 | }); 101 | 102 | it('sets custom style on leading icon container', () => { 103 | const mockStyle = { 104 | width: 40 105 | }; 106 | const { queryByTestId } = render( 107 | 111 | ); 112 | const leadingIcon = queryByTestId('leadingIcon'); 113 | 114 | expect(leadingIcon).toHaveStyle(mockStyle); 115 | }); 116 | 117 | it('sets custom style on trailing icon container', () => { 118 | const mockStyle = { 119 | width: 40 120 | }; 121 | const { queryByTestId } = render( 122 | 123 | ); 124 | const trailingIcon = queryByTestId('trailingIcon'); 125 | 126 | expect(trailingIcon).toHaveStyle(mockStyle); 127 | }); 128 | 129 | it('sets custom style on leading icon', () => { 130 | const mockStyle = { 131 | width: 25, 132 | height: 25 133 | }; 134 | const { queryByTestId } = render( 135 | 136 | ); 137 | const leadingIcon = queryByTestId('leadingIcon'); 138 | 139 | expect(leadingIcon.children[0]).toHaveStyle(mockStyle); 140 | }); 141 | 142 | it('sets custom style on trailing icon', () => { 143 | const mockStyle = { 144 | width: 25, 145 | height: 25 146 | }; 147 | const { queryByTestId } = render( 148 | 149 | ); 150 | const trailingIcon = queryByTestId('trailingIcon'); 151 | 152 | expect(trailingIcon.children[0]).toHaveStyle(mockStyle); 153 | }); 154 | 155 | it('has default content container style', () => { 156 | const { queryByTestId } = render(); 157 | const contentContainer = queryByTestId('contentContainer'); 158 | 159 | expect(contentContainer).toHaveStyle({ 160 | flex: 1, 161 | justifyContent: 'center', 162 | alignItems: 'flex-start' 163 | }); 164 | }); 165 | 166 | it('sets custom content container style', () => { 167 | const mockStyle = { 168 | backgroundColor: 'tomato' 169 | }; 170 | const { queryByTestId } = render( 171 | 172 | ); 173 | const contentContainer = queryByTestId('contentContainer'); 174 | 175 | expect(contentContainer).toHaveStyle(mockStyle); 176 | }); 177 | 178 | it('sets custom text1 and text2 style', () => { 179 | const mockStyle1 = { 180 | fontSize: 10 181 | }; 182 | const mockStyle2 = { 183 | fontSize: 8 184 | }; 185 | const { queryByTestId } = render( 186 | 192 | ); 193 | const text1 = queryByTestId('text1'); 194 | const text2 = queryByTestId('text2'); 195 | 196 | expect(text1).toHaveStyle(mockStyle1); 197 | expect(text2).toHaveStyle(mockStyle2); 198 | }); 199 | 200 | it('renders default number of lines', () => { 201 | const { queryByTestId } = render(); 202 | const text1 = queryByTestId('text1'); 203 | const text2 = queryByTestId('text2'); 204 | 205 | expect(text1.props.numberOfLines).toBe(1); 206 | expect(text2.props.numberOfLines).toBe(2); 207 | }); 208 | 209 | it('sets custom number of lines', () => { 210 | const { queryByTestId } = render( 211 | 217 | ); 218 | const text1 = queryByTestId('text1'); 219 | const text2 = queryByTestId('text2'); 220 | 221 | expect(text1.props.numberOfLines).toBe(2); 222 | expect(text2.props.numberOfLines).toBe(3); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-toast-message 2 | 3 | [![npm version](https://img.shields.io/npm/v/react-native-toast-message)](https://www.npmjs.com/package/react-native-toast-message) 4 | [![npm downloads](https://img.shields.io/npm/dw/react-native-toast-message)](https://www.npmjs.com/package/react-native-toast-message) 5 | [![Build](https://github.com/calintamas/react-native-toast-message/workflows/tests/badge.svg)](https://github.com/calintamas/react-native-toast-message/actions?query=workflow%3Atests) 6 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 7 | 8 | Animated toast message component for React Native. 9 | 10 | - Imperative API 11 | - Keyboard aware 12 | - Flexible config 13 | 14 | ## Install 15 | 16 | ``` 17 | yarn add react-native-toast-message 18 | ``` 19 | 20 | ![ToastSuccess](success-toast.gif) 21 | 22 | ## Usage 23 | 24 | Render the `Toast` component in your app entry file (along with everything that might be rendered there) and set a ref to it. 25 | 26 | ```js 27 | // App.jsx 28 | import Toast from 'react-native-toast-message'; 29 | 30 | function App(props) { 31 | return ( 32 | <> 33 | {/* ... */} 34 | Toast.setRef(ref)} /> 35 | 36 | ); 37 | } 38 | 39 | export default App; 40 | ``` 41 | 42 | Then use it anywhere in your app (even outside React components), by calling any `Toast` method directly: 43 | 44 | ```js 45 | import Toast from 'react-native-toast-message'; 46 | 47 | function SomeComponent() { 48 | React.useEffect(() => { 49 | Toast.show({ 50 | text1: 'Hello', 51 | text2: 'This is some something 👋' 52 | }); 53 | }, []); 54 | 55 | return ; 56 | } 57 | ``` 58 | 59 | ## API 60 | 61 | ### `show(options = {})` 62 | 63 | When calling the `show` method, you can use the following `options` to suit your needs. Everything is optional, unless specified otherwise. 64 | 65 | The usage of `|` below, means that only one of the values show should be used. 66 | If only one value is shown, that's the default. 67 | 68 | ```js 69 | Toast.show({ 70 | type: 'success | error | info', 71 | position: 'top | bottom', 72 | text1: 'Hello', 73 | text2: 'This is some something 👋', 74 | visibilityTime: 4000, 75 | autoHide: true, 76 | topOffset: 30, 77 | bottomOffset: 40, 78 | onShow: () => {}, 79 | onHide: () => {}, // called when Toast hides (if `autoHide` was set to `true`) 80 | onPress: () => {}, 81 | props: {} // any custom props passed to the Toast component 82 | }); 83 | ``` 84 | 85 | ### `hide()` 86 | 87 | ```js 88 | Toast.hide(); 89 | ``` 90 | 91 | ## props 92 | 93 | Props that can be set on the `Toast` instance. They act as defaults for all Toasts that are shown. 94 | 95 | ```js 96 | const props = { 97 | type: 'success | error | info', 98 | position: 'top' | 'bottom', 99 | visibilityTime: Number, 100 | autoHide: Boolean, 101 | topOffset: Number, 102 | bottomOffset: Number, 103 | keyboardOffset: Number, 104 | config: Object, 105 | style: ViewStyle, 106 | height: Number 107 | }; 108 | ``` 109 | 110 | > Default `Animated.View` styles can be found in [styles.js](https://github.com/calintamas/react-native-toast-message/blob/master/src/styles.js#L4). They can be extended using the `style` prop. 111 | 112 | ## Customize layout 113 | 114 | If you want to add custom types - or overwrite the existing ones - you can add a `config` prop when rendering the `Toast` in your app `root`. 115 | 116 | You can either use the default `BaseToast` style and adjust its layout, or create Toast layouts from scratch. 117 | 118 | ```js 119 | // App.jsx 120 | import Toast, { BaseToast } from 'react-native-toast-message'; 121 | 122 | const toastConfig = { 123 | /* 124 | overwrite 'success' type, 125 | modifying the existing `BaseToast` component 126 | */ 127 | success: ({ text1, props, ...rest }) => ( 128 | 139 | ), 140 | 141 | /* 142 | or create a completely new type - `my_custom_type`, 143 | building the layout from scratch 144 | */ 145 | my_custom_type: ({ text1, props, ...rest }) => ( 146 | 147 | {text1} 148 | 149 | ) 150 | }; 151 | 152 | function App(props) { 153 | // pass `toastConfig` to the Toast instance 154 | return ( 155 | <> 156 | Toast.setRef(ref)} /> 157 | 158 | ); 159 | } 160 | 161 | export default App; 162 | ``` 163 | 164 | Then just use the library as before 165 | 166 | ```js 167 | Toast.show({ 168 | type: 'my_custom_type', 169 | props: { uuid: 'bba1a7d0-6ab2-4a0a-a76e-ebbe05ae6d70' } 170 | }); 171 | ``` 172 | 173 | Available `props` on `BaseToast`: 174 | 175 | ```js 176 | const baseToastProps = { 177 | leadingIcon: ImageSource, 178 | trailingIcon: ImageSource, 179 | text1: String, 180 | text2: String, 181 | onPress: Function, 182 | onLeadingIconPress: Function, 183 | onTrailingIconPress: Function, 184 | style: ViewStyle, 185 | leadingIconContainerStyle: ViewStyle, 186 | trailingIconContainerStyle: ViewStyle, 187 | leadingIconStyle: ViewStyle, 188 | trailingIconStyle: ViewStyle, 189 | contentContainerStyle: ViewStyle, 190 | text1Style: ViewStyle, 191 | text2Style: ViewStyle, 192 | activeOpacity: Number 193 | }; 194 | ``` 195 | 196 | ## FAQ 197 | 198 | ### How to render the Toast when using [react-navigation](https://reactnavigation.org)? 199 | 200 | To have the toast visible on top of the navigation `View` hierarchy, simply render it inside the `NavigationContainer`. 201 | 202 | ```js 203 | import Toast from 'react-native-toast-message' 204 | import { NavigationContainer } from '@react-navigation/native'; 205 | 206 | export default function App() { 207 | return ( 208 | 209 | {...} 210 | Toast.setRef(ref)} /> 211 | 212 | ); 213 | } 214 | ``` 215 | 216 | ### How to mock the library for testing with [jest](https://jestjs.io)? 217 | 218 | ```js 219 | jest.mock('react-native-toast-message', () => ({ 220 | show: jest.fn(), 221 | hide: jest.fn() 222 | })); 223 | ``` 224 | 225 | ### How to show the Toast inside a Modal? 226 | 227 | When a `Modal` is visible, the Toast gets rendered behind it. This is due to [the way `Modal` is implemented](https://stackoverflow.com/a/57402783). As a workaround, you can have 2 separate Toast instances: one inside the Modal (let's call it a "modal toast") and a normal one outside. 228 | 229 | For the one outside, set the ref on the Toast object (like usual) 230 | ```js 231 | Toast.setRef(ref) /> 232 | ``` 233 | 234 | And for the "modal toast", use another ref 235 | ```js 236 | function ScreenWithModal() { 237 | const modalToastRef = React.useRef(); 238 | 239 | return ( 240 | 241 | 242 | 243 | ); 244 | } 245 | ``` 246 | Then, when you want to show the "modal toast", call it using `modalToastRef.current.show()`. 247 | 248 | ## Credits 249 | 250 | The icons for the default `success`, `error` and `info` types are made by [Pixel perfect](https://www.flaticon.com/authors/pixel-perfect) from [flaticon.com](www.flaticon.com). 251 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Animated, PanResponder, Keyboard } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import SuccessToast from './components/success'; 6 | import ErrorToast from './components/error'; 7 | import InfoToast from './components/info'; 8 | import { complement } from './utils/arr'; 9 | import { includeKeys } from './utils/obj'; 10 | import { stylePropType } from './utils/prop-types'; 11 | import { isIOS } from './utils/platform'; 12 | import styles from './styles'; 13 | 14 | const FRICTION = 8; 15 | 16 | const defaultComponentsConfig = { 17 | // eslint-disable-next-line react/prop-types 18 | success: ({ hide, ...rest }) => ( 19 | 20 | ), 21 | // eslint-disable-next-line react/prop-types 22 | error: ({ hide, ...rest }) => ( 23 | 24 | ), 25 | // eslint-disable-next-line react/prop-types 26 | info: ({ hide, ...rest }) => ( 27 | 28 | ) 29 | }; 30 | 31 | function shouldSetPanResponder(gesture) { 32 | const { dx, dy } = gesture; 33 | // Fixes onPress handler https://github.com/calintamas/react-native-toast-message/issues/113 34 | return Math.abs(dx) > 2 || Math.abs(dy) > 2; 35 | } 36 | 37 | const getInitialState = ({ 38 | topOffset, 39 | bottomOffset, 40 | keyboardOffset, 41 | visibilityTime, 42 | height, 43 | autoHide, 44 | position, 45 | type 46 | }) => ({ 47 | // layout 48 | topOffset, 49 | bottomOffset, 50 | keyboardOffset, 51 | height, 52 | position, 53 | type, 54 | 55 | // timing (in ms) 56 | visibilityTime, 57 | autoHide, 58 | 59 | // content 60 | text1: undefined, 61 | text2: undefined, 62 | 63 | onPress: undefined, 64 | onShow: undefined, 65 | onHide: undefined 66 | }); 67 | 68 | class Toast extends Component { 69 | static _ref = null; 70 | 71 | static setRef(ref = {}) { 72 | Toast._ref = ref; 73 | } 74 | 75 | static getRef() { 76 | return Toast._ref; 77 | } 78 | 79 | static clearRef() { 80 | Toast._ref = null; 81 | } 82 | 83 | static show(options = {}) { 84 | Toast._ref.show(options); 85 | } 86 | 87 | static hide() { 88 | Toast._ref.hide(); 89 | } 90 | 91 | constructor(props) { 92 | super(props); 93 | 94 | this._setState = this._setState.bind(this); 95 | this._animateMovement = this._animateMovement.bind(this); 96 | this._animateRelease = this._animateRelease.bind(this); 97 | this.startTimer = this.startTimer.bind(this); 98 | this.animate = this.animate.bind(this); 99 | this.show = this.show.bind(this); 100 | this.hide = this.hide.bind(this); 101 | this.onLayout = this.onLayout.bind(this); 102 | 103 | this.state = { 104 | ...getInitialState(props), 105 | 106 | inProgress: false, 107 | isVisible: false, 108 | animation: new Animated.Value(0), 109 | keyboardHeight: 0, 110 | keyboardVisible: false, 111 | 112 | customProps: {} 113 | }; 114 | 115 | this.panResponder = PanResponder.create({ 116 | onMoveShouldSetPanResponder: (event, gesture) => 117 | shouldSetPanResponder(gesture), 118 | onMoveShouldSetPanResponderCapture: (event, gesture) => 119 | shouldSetPanResponder(gesture), 120 | onPanResponderMove: (event, gesture) => { 121 | this._animateMovement(gesture); 122 | }, 123 | onPanResponderRelease: (event, gesture) => { 124 | this._animateRelease(gesture); 125 | } 126 | }); 127 | } 128 | 129 | componentDidMount() { 130 | const noop = { 131 | remove: () => {} 132 | }; 133 | this.keyboardDidShowListener = isIOS 134 | ? Keyboard.addListener('keyboardDidShow', this.keyboardDidShow) 135 | : noop; 136 | this.keyboardDidHideListner = isIOS 137 | ? Keyboard.addListener('keyboardDidHide', this.keyboardDidHide) 138 | : noop; 139 | } 140 | 141 | componentWillUnmount() { 142 | this.keyboardDidShowListener.remove(); 143 | this.keyboardDidHideListner.remove(); 144 | clearTimeout(this.timer); 145 | } 146 | 147 | keyboardDidShow = (e) => { 148 | const { isVisible, position } = this.state; 149 | this.setState({ 150 | keyboardHeight: e.endCoordinates.height, 151 | keyboardVisible: true 152 | }); 153 | 154 | if (isVisible && position === 'bottom') { 155 | this.animate({ toValue: 2 }); 156 | } 157 | }; 158 | 159 | keyboardDidHide = () => { 160 | const { isVisible, position } = this.state; 161 | this.setState({ 162 | keyboardVisible: false 163 | }); 164 | if (isVisible && position === 'bottom') { 165 | this.animate({ toValue: 1 }); 166 | } 167 | }; 168 | 169 | _setState(reducer) { 170 | return new Promise((resolve) => this.setState(reducer, () => resolve())); 171 | } 172 | 173 | _animateMovement(gesture) { 174 | const { position, animation, keyboardVisible } = this.state; 175 | const { dy } = gesture; 176 | let value = 1 + dy / 100; 177 | const start = keyboardVisible && position === 'bottom' ? 2 : 1; 178 | 179 | if (position === 'bottom') { 180 | value = start - dy / 100; 181 | } 182 | 183 | if (value <= start) { 184 | animation.setValue(value); 185 | } 186 | } 187 | 188 | _animateRelease(gesture) { 189 | const { position, animation, keyboardVisible } = this.state; 190 | const { dy, vy } = gesture; 191 | 192 | const isBottom = position === 'bottom'; 193 | let value = 1 + dy / 100; 194 | 195 | if (isBottom) { 196 | value = 1 - dy / 100; 197 | } 198 | 199 | const treshold = 0.65; 200 | if (value <= treshold || Math.abs(vy) >= treshold) { 201 | this.hide({ 202 | speed: Math.abs(vy) * 3 203 | }); 204 | } else { 205 | Animated.spring(animation, { 206 | toValue: keyboardVisible && isBottom ? 2 : 1, 207 | velocity: vy, 208 | useNativeDriver: true 209 | }).start(); 210 | } 211 | } 212 | 213 | async show(options = {}) { 214 | const { inProgress, isVisible } = this.state; 215 | if (inProgress || isVisible) { 216 | await this.hide(); 217 | } 218 | 219 | await this._setState((prevState) => ({ 220 | ...prevState, 221 | ...getInitialState(this.props), // Reset layout 222 | /* 223 | Preserve the previously computed height (via onLayout). 224 | If the height of the component corresponding to this `show` call is different, 225 | onLayout will be called again and `height` state will adjust. 226 | 227 | This fixes an issue where a succession of calls to components with custom heights (custom Toast types) 228 | fails to hide them completely due to always resetting to the default component height 229 | */ 230 | height: prevState.height, 231 | inProgress: true, 232 | ...options, 233 | ...(options?.props ? { customProps: options.props } : { customProps: {} }) 234 | })); 235 | await this.animateShow(); 236 | await this._setState((prevState) => ({ 237 | ...prevState, 238 | isVisible: true, 239 | inProgress: false 240 | })); 241 | this.clearTimer(); 242 | 243 | const { autoHide, onShow } = this.state; 244 | 245 | if (autoHide) { 246 | this.startTimer(); 247 | } 248 | 249 | if (onShow) { 250 | onShow(); 251 | } 252 | } 253 | 254 | async hide({ speed } = {}) { 255 | await this._setState((prevState) => ({ 256 | ...prevState, 257 | inProgress: true 258 | })); 259 | await this.animateHide({ 260 | speed 261 | }); 262 | this.clearTimer(); 263 | await this._setState((prevState) => ({ 264 | ...prevState, 265 | isVisible: false, 266 | inProgress: false 267 | })); 268 | 269 | const { onHide } = this.state; 270 | if (onHide) { 271 | onHide(); 272 | } 273 | } 274 | 275 | animateShow = () => { 276 | const { keyboardVisible, position } = this.state; 277 | const toValue = keyboardVisible && position === 'bottom' ? 2 : 1; 278 | return this.animate({ toValue }); 279 | }; 280 | 281 | animateHide({ speed } = {}) { 282 | return this.animate({ toValue: 0, speed }); 283 | } 284 | 285 | animate({ toValue, speed = 0 }) { 286 | const { animation } = this.state; 287 | return new Promise((resolve) => { 288 | const config = { 289 | toValue, 290 | useNativeDriver: true, 291 | ...(speed > 0 ? { speed } : { friction: FRICTION }) 292 | }; 293 | Animated.spring(animation, config).start(() => resolve()); 294 | }); 295 | } 296 | 297 | startTimer() { 298 | const { visibilityTime } = this.state; 299 | this.timer = setTimeout(() => this.hide(), visibilityTime); 300 | } 301 | 302 | clearTimer() { 303 | clearTimeout(this.timer); 304 | this.timer = null; 305 | } 306 | 307 | renderContent({ config }) { 308 | const componentsConfig = { 309 | ...defaultComponentsConfig, 310 | ...config 311 | }; 312 | 313 | const { type, customProps } = this.state; 314 | const toastComponent = componentsConfig[type]; 315 | 316 | if (!toastComponent) { 317 | // eslint-disable-next-line no-console 318 | console.error( 319 | `Type '${type}' does not exist. Make sure to add it to your 'config'. You can read the documentation here: https://github.com/calintamas/react-native-toast-message/blob/master/README.md` 320 | ); 321 | return null; 322 | } 323 | 324 | return toastComponent({ 325 | ...includeKeys({ 326 | obj: this.state, 327 | keys: [ 328 | 'position', 329 | 'type', 330 | 'inProgress', 331 | 'isVisible', 332 | 'text1', 333 | 'text2', 334 | 'hide', 335 | 'show', 336 | 'onPress' 337 | ] 338 | }), 339 | props: { ...customProps }, 340 | hide: this.hide, 341 | show: this.show 342 | }); 343 | } 344 | 345 | getBaseStyle(position = 'bottom', keyboardHeight) { 346 | const { 347 | topOffset, 348 | bottomOffset, 349 | keyboardOffset, 350 | height, 351 | animation 352 | } = this.state; 353 | const offset = position === 'bottom' ? bottomOffset : topOffset; 354 | 355 | // +5 px to completely hide the toast under StatusBar (on Android) 356 | const range = [height + 5, -offset, -(keyboardOffset + keyboardHeight)]; 357 | const outputRange = position === 'bottom' ? range : complement(range); 358 | 359 | const translateY = animation.interpolate({ 360 | inputRange: [0, 1, 2], 361 | outputRange 362 | }); 363 | 364 | return [ 365 | styles.base, 366 | styles[position], 367 | { 368 | transform: [{ translateY }] 369 | } 370 | ]; 371 | } 372 | 373 | onLayout(e) { 374 | this.setState({ height: e.nativeEvent.layout.height }); 375 | } 376 | 377 | render() { 378 | const { style } = this.props; 379 | const { position, keyboardHeight } = this.state; 380 | const baseStyle = this.getBaseStyle(position, keyboardHeight); 381 | 382 | return ( 383 | 388 | {this.renderContent(this.props)} 389 | 390 | ); 391 | } 392 | } 393 | 394 | Toast.propTypes = { 395 | config: PropTypes.objectOf(PropTypes.func), 396 | style: stylePropType, 397 | topOffset: PropTypes.number, 398 | bottomOffset: PropTypes.number, 399 | keyboardOffset: PropTypes.number, 400 | visibilityTime: PropTypes.number, 401 | autoHide: PropTypes.bool, 402 | height: PropTypes.number, 403 | position: PropTypes.oneOf(['top', 'bottom']), 404 | type: PropTypes.string 405 | }; 406 | 407 | Toast.defaultProps = { 408 | config: {}, 409 | style: undefined, 410 | topOffset: 30, 411 | bottomOffset: 40, 412 | keyboardOffset: 15, 413 | visibilityTime: 4000, 414 | autoHide: true, 415 | height: 60, 416 | position: 'top', 417 | type: 'success' 418 | }; 419 | 420 | export default Toast; 421 | --------------------------------------------------------------------------------