DevTools supports multiple modal dialogs at once (#21370)
Brian Vaughn committed
Apr 27, 2021 at 20:33 UTC
ecb599cd87b39c23b44c2cc7daad9df42f4c2385
7 files changed
+84
-45
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+1
@@ -120,6 +120,7 @@ export default function InspectedElementWrapper(_: Props) {
120
// Instead we can show a warning to the user.
121
if (nearestSuspenseElement === null) {
122
modalDialogDispatch({
123
+ id: 'InspectedElement',
124
type: 'SHOW',
125
content: <CannotSuspendWarningMessage />,
126
});
packages/react-devtools-shared/src/devtools/views/ModalDialog.css
+2
@@ -3,6 +3,7 @@
3
width: 100%;
4
height: 100%;
5
display: flex;
6
+ flex-direction: row;
7
align-items: flex-start;
8
justify-content: center;
9
padding: 1rem;
@@ -13,6 +14,7 @@
14
.Dialog {
15
position: relative;
16
z-index: 3;
17
+ margin: 0 0.25rem;
18
width: 25rem;
19
min-width: 20rem;
20
max-width: 100%;
packages/react-devtools-shared/src/devtools/views/ModalDialog.js
+69
-43
@@ -21,13 +21,17 @@ import {useModalDismissSignal} from './hooks';
21
22
import styles from './ModalDialog.css';
23
24
+type ID = any;
25
+
26
type DIALOG_ACTION_HIDE = {|
27
type: 'HIDE',
28
+ id: ID,
29
|};
30
type DIALOG_ACTION_SHOW = {|
31
type: 'SHOW',
32
canBeDismissed?: boolean,
33
content: React$Node,
34
+ id: ID,
35
title?: React$Node | null,
36
|};
37
@@ -35,13 +39,17 @@ type Action = DIALOG_ACTION_HIDE | DIALOG_ACTION_SHOW;
39
40
type Dispatch = (action: Action) => void;
41
38
-type State = {|
42
+type Dialog = {|
43
canBeDismissed: boolean,
44
content: React$Node | null,
41
- isVisible: boolean,
45
+ id: ID,
46
title: React$Node | null,
47
|};
48
49
+type State = {|
50
+ dialogs: Array<Dialog>,
51
+|};
52
+
53
type ModalDialogContextType = {|
54
...State,
55
dispatch: Dispatch,
@@ -56,17 +64,19 @@ function dialogReducer(state, action) {
64
switch (action.type) {
65
case 'HIDE':
66
return {
59
- canBeDismissed: true,
60
- content: null,
61
- isVisible: false,
62
- title: null,
67
+ dialogs: state.dialogs.filter(dialog => dialog.id !== action.id),
68
};
69
case 'SHOW':
70
return {
66
- canBeDismissed: action.canBeDismissed !== false,
67
- content: action.content,
68
- isVisible: true,
69
- title: action.title || null,
71
+ dialogs: [
72
+ ...state.dialogs,
73
+ {
74
+ canBeDismissed: action.canBeDismissed !== false,
75
+ content: action.content,
76
+ id: action.id,
77
+ title: action.title || null,
78
+ },
79
+ ],
80
};
81
default:
82
throw new Error(`Invalid action "${action.type}"`);
@@ -79,18 +89,12 @@ type Props = {|
89
90
function ModalDialogContextController({children}: Props) {
91
const [state, dispatch] = useReducer<State, State, Action>(dialogReducer, {
82
- canBeDismissed: true,
83
- content: null,
84
- isVisible: false,
85
- title: null,
92
+ dialogs: [],
93
});
94
95
const value = useMemo<ModalDialogContextType>(
96
() => ({
90
- canBeDismissed: state.canBeDismissed,
91
- content: state.content,
92
- isVisible: state.isVisible,
93
- title: state.title,
97
+ dialogs: state.dialogs,
98
dispatch,
99
}),
100
[state, dispatch],
@@ -104,17 +108,44 @@ function ModalDialogContextController({children}: Props) {
108
}
109
110
function ModalDialog(_: {||}) {
107
- const {isVisible} = useContext(ModalDialogContext);
108
- return isVisible ? <ModalDialogImpl /> : null;
109
-}
111
+ const {dialogs, dispatch} = useContext(ModalDialogContext);
112
+
113
+ if (dialogs.length === 0) {
114
+ return null;
115
+ }
116
111
-function ModalDialogImpl(_: {||}) {
112
- const {canBeDismissed, content, dispatch, title} = useContext(
113
- ModalDialogContext,
117
+ return (
118
+ <div className={styles.Background}>
119
+ {dialogs.map(dialog => (
120
+ <ModalDialogImpl
121
+ key={dialog.id}
122
+ canBeDismissed={dialog.canBeDismissed}
123
+ content={dialog.content}
124
+ dispatch={dispatch}
125
+ id={dialog.id}
126
+ title={dialog.title}
127
+ />
128
+ ))}
129
+ </div>
130
);
131
+}
132
+
133
+function ModalDialogImpl({
134
+ canBeDismissed,
135
+ content,
136
+ dispatch,
137
+ id,
138
+ title,
139
+}: {|
140
+ canBeDismissed: boolean,
141
+ content: React$Node | null,
142
+ dispatch: Dispatch,
143
+ id: ID,
144
+ title: React$Node | null,
145
+|}) {
146
const dismissModal = useCallback(() => {
147
if (canBeDismissed) {
117
- dispatch({type: 'HIDE'});
148
+ dispatch({type: 'HIDE', id});
149
}
150
}, [canBeDismissed, dispatch]);
151
const dialogRef = useRef<HTMLDivElement | null>(null);
@@ -135,24 +166,19 @@ function ModalDialogImpl(_: {||}) {
166
};
167
168
return (
138
- <div className={styles.Background} onClick={dismissModal}>
139
- <div
140
- ref={dialogRef}
141
- className={styles.Dialog}
142
- onClick={handleDialogClick}>
143
- {title !== null && <div className={styles.Title}>{title}</div>}
144
- {content}
145
- {canBeDismissed && (
146
- <div className={styles.Buttons}>
147
- <Button
148
- autoFocus={true}
149
- className={styles.Button}
150
- onClick={dismissModal}>
151
- Okay
152
- </Button>
153
- </div>
154
- )}
155
- </div>
169
+ <div ref={dialogRef} className={styles.Dialog} onClick={handleDialogClick}>
170
+ {title !== null && <div className={styles.Title}>{title}</div>}
171
+ {content}
172
+ {canBeDismissed && (
173
+ <div className={styles.Buttons}>
174
+ <Button
175
+ autoFocus={true}
176
+ className={styles.Button}
177
+ onClick={dismissModal}>
178
+ Okay
179
+ </Button>
180
+ </div>
181
+ )}
182
</div>
183
);
184
}
packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js
+1
@@ -85,6 +85,7 @@ export default function ProfilingImportExportButtons() {
85
);
86
} catch (error) {
87
modalDialogDispatch({
88
+ id: 'ProfilingImportExportButtons',
89
type: 'SHOW',
90
title: 'Import failed',
91
content: (
packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js
+9
-2
@@ -21,17 +21,21 @@ import type {BridgeProtocol} from 'react-devtools-shared/src/bridge';
21
22
const DEVTOOLS_VERSION = process.env.DEVTOOLS_VERSION;
23
const INSTRUCTIONS_FB_URL = 'https://fburl.com/devtools-bridge-protocol';
24
+const MODAL_DIALOG_ID = 'UnsupportedBridgeProtocolDialog';
25
26
export default function UnsupportedBridgeProtocolDialog(_: {||}) {
26
- const {dispatch, isVisible} = useContext(ModalDialogContext);
27
+ const {dialogs, dispatch} = useContext(ModalDialogContext);
28
const store = useContext(StoreContext);
29
30
+ const isVisible = !!dialogs.find(dialog => dialog.id === MODAL_DIALOG_ID);
31
+
32
useEffect(() => {
33
const updateDialog = () => {
34
if (!isVisible) {
35
if (store.unsupportedBridgeProtocol !== null) {
36
dispatch({
37
canBeDismissed: false,
38
+ id: MODAL_DIALOG_ID,
39
type: 'SHOW',
40
content: (
41
<DialogContent
@@ -42,7 +46,10 @@ export default function UnsupportedBridgeProtocolDialog(_: {||}) {
46
}
47
} else {
48
if (store.unsupportedBridgeProtocol === null) {
45
- dispatch({type: 'HIDE'});
49
+ dispatch({
50
+ type: 'HIDE',
51
+ id: MODAL_DIALOG_ID,
52
+ });
53
}
54
}
55
};
packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.js
+1
@@ -30,6 +30,7 @@ export default function UnsupportedVersionDialog(_: {||}) {
30
setState('show-dialog');
31
dispatch({
32
canBeDismissed: true,
33
+ id: 'UnsupportedVersionDialog',
34
type: 'SHOW',
35
content: <DialogContent />,
36
});
packages/react-devtools-shared/src/devtools/views/WarnIfLegacyBackendDetected.js
+1
@@ -31,6 +31,7 @@ export default function WarnIfLegacyBackendDetected(_: {||}) {
31
// Any of these types indicate the v3 backend.
32
dispatch({
33
canBeDismissed: false,
34
+ id: 'WarnIfLegacyBackendDetected',
35
type: 'SHOW',
36
title: 'DevTools v4 is incompatible with this version of React',
37
content: <InvalidBackendDetected />,