├── 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 | [](https://www.npmjs.com/package/react-native-toast-message)
4 | [](https://www.npmjs.com/package/react-native-toast-message)
5 | [](https://github.com/calintamas/react-native-toast-message/actions?query=workflow%3Atests)
6 | [](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 | 
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 |
--------------------------------------------------------------------------------