Skip to content

Commit b6c8bc1

Browse files
authored
#86 modal config (#87)
* #86 added global configuration object and enforceProvider rule * #86 bump version
1 parent a1acd6c commit b6c8bc1

10 files changed

+129
-44
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "2.3.1",
2+
"version": "2.4.0",
33
"license": "MIT",
44
"name": "mui-modal-provider",
55
"author": "Quernest",

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export {
55
} from './modal-provider';
66
export { default as useModal, UseModalOptions } from './use-modal';
77
export * from './types';
8+
export * from './modal-config';

src/modal-config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ModalConfig } from './types';
2+
3+
const config: ModalConfig = {
4+
/**
5+
* If set to `true` you will get an error when trying to access
6+
* a context without the `ModalProvider` declared above.
7+
*/
8+
enforceProvider: false,
9+
};
10+
11+
export function setModalConfig(newConfig: Partial<ModalConfig>) {
12+
Object.assign(config, newConfig);
13+
}
14+
15+
export function getModalConfig() {
16+
return config;
17+
}

src/modal-context.test.tsx

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,85 @@
11
import React from 'react';
2-
import { act, renderHook } from '@testing-library/react-hooks';
3-
import { ModalProviderWrapper as wrapper } from './test-utils';
4-
import useModalContext from './use-modal-context';
2+
import {
3+
act,
4+
renderHook,
5+
WrapperComponent,
6+
} from '@testing-library/react-hooks';
7+
import { ModalProviderWrapper } from './test-utils';
8+
import useModal from './use-modal';
9+
import { ModalContextState } from './modal-context';
510

