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
5 changes: 5 additions & 0 deletions .changeset/stale-taxis-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"overlay-kit": minor
---

refactor: migrate event based store
4 changes: 0 additions & 4 deletions packages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@
"test:attw": "attw --pack",
"test:publint": "publint"
},
"dependencies": {
"use-sync-external-store": "^1.4.0"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.1",
"@testing-library/dom": "^10.4.0",
Expand All @@ -59,7 +56,6 @@
"@testing-library/user-event": "^14.5.2",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@types/use-sync-external-store": "^0.0.6",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^2.1.8",
"jsdom": "^25.0.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/src/context/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type OverlayData } from './store';
import { type OverlayData } from './reducer';
import { createSafeContext } from '../utils/create-safe-context';

export function createOverlaySafeContext() {
Expand Down
46 changes: 37 additions & 9 deletions packages/src/context/provider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,48 @@
import { useEffect, type PropsWithChildren } from 'react';
import { useCallback, useEffect, useReducer, type PropsWithChildren } from 'react';
import { ContentOverlayController } from './content-overlay-controller';
import { useSyncOverlayStore } from './use-sync-overlay-store';
import { createOverlay } from '../../event';
import { type OverlayEvent, createOverlay } from '../../event';
import { createOverlaySafeContext } from '../context';
import { type OverlayStore } from '../store';
import { overlayReducer } from '../reducer';

export function createOverlayProvider(overlayStore: OverlayStore) {
const overlay = createOverlay(overlayStore);
export function createOverlayProvider() {
const { useOverlayEvent, ...overlay } = createOverlay();
const { OverlayContextProvider, useCurrentOverlay, useOverlayData } = createOverlaySafeContext();

function OverlayProvider({ children }: PropsWithChildren) {
const overlayState = useSyncOverlayStore(overlayStore);
const [overlayState, overlayDispatch] = useReducer(overlayReducer, {
current: null,
overlayOrderList: [],
overlayData: {},
});

const open: OverlayEvent['open'] = useCallback(({ controller, overlayId }) => {
overlayDispatch({
type: 'ADD',
overlay: {
id: overlayId,
isOpen: false,
controller: controller,
},
});
}, []);
const close: OverlayEvent['close'] = useCallback((overlayId: string) => {
overlayDispatch({ type: 'CLOSE', overlayId });
}, []);
const unmount: OverlayEvent['unmount'] = useCallback((overlayId: string) => {
overlayDispatch({ type: 'REMOVE', overlayId });
}, []);
const closeAll: OverlayEvent['closeAll'] = useCallback(() => {
overlayDispatch({ type: 'CLOSE_ALL' });
}, []);
const unmountAll: OverlayEvent['unmountAll'] = useCallback(() => {
overlayDispatch({ type: 'REMOVE_ALL' });
}, []);

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

useEffect(() => {
return () => {
overlayStore.dispatchOverlay({ type: 'REMOVE_ALL' });
overlayDispatch({ type: 'REMOVE_ALL' });
};
}, []);

Expand All @@ -32,7 +60,7 @@ export function createOverlayProvider(overlayStore: OverlayStore) {
overlayId={currentOverlayId}
onMounted={() => {
requestAnimationFrame(() => {
overlayStore.dispatchOverlay({ type: 'OPEN', overlayId: currentOverlayId });
overlayDispatch({ type: 'OPEN', overlayId: currentOverlayId });
});
}}
onCloseModal={() => overlay.close(currentOverlayId)}
Expand Down
10 changes: 0 additions & 10 deletions packages/src/context/provider/use-sync-overlay-store.ts

This file was deleted.

18 changes: 15 additions & 3 deletions packages/src/context/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { type OverlayData, type OverlayItem } from './store';

export type OverlayReducerAction =
import { type OverlayControllerComponent } from './provider/content-overlay-controller';

type OverlayId = string;
type OverlayItem = {
id: OverlayId;
isOpen: boolean;
controller: OverlayControllerComponent;
};
export type OverlayData = {
current: OverlayId | null;
overlayOrderList: OverlayId[];
overlayData: Record<OverlayId, OverlayItem>;
};

type OverlayReducerAction =
| { type: 'ADD'; overlay: OverlayItem }
| { type: 'OPEN'; overlayId: string }
| { type: 'CLOSE'; overlayId: string }
Expand Down
54 changes: 0 additions & 54 deletions packages/src/context/store.ts

This file was deleted.

6 changes: 4 additions & 2 deletions packages/src/event.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { useEffect, type PropsWithChildren } from 'react';
import { describe, expect, it, vi } from 'vitest';
Expand Down Expand Up @@ -377,7 +377,9 @@ describe('overlay object', () => {

// Using the same overlayId causes an error
expect(() => {
overlay.open(({ isOpen }) => isOpen && <div data-testid="overlay-2" />, { overlayId: sameOverlayId });
act(() => {
overlay.open(({ isOpen }) => isOpen && <div data-testid="overlay-2" />, { overlayId: sameOverlayId });
});
}).toThrowError("You can't open the multiple overlays with the same overlayId. Please set a different id.");
});

Expand Down
58 changes: 23 additions & 35 deletions packages/src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,33 @@ import {
type OverlayAsyncControllerComponent,
type OverlayControllerComponent,
} from './context/provider/content-overlay-controller';
import { type OverlayStore } from './context/store';
import { createUseExternalEvents } from './utils';
import { randomId } from './utils/random-id';

export type OverlayEvent = {
open: (args: { controller: OverlayControllerComponent; overlayId: string }) => void;
close: (overlayId: string) => void;
unmount: (overlayId: string) => void;
closeAll: () => void;
unmountAll: () => void;
};

type OpenOverlayOptions = {
overlayId?: string;
};

export function createOverlay(overlayStore: OverlayStore) {
function open(controller: OverlayControllerComponent, options?: OpenOverlayOptions) {
const overlayId = options?.overlayId ?? randomId();
export function createOverlay() {
const [useOverlayEvent, createEvent] = createUseExternalEvents<OverlayEvent>('overlay-kit');

overlayStore.dispatchOverlay({
type: 'ADD',
overlay: {
id: overlayId,
isOpen: false,
controller: controller,
},
});
const open = (controller: OverlayControllerComponent, options?: OpenOverlayOptions) => {
const overlayId = options?.overlayId ?? randomId();
const dispatchOpenEvent = createEvent('open');

dispatchOpenEvent({ controller, overlayId });
return overlayId;
}
};

async function openAsync<T>(controller: OverlayAsyncControllerComponent<T>, options?: OpenOverlayOptions) {
const openAsync = async <T>(controller: OverlayAsyncControllerComponent<T>, options?: OpenOverlayOptions) => {
return new Promise<T>((resolve) => {
open((overlayProps, ...deprecatedLegacyContext) => {
/**
Expand All @@ -42,27 +45,12 @@ export function createOverlay(overlayStore: OverlayStore) {
return controller(props, ...deprecatedLegacyContext);
}, options);
});
}
};

function close(overlayId: string) {
overlayStore.dispatchOverlay({ type: 'CLOSE', overlayId });
}
function unmount(overlayId: string) {
overlayStore.dispatchOverlay({ type: 'REMOVE', overlayId });
}
function closeAll() {
overlayStore.dispatchOverlay({ type: 'CLOSE_ALL' });
}
function unmountAll() {
overlayStore.dispatchOverlay({ type: 'REMOVE_ALL' });
}
const close = createEvent('close');
const unmount = createEvent('unmount');
const closeAll = createEvent('closeAll');
const unmountAll = createEvent('unmountAll');

return {
open,
close,
unmount,
closeAll,
unmountAll,
openAsync,
};
return { open, openAsync, close, unmount, closeAll, unmountAll, useOverlayEvent };
}
5 changes: 1 addition & 4 deletions packages/src/utils/create-overlay-context.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { createOverlayProvider } from '../context/provider';
import { createRegisterOverlaysStore } from '../context/store';

export const { overlay, OverlayProvider, useCurrentOverlay, useOverlayData } = experimental_createOverlayContext();

// eslint-disable-next-line @typescript-eslint/naming-convention
export function experimental_createOverlayContext() {
const localOverlayStore = createRegisterOverlaysStore();

return createOverlayProvider(localOverlayStore);
return createOverlayProvider();
}
48 changes: 48 additions & 0 deletions packages/src/utils/create-use-external-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useLayoutEffect } from 'react';
import { createEmitter } from './emitter';

const emitter = createEmitter();
function useClientLayoutEffect(...args: Parameters<typeof useLayoutEffect>) {
if (typeof document === 'undefined') return;

useLayoutEffect(...args);
}

function dispatchEvent<Detail>(type: string, detail?: Detail) {
emitter.emit(type, detail);
}

// When creating an event, params can be of any type, so specify the type as any.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createUseExternalEvents<EventHandlers extends Record<string, (params: any) => void>>(prefix: string) {
function useExternalEvents(events: EventHandlers) {
const handlers = Object.keys(events).reduce<Record<string, () => void>>((prev, eventKey) => {
const currentEventKeys = `${prefix}:${eventKey}`;

return {
...prev,
[currentEventKeys]: function (event: unknown) {
events[eventKey](event);
} as () => void,
};
}, {});

useClientLayoutEffect(() => {
Object.keys(handlers).forEach((eventKey) => {
emitter.off(eventKey, handlers[eventKey]);
emitter.on(eventKey, handlers[eventKey]);
});

return () =>
Object.keys(handlers).forEach((eventKey) => {
emitter.off(eventKey, handlers[eventKey]);
});
}, [handlers]);
}

function createEvent<EventKey extends keyof EventHandlers>(event: EventKey) {
return (...payload: Parameters<EventHandlers[EventKey]>) => dispatchEvent(`${prefix}:${String(event)}`, payload[0]);
}

return [useExternalEvents, createEvent] as const;
}
23 changes: 23 additions & 0 deletions packages/src/utils/crete-use-external-events.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { renderHook } from '@testing-library/react';
import { describe, expect, it, vitest } from 'vitest';
import { createUseExternalEvents } from './create-use-external-events';

describe('createUseExternalEvents는', () => {
it('should be able to generate events.', () => {
type TestEvent = {
event: () => void;
};

const [useEvent, createEvent] = createUseExternalEvents<TestEvent>('eventPrefix');
const mockedEvent = vitest.fn();

renderHook(() => {
useEvent({ event: mockedEvent });
});

const emitEvent = createEvent('event');
emitEvent();

expect(mockedEvent).toBeCalledTimes(1);
});
});
Loading