@samitouri / QOS-React-2 / commits / ecb599cd87

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 />,