611
describe('ModalContext', () => {
712
const rootId = '123';
813
const modalId = '321';
914

10-
test('should be initialized with correct state', () => {
11-
const { result } = renderHook(() => useModalContext(), {
12-
wrapper,
13-
});
15+
const testHook = (wrapper?: WrapperComponent<any>) => {
16+
const { result } = renderHook(() => useModal(), { wrapper });
17+
const ctx = result.current as ModalContextState;
1418

15-
expect(result.current).toMatchObject({
16-
destroyModal: expect.any(Function),
17-
destroyModalsByRootId: expect.any(Function),
18-
hideModal: expect.any(Function),
19-
showModal: expect.any(Function),
20-
state: {},
21-
updateModal: expect.any(Function),
22-
});
19+
const performModalActions = (modal: any) => {
20+
act(() => {
21+
modal.update({});
22+
modal.hide();
23+
modal.destroy();
24+
});
25+
};
26+
27+
return { ctx, performModalActions };
28+
};
29+
30+
const runTests = (
31+
context: ModalContextState,
32+
wrapper?: WrapperComponent<any>
33+
) => {
34+
const { performModalActions } = testHook(wrapper);
2335

2436
act(() => {
25-
const modal = result.current.showModal(() => <div>test</div>);
26-
modal.update({});
27-
modal.hide();
28-
modal.destroy();
37+
const modal = context.showModal(() => <div>test</div>);
38+
performModalActions(modal);
2939
});
3040

3141
act(() => {
32-
result.current.updateModal(modalId, {});
42+
context.updateModal(modalId, {});
3343
});
3444

3545
act(() => {
36-
result.current.hideModal(modalId);
46+
context.hideModal(modalId);
3747
});
3848

3949
act(() => {
40-
result.current.destroyModal(modalId);
50+
context.destroyModal(modalId);
4151
});
4252

4353
act(() => {
44-
result.current.destroyModalsByRootId(rootId);
54+
context.destroyModalsByRootId(rootId);
4555
});
56+
};
57+
58+
test('should be initialized with correct state', () => {
59+
const { ctx } = testHook(ModalProviderWrapper);
60+
expect(ctx).toMatchObject({
61+
destroyModal: expect.any(Function),
62+
destroyModalsByRootId: expect.any(Function),
63+
hideModal: expect.any(Function),
64+
showModal: expect.any(Function),
65+
state: {},
66+
updateModal: expect.any(Function),
67+
});
68+
69+
runTests(ctx, ModalProviderWrapper);
70+
});
71+
72+
test('should be initialized without context provider and return fallback', () => {
73+
const { ctx } = testHook(undefined);
74+
expect(ctx).toMatchObject({
75+
destroyModal: expect.any(Function),
76+
destroyModalsByRootId: expect.any(Function),
77+
hideModal: expect.any(Function),
78+
showModal: expect.any(Function),
79+
state: {},
80+
updateModal: expect.any(Function),
81+
});
82+
83+
runTests(ctx);
4684
});
4785
});

src/modal-context.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ export interface ModalContextState {
1717
showModal: ShowFn;
1818
}
1919

20+
export const modalContextFallback: ModalContextState = {
21+
state: {},
22+
updateModal: () => undefined,
23+
hideModal: () => undefined,
24+
destroyModal: () => undefined,
25+
destroyModalsByRootId: () => undefined,
26+
showModal: () => ({
27+
id: 'id',
28+
hide: () => undefined,
29+
destroy: () => undefined,
30+
update: () => undefined,
31+
}),
32+
};
33+
2034
const ModalContext = createContext<ModalContextState | undefined>(undefined);
2135

2236
export default ModalContext;

src/modal-provider.test.tsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { act, renderHook } from '@testing-library/react-hooks';
22
import * as utils from './utils';
3-
import useModalContext from './use-modal-context';
43
import {
54
LegacyModalProviderWrapper as legacyWrapper,
65
ModalProviderWrapper as wrapper,
@@ -12,6 +11,7 @@ import Modal, { ModalProps } from './test-utils/modal';
1211
import LegacyModal from './test-utils/legacy-modal';
1312
import { Options, ShowFnOutput, State } from './types';
1413
import { MISSED_MODAL_ID_ERROR_MESSAGE } from './constants';
14+
import useModal from './use-modal';
1515

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

3535
beforeEach(() => {
36-
uidSpy = jest.spyOn(utils, 'uid').mockReturnValueOnce(modalId);
36+
uidSpy = jest.spyOn(utils, 'uid').mockReturnValue(modalId);
3737
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
3838
});
3939

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

4545
test('happy path scenario (with options)', () => {
46-
const { result } = renderHook(() => useModalContext(), {
46+
uidSpy = jest.spyOn(utils, 'uid').mockReturnValueOnce(rootId);
47+
const { result } = renderHook(() => useModal(), {
4748
wrapper,
4849
});
4950

@@ -86,7 +87,7 @@ describe('ModalProvider', () => {
8687
});
8788

8889
test('unhappy path (missed ID errors)', () => {
89-
const { result } = renderHook(() => useModalContext(), {
90+
const { result } = renderHook(() => useModal(), {
9091
wrapper,
9192
});
9293

@@ -123,8 +124,9 @@ describe('ModalProvider', () => {
123124
});
124125
});
125126

126-
test('happy path scenario (without options)', () => {
127-
const { result } = renderHook(() => useModalContext(), {
127+
test('happy path scenario (without options provided)', () => {
128+
uidSpy = jest.spyOn(utils, 'uid').mockReturnValueOnce(rootId);
129+
const { result } = renderHook(() => useModal(), {
128130
wrapper,
129131
});
130132

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

135137
const expectedState: State = {
136-
[modalId]: {
138+
[id]: {
137139
component: Modal,
140+
options: {
141+
rootId: rootId,
142+
},
138143
props: {
139144
open: true,
140145
...modalProps,
@@ -146,7 +151,7 @@ describe('ModalProvider', () => {
146151
});
147152

148153
it('should automaticaly destroy on close', () => {
149-
const { result } = renderHook(() => useModalContext(), {
154+
const { result } = renderHook(() => useModal(), {
150155
wrapper,
151156
});
152157

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

165170
it('should fire onClose prop event on hide', () => {
166-
const { result } = renderHook(() => useModalContext(), {
171+
const { result } = renderHook(() => useModal(), {
167172
wrapper,
168173
});
169174

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

186191
it('should fire TransitionProps.onExited prop event on hide', () => {
187-
const { result } = renderHook(() => useModalContext(), {
192+
const { result } = renderHook(() => useModal(), {
188193
wrapper: noSuspenseWrapper,
189194
});
190195

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

207212
it('should fire onExited prop event on hide', () => {
208-
const { result } = renderHook(() => useModalContext(), {
213+
const { result } = renderHook(() => useModal(), {
209214
wrapper: legacyWrapper,
210215
});
211216

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,7 @@ export interface ShowFnOutput<P> {
5656
destroy: () => void;
5757
update: (newProps: Partial<ModalComponentProps<P>>) => void;
5858
}
59+
60+
export interface ModalConfig {
61+
enforceProvider: boolean;
62+
}

src/use-modal-context.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { setModalConfig } from './modal-config';
12
import { useContext } from 'react';
23
import useModalContext from './use-modal-context';
34

@@ -9,9 +10,11 @@ jest.mock('react', () => ({
910
describe('useModalContext', () => {
1011
it('throws an error when not used within ModalProvider', () => {
1112
(useContext as jest.Mock).mockReturnValue(undefined);
13+
setModalConfig({ enforceProvider: true });
1214
expect(() => useModalContext()).toThrow(
1315
'useModalContext must be used within a ModalProvider'
1416
);
17+
setModalConfig({ enforceProvider: false });
1518
});
1619

1720
it('returns the context when used within ModalProvider', () => {

src/use-modal-context.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { useContext } from 'react';
2-
import ModalContext from './modal-context';
2+
import ModalContext, { modalContextFallback } from './modal-context';
3+
import { getModalConfig } from './modal-config';
34

45
export default function useModalContext() {
56
const context = useContext(ModalContext);
7+
const { enforceProvider } = getModalConfig();
68

7-
if (context === undefined) {
9+
if (enforceProvider && context === undefined) {
810
throw new Error('useModalContext must be used within a ModalProvider');
911
}
1012

11-
return context;
13+
return context || modalContextFallback;
1214
}

src/use-modal.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@ export default function useModal(options: UseModalOptions = defaultOptions) {
1515
const { disableAutoDestroy } = { ...defaultOptions, ...options };
1616
const {
1717
showModal,
18-
destroyModalsByRootId: destroy,
19-
...otherContextProps
18+
destroyModal,
19+
...otherModalContextProps
2020
} = useModalContext();
2121
const id = useRef<string>(uid(6));
2222

2323
useEffect(
2424
() => () => {
25-
if (!disableAutoDestroy) {
26-
destroy(id.current);
25+
if (!disableAutoDestroy && destroyModal) {
26+
destroyModal(id.current);
2727
}
2828
},
29-
[disableAutoDestroy, destroy]
29+
[disableAutoDestroy, destroyModal]
3030
);
3131

3232
return {
@@ -35,6 +35,7 @@ export default function useModal(options: UseModalOptions = defaultOptions) {
3535
showModal(component, props, { rootId: id.current, ...options }),
3636
[showModal]
3737
),
38-
...otherContextProps,
38+
destroyModal,
39+
...otherModalContextProps,
3940
};
4041
}

0 commit comments

Comments
 (0)