diff --git a/CHANGELOG.md b/CHANGELOG.md index 62aaf4d..d3a8382 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog All notable changes to this project will be documented in this file. + +## [2.2.0] - 2024-08-29 +### Added +- `response` - `response` was added to the return value of the `open` method.
+ `response` promise that is resolved after modal was closed. can be used instead of `onClose` `popupProps` callback.
+If `onClose` callback is provided, it returns the callback's return value.
+If `onClose` wasn't provided, it returns the arguments that `onClose` was called with.
+ ## [2.1.7] - 2024-01-31 ### Added - `unmount` - `popupManager.open(Modal)` returns an object that now also has `unmount` function that removes popup's instance.
diff --git a/README.md b/README.md index fbd9ff3..8f4ae40 100644 --- a/README.md +++ b/README.md @@ -133,10 +133,14 @@ If not extended, it has 2 methods: `open(componentClass, popupProps)` - opens popup. render's popup component * `componentClass` - component's class or function * `popupProps` (optional) - consumer's popup props and also accepts these: - * `onClose` - will be called on actual popup close with arguments - > `isOpen` is not allowed. + * `onClose` - Will be called on actual popup close with arguments + > `isOpen` Is not allowed. * returns - object of instance of open popup - * `close` - closes the popup - sets `isOpen` to `false`. Doesn't call `onClose` callback - * `unmount` - removes popup instance + * `close` - Closes the popup - sets `isOpen` to `false`. Doesn't call `onClose` callback + * `unmount` - Removes popup instance + * `response` - A promise that resolves once the modal is closed.
+If `onClose` callback is provided, it returns the callback's return value.
+If `onClose` wasn't provided, it returns the arguments that `onClose` was called with.
+ note: can be used instead of passing `onClose` to the `popupProps` `closeAll()` - closes all open popups. diff --git a/package.json b/package.json index 265f8f0..0115df9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-popup-manager", - "version": "2.1.13", + "version": "2.2.0", "description": "Manage react popups, Modals, Lightboxes, Notifications, etc. easily", "license": "MIT", "main": "dist/src/index.js", diff --git a/src/PopupsWrapper.tsx b/src/PopupsWrapper.tsx index 39943cb..3b9f89d 100644 --- a/src/PopupsWrapper.tsx +++ b/src/PopupsWrapper.tsx @@ -8,15 +8,18 @@ interface PopupsWrapperProps { interface SinglePopupLifeCycleProps { currentPopup: PopupItem; - onClose(guid: string): any; + + onClose(guid: string, onAfterClose: Function): any; + isOpen: boolean; } class SinglePopupLifeCycle extends React.Component { state = { isOpen: false }; + constructor(props) { - super(props); - this.onClose = this.onClose.bind(this) + super(props); + this.onClose = this.onClose.bind(this); } componentDidMount(): void { @@ -35,10 +38,14 @@ class SinglePopupLifeCycle extends React.Component { return null; } - private onClose(...params: any[]) { - const {currentPopup, onClose} = this.props; - onClose(currentPopup.guid); - currentPopup.props?.onClose?.(...params); + + private async onClose(...params: any[]) { + const { currentPopup, onClose } = this.props; + if (currentPopup.props?.onClose) { + onClose(currentPopup.guid, () => currentPopup.props?.onClose(...params)); + } else { + onClose(currentPopup.guid, () => (params?.length ? params : undefined)); + } } render() { @@ -54,16 +61,17 @@ class SinglePopupLifeCycle extends React.Component { } export class PopupsWrapper extends React.Component { - constructor(props) { - super(props); - this.onClose = this.onClose.bind(this); - } + constructor(props) { + super(props); + this.onClose = this.onClose.bind(this); + } + componentDidMount(): void { this.props.popupManager.subscribeOnPopupsChange(() => this.forceUpdate()); } - private onClose(guid: string) { - this.props.popupManager.close(guid); + private onClose(guid: string, onAfterClose?: Function) { + this.props.popupManager.close(guid, onAfterClose); } public render() { diff --git a/src/__internal__/PopupItem.ts b/src/__internal__/PopupItem.ts index 55c1386..cf79f2e 100644 --- a/src/__internal__/PopupItem.ts +++ b/src/__internal__/PopupItem.ts @@ -4,6 +4,9 @@ type PopupItemProps = PopupProps & { [key: string]: any }; export class PopupItem { private _isOpen: boolean; + private readonly _response: Promise; + private _resolve: any; + private _reject: any; constructor( public ComponentClass: any, @@ -11,13 +14,34 @@ export class PopupItem { public guid: string, ) { this._isOpen = true; + this._response = new Promise(async (resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); } public get isOpen() { return this._isOpen; } - public close() { + public get response() { + return this._response; + } + + private async resolveResponse(onAfterClose: Function) { + if (!onAfterClose) { + this._resolve(); + } + + try { + this._resolve(await onAfterClose()); + } catch (ex) { + this._reject(ex); + } + } + + public close(onAfterClose?: Function) { this._isOpen = false; + void this.resolveResponse(onAfterClose); } } diff --git a/src/__internal__/popupManagerInternal.ts b/src/__internal__/popupManagerInternal.ts index cf1ea24..636a79f 100644 --- a/src/__internal__/popupManagerInternal.ts +++ b/src/__internal__/popupManagerInternal.ts @@ -50,10 +50,11 @@ export class PopupManagerInternal implements PopupManager { return { close: () => this.close(guid), unmount: () => this.unmount(guid), + response: newPopupItem.response, }; }; - public close(popupGuid: string): void { + public close(popupGuid: string, onAfterClose?: Function): void { const currentPopupIndex = this.openPopups.findIndex( ({ guid }) => guid === popupGuid, ); @@ -64,7 +65,7 @@ export class PopupManagerInternal implements PopupManager { const currentPopup = this.openPopups[currentPopupIndex]; - currentPopup.close(); + currentPopup.close(onAfterClose); const closedPopup = this.openPopups.splice(currentPopupIndex, 1)[0]; this.closedPopups.unshift(closedPopup); diff --git a/src/popupsDef.ts b/src/popupsDef.ts index b40256b..8a41342 100644 --- a/src/popupsDef.ts +++ b/src/popupsDef.ts @@ -3,6 +3,7 @@ import { PopupManager } from './popupManager'; export interface popupInstance { close: Function; unmount: Function; + response: Promise; } export interface PopupProps { diff --git a/src/tests/TestPopupUsesIsOpen/TestPopupUsesIsOpen.tsx b/src/tests/TestPopupUsesIsOpen/TestPopupUsesIsOpen.tsx index e5ae1fe..0a0c7e3 100644 --- a/src/tests/TestPopupUsesIsOpen/TestPopupUsesIsOpen.tsx +++ b/src/tests/TestPopupUsesIsOpen/TestPopupUsesIsOpen.tsx @@ -4,12 +4,13 @@ import {PopupProps} from '../../popupsDef'; interface TestPopupUsesIsOpen extends PopupProps { content?: string; dataHook?: string; + overrideCloseArgs?: any[]; } export const generateDataHook = (index = 0) => `test-popup-${index}`; export const TestPopupUsesIsOpen = (props: TestPopupUsesIsOpen) => (
{props.content} -
); diff --git a/src/tests/specs/testPopups.response.spec.tsx b/src/tests/specs/testPopups.response.spec.tsx new file mode 100644 index 0000000..2f17dd3 --- /dev/null +++ b/src/tests/specs/testPopups.response.spec.tsx @@ -0,0 +1,177 @@ +import * as React from 'react'; +import {generateDataHook, TestPopupUsesIsOpen} from "../TestPopupUsesIsOpen/TestPopupUsesIsOpen"; +import {PopupManager} from "../../popupManager"; +import {TestPopupsDriver} from "../TestPopups.driver"; + +describe('testPopups - "response" ', () => { + let driver: TestPopupsDriver; + const buttonOpenDataHook = 'button-open'; + + it('should return "response" of consumer\'s "onClose" override with SYNCHRONOUS function', async () => { + const popupManager = new PopupManager(); + let responsePromise: Promise; + const expectedResponse = 'expectedResponseForOnCloseOverride'; + const onClick = () => { + const {response} = popupManager.open(TestPopupUsesIsOpen, { + onClose: () => { + return expectedResponse; + } + }); + responsePromise = response; + } + + const testedComponent = () => ( +
+
+ ); + + driver = new TestPopupsDriver(); + driver.given.component(testedComponent).given.popupManager(popupManager); + driver.when.create(); + + driver.when.inGivenComponent.clickOn(buttonOpenDataHook); + + expect(driver.get.isPopupOpen()).toBe(true); + driver.get.popupDriver(generateDataHook()).when.closePopup(); + expect(driver.get.popupDriver(generateDataHook()).get.isOpen()).toBe(false); + expect(await responsePromise).toBe(expectedResponse); + }); + + it('should return "response" of consumer\'s "onClose" override with ASYNCHRONOUS function', async () => { + const popupManager = new PopupManager(); + let responsePromise: Promise; + const expectedResponse = 'expectedResponseForOnCloseOverride'; + const onClick = () => { + const {response} = popupManager.open(TestPopupUsesIsOpen, { + onClose: () => { + return new Promise(resolve => setTimeout(() => resolve(expectedResponse), 100)); + } + }); + responsePromise = response; + } + + const testedComponent = () => ( +
+
+ ); + + driver = new TestPopupsDriver(); + driver.given.component(testedComponent).given.popupManager(popupManager); + driver.when.create(); + + driver.when.inGivenComponent.clickOn(buttonOpenDataHook); + + expect(driver.get.isPopupOpen()).toBe(true); + driver.get.popupDriver(generateDataHook()).when.closePopup(); + expect(driver.get.popupDriver(generateDataHook()).get.isOpen()).toBe(false); + expect(await responsePromise).toBe(expectedResponse); + }); + + it('should return arguments that modal\'s onClose sent, and that hasn\'t received "onClose" override', async () => { + const buttonOpenDataHook = 'button-open'; + const popupManager = new PopupManager(); + let responsePromise: Promise; + const expectedResponse = ['modalResponse', false, -22]; + const onClick = () => { + const {response} = popupManager.open(TestPopupUsesIsOpen, {overrideCloseArgs: expectedResponse}); + responsePromise = response; + } + + const testedComponent = () => ( +
+
+ ); + + driver = new TestPopupsDriver(); + driver.given.component(testedComponent).given.popupManager(popupManager); + driver.when.create(); + + driver.when.inGivenComponent.clickOn(buttonOpenDataHook); + + expect(driver.get.isPopupOpen()).toBe(true); + driver.get.popupDriver(generateDataHook()).when.closePopup(); + expect(driver.get.popupDriver(generateDataHook()).get.isOpen()).toBe(false); + expect(await responsePromise).toEqual(expectedResponse); + }); + + it('should return NOTHING when when modal\'s onClose called with not arguments', async () => { + const buttonOpenDataHook = 'button-open'; + const popupManager = new PopupManager(); + let responsePromise: Promise; + const onClick = () => { + const {response} = popupManager.open(TestPopupUsesIsOpen, {overrideCloseArgs: null}); + responsePromise = response; + } + + const testedComponent = () => ( +
+
+ ); + + driver = new TestPopupsDriver(); + driver.given.component(testedComponent).given.popupManager(popupManager); + driver.when.create(); + + driver.when.inGivenComponent.clickOn(buttonOpenDataHook); + + expect(driver.get.isPopupOpen()).toBe(true); + driver.get.popupDriver(generateDataHook()).when.closePopup(); + expect(driver.get.popupDriver(generateDataHook()).get.isOpen()).toBe(false); + expect(await responsePromise).toBe(undefined); + }); + + + it('should return exception when "onClose" has inner promise that is thrown', async () => { + const popupManager = new PopupManager(); + let responsePromise: Promise; + const expectedError = 'Error in onClose'; + const onClick = () => { + const {response} = popupManager.open(TestPopupUsesIsOpen, { + onClose: async () => { + await new Promise(() => { + throw new Error(expectedError) + }) + + } + }); + responsePromise = response; + } + + const testedComponent = () => ( +
+
+ ); + + driver = new TestPopupsDriver(); + driver.given.component(testedComponent).given.popupManager(popupManager); + driver.when.create(); + + driver.when.inGivenComponent.clickOn(buttonOpenDataHook); + + expect(driver.get.isPopupOpen()).toBe(true); + driver.get.popupDriver(generateDataHook()).when.closePopup(); + expect(driver.get.popupDriver(generateDataHook()).get.isOpen()).toBe(false); + await expect(responsePromise).rejects.toThrow(expectedError); + }); + } +); \ No newline at end of file diff --git a/src/tests/testPopups.spec.tsx b/src/tests/specs/testPopups.spec.tsx similarity index 96% rename from src/tests/testPopups.spec.tsx rename to src/tests/specs/testPopups.spec.tsx index 474d8f1..22f34af 100644 --- a/src/tests/testPopups.spec.tsx +++ b/src/tests/specs/testPopups.spec.tsx @@ -1,11 +1,11 @@ -import {TestPopupsDriver} from './TestPopups.driver'; -import {TestPopupsManager} from './testPopupsManager'; +import {TestPopupsDriver} from '../TestPopups.driver'; +import {TestPopupsManager} from '../testPopupsManager'; import * as React from 'react'; -import {generateDataHook, TestPopupUsesIsOpen} from "./TestPopupUsesIsOpen/TestPopupUsesIsOpen"; -import {PopupManager, PopupProps} from '../index'; -import {usePopupManager} from '../index'; +import {generateDataHook, TestPopupUsesIsOpen} from "../TestPopupUsesIsOpen/TestPopupUsesIsOpen"; +import {PopupManager, PopupProps} from '../../index'; +import {usePopupManager} from '../../index'; import {useEffect} from "react"; -import {getByDataHook} from "./getByDataHook"; +import {getByDataHook} from "../getByDataHook"; describe('Popups', () => { let driver: TestPopupsDriver; @@ -170,13 +170,14 @@ describe('Popups', () => { it('should close popup with params', () => { const onClose = jest.fn(); + const expectedArgs = ['value', true, 1]; justBeforeEachTest({popupManager: new TestPopupsManager()}); - (popupManager as TestPopupsManager).openTestPopup(generateDataHook(), onClose); + (popupManager as TestPopupsManager).openTestPopup(generateDataHook(), onClose, '', expectedArgs); driver.update(); driver.get.popupDriver(generateDataHook()).when.closePopup(); expect(driver.get.popupDriver(generateDataHook()).get.isOpen()).toBe(false); - expect(onClose).toHaveBeenCalledWith('value', true, 1); + expect(onClose).toHaveBeenCalledWith(...expectedArgs); }); it('should pass popup its own props', () => { diff --git a/src/tests/testPopupsManager.ts b/src/tests/testPopupsManager.ts index 655ca69..3d5e81f 100644 --- a/src/tests/testPopupsManager.ts +++ b/src/tests/testPopupsManager.ts @@ -3,7 +3,7 @@ import { TestPopupUsesIsOpen } from './TestPopupUsesIsOpen/TestPopupUsesIsOpen' import {popupInstance} from '../popupsDef'; export class TestPopupsManager extends PopupManager { - public openTestPopup(dataHook: string, onClose?: () => void, content?: string): popupInstance { - return this.open(TestPopupUsesIsOpen, { onClose, content, dataHook }); + public openTestPopup(dataHook: string, onClose?: () => void, content?: string, overrideCloseArgs?: any[]): popupInstance { + return this.open(TestPopupUsesIsOpen, { onClose, content, dataHook , overrideCloseArgs}); } }