Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-popup-manager) - adding 'response' to return value of open popup. is resolved after modal is closed. #28

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Copy link

@itayavra itayavra Aug 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure its clear what this does

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, onAfterClose?: Function) {
this.props.popupManager.close(guid, onAfterClose);
}

public render() {
Expand Down
26 changes: 25 additions & 1 deletion src/__internal__/PopupItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,44 @@ 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();
}

try {
this._resolve(await onAfterClose());
} catch (ex) {
this._reject(ex);
}
}

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.response.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('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<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
Loading