DevTools supports multiple modal dialogs at once (#21370)

This commit is contained in:
Brian Vaughn
2021-04-27 20:33:11 -04:00
committed by GitHub
parent 8e2bb3e89c
commit ecb599cd87
7 changed files with 84 additions and 45 deletions
@@ -120,6 +120,7 @@ export default function InspectedElementWrapper(_: Props) {
// Instead we can show a warning to the user.
if (nearestSuspenseElement === null) {
modalDialogDispatch({
id: 'InspectedElement',
type: 'SHOW',
content: <CannotSuspendWarningMessage />,
});
@@ -3,6 +3,7 @@
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
padding: 1rem;
@@ -13,6 +14,7 @@
.Dialog {
position: relative;
z-index: 3;
margin: 0 0.25rem;
width: 25rem;
min-width: 20rem;
max-width: 100%;
@@ -21,13 +21,17 @@ import {useModalDismissSignal} from './hooks';
import styles from './ModalDialog.css';
type ID = any;
type DIALOG_ACTION_HIDE = {|
type: 'HIDE',
id: ID,
|};
type DIALOG_ACTION_SHOW = {|
type: 'SHOW',
canBeDismissed?: boolean,
content: React$Node,
id: ID,
title?: React$Node | null,
|};
@@ -35,13 +39,17 @@ type Action = DIALOG_ACTION_HIDE | DIALOG_ACTION_SHOW;
type Dispatch = (action: Action) => void;
type State = {|
type Dialog = {|
canBeDismissed: boolean,
content: React$Node | null,
isVisible: boolean,
id: ID,
title: React$Node | null,
|};
type State = {|
dialogs: Array<Dialog>,
|};
type ModalDialogContextType = {|
...State,
dispatch: Dispatch,
@@ -56,17 +64,19 @@ function dialogReducer(state, action) {
switch (action.type) {
case 'HIDE':
return {
canBeDismissed: true,
content: null,
isVisible: false,
title: null,
dialogs: state.dialogs.filter(dialog => dialog.id !== action.id),
};
case 'SHOW':
return {
canBeDismissed: action.canBeDismissed !== false,
content: action.content,
isVisible: true,
title: action.title || null,
dialogs: [
...state.dialogs,
{
canBeDismissed: action.canBeDismissed !== false,
content: action.content,
id: action.id,
title: action.title || null,
},
],
};
default:
throw new Error(`Invalid action "${action.type}"`);
@@ -79,18 +89,12 @@ type Props = {|
function ModalDialogContextController({children}: Props) {
const [state, dispatch] = useReducer<State, State, Action>(dialogReducer, {
canBeDismissed: true,
content: null,
isVisible: false,
title: null,
dialogs: [],
});
const value = useMemo<ModalDialogContextType>(
() => ({
canBeDismissed: state.canBeDismissed,
content: state.content,
isVisible: state.isVisible,
title: state.title,
dialogs: state.dialogs,
dispatch,
}),
[state, dispatch],
@@ -104,17 +108,44 @@ function ModalDialogContextController({children}: Props) {
}
function ModalDialog(_: {||}) {
const {isVisible} = useContext(ModalDialogContext);
return isVisible ? <ModalDialogImpl /> : null;
const {dialogs, dispatch} = useContext(ModalDialogContext);
if (dialogs.length === 0) {
return null;
}
return (
<div className={styles.Background}>
{dialogs.map(dialog => (
<ModalDialogImpl
key={dialog.id}
canBeDismissed={dialog.canBeDismissed}
content={dialog.content}
dispatch={dispatch}
id={dialog.id}
title={dialog.title}
/>
))}
</div>
);
}
function ModalDialogImpl(_: {||}) {
const {canBeDismissed, content, dispatch, title} = useContext(
ModalDialogContext,
);
function ModalDialogImpl({
canBeDismissed,
content,
dispatch,
id,
title,
}: {|
canBeDismissed: boolean,
content: React$Node | null,
dispatch: Dispatch,
id: ID,
title: React$Node | null,
|}) {
const dismissModal = useCallback(() => {
if (canBeDismissed) {
dispatch({type: 'HIDE'});
dispatch({type: 'HIDE', id});
}
}, [canBeDismissed, dispatch]);
const dialogRef = useRef<HTMLDivElement | null>(null);
@@ -135,24 +166,19 @@ function ModalDialogImpl(_: {||}) {
};
return (
<div className={styles.Background} onClick={dismissModal}>
<div
ref={dialogRef}
className={styles.Dialog}
onClick={handleDialogClick}>
{title !== null && <div className={styles.Title}>{title}</div>}
{content}
{canBeDismissed && (
<div className={styles.Buttons}>
<Button
autoFocus={true}
className={styles.Button}
onClick={dismissModal}>
Okay
</Button>
</div>
)}
</div>
<div ref={dialogRef} className={styles.Dialog} onClick={handleDialogClick}>
{title !== null && <div className={styles.Title}>{title}</div>}
{content}
{canBeDismissed && (
<div className={styles.Buttons}>
<Button
autoFocus={true}
className={styles.Button}
onClick={dismissModal}>
Okay
</Button>
</div>
)}
</div>
);
}
@@ -85,6 +85,7 @@ export default function ProfilingImportExportButtons() {
);
} catch (error) {
modalDialogDispatch({
id: 'ProfilingImportExportButtons',
type: 'SHOW',
title: 'Import failed',
content: (
@@ -21,17 +21,21 @@ import type {BridgeProtocol} from 'react-devtools-shared/src/bridge';
const DEVTOOLS_VERSION = process.env.DEVTOOLS_VERSION;
const INSTRUCTIONS_FB_URL = 'https://fburl.com/devtools-bridge-protocol';
const MODAL_DIALOG_ID = 'UnsupportedBridgeProtocolDialog';
export default function UnsupportedBridgeProtocolDialog(_: {||}) {
const {dispatch, isVisible} = useContext(ModalDialogContext);
const {dialogs, dispatch} = useContext(ModalDialogContext);
const store = useContext(StoreContext);
const isVisible = !!dialogs.find(dialog => dialog.id === MODAL_DIALOG_ID);
useEffect(() => {
const updateDialog = () => {
if (!isVisible) {
if (store.unsupportedBridgeProtocol !== null) {
dispatch({
canBeDismissed: false,
id: MODAL_DIALOG_ID,
type: 'SHOW',
content: (
<DialogContent
@@ -42,7 +46,10 @@ export default function UnsupportedBridgeProtocolDialog(_: {||}) {
}
} else {
if (store.unsupportedBridgeProtocol === null) {
dispatch({type: 'HIDE'});
dispatch({
type: 'HIDE',
id: MODAL_DIALOG_ID,
});
}
}
};
@@ -30,6 +30,7 @@ export default function UnsupportedVersionDialog(_: {||}) {
setState('show-dialog');
dispatch({
canBeDismissed: true,
id: 'UnsupportedVersionDialog',
type: 'SHOW',
content: <DialogContent />,
});
@@ -31,6 +31,7 @@ export default function WarnIfLegacyBackendDetected(_: {||}) {
// Any of these types indicate the v3 backend.
dispatch({
canBeDismissed: false,
id: 'WarnIfLegacyBackendDetected',
type: 'SHOW',
title: 'DevTools v4 is incompatible with this version of React',
content: <InvalidBackendDetected />,