From e504790f31265033edcecaf7af05d5fdcddab718 Mon Sep 17 00:00:00 2001 From: guyw Date: Thu, 29 Aug 2024 17:30:03 +0300 Subject: [PATCH] feat(react-popup-manager) - adding 'response' to return value of `open` popup. is resolved after modal is closed. #pr-cr@itaya,nitzansi,alexko --- CHANGELOG.md | 7 + README.md | 3 + package.json | 2 +- src/PopupsWrapper.tsx | 34 ++-- src/__internal__/PopupItem.ts | 36 +++- src/__internal__/popupManagerInternal.ts | 5 +- src/popupsDef.ts | 1 + .../TestPopupUsesIsOpen.tsx | 3 +- src/tests/specs/testPopups.resolve.spec.tsx | 177 ++++++++++++++++++ src/tests/{ => specs}/testPopups.spec.tsx | 17 +- src/tests/testPopupsManager.ts | 4 +- 11 files changed, 261 insertions(+), 28 deletions(-) create mode 100644 src/tests/specs/testPopups.resolve.spec.tsx rename src/tests/{ => specs}/testPopups.spec.tsx (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62aaf4d..f59a983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog All notable changes to this project will be documented in this file. + +## [2.2.0] - 2024-08-29 +### Added +- `response` - `popupManager.open(Modal)` returns an object that now also has `response` promise that is resolved with consumer's `onClose` `prop`'s response, and if not exist that the arguments that onClose were called with
+ This is great for scenarios having confirmation modals that are needed only in some cases - such as navigating out of a page that hasn't been saved + This can also replace the need for `onClose` callback `prop` entirely. + ## [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..a8e7e86 100644 --- a/README.md +++ b/README.md @@ -138,5 +138,8 @@ If not extended, it has 2 methods: * returns - object of instance of open popup * `close` - closes the popup - sets `isOpen` to `false`. Doesn't call `onClose` callback * `unmount` - removes popup instance + * `response` - promise that is resolved after modal was closed. can be used instead of `onClose` `popupProps` callback.
+ Returns response of `onClose` callback, otherwise, if `onClose` wasn't passed, 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..a822423 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, result: any) { + this.props.popupManager.close(guid, result); } public render() { diff --git a/src/__internal__/PopupItem.ts b/src/__internal__/PopupItem.ts index 55c1386..c9b39da 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,44 @@ 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(); + } + + let result: any; + let error: any; + try { + result = await onAfterClose(); + } catch (ex) { + error = ex; + } + + if (error) { + this._reject(error); + } else if (result) { + this._resolve(result); + } else { + this._resolve(); + } + } + + 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.resolve.spec.tsx b/src/tests/specs/testPopups.resolve.spec.tsx new file mode 100644 index 0000000..6457ac2 --- /dev/null +++ b/src/tests/specs/testPopups.resolve.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('TestPopupUsesIsOpen', () => { + 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}); } }