Skip to content

Commit

Permalink
DevTools supports multiple modal dialogs at once (facebook#21370)
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn authored and koto committed Jun 15, 2021
1 parent 10966c9 commit 40374a2
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 />,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
padding: 1rem;
Expand All @@ -13,6 +14,7 @@
.Dialog {
position: relative;
z-index: 3;
margin: 0 0.25rem;
width: 25rem;
min-width: 20rem;
max-width: 100%;
Expand Down
112 changes: 69 additions & 43 deletions packages/react-devtools-shared/src/devtools/views/ModalDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,35 @@ 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,
|};

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,
Expand All @@ -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}"`);
Expand All @@ -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],
Expand All @@ -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;
}

function ModalDialogImpl(_: {||}) {
const {canBeDismissed, content, dispatch, title} = useContext(
ModalDialogContext,
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({
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);
Expand All @@ -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>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default function ProfilingImportExportButtons() {
);
} catch (error) {
modalDialogDispatch({
id: 'ProfilingImportExportButtons',
type: 'SHOW',
title: 'Import failed',
content: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,7 +46,10 @@ export default function UnsupportedBridgeProtocolDialog(_: {||}) {
}
} else {
if (store.unsupportedBridgeProtocol === null) {
dispatch({type: 'HIDE'});
dispatch({
type: 'HIDE',
id: MODAL_DIALOG_ID,
});
}
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function UnsupportedVersionDialog(_: {||}) {
setState('show-dialog');
dispatch({
canBeDismissed: true,
id: 'UnsupportedVersionDialog',
type: 'SHOW',
content: <DialogContent />,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 />,
Expand Down

0 comments on commit 40374a2

Please sign in to comment.