Skip to content

Commit a2bf928

Browse files
authored
Allow global configuration with ModalConfig component (#50)
* focus-trapをインストール * focus-trapを使用したフォーカストラップ周りの実装 * examplesも実装に合わせて修正する * close-buttonオプションを仮実装 * READMEとexamples追加 * hooksディレクトリに移動 wip * componentsディレクトリに移動 * deepmergeインストール * ModalConfig実装 * READMEにグローバルな設定の項目を追加 * examplesに追加 * defaultButtonのスタイル調整 * マージしたコミットを修正 * deepmergeを使用 * CloseButtonStyleの名称を修正
1 parent 0d2f689 commit a2bf928

File tree

14 files changed

+337
-125
lines changed

14 files changed

+337
-125
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,50 @@ useModal('root', {
103103
});
104104
```
105105
106+
## Global Settings
107+
108+
The `ModalConfig` component allows you to apply a common default configuration to all `useModal` hooks.
109+
110+
```jsx
111+
<ModalConfig value={options}>
112+
<Component />
113+
</ModalConfig>
114+
```
115+
116+
The following example sets all `useModal` hooks to not scroll outside the modal by default.
117+
118+
```jsx
119+
const Component1 = () => {
120+
const [Modal] = useModal('root');
121+
return (
122+
<Modal>
123+
<h2>Common</h2>
124+
</Modal>
125+
);
126+
};
127+
const Component2 = () => {
128+
const [Modal] = useModal('root', { preventScroll: false }); // override
129+
return (
130+
<Modal>
131+
<h2>Override options</h2>
132+
</Modal>
133+
);
134+
};
135+
136+
const App = () => {
137+
return (
138+
<ModalConfig
139+
value={{
140+
preventScroll: true,
141+
}}
142+
>
143+
<Component1 />
144+
<Component2 />
145+
</ModalConfig>
146+
);
147+
};
148+
```
149+
106150
## Demo
107151
108152
https://microcmsio.github.io/react-hooks-use-modal/

examples/src/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { render } from 'react-dom';
44
import { Modal as CommonModal } from './js';
55
import { Modal as CloseButtonModal } from './js/close-button';
66
import { Modal as CloseButtonWithRenderOptionModal } from './js/close-button/render-option';
7+
import { ModalWrapper as ModalConfigModal } from './js/modal-config';
78

89
const CurrentModal = () => {
910
switch (window.location.pathname.replace(/\/$/, '')) {
@@ -13,13 +14,21 @@ const CurrentModal = () => {
1314
case '/close-button/render-option': {
1415
return <CloseButtonWithRenderOptionModal />;
1516
}
17+
case '/modal-config': {
18+
return <ModalConfigModal />;
19+
}
1620
default: {
1721
return <CommonModal />;
1822
}
1923
}
2024
};
2125

22-
const routes = ['/', '/close-button', '/close-button/render-option'];
26+
const routes = [
27+
'/',
28+
'/close-button',
29+
'/close-button/render-option',
30+
'/modal-config',
31+
];
2332
const Wrapper = ({ children }: PropsWithChildren<{}>) => {
2433
return (
2534
<div>

examples/src/js/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ export const Modal = () => {
2424
<div style={modalStyle}>
2525
<h1>Title</h1>
2626
<p>This is a customizable modal.</p>
27-
{window.location.pathname.startsWith('/close-button') && (
28-
<button onClick={close}>CLOSE</button>
29-
)}
27+
<button onClick={close}>CLOSE</button>
3028
</div>
3129
</Modal>
3230
</div>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
import { ModalConfig, useModal } from '../../../../src';
3+
4+
const modalStyle: React.CSSProperties = {
5+
backgroundColor: '#fff',
6+
padding: '60px 100px',
7+
borderRadius: '10px',
8+
};
9+
10+
const Modal = () => {
11+
const [Modal, open, close, isOpen] = useModal('root');
12+
13+
return (
14+
<div>
15+
<div>Modal is Open? {isOpen ? 'Yes' : 'No'}</div>
16+
<button onClick={open}>OPEN</button>
17+
<Modal>
18+
<div style={modalStyle}>
19+
<h1>Title</h1>
20+
<p>This is a customizable modal.</p>
21+
<button onClick={close}>CLOSE</button>
22+
</div>
23+
</Modal>
24+
</div>
25+
);
26+
};
27+
const ModalWithOverrideOptions = () => {
28+
const [Modal, open, close, isOpen] = useModal('root', {
29+
focusTrapOptions: {
30+
clickOutsideDeactivates: false,
31+
},
32+
});
33+
34+
return (
35+
<div style={{ marginTop: '40px' }}>
36+
<div>Modal overridden by options is Open? {isOpen ? 'Yes' : 'No'}</div>
37+
<button onClick={open}>OPEN</button>
38+
<Modal>
39+
<div style={modalStyle}>
40+
<h1>Title</h1>
41+
<p>This is a customizable modal.</p>
42+
<button onClick={close}>CLOSE</button>
43+
</div>
44+
</Modal>
45+
</div>
46+
);
47+
};
48+
49+
export const ModalWrapper = () => {
50+
return (
51+
<ModalConfig
52+
value={{
53+
focusTrapOptions: {
54+
clickOutsideDeactivates: true,
55+
},
56+
}}
57+
>
58+
<Modal />
59+
<ModalWithOverrideOptions />
60+
</ModalConfig>
61+
);
62+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<html>
2+
<head>
3+
<title>useModal with ModalConfig example</title>
4+
<meta charset="utf-8" />
5+
<meta
6+
name="viewport"
7+
content="width=device-width, initial-scale=1, shrink-to-fit=no"
8+
/>
9+
</head>
10+
<body>
11+
<noscript> You need to enable JavaScript to run this app. </noscript>
12+
<div id="root"></div>
13+
</body>
14+
</html>

package-lock.json

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
"types": "dist/index.d.ts",
1919
"dependencies": {
2020
"body-scroll-lock": "^3.1.5",
21+
"deepmerge": "^4.2.2",
2122
"focus-trap": "^7.0.0"
2223
},
2324
"devDependencies": {
2425
"@types/body-scroll-lock": "^3.1.0",
26+
"@types/deepmerge": "^2.2.0",
2527
"@types/react": "^17.0.16",
2628
"@types/react-dom": "^17.0.9",
2729
"eslint": "^8.22.0",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from 'react';
2+
3+
interface DefaultCloseButtonProps {
4+
onClose: () => void;
5+
}
6+
7+
const defaultCloseButtonStyle: React.CSSProperties = {
8+
// reset
9+
backgroundColor: 'transparent',
10+
border: 'none',
11+
cursor: 'pointer',
12+
padding: 0,
13+
appearance: 'none',
14+
15+
position: 'absolute',
16+
right: 0,
17+
top: 0,
18+
width: '40px',
19+
height: '40px',
20+
display: 'flex',
21+
alignItems: 'center',
22+
justifyContent: 'center',
23+
fontSize: '28px',
24+
color: '#fff',
25+
transform: 'translateX(100%)',
26+
};
27+
28+
export const DefaultCloseButton: React.FC<DefaultCloseButtonProps> = ({
29+
onClose,
30+
}) => {
31+
return (
32+
<button
33+
type="button"
34+
style={defaultCloseButtonStyle}
35+
onClick={onClose}
36+
aria-label="close"
37+
>
38+
×
39+
</button>
40+
);
41+
};

src/components/Modal.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Options as FocusTrapOptions } from 'focus-trap';
2+
import React, { useRef } from 'react';
3+
import { createPortal } from 'react-dom';
4+
5+
import { useBodyScrollLock } from '../hooks/useBodyScrollLock';
6+
import { useFocusTrap } from '../hooks/useFocusTrap';
7+
8+
export interface ModalProps {
9+
children: React.ReactNode;
10+
isOpen: boolean;
11+
close: () => void;
12+
elementId: 'root' | string;
13+
preventScroll: boolean;
14+
focusTrapOptions: FocusTrapOptions;
15+
closeButton: React.ReactElement | null;
16+
}
17+
18+
const wrapperStyle: React.CSSProperties = {
19+
position: 'fixed',
20+
top: 0,
21+
left: 0,
22+
bottom: 0,
23+
right: 0,
24+
display: 'flex',
25+
justifyContent: 'center',
26+
alignItems: 'center',
27+
zIndex: 1000,
28+
};
29+
30+
const overlayStyle: React.CSSProperties = {
31+
position: 'fixed',
32+
top: 0,
33+
left: 0,
34+
bottom: 0,
35+
right: 0,
36+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
37+
zIndex: 100000,
38+
};
39+
40+
const containerStyle: React.CSSProperties = {
41+
position: 'relative',
42+
zIndex: 100001,
43+
};
44+
45+
export const Modal: React.FC<ModalProps> = ({
46+
children,
47+
isOpen,
48+
close,
49+
elementId = 'root',
50+
preventScroll,
51+
focusTrapOptions,
52+
closeButton,
53+
}) => {
54+
const dialogRef = useRef<HTMLDivElement>(null);
55+
useFocusTrap(dialogRef, isOpen, {
56+
onDeactivate: close,
57+
clickOutsideDeactivates: true,
58+
...focusTrapOptions,
59+
});
60+
useBodyScrollLock(dialogRef, isOpen, preventScroll);
61+
62+
if (isOpen === false) {
63+
return null;
64+
}
65+
66+
return createPortal(
67+
<div style={wrapperStyle}>
68+
<div style={overlayStyle} />
69+
<div
70+
ref={dialogRef}
71+
role="dialog"
72+
aria-modal="true"
73+
style={containerStyle}
74+
tabIndex={-1}
75+
>
76+
{children}
77+
{closeButton}
78+
</div>
79+
</div>,
80+
document.getElementById(elementId) as HTMLElement
81+
);
82+
};

src/components/ModalConfig.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React, { PropsWithChildren } from 'react';
2+
import { ModalOptions } from '..';
3+
import { ModalConfigContext } from '../hooks/useModalConfig';
4+
5+
interface ModalConfigProps {
6+
value: ModalOptions;
7+
}
8+
9+
export const ModalConfig: React.FC<PropsWithChildren<ModalConfigProps>> = ({
10+
value,
11+
children,
12+
}) => {
13+
return (
14+
<ModalConfigContext.Provider value={value}>
15+
{children}
16+
</ModalConfigContext.Provider>
17+
);
18+
};

0 commit comments

Comments
 (0)