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.
This commit is contained in:
Brian Vaughn 2019-07-31 11:22:51 -07:00
parent c57d2a2901
commit 4f8b7864ee
13 changed files with 180 additions and 15 deletions

View File

@ -29,6 +29,11 @@ let nodeWaitingToConnectHTML: string = '';
let projectRoots: Array<string> = [];
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,
};

View File

@ -155,6 +155,7 @@ function createPanelIfReactLoaded() {
profilerPortalContainer,
settingsPortalContainer,
showTabBar: false,
showWelcomeToTheNewDevToolsDialog: true,
store,
viewElementSourceFunction,
})

View File

@ -76,6 +76,7 @@ inject('dist/app.js', () => {
bridge,
browserTheme: 'light',
showTabBar: true,
showWelcomeToTheNewDevToolsDialog: true,
store,
warnIfLegacyBackendDetected: true,
})

View File

@ -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';

View File

@ -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({
</ViewElementSourceContext.Provider>
</SettingsContextController>
{warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
{showWelcomeToTheNewDevToolsDialog && (
<ShowWelcomeToTheNewDevToolsDialog />
)}
</ModalDialogContextController>
</StoreContext.Provider>
</BridgeContext.Provider>

View File

@ -34,3 +34,7 @@
text-align: right;
margin-top: 0.5rem;
}
.Button {
font-size: var(--font-size-sans-large);
}

View File

@ -109,18 +109,35 @@ function ModalDialogImpl(_: {||}) {
dispatch({ type: 'HIDE' });
}
}, [canBeDismissed, dispatch]);
const modalRef = useRef<HTMLDivElement | null>(null);
const dialogRef = useRef<HTMLDivElement | null>(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 (
<div className={styles.Background}>
<div className={styles.Dialog} ref={modalRef}>
<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 onClick={dismissModal}>
<Button autoFocus className={styles.Button} onClick={dismissModal}>
Okay
</Button>
</div>

View File

@ -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 (
<svg
xmlns="http://www.w3.org/2000/svg"
className={styles.ReactLogo}
className={`${styles.ReactLogo} ${className || ''}`}
viewBox="-11.5 -10.23174 23 20.46348"
>
<circle cx="0" cy="0" r="2.05" fill="currentColor" />

View File

@ -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.
</label>
</div>
<div className={styles.ReleaseNotes}>
<a
className={styles.ReleaseNotesLink}
target="_blank"
rel="noopener noreferrer"
href={CHANGE_LOG_URL}
>
View release notes
</a>{' '}
for DevTools version {process.env.DEVTOOLS_VERSION}
</div>
</div>
);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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<boolean>(LOCAL_STORAGE_KEY, false);
useEffect(() => {
if (!hasShownWelcomeToTheNewDevToolsDialog) {
batchedUpdates(() => {
setHasShownWelcomeToTheNewDevToolsDialog(true);
dispatch({
canBeDismissed: true,
type: 'SHOW',
content: <DialogContent />,
});
});
}
}, [
dispatch,
hasShownWelcomeToTheNewDevToolsDialog,
setHasShownWelcomeToTheNewDevToolsDialog,
]);
return null;
}
function DialogContent(_: {||}) {
return (
<Fragment>
<div className={styles.Row}>
<ReactLogo className={styles.Logo} />
<div>
<div className={styles.Title}>Welcome to the new React DevTools!</div>
<div>
<a
className={styles.ReleaseNotesLink}
target="_blank"
rel="noopener noreferrer"
href={CHANGE_LOG_URL}
>
Learn more
</a>{' '}
about changes in this version.
</div>
</div>
</div>
</Fragment>
);
}

View File

@ -95,20 +95,21 @@ export function useLocalStorage<T>(
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