From 4f8b7864eea378841ef7e4a41aeec92d54431084 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 31 Jul 2019 11:22:51 -0700 Subject: [PATCH] Add "Welcome to the new DevTools" notification This dialog is shown in the browser extension the first time a user views v4. It is off by default for the standalone extension, but can be enabled via a public API. --- .../react-devtools-core/src/standalone.js | 12 ++++ shells/browser/shared/src/main.js | 1 + shells/dev/src/devtools.js | 1 + src/constants.js | 3 + src/devtools/views/DevTools.js | 6 ++ src/devtools/views/ModalDialog.css | 4 ++ src/devtools/views/ModalDialog.js | 27 ++++++-- src/devtools/views/ReactLogo.js | 8 ++- .../views/Settings/GeneralSettings.js | 13 ++++ .../views/Settings/SettingsShared.css | 11 ++++ .../ShowWelcomeToTheNewDevToolsDialog.css | 26 ++++++++ .../ShowWelcomeToTheNewDevToolsDialog.js | 64 +++++++++++++++++++ src/devtools/views/hooks.js | 19 +++--- 13 files changed, 180 insertions(+), 15 deletions(-) create mode 100644 src/devtools/views/ShowWelcomeToTheNewDevToolsDialog.css create mode 100644 src/devtools/views/ShowWelcomeToTheNewDevToolsDialog.js diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index a88691d77ed8c..c93a3de812546 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -29,6 +29,11 @@ let nodeWaitingToConnectHTML: string = ''; let projectRoots: Array = []; let statusListener: StatusListener = (message: string) => {}; +// Unlike browser extension users, people using the standalone have actively installed version 4, +// So we probably don't need to show them a changelog notice. +// We should give embedded users (e.g. Nuclide, Sonar) a way of showing this dialog though. +let showWelcomeToTheNewDevToolsDialog: boolean = false; + function setContentDOMNode(value: HTMLElement) { node = value; @@ -47,6 +52,11 @@ function setStatusListener(value: StatusListener) { return DevtoolsUI; } +function setShowWelcomeToTheNewDevToolsDialog(value: boolean) { + showWelcomeToTheNewDevToolsDialog = value; + return DevtoolsUI; +} + let bridge: FrontendBridge | null = null; let store: Store | null = null; let root = null; @@ -87,6 +97,7 @@ function reload() { bridge: ((bridge: any): FrontendBridge), canViewElementSourceFunction, showTabBar: true, + showWelcomeToTheNewDevToolsDialog, store: ((store: any): Store), warnIfLegacyBackendDetected: true, viewElementSourceFunction, @@ -300,6 +311,7 @@ const DevtoolsUI = { connectToSocket, setContentDOMNode, setProjectRoots, + setShowWelcomeToTheNewDevToolsDialog, setStatusListener, startServer, }; diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js index 3554ed03eee1e..8263adab5367d 100644 --- a/shells/browser/shared/src/main.js +++ b/shells/browser/shared/src/main.js @@ -155,6 +155,7 @@ function createPanelIfReactLoaded() { profilerPortalContainer, settingsPortalContainer, showTabBar: false, + showWelcomeToTheNewDevToolsDialog: true, store, viewElementSourceFunction, }) diff --git a/shells/dev/src/devtools.js b/shells/dev/src/devtools.js index 2afed0c208637..5727856dd98b9 100644 --- a/shells/dev/src/devtools.js +++ b/shells/dev/src/devtools.js @@ -76,6 +76,7 @@ inject('dist/app.js', () => { bridge, browserTheme: 'light', showTabBar: true, + showWelcomeToTheNewDevToolsDialog: true, store, warnIfLegacyBackendDetected: true, }) diff --git a/src/constants.js b/src/constants.js index da55631a01e45..d18624fbf04fa 100644 --- a/src/constants.js +++ b/src/constants.js @@ -24,3 +24,6 @@ export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY = 'React::DevTools::appendComponentStack'; export const PROFILER_EXPORT_VERSION = 4; + +export const CHANGE_LOG_URL = + 'https://github.com/bvaughn/react-devtools-experimental/blob/master/CHANGELOG.md'; diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js index 0dd3bd3ed22d5..864198c4b6944 100644 --- a/src/devtools/views/DevTools.js +++ b/src/devtools/views/DevTools.js @@ -18,6 +18,7 @@ import { ProfilerContextController } from './Profiler/ProfilerContext'; import { ModalDialogContextController } from './ModalDialog'; import ReactLogo from './ReactLogo'; import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected'; +import ShowWelcomeToTheNewDevToolsDialog from './ShowWelcomeToTheNewDevToolsDialog'; import styles from './DevTools.css'; @@ -42,6 +43,7 @@ export type Props = {| canViewElementSourceFunction?: ?CanViewElementSource, defaultTab?: TabID, showTabBar?: boolean, + showWelcomeToTheNewDevToolsDialog?: boolean, store: Store, warnIfLegacyBackendDetected?: boolean, viewElementSourceFunction?: ?ViewElementSource, @@ -85,6 +87,7 @@ export default function DevTools({ profilerPortalContainer, settingsPortalContainer, showTabBar = false, + showWelcomeToTheNewDevToolsDialog = false, store, warnIfLegacyBackendDetected = false, viewElementSourceFunction = null, @@ -150,6 +153,9 @@ export default function DevTools({ {warnIfLegacyBackendDetected && } + {showWelcomeToTheNewDevToolsDialog && ( + + )} diff --git a/src/devtools/views/ModalDialog.css b/src/devtools/views/ModalDialog.css index f01a1af5a0aee..378e8e73b4b3e 100644 --- a/src/devtools/views/ModalDialog.css +++ b/src/devtools/views/ModalDialog.css @@ -34,3 +34,7 @@ text-align: right; margin-top: 0.5rem; } + +.Button { + font-size: var(--font-size-sans-large); +} diff --git a/src/devtools/views/ModalDialog.js b/src/devtools/views/ModalDialog.js index 2cceb9a49e034..e8fd44c93a8e5 100644 --- a/src/devtools/views/ModalDialog.js +++ b/src/devtools/views/ModalDialog.js @@ -109,18 +109,35 @@ function ModalDialogImpl(_: {||}) { dispatch({ type: 'HIDE' }); } }, [canBeDismissed, dispatch]); - const modalRef = useRef(null); + const dialogRef = useRef(null); - useModalDismissSignal(modalRef, dismissModal); + // It's important to trap click events within the dialog, + // so the dismiss hook will use it for click hit detection. + // Because multiple tabs may be showing this ModalDialog, + // the normal `dialog.contains(target)` check would fail on a background tab. + useModalDismissSignal(dialogRef, dismissModal, false); + + // Clicks on the dialog should not bubble. + // This way we can dismiss by listening to clicks on the background. + const handleDialogClick = (event: any) => { + event.stopPropagation(); + + // It is important that we don't also prevent default, + // or clicks within the dialog (e.g. on links) won't work. + }; return ( -
-
+
+
{title !== null &&
{title}
} {content} {canBeDismissed && (
-
diff --git a/src/devtools/views/ReactLogo.js b/src/devtools/views/ReactLogo.js index d7f33639c7d82..358a3a8ec8a8e 100644 --- a/src/devtools/views/ReactLogo.js +++ b/src/devtools/views/ReactLogo.js @@ -4,11 +4,15 @@ import React from 'react'; import styles from './ReactLogo.css'; -export default function ReactLogo() { +type Props = {| + className?: string, +|}; + +export default function ReactLogo({ className }: Props) { return ( diff --git a/src/devtools/views/Settings/GeneralSettings.js b/src/devtools/views/Settings/GeneralSettings.js index 60d61bb817fa5..8b8692d739b5a 100644 --- a/src/devtools/views/Settings/GeneralSettings.js +++ b/src/devtools/views/Settings/GeneralSettings.js @@ -2,6 +2,7 @@ import React, { useContext } from 'react'; import { SettingsContext } from './SettingsContext'; +import { CHANGE_LOG_URL } from 'src/constants'; import styles from './SettingsShared.css'; @@ -54,6 +55,18 @@ export default function GeneralSettings(_: {||}) { Append component stacks to console warnings and errors.
+ +
+ + View release notes + {' '} + for DevTools version {process.env.DEVTOOLS_VERSION} +
); } diff --git a/src/devtools/views/Settings/SettingsShared.css b/src/devtools/views/Settings/SettingsShared.css index f816d4346b698..cafbe1bedef45 100644 --- a/src/devtools/views/Settings/SettingsShared.css +++ b/src/devtools/views/Settings/SettingsShared.css @@ -134,3 +134,14 @@ height: 0.375rem; background-color: var(--color-toggle-text); } + +.ReleaseNotes { + width: 100%; + background-color: var(--color-background-hover); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} + +.ReleaseNotesLink { + color: var(--color-button-active); +} diff --git a/src/devtools/views/ShowWelcomeToTheNewDevToolsDialog.css b/src/devtools/views/ShowWelcomeToTheNewDevToolsDialog.css new file mode 100644 index 0000000000000..8168d947d2183 --- /dev/null +++ b/src/devtools/views/ShowWelcomeToTheNewDevToolsDialog.css @@ -0,0 +1,26 @@ +.Row { + display: flex; + flex-direction: row; + align-items: center; +} + +.Column { + display: flex; + flex-direction: column; + align-items: center; +} + +.Logo { + height: 4rem; + width: 4rem; + margin: 1rem; +} + +.Title { + font-size: var(--font-size-sans-large); + margin-bottom: 0.5rem; +} + +.ReleaseNotesLink { + color: var(--color-button-active); +} diff --git a/src/devtools/views/ShowWelcomeToTheNewDevToolsDialog.js b/src/devtools/views/ShowWelcomeToTheNewDevToolsDialog.js new file mode 100644 index 0000000000000..025c3f338c0c9 --- /dev/null +++ b/src/devtools/views/ShowWelcomeToTheNewDevToolsDialog.js @@ -0,0 +1,64 @@ +// @flow + +import React, { Fragment, useContext, useEffect } from 'react'; +import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'; +import { useLocalStorage } from './hooks'; +import { ModalDialogContext } from './ModalDialog'; +import ReactLogo from './ReactLogo'; +import { CHANGE_LOG_URL } from 'src/constants'; + +import styles from './ShowWelcomeToTheNewDevToolsDialog.css'; + +const LOCAL_STORAGE_KEY = + 'React::DevTools::hasShownWelcomeToTheNewDevToolsDialog'; + +export default function ShowWelcomeToTheNewDevToolsDialog(_: {||}) { + const { dispatch } = useContext(ModalDialogContext); + const [ + hasShownWelcomeToTheNewDevToolsDialog, + setHasShownWelcomeToTheNewDevToolsDialog, + ] = useLocalStorage(LOCAL_STORAGE_KEY, false); + + useEffect(() => { + if (!hasShownWelcomeToTheNewDevToolsDialog) { + batchedUpdates(() => { + setHasShownWelcomeToTheNewDevToolsDialog(true); + dispatch({ + canBeDismissed: true, + type: 'SHOW', + content: , + }); + }); + } + }, [ + dispatch, + hasShownWelcomeToTheNewDevToolsDialog, + setHasShownWelcomeToTheNewDevToolsDialog, + ]); + + return null; +} + +function DialogContent(_: {||}) { + return ( + +
+ +
+
Welcome to the new React DevTools!
+
+ + Learn more + {' '} + about changes in this version. +
+
+
+
+ ); +} diff --git a/src/devtools/views/hooks.js b/src/devtools/views/hooks.js index ae963047a41ed..abbf5bcdde825 100644 --- a/src/devtools/views/hooks.js +++ b/src/devtools/views/hooks.js @@ -95,20 +95,21 @@ export function useLocalStorage( export function useModalDismissSignal( modalRef: { current: HTMLDivElement | null }, - dismissCallback: () => void + dismissCallback: () => void, + dismissOnClickOutside?: boolean = true ): void { useEffect(() => { if (modalRef.current === null) { return () => {}; } - const handleKeyDown = ({ key }: any) => { + const handleDocumentKeyDown = ({ key }: any) => { if (key === 'Escape') { dismissCallback(); } }; - const handleClick = (event: any) => { + const handleDocumentClick = (event: any) => { // $FlowFixMe if ( modalRef.current !== null && @@ -125,14 +126,16 @@ export function useModalDismissSignal( // Here we use portals to render individual tabs (e.g. Profiler), // and the root document might belong to a different window. const ownerDocument = modalRef.current.ownerDocument; - ownerDocument.addEventListener('keydown', handleKeyDown); - ownerDocument.addEventListener('click', handleClick); + ownerDocument.addEventListener('keydown', handleDocumentKeyDown); + if (dismissOnClickOutside) { + ownerDocument.addEventListener('click', handleDocumentClick); + } return () => { - ownerDocument.removeEventListener('keydown', handleKeyDown); - ownerDocument.removeEventListener('click', handleClick); + ownerDocument.removeEventListener('keydown', handleDocumentKeyDown); + ownerDocument.removeEventListener('click', handleDocumentClick); }; - }, [modalRef, dismissCallback]); + }, [modalRef, dismissCallback, dismissOnClickOutside]); } // Copied from https://github.com/facebook/react/pull/15022