diff --git a/README.md b/README.md index 4d27588a..bf52fe91 100644 --- a/README.md +++ b/README.md @@ -143,20 +143,51 @@ containerStyle: PropTypes.Object // style to be applied in the alerts container template: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired // the alert template to be used ``` +Note that the position, type and transition strings are available as constants which can be imported the next way: +```js +import { positions, transitions, types } from 'react-alert' +``` + +and have such values: +```js +export const positions = { + TOP_LEFT: 'top left', + TOP_CENTER: 'top center', + TOP_RIGHT: 'top right', + MIDDLE_LEFT: 'middle left', + MIDDLE: 'middle', + MIDDLE_RIGHT: 'middle right', + BOTTOM_LEFT: 'bottom left', + BOTTOM_CENTER: 'bottom center', + BOTTOM_RIGHT: 'bottom right' +} + +export const types = { + INFO: 'info', + SUCCESS: 'success', + ERROR: 'error' +} + +export const transitions = { + FADE: 'fade', + SCALE: 'scale' +} +``` + Here's the defaults: ```js offset: '10px' -position: 'top center' +position: positions.TOP_CENTER timeout: 0 -type: 'info' -transition: 'fade', +type: types.INFO +transition: transitions.FADE, containerStyle: { zIndex: 100 } ``` -Those options will be applied to all alerts. +Those options will be applied to all alerts (please, also have a look at [edge-cases](#showing-alerts-in-different-positions-at-the-same-time)) ## Api @@ -259,9 +290,13 @@ You can also pass in a component as a message, like this: alert.show(
Some Message
) ``` -## Using multiple Providers +## Showing alerts in different positions at the same time + +First of all, if have a need to separate the logic of showing alerts in different +positions at the same time it is possible to use multiple AlertProviders in one project and +nest them across the DOM tree. Also you can use different Contexts to get the references +to each type of alert separately. -You can use different Contexts to show alerts in different style and position: ```js import React, { createContext } from 'react' @@ -305,3 +340,61 @@ const Root = () => ( render(, document.getElementById('root')) ``` + +Another use case is when you don't want to nest a couple of AlertProviders +because it will somehow complicate management of alerts (for example when you +need to show alerts in more than three different positions). + +In this case you can pass the position directly to the alert. The alerts without +individual position property will take it from the Provider. +Instead, passing the position to methods `show`, `success`, `info`, `error` will +overlap the Provider's position. + +Passing the property `position` will look like this: +```js +alert.show('Oh look, an alert!', {position: positions.BOTTOM_LEFT}) +``` + +An example of showing alerts simultaneously in three different positions: +```js +import React from 'react'; +import { render } from 'react-dom' +import { Provider as AlertProvider, useAlert, positions, transitions } from 'react-alert'; +import AlertTemplate from 'react-alert-template-basic' + +const alertOptions = { + offset: '25px', + timeout: 3000, + transition: transitions.SCALE +}; + +const App = () => { + const alert = useAlert() + + const showAlert = () => { + alert.show('Oh look, an alert!', {position: positions.BOTTOM_LEFT}) //alert will be shown in bottom left + alert.show('Oh look, an alert!', {position: positions.BOTTOM_RIGHT}) //alert will be shown in bottom right + alert.show('Oh look, an alert!') //alert will use the Provider's position `top right` + } + + return ( + + ) +} + +const Root = () => ( + + + + + +) + +render(, document.getElementById('root')) +``` diff --git a/__tests__/react-alert.spec.js b/__tests__/react-alert.spec.js index d9705279..97b5b196 100644 --- a/__tests__/react-alert.spec.js +++ b/__tests__/react-alert.spec.js @@ -1,23 +1,16 @@ import React, { useRef, createContext } from 'react' import 'jest-dom/extend-expect' -import { render, fireEvent, cleanup, act } from 'react-testing-library' +import { render, fireEvent, cleanup, act, wait } from 'react-testing-library' import { positions, Provider, useAlert, withAlert } from '../src' import { getStyles } from '../src/Wrapper' +jest.useFakeTimers() + const styleString = style => Object.entries(style).reduce((styleString, [propName, propValue]) => { return `${styleString}${propName}:${propValue};` }, '') -jest.useFakeTimers() - -jest.mock('react-transition-group', () => { - const FakeTransition = ({ children }) => children('entered') - const FakeTransitionGroup = ({ children }) => children - - return { TransitionGroup: FakeTransitionGroup, Transition: FakeTransition } -}) - describe('react-alert', () => { const Template = ({ message, close, options: { type }, style }) => (
@@ -36,21 +29,20 @@ describe('react-alert', () => { describe('react-alert with one Provider and minimum needed options', () => { let tree - let getByText, getByTestId, queryAllByText + let getByText, queryAllByText let renderApp = () => { const App = () => ( - + ) - return render() + return render() } beforeEach(() => { tree = renderApp() getByText = tree.getByText - getByTestId = tree.getByTestId queryAllByText = tree.queryAllByText }) @@ -65,6 +57,9 @@ describe('react-alert', () => { expect(getByText(/message/i)).toBeInTheDocument() fireEvent.click(getByText(/close/i)) + + act(jest.runAllTimers) + expect(alertElement).not.toBeInTheDocument() }) @@ -80,10 +75,10 @@ describe('react-alert', () => { let renderApp = CustomChild => { const App = () => ( - + ) - return render() + return render() } it('should be able to show an alert calling success', () => { @@ -153,17 +148,20 @@ describe('react-alert', () => { it('should close an alert automatic after the given timeout', () => { const App = () => ( - + ) - const { getByText } = render() + const { getByText } = render() fireEvent.click(getByText(/show alert/i)) const alertElement = getByText(/message/i) expect(alertElement).toBeInTheDocument() - act(() => jest.runAllTimers()) - expect(alertElement).not.toBeInTheDocument() + + wait(() => { + act(jest.runOnlyPendingTimers) + expect(alertElement).not.toBeInTheDocument() + }) }) it('should accept different position options', () => { @@ -174,14 +172,15 @@ describe('react-alert', () => { template={Template} position={position} > - + ) - const { getByText, getByTestId } = render() + const { getByText, getByTestId } = render() fireEvent.click(getByText(/show alert/i)) const providerElement = getByTestId('provider') + const styles = styleString(getStyles(position)) expect(providerElement).toHaveStyle(styles) cleanup() @@ -196,11 +195,11 @@ describe('react-alert', () => { template={Template} containerStyle={containerStyle} > - + ) - const { getByText, getByTestId } = render() + const { getByText, getByTestId } = render() fireEvent.click(getByText(/show alert/i)) const providerElement = getByTestId('provider') @@ -211,16 +210,89 @@ describe('react-alert', () => { it('should respect the given offset option', () => { const App = props => ( - + ) - const { getByText, getByTestId } = render() + const { getByText, getByTestId } = render() fireEvent.click(getByText(/show alert/i)) const alertElement = getByTestId('alert') expect(alertElement).toHaveStyle('margin: 30px') }) + it('should render an alert in position passed to alert directly', () => { + const alertPosition = positions.BOTTOM_LEFT + Child = () => { + const alert = useAlert() + return + } + + const App = props => ( + + + + ) + + const { getByText, getByTestId } = render() + fireEvent.click(getByText(/show alert/i)) + + const providerElement = getByTestId('provider') + const alertElement = getByTestId('alert') + + expect(alertElement).toBeInTheDocument() + const styles = styleString(getStyles(alertPosition)) + expect(providerElement).toHaveStyle(styles) + }) + + it('should render multiple wrappers relying on amount of positions giving to alerts', () => { + const parentPosition = positions.TOP_CENTER + Child = () => { + const alert = useAlert() + return ( +
+ + + +
+ ) + } + + const App = props => ( + + + + ) + + const { getByText, getByTestId } = render() + fireEvent.click(getByText(/bottom right/i)) + fireEvent.click(getByText(/bottom left/i)) + wait (() => { + const provider = getByTestId('provider') + expect(provider).toHaveLength(2) + }) + fireEvent.click(getByText(/parent position/i)) + wait(() => { + const provider = getByTestId('provider') + expect(provider).toHaveLength(1) + const styles = styleString(getStyles(parentPosition)) + expect(provider).toHaveStyle(styles) + }) + }) + it('should remove the alert matching the given id on remove call', () => { Child = () => { const alert = useAlert() @@ -247,17 +319,19 @@ describe('react-alert', () => { } const App = props => ( - - + + ) - const { getByText } = render() + const { getByText } = render() fireEvent.click(getByText(/show alert/i)) const alertElement = getByText(/message/i) expect(getByText(/message/i)).toBeInTheDocument() fireEvent.click(getByText(/remove alert/i)) + act(jest.runOnlyPendingTimers) + expect(alertElement).not.toBeInTheDocument() }) @@ -268,11 +342,11 @@ describe('react-alert', () => { const App = props => ( - + ) - const { getByText } = render() + const { getByText } = render() fireEvent.click(getByText(/show alert/i)) expect(getByText(/message/i)).toBeInTheDocument() @@ -312,12 +386,12 @@ describe('react-alert', () => { context={BottomLeftAlertContext} position="bottom left" > - + ) - const { getByText, getByTestId } = render() + const { getByText, getByTestId } = render() fireEvent.click(getByTestId('default-context')) expect(getByText(/message/i)).toBeInTheDocument() diff --git a/src/Provider.js b/src/Provider.js index 42b55947..db00a290 100644 --- a/src/Provider.js +++ b/src/Provider.js @@ -6,6 +6,7 @@ import DefaultContext from './Context' import Wrapper from './Wrapper' import Transition from './Transition' import { positions, transitions, types } from './options' +import { groupBy } from './helpers' const Provider = ({ children, @@ -53,6 +54,7 @@ const Provider = ({ .substr(2, 9) const alertOptions = { + position: options.position || position, timeout, type, ...options @@ -107,20 +109,32 @@ const Provider = ({ info } + const alertsByPosition = groupBy(alerts, alert => alert.options.position) + return ( {children} {root.current && createPortal( - - - {alerts.map(alert => ( - - - - ))} - - , + <> + {Object.values(positions).map(position => ( + + {alertsByPosition[position] + ? alertsByPosition[position].map(alert => ( + + + + )) + : null} + + ))} + , root.current )} diff --git a/src/Wrapper.js b/src/Wrapper.js index a61aca08..ba53a8f2 100644 --- a/src/Wrapper.js +++ b/src/Wrapper.js @@ -74,9 +74,11 @@ const Wrapper = ({ const styles = useMemo(() => getStyles(position), [position]) return ( -
- {children} -
+ children.length > 0 && ( +
+ {children} +
+ ) ) } diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 00000000..84c8c1a7 --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,7 @@ +export const groupBy = (array, fn) => + array.reduce((result, item) => { + const key = fn(item) + if (!result[key]) result[key] = [] + result[key].push(item) + return result + }, {})