diff --git a/package.json b/package.json index 31c133e..b839f41 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "lint": "aqu lint", "lint:fix": "aqu lint --fix", "test": "aqu test", - "test:watch": "npm test -- --watch", - "test:coverage": "npm test -- --coverage --json --silent --ci --outputFile=\"report.json\"", - "test:log-coverage": "npm test -- --coverage --silent --ci --coverageReporters=text", + "test:watch": "pnpm test -- --watch", + "test:coverage": "pnpm test -- --coverage --json --silent --ci --outputFile=\"report.json\"", + "test:log-coverage": "pnpm test -- --coverage --silent --ci --coverageReporters=text", "release": "np", "prepublishOnly": "aqu build" }, diff --git a/src/hooks/usePopup.ts b/src/hooks/usePopup.ts index 857be22..a240981 100644 --- a/src/hooks/usePopup.ts +++ b/src/hooks/usePopup.ts @@ -1,4 +1,4 @@ -import { ComponentType, useCallback, useRef } from 'react'; +import { ComponentType, useCallback, useEffect, useRef } from 'react'; import { nanoid } from 'nanoid/non-secure'; import { useEvent } from './useEvent'; @@ -10,15 +10,15 @@ import { PopupIdentifier } from '../types/PopupIdentifier'; export type UsePopupBag = [ open: OptionalParamFunction, void>, - close: () => void + close: () => void, ]; export const usePopup = ( PopupComponent: ComponentType

, props: Pick, - group: PopupGroup + group: PopupGroup, ): UsePopupBag => { - const { mount, close: closePopup, unmount } = usePopupsContext(); + const { mount, close: closePopup, unmount, update } = usePopupsContext(); const popupIdentifier = useRef({ id: nanoid(), @@ -35,16 +35,20 @@ export const usePopup = ( PopupComponent as ComponentType<{}>, { ...props, ...omittedProps }, popupIdentifier.current, - defaultClose + defaultClose, ); mount(popup); - } + }, ); const close = useCallback(() => { closePopup(popupIdentifier.current); }, [closePopup]); + useEffect(() => { + update(popupIdentifier.current, props); + }, [props]); + return [open, close]; }; diff --git a/src/hooks/usePopupsBag.ts b/src/hooks/usePopupsBag.ts index 54508d2..18bcc18 100644 --- a/src/hooks/usePopupsBag.ts +++ b/src/hooks/usePopupsBag.ts @@ -36,17 +36,31 @@ export const usePopupsBag = (): PopupsBag => { return popup.popupIdentifier; }, []); + const update = useCallback( + (popupIdentifier: PopupIdentifier, props: object) => { + dispatch({ + type: ActionType.UPDATE, + payload: { + popupIdentifier, + props, + }, + }); + }, + [], + ); + const close = useCallback( (popupIdentifier: PopupIdentifier) => { const popup = getPopup(popupIdentifier); popup?.close(); }, - [getPopup] + [getPopup], ); return { mount, unmount, + update, getPopup, close, popupsState, diff --git a/src/types/PopupsBag.ts b/src/types/PopupsBag.ts index 50e9341..d822a5b 100644 --- a/src/types/PopupsBag.ts +++ b/src/types/PopupsBag.ts @@ -5,6 +5,7 @@ import { PopupsState } from '../utils/popupsReducer'; export type PopupsBag = { mount:

(popup: Popup

) => PopupIdentifier; unmount: (popupIdentifier: PopupIdentifier) => void; + update: (popupIdentifier: PopupIdentifier, props: object) => void; close: (popupIdentifier: PopupIdentifier) => void; popupsState: PopupsState; diff --git a/src/utils/popupsReducer.ts b/src/utils/popupsReducer.ts index 4f1d285..1458c37 100644 --- a/src/utils/popupsReducer.ts +++ b/src/utils/popupsReducer.ts @@ -5,6 +5,7 @@ import { PopupsRegistry } from '../types/PopupsRegistry'; export enum ActionType { MOUNT, UNMOUNT, + UPDATE, } type MountAction = { @@ -21,14 +22,21 @@ type UnmountAction = { }; }; -export type PopupsAction = MountAction | UnmountAction; +type UpdateAction = { + type: ActionType.UPDATE; + payload: { + popupIdentifier: PopupIdentifier; + props: object; + }; +}; + +export type PopupsAction = MountAction | UnmountAction | UpdateAction; export type PopupsState = { popups: PopupsRegistry }; -export const popupsReducer = ( - { popups }: PopupsState, - action: PopupsAction -) => { +export const popupsReducer = (prevState: PopupsState, action: PopupsAction) => { + const { popups } = prevState; + switch (action.type) { case ActionType.MOUNT: { const { popup } = action.payload; @@ -47,6 +55,22 @@ export const popupsReducer = ( }; } + case ActionType.UPDATE: { + const { popupIdentifier, props } = action.payload; + + const { groupId, id } = popupIdentifier; + + if (!popups[groupId]?.[id]) { + return prevState; + } + + popups[groupId][id].props = props; + + return { + popups, + }; + } + case ActionType.UNMOUNT: { const { groupId, id } = action.payload.popupIdentifier; diff --git a/test/hooks/usePopup.test.tsx b/test/hooks/usePopup.test.tsx index 74c1cd2..fa91a84 100644 --- a/test/hooks/usePopup.test.tsx +++ b/test/hooks/usePopup.test.tsx @@ -14,7 +14,7 @@ const SimplePopupComponent: React.FC = jest.fn(() => { }); const CustomizablePopupComponent: React.FC<{ message: string }> = jest.fn( - ({ message }) =>

{message}
+ ({ message }) =>
{message}
, ); const PopupComponentWithProps: React.FC<{ @@ -24,11 +24,12 @@ const PopupComponentWithProps: React.FC<{ describe('usePopup', () => { it('should render only one popup', () => { + const initialProps = {}; const { result } = renderHook( - () => usePopup(SimplePopupComponent, {}, group), + () => usePopup(SimplePopupComponent, initialProps, group), { wrapper: TestHookWrapper, - } + }, ); act(() => { @@ -45,11 +46,12 @@ describe('usePopup', () => { }); it('should close popup', () => { + const initialProps = {}; const { result } = renderHook( - () => usePopup(SimplePopupComponent, {}, group), + () => usePopup(SimplePopupComponent, initialProps, group), { wrapper: TestHookWrapper, - } + }, ); act(() => { @@ -65,13 +67,14 @@ describe('usePopup', () => { expect(() => screen.getByText('simple popup')).toThrow(); }); - it('should update popup', () => { + it('should reopen popup with new props', () => { const initialMessage = 'initial message'; const updatedMessage = 'updated message'; + const initialProps = {}; const { result } = renderHook( - () => usePopup(CustomizablePopupComponent, {}, group), - { wrapper: TestHookWrapper } + () => usePopup(CustomizablePopupComponent, initialProps, group), + { wrapper: TestHookWrapper }, ); act(() => { @@ -90,11 +93,12 @@ describe('usePopup', () => { }); it('should merge props', () => { + const initialProps = { prop1: 42 }; const { result } = renderHook( - () => usePopup(PopupComponentWithProps, { prop1: 42 }, group), + () => usePopup(PopupComponentWithProps, initialProps, group), { wrapper: TestHookWrapper, - } + }, ); act(() => { @@ -102,10 +106,35 @@ describe('usePopup', () => { }); expect( - (PopupComponentWithProps as jest.Mock).mock.calls[0][0] + (PopupComponentWithProps as jest.Mock).mock.calls[0][0], ).toStrictEqual({ prop1: 42, prop2: 'hello', }); }); + + it('should update popup when props changing', () => { + const { result, rerender } = renderHook( + (props: { message: string }) => + usePopup(CustomizablePopupComponent, props, group), + { + wrapper: TestHookWrapper, + initialProps: { + message: 'initial', + }, + }, + ); + + act(() => { + result.current[0](); + }); + + expect(screen.getByText('initial')).toBeDefined(); + + rerender({ + message: 'updated', + }); + + expect(screen.getByText('updated')).toBeDefined(); + }); });