Skip to content

Commit

Permalink
feat(react-popup-manager) - adding 'response' to return value of `ope…
Browse files Browse the repository at this point in the history
…n` popup. is resolved after modal is closed.

#pr-cr@itaya,nitzansi,alexko
  • Loading branch information
varzager committed Aug 29, 2024
1 parent c56a549 commit e504790
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 28 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 <br>
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. <br>
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`. <i>Doesn't call `onClose` callback</i>
* `unmount` - removes popup instance
* `response` - promise that is resolved after modal was closed. can be used instead of `onClose` `popupProps` callback.<br>
Returns response of `onClose` callback, otherwise, if `onClose` wasn't passed, the arguments that `onClose` was called with.<br>
<i>note: can be used instead of passing `onClose` to the `popupProps`</i>

`closeAll()` - closes all open popups.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
34 changes: 21 additions & 13 deletions src/PopupsWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SinglePopupLifeCycleProps> {
state = { isOpen: false };

constructor(props) {
super(props);
this.onClose = this.onClose.bind(this)
super(props);
this.onClose = this.onClose.bind(this);
}

componentDidMount(): void {
Expand All @@ -35,10 +38,14 @@ class SinglePopupLifeCycle extends React.Component<SinglePopupLifeCycleProps> {

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() {
Expand All @@ -54,16 +61,17 @@ class SinglePopupLifeCycle extends React.Component<SinglePopupLifeCycleProps> {
}

export class PopupsWrapper extends React.Component<PopupsWrapperProps> {
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() {
Expand Down
36 changes: 35 additions & 1 deletion src/__internal__/PopupItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,54 @@ type PopupItemProps = PopupProps & { [key: string]: any };

export class PopupItem {
private _isOpen: boolean;
private readonly _response: Promise<any>;
private _resolve: any;
private _reject: any;

constructor(
public ComponentClass: any,
public props: PopupItemProps,
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);
}
}
5 changes: 3 additions & 2 deletions src/__internal__/popupManagerInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/popupsDef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PopupManager } from './popupManager';
export interface popupInstance {
close: Function;
unmount: Function;
response: Promise<any>;
}

export interface PopupProps {
Expand Down
3 changes: 2 additions & 1 deletion src/tests/TestPopupUsesIsOpen/TestPopupUsesIsOpen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
<div data-is-open={props.isOpen} data-hook={props.dataHook || generateDataHook()}>
<span data-hook="popup-content">{props.content}</span>
<button data-hook="close-button" onClick={() => props.onClose('value', true, 1)}/>
<button data-hook="close-button" onClick={() => props.overrideCloseArgs ? props.onClose(...props.overrideCloseArgs): props.onClose()}/>
</div>
);
177 changes: 177 additions & 0 deletions src/tests/specs/testPopups.resolve.spec.tsx
Original file line number Diff line number Diff line change
@@ -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<any>;
const expectedResponse = 'expectedResponseForOnCloseOverride';
const onClick = () => {
const {response} = popupManager.open(TestPopupUsesIsOpen, {
onClose: () => {
return expectedResponse;
}
});
responsePromise = response;
}

const testedComponent = () => (
<div>
<button
data-hook={buttonOpenDataHook}
onClick={onClick}
/>
</div>
);

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<any>;
const expectedResponse = 'expectedResponseForOnCloseOverride';
const onClick = () => {
const {response} = popupManager.open(TestPopupUsesIsOpen, {
onClose: () => {
return new Promise(resolve => setTimeout(() => resolve(expectedResponse), 100));
}
});
responsePromise = response;
}

const testedComponent = () => (
<div>
<button
data-hook={buttonOpenDataHook}
onClick={onClick}
/>
</div>
);

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<any>;
const expectedResponse = ['modalResponse', false, -22];
const onClick = () => {
const {response} = popupManager.open(TestPopupUsesIsOpen, {overrideCloseArgs: expectedResponse});
responsePromise = response;
}

const testedComponent = () => (
<div>
<button
data-hook={buttonOpenDataHook}
onClick={onClick}
/>
</div>
);

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<any>;
const onClick = () => {
const {response} = popupManager.open(TestPopupUsesIsOpen, {overrideCloseArgs: null});
responsePromise = response;
}

const testedComponent = () => (
<div>
<button
data-hook={buttonOpenDataHook}
onClick={onClick}
/>
</div>
);

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<any>;
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 = () => (
<div>
<button
data-hook={buttonOpenDataHook}
onClick={onClick}
/>
</div>
);

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);
});
}
);
Loading

0 comments on commit e504790

Please sign in to comment.