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 all 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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. <br>
`response` promise that is resolved after modal was closed. can be used instead of `onClose` `popupProps` callback.<br>
If `onClose` callback is provided, it returns the callback's return value. <br>
If `onClose` wasn't provided, it returns the arguments that `onClose` was called with. <br>

## [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
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` <i>(optional)</i> - 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`. <i>Doesn't call `onClose` callback</i>
* `unmount` - removes popup instance
* `close` - Closes the popup - sets `isOpen` to `false`. <i>Doesn't call `onClose` callback</i>
* `unmount` - Removes popup instance
* `response` - A promise that resolves once the modal is closed.<br>
If `onClose` callback is provided, it returns the callback's return value. <br>
If `onClose` wasn't provided, it returns 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