Skip to content

Commit

Permalink
#86 added global configuration object and enforceProvider rule
Browse files Browse the repository at this point in the history
  • Loading branch information
Quernest committed Dec 3, 2023
1 parent a1acd6c commit e3238b3
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 43 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export {
} from './modal-provider';
export { default as useModal, UseModalOptions } from './use-modal';
export * from './types';
export * from './modal-config';
17 changes: 17 additions & 0 deletions src/modal-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ModalConfig } from './types';

const config: ModalConfig = {
/**
* If set to `true` you will get an error when trying to access
* a context without the `ModalProvider` declared above.
*/
enforceProvider: false,
};

export function setModalConfig(newConfig: Partial<ModalConfig>) {
Object.assign(config, newConfig);
}

export function getModalConfig() {
return config;
}
84 changes: 61 additions & 23 deletions src/modal-context.test.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,85 @@
import React from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
import { ModalProviderWrapper as wrapper } from './test-utils';
import useModalContext from './use-modal-context';
import {
act,
renderHook,
WrapperComponent,
} from '@testing-library/react-hooks';
import { ModalProviderWrapper } from './test-utils';
import useModal from './use-modal';
import { ModalContextState } from './modal-context';

describe('ModalContext', () => {
const rootId = '123';
const modalId = '321';

test('should be initialized with correct state', () => {
const { result } = renderHook(() => useModalContext(), {
wrapper,
});
const testHook = (wrapper?: WrapperComponent<any>) => {
const { result } = renderHook(() => useModal(), { wrapper });
const ctx = result.current as ModalContextState;

expect(result.current).toMatchObject({
destroyModal: expect.any(Function),
destroyModalsByRootId: expect.any(Function),
hideModal: expect.any(Function),
showModal: expect.any(Function),
state: {},
updateModal: expect.any(Function),
});
const performModalActions = (modal: any) => {
act(() => {
modal.update({});
modal.hide();
modal.destroy();
});
};

return { ctx, performModalActions };
};

const runTests = (
context: ModalContextState,
wrapper?: WrapperComponent<any>
) => {
const { performModalActions } = testHook(wrapper);

act(() => {
const modal = result.current.showModal(() => <div>test</div>);
modal.update({});
modal.hide();
modal.destroy();
const modal = context.showModal(() => <div>test</div>);
performModalActions(modal);
});

act(() => {
result.current.updateModal(modalId, {});
context.updateModal(modalId, {});
});

act(() => {
result.current.hideModal(modalId);
context.hideModal(modalId);
});

act(() => {
result.current.destroyModal(modalId);
context.destroyModal(modalId);
});

act(() => {
result.current.destroyModalsByRootId(rootId);
context.destroyModalsByRootId(rootId);
});
};

test('should be initialized with correct state', () => {
const { ctx } = testHook(ModalProviderWrapper);
expect(ctx).toMatchObject({
destroyModal: expect.any(Function),
destroyModalsByRootId: expect.any(Function),
hideModal: expect.any(Function),
showModal: expect.any(Function),
state: {},
updateModal: expect.any(Function),
});

runTests(ctx, ModalProviderWrapper);
});

test('should be initialized without context provider and return fallback', () => {
const { ctx } = testHook(undefined);
expect(ctx).toMatchObject({
destroyModal: expect.any(Function),
destroyModalsByRootId: expect.any(Function),
hideModal: expect.any(Function),
showModal: expect.any(Function),
state: {},
updateModal: expect.any(Function),
});

runTests(ctx);
});
});
14 changes: 14 additions & 0 deletions src/modal-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ export interface ModalContextState {
showModal: ShowFn;
}

export const modalContextFallback: ModalContextState = {
state: {},
updateModal: () => undefined,
hideModal: () => undefined,
destroyModal: () => undefined,
destroyModalsByRootId: () => undefined,
showModal: () => ({
id: 'id',
hide: () => undefined,
destroy: () => undefined,
update: () => undefined,
}),
};

const ModalContext = createContext<ModalContextState | undefined>(undefined);

export default ModalContext;
27 changes: 16 additions & 11 deletions src/modal-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { act, renderHook } from '@testing-library/react-hooks';
import * as utils from './utils';
import useModalContext from './use-modal-context';
import {
LegacyModalProviderWrapper as legacyWrapper,
ModalProviderWrapper as wrapper,
Expand All @@ -12,6 +11,7 @@ import Modal, { ModalProps } from './test-utils/modal';
import LegacyModal from './test-utils/legacy-modal';
import { Options, ShowFnOutput, State } from './types';
import { MISSED_MODAL_ID_ERROR_MESSAGE } from './constants';
import useModal from './use-modal';

describe('ModalProvider', () => {
const rootId = '000';
Expand All @@ -33,7 +33,7 @@ describe('ModalProvider', () => {
let consoleErrorSpy: jest.SpyInstance;

beforeEach(() => {
uidSpy = jest.spyOn(utils, 'uid').mockReturnValueOnce(modalId);
uidSpy = jest.spyOn(utils, 'uid').mockReturnValue(modalId);
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});

Expand All @@ -43,7 +43,8 @@ describe('ModalProvider', () => {
});

test('happy path scenario (with options)', () => {
const { result } = renderHook(() => useModalContext(), {
uidSpy = jest.spyOn(utils, 'uid').mockReturnValueOnce(rootId);
const { result } = renderHook(() => useModal(), {
wrapper,
});

Expand Down Expand Up @@ -86,7 +87,7 @@ describe('ModalProvider', () => {
});

test('unhappy path (missed ID errors)', () => {
const { result } = renderHook(() => useModalContext(), {
const { result } = renderHook(() => useModal(), {
wrapper,
});

Expand Down Expand Up @@ -123,8 +124,9 @@ describe('ModalProvider', () => {
});
});

test('happy path scenario (without options)', () => {
const { result } = renderHook(() => useModalContext(), {
test('happy path scenario (without options provided)', () => {
uidSpy = jest.spyOn(utils, 'uid').mockReturnValueOnce(rootId);
const { result } = renderHook(() => useModal(), {
wrapper,
});

Expand All @@ -133,8 +135,11 @@ describe('ModalProvider', () => {
});

const expectedState: State = {
[modalId]: {
[id]: {
component: Modal,
options: {
rootId: rootId,
},
props: {
open: true,
...modalProps,
Expand All @@ -146,7 +151,7 @@ describe('ModalProvider', () => {
});

it('should automaticaly destroy on close', () => {
const { result } = renderHook(() => useModalContext(), {
const { result } = renderHook(() => useModal(), {
wrapper,
});

Expand All @@ -163,7 +168,7 @@ describe('ModalProvider', () => {
});

it('should fire onClose prop event on hide', () => {
const { result } = renderHook(() => useModalContext(), {
const { result } = renderHook(() => useModal(), {
wrapper,
});

Expand All @@ -184,7 +189,7 @@ describe('ModalProvider', () => {
});

it('should fire TransitionProps.onExited prop event on hide', () => {
const { result } = renderHook(() => useModalContext(), {
const { result } = renderHook(() => useModal(), {
wrapper: noSuspenseWrapper,
});

Expand All @@ -205,7 +210,7 @@ describe('ModalProvider', () => {
});

it('should fire onExited prop event on hide', () => {
const { result } = renderHook(() => useModalContext(), {
const { result } = renderHook(() => useModal(), {
wrapper: legacyWrapper,
});

Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ export interface ShowFnOutput<P> {
destroy: () => void;
update: (newProps: Partial<ModalComponentProps<P>>) => void;
}

export interface ModalConfig {
enforceProvider: boolean;
}
3 changes: 3 additions & 0 deletions src/use-modal-context.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setModalConfig } from './modal-config';
import { useContext } from 'react';
import useModalContext from './use-modal-context';

Expand All @@ -9,9 +10,11 @@ jest.mock('react', () => ({
describe('useModalContext', () => {
it('throws an error when not used within ModalProvider', () => {
(useContext as jest.Mock).mockReturnValue(undefined);
setModalConfig({ enforceProvider: true });
expect(() => useModalContext()).toThrow(
'useModalContext must be used within a ModalProvider'
);
setModalConfig({ enforceProvider: false });
});

it('returns the context when used within ModalProvider', () => {
Expand Down
8 changes: 5 additions & 3 deletions src/use-modal-context.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useContext } from 'react';
import ModalContext from './modal-context';
import ModalContext, { modalContextFallback } from './modal-context';
import { getModalConfig } from './modal-config';

export default function useModalContext() {
const context = useContext(ModalContext);
const { enforceProvider } = getModalConfig();

if (context === undefined) {
if (enforceProvider && context === undefined) {
throw new Error('useModalContext must be used within a ModalProvider');
}

return context;
return context || modalContextFallback;
}
13 changes: 7 additions & 6 deletions src/use-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ export default function useModal(options: UseModalOptions = defaultOptions) {
const { disableAutoDestroy } = { ...defaultOptions, ...options };
const {
showModal,
destroyModalsByRootId: destroy,
...otherContextProps
destroyModal,
...otherModalContextProps
} = useModalContext();
const id = useRef<string>(uid(6));

useEffect(
() => () => {
if (!disableAutoDestroy) {
destroy(id.current);
if (!disableAutoDestroy && destroyModal) {
destroyModal(id.current);
}
},
[disableAutoDestroy, destroy]
[disableAutoDestroy, destroyModal]
);

return {
Expand All @@ -35,6 +35,7 @@ export default function useModal(options: UseModalOptions = defaultOptions) {
showModal(component, props, { rootId: id.current, ...options }),
[showModal]
),
...otherContextProps,
destroyModal,
...otherModalContextProps,
};
}

0 comments on commit e3238b3

Please sign in to comment.