Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .changeset/tiny-pots-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"overlay-kit": patch
---

Fixed a bug where overlays could not be reopened after closing with the same ID.

### Fixed Issues
- Resolved issue where overlays would not display when reopened with the same `overlayId` after being closed
- Improved mounting state tracking with added `isMounted` property in overlay state management
- Added state change detection logic to ensure proper reopen handling

### Changes
- Added `isMounted` property to `OverlayItem` type for better mounting state tracking
- Enhanced `ADD` action in reducer to handle reopening of existing closed overlays
- Implemented automatic reopen mechanism through state comparison in `OverlayProvider`
- Added test cases for overlay reopen scenarios

This fix ensures overlays work as expected when closing and reopening them.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type FC, useEffect, type ActionDispatch, memo } from 'react';
import { type FC, type ActionDispatch, memo, useEffect } from 'react';
import { type OverlayReducerAction } from '../reducer';

type OverlayControllerProps = {
Expand Down Expand Up @@ -32,8 +32,8 @@ export const ContentOverlayController = memo(

return (
<Controller
overlayId={overlayId}
isOpen={isOpen}
overlayId={overlayId}
close={() => overlayDispatch({ type: 'CLOSE', overlayId })}
unmount={() => overlayDispatch({ type: 'REMOVE', overlayId })}
/>
Expand Down
33 changes: 25 additions & 8 deletions packages/src/context/provider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useReducer, type PropsWithChildren } from 'react';
import { useCallback, useEffect, useReducer, useRef, type PropsWithChildren } from 'react';
import { ContentOverlayController } from './content-overlay-controller';
import { type OverlayEvent, createOverlay } from '../../event';
import { randomId } from '../../utils/random-id';
Expand All @@ -16,6 +16,7 @@ export function createOverlayProvider() {
overlayOrderList: [],
overlayData: {},
});
const prevOverlayState = useRef(overlayState);

const open: OverlayEvent['open'] = useCallback(({ controller, overlayId, componentKey }) => {
overlayDispatch({
Expand All @@ -24,6 +25,7 @@ export function createOverlayProvider() {
id: overlayId,
componentKey,
isOpen: false,
isMounted: false,
controller: controller,
},
});
Expand All @@ -43,6 +45,26 @@ export function createOverlayProvider() {

useOverlayEvent({ open, close, unmount, closeAll, unmountAll });

if (prevOverlayState.current !== overlayState) {
overlayState.overlayOrderList.forEach((overlayId) => {
const prevOverlayData = prevOverlayState.current.overlayData;
const currOverlayData = overlayState.overlayData;

if (prevOverlayData[overlayId] != null && prevOverlayData[overlayId].isMounted === true) {
const isPrevOverlayClosed = prevOverlayData[overlayId].isOpen === false;
const isCurrOverlayOpened = currOverlayData[overlayId].isOpen === true;

if (isPrevOverlayClosed && isCurrOverlayOpened) {
requestAnimationFrame(() => {
overlayDispatch({ type: 'OPEN', overlayId });
});
}
}
});

prevOverlayState.current = overlayState;
}

useEffect(() => {
return () => {
overlayDispatch({ type: 'REMOVE_ALL' });
Expand All @@ -53,20 +75,15 @@ export function createOverlayProvider() {
<OverlayContextProvider value={overlayState}>
{children}
{overlayState.overlayOrderList.map((item) => {
const {
id: currentOverlayId,
componentKey,
isOpen,
controller: currentController,
} = overlayState.overlayData[item];
const { id: currentOverlayId, componentKey, isOpen, controller: Controller } = overlayState.overlayData[item];

return (
<ContentOverlayController
key={componentKey}
isOpen={isOpen}
controller={Controller}
overlayId={currentOverlayId}
overlayDispatch={overlayDispatch}
controller={currentController}
/>
);
})}
Expand Down
24 changes: 20 additions & 4 deletions packages/src/context/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type OverlayItem = {
*/
componentKey: string;
isOpen: boolean;
isMounted: boolean;
controller: OverlayControllerComponent;
};
export type OverlayData = {
Expand Down Expand Up @@ -63,6 +64,24 @@ export const determineCurrentOverlayId = (
export function overlayReducer(state: OverlayData, action: OverlayReducerAction): OverlayData {
switch (action.type) {
case 'ADD': {
if (state.overlayData[action.overlay.id] != null && state.overlayData[action.overlay.id].isOpen === false) {
const overlay = state.overlayData[action.overlay.id];

// ignore if the overlay don't exist or already open
if (overlay == null || overlay.isOpen) {
return state;
}

return {
...state,
current: action.overlay.id,
overlayData: {
...state.overlayData,
[action.overlay.id]: { ...overlay, isOpen: true },
},
};
}

const isExisted = state.overlayOrderList.includes(action.overlay.id);

if (isExisted && state.overlayData[action.overlay.id].isOpen === true) {
Expand Down Expand Up @@ -97,10 +116,7 @@ export function overlayReducer(state: OverlayData, action: OverlayReducerAction)
...state,
overlayData: {
...state.overlayData,
[action.overlayId]: {
...overlay,
isOpen: true,
},
[action.overlayId]: { ...overlay, isOpen: true, isMounted: true },
},
};
}
Expand Down
42 changes: 42 additions & 0 deletions packages/src/event.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,13 @@ describe('overlay object', () => {
expect(screen.queryByTestId(/^overlay-/)).not.toBeInTheDocument();
expect(screen.queryByText('has Open overlay')).not.toBeInTheDocument();
});

overlay.open(({ isOpen }) => isOpen && <div data-testid="overlay-1">{contents.first}</div>);
overlay.open(({ isOpen }) => isOpen && <div data-testid="overlay-2">{contents.second}</div>);
await waitFor(() => {
expect(screen.getByTestId('overlay-1')).toBeInTheDocument();
expect(screen.getByTestId('overlay-2')).toBeInTheDocument();
});
});

it('should not be able to get current overlay when all overlays are closed', async () => {
Expand Down Expand Up @@ -414,4 +421,39 @@ describe('overlay object', () => {
expect(screen.getByTestId('overlay-1')).toBeInTheDocument();
});
});

it('should be able to open an overlay after closing it', async () => {
const overlayId = 'overlay-content-1';

function Component() {
const current = useCurrentOverlay();

useEffect(() => {
overlay.open(({ isOpen }) => isOpen && <div data-testid="overlay-1">{overlayId}</div>, {
overlayId: overlayId,
});
}, []);

return <div data-testid="current-overlay">{current}</div>;
}

render(<Component />, { wrapper });

await waitFor(() => {
expect(screen.getByTestId('overlay-1')).toBeVisible();
expect(screen.getByTestId('current-overlay')).toHaveTextContent(overlayId);
});

overlay.close(overlayId);
await waitFor(() => {
expect(screen.queryByTestId('overlay-1')).not.toBeInTheDocument();
expect(screen.getByTestId('current-overlay')).toHaveTextContent('');
});

overlay.open(({ isOpen }) => isOpen && <div data-testid="overlay-1">{overlayId}</div>, { overlayId });
await waitFor(() => {
expect(screen.getByTestId('overlay-1')).toBeVisible();
expect(screen.getByTestId('current-overlay')).toHaveTextContent(overlayId);
});
});
});