@samitouri / QOS-React-2 / commits / 4f8b7864ee

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.

Brian Vaughn committed Jul 31, 2019 at 11:22 UTC 4f8b7864eea378841ef7e4a41aeec92d54431084
13 files changed +180 -15
packages/react-devtools-core/src/standalone.js
+12
@@ -29,6 +29,11 @@ let nodeWaitingToConnectHTML: string = '';
29 let projectRoots: Array<string> = [];
30 let statusListener: StatusListener = (message: string) => {};
31
32 +// Unlike browser extension users, people using the standalone have actively installed version 4,
33 +// So we probably don't need to show them a changelog notice.
34 +// We should give embedded users (e.g. Nuclide, Sonar) a way of showing this dialog though.
35 +let showWelcomeToTheNewDevToolsDialog: boolean = false;
36 +
37 function setContentDOMNode(value: HTMLElement) {
38 node = value;
39
@@ -47,6 +52,11 @@ function setStatusListener(value: StatusListener) {
52 return DevtoolsUI;
53 }
54
55 +function setShowWelcomeToTheNewDevToolsDialog(value: boolean) {
56 + showWelcomeToTheNewDevToolsDialog = value;
57 + return DevtoolsUI;
58 +}
59 +
60 let bridge: FrontendBridge | null = null;
61 let store: Store | null = null;
62 let root = null;
@@ -87,6 +97,7 @@ function reload() {
97 bridge: ((bridge: any): FrontendBridge),
98 canViewElementSourceFunction,
99 showTabBar: true,
100 + showWelcomeToTheNewDevToolsDialog,
101 store: ((store: any): Store),
102 warnIfLegacyBackendDetected: true,
103 viewElementSourceFunction,
@@ -300,6 +311,7 @@ const DevtoolsUI = {
311 connectToSocket,
312 setContentDOMNode,
313 setProjectRoots,
314 + setShowWelcomeToTheNewDevToolsDialog,
315 setStatusListener,
316 startServer,
317 };
shells/browser/shared/src/main.js
+1
@@ -155,6 +155,7 @@ function createPanelIfReactLoaded() {
155 profilerPortalContainer,
156 settingsPortalContainer,
157 showTabBar: false,
158 + showWelcomeToTheNewDevToolsDialog: true,
159 store,
160 viewElementSourceFunction,
161 })
shells/dev/src/devtools.js
+1
@@ -76,6 +76,7 @@ inject('dist/app.js', () => {
76 bridge,
77 browserTheme: 'light',
78 showTabBar: true,
79 + showWelcomeToTheNewDevToolsDialog: true,
80 store,
81 warnIfLegacyBackendDetected: true,
82 })
src/constants.js
+3
@@ -24,3 +24,6 @@ export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
24 'React::DevTools::appendComponentStack';
25
26 export const PROFILER_EXPORT_VERSION = 4;
27 +
28 +export const CHANGE_LOG_URL =
29 + 'https://github.com/bvaughn/react-devtools-experimental/blob/master/CHANGELOG.md';
src/devtools/views/DevTools.js
+6
@@ -18,6 +18,7 @@ import { ProfilerContextController } from './Profiler/ProfilerContext';
18 import { ModalDialogContextController } from './ModalDialog';
19 import ReactLogo from './ReactLogo';
20 import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected';
21 +import ShowWelcomeToTheNewDevToolsDialog from './ShowWelcomeToTheNewDevToolsDialog';
22
23 import styles from './DevTools.css';
24
@@ -42,6 +43,7 @@ export type Props = {|
43 canViewElementSourceFunction?: ?CanViewElementSource,
44 defaultTab?: TabID,
45 showTabBar?: boolean,
46 + showWelcomeToTheNewDevToolsDialog?: boolean,
47 store: Store,
48 warnIfLegacyBackendDetected?: boolean,
49 viewElementSourceFunction?: ?ViewElementSource,
@@ -85,6 +87,7 @@ export default function DevTools({
87 profilerPortalContainer,
88 settingsPortalContainer,
89 showTabBar = false,
90 + showWelcomeToTheNewDevToolsDialog = false,
91 store,
92 warnIfLegacyBackendDetected = false,
93 viewElementSourceFunction = null,
@@ -150,6 +153,9 @@ export default function DevTools({
153 </ViewElementSourceContext.Provider>
154 </SettingsContextController>
155 {warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
156 + {showWelcomeToTheNewDevToolsDialog && (
157 + <ShowWelcomeToTheNewDevToolsDialog />
158 + )}
159 </ModalDialogContextController>
160 </StoreContext.Provider>
161 </BridgeContext.Provider>
src/devtools/views/ModalDialog.css
+4
@@ -34,3 +34,7 @@
34 text-align: right;
35 margin-top: 0.5rem;
36 }
37 +
38 +.Button {
39 + font-size: var(--font-size-sans-large);
40 +}
src/devtools/views/ModalDialog.js
+22 -5
@@ -109,18 +109,35 @@ function ModalDialogImpl(_: {||}) {
109 dispatch({ type: 'HIDE' });
110 }
111 }, [canBeDismissed, dispatch]);
112 - const modalRef = useRef<HTMLDivElement | null>(null);
112 + const dialogRef = useRef<HTMLDivElement | null>(null);
113
114 - useModalDismissSignal(modalRef, dismissModal);
114 + // It's important to trap click events within the dialog,
115 + // so the dismiss hook will use it for click hit detection.
116 + // Because multiple tabs may be showing this ModalDialog,
117 + // the normal `dialog.contains(target)` check would fail on a background tab.
118 + useModalDismissSignal(dialogRef, dismissModal, false);
119 +
120 + // Clicks on the dialog should not bubble.
121 + // This way we can dismiss by listening to clicks on the background.
122 + const handleDialogClick = (event: any) => {
123 + event.stopPropagation();
124 +
125 + // It is important that we don't also prevent default,
126 + // or clicks within the dialog (e.g. on links) won't work.
127 + };
128
129 return (
117 - <div className={styles.Background}>
118 - <div className={styles.Dialog} ref={modalRef}>
130 + <div className={styles.Background} onClick={dismissModal}>
131 + <div
132 + ref={dialogRef}
133 + className={styles.Dialog}
134 + onClick={handleDialogClick}
135 + >
136 {title !== null && <div className={styles.Title}>{title}</div>}
137 {content}
138 {canBeDismissed && (
139 <div className={styles.Buttons}>
123 - <Button autoFocus onClick={dismissModal}>
140 + <Button autoFocus className={styles.Button} onClick={dismissModal}>
141 Okay
142 </Button>
143 </div>
src/devtools/views/ReactLogo.js
+6 -2
@@ -4,11 +4,15 @@ import React from 'react';
4
5 import styles from './ReactLogo.css';
6
7 -export default function ReactLogo() {
7 +type Props = {|
8 + className?: string,
9 +|};
10 +
11 +export default function ReactLogo({ className }: Props) {
12 return (
13 <svg
14 xmlns="http://www.w3.org/2000/svg"
11 - className={styles.ReactLogo}
15 + className={`${styles.ReactLogo} ${className || ''}`}
16 viewBox="-11.5 -10.23174 23 20.46348"
17 >
18 <circle cx="0" cy="0" r="2.05" fill="currentColor" />
src/devtools/views/Settings/GeneralSettings.js
+13
@@ -2,6 +2,7 @@
2
3 import React, { useContext } from 'react';
4 import { SettingsContext } from './SettingsContext';
5 +import { CHANGE_LOG_URL } from 'src/constants';
6
7 import styles from './SettingsShared.css';
8
@@ -54,6 +55,18 @@ export default function GeneralSettings(_: {||}) {
55 Append component stacks to console warnings and errors.
56 </label>
57 </div>
58 +
59 + <div className={styles.ReleaseNotes}>
60 + <a
61 + className={styles.ReleaseNotesLink}
62 + target="_blank"
63 + rel="noopener noreferrer"
64 + href={CHANGE_LOG_URL}
65 + >
66 + View release notes
67 + </a>{' '}
68 + for DevTools version {process.env.DEVTOOLS_VERSION}
69 + </div>
70 </div>
71 );
72 }
src/devtools/views/Settings/SettingsShared.css
+11
@@ -134,3 +134,14 @@
134 height: 0.375rem;
135 background-color: var(--color-toggle-text);
136 }
137 +
138 +.ReleaseNotes {
139 + width: 100%;
140 + background-color: var(--color-background-hover);
141 + padding: 0.25rem 0.5rem;
142 + border-radius: 0.25rem;
143 +}
144 +
145 +.ReleaseNotesLink {
146 + color: var(--color-button-active);
147 +}
src/devtools/views/ShowWelcomeToTheNewDevToolsDialog.css new
+26
@@ -0,0 +1,26 @@
1 +.Row {
2 + display: flex;
3 + flex-direction: row;
4 + align-items: center;
5 +}
6 +
7 +.Column {
8 + display: flex;
9 + flex-direction: column;
10 + align-items: center;
11 +}
12 +
13 +.Logo {
14 + height: 4rem;
15 + width: 4rem;
16 + margin: 1rem;
17 +}
18 +
19 +.Title {
20 + font-size: var(--font-size-sans-large);
21 + margin-bottom: 0.5rem;
22 +}
23 +
24 +.ReleaseNotesLink {
25 + color: var(--color-button-active);
26 +}
src/devtools/views/ShowWelcomeToTheNewDevToolsDialog.js new
+64
@@ -0,0 +1,64 @@
1 +// @flow
2 +
3 +import React, { Fragment, useContext, useEffect } from 'react';
4 +import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
5 +import { useLocalStorage } from './hooks';
6 +import { ModalDialogContext } from './ModalDialog';
7 +import ReactLogo from './ReactLogo';
8 +import { CHANGE_LOG_URL } from 'src/constants';
9 +
10 +import styles from './ShowWelcomeToTheNewDevToolsDialog.css';
11 +
12 +const LOCAL_STORAGE_KEY =
13 + 'React::DevTools::hasShownWelcomeToTheNewDevToolsDialog';
14 +
15 +export default function ShowWelcomeToTheNewDevToolsDialog(_: {||}) {
16 + const { dispatch } = useContext(ModalDialogContext);
17 + const [
18 + hasShownWelcomeToTheNewDevToolsDialog,
19 + setHasShownWelcomeToTheNewDevToolsDialog,
20 + ] = useLocalStorage<boolean>(LOCAL_STORAGE_KEY, false);
21 +
22 + useEffect(() => {
23 + if (!hasShownWelcomeToTheNewDevToolsDialog) {
24 + batchedUpdates(() => {
25 + setHasShownWelcomeToTheNewDevToolsDialog(true);
26 + dispatch({
27 + canBeDismissed: true,
28 + type: 'SHOW',
29 + content: <DialogContent />,
30 + });
31 + });
32 + }
33 + }, [
34 + dispatch,
35 + hasShownWelcomeToTheNewDevToolsDialog,
36 + setHasShownWelcomeToTheNewDevToolsDialog,
37 + ]);
38 +
39 + return null;
40 +}
41 +
42 +function DialogContent(_: {||}) {
43 + return (
44 + <Fragment>
45 + <div className={styles.Row}>
46 + <ReactLogo className={styles.Logo} />
47 + <div>
48 + <div className={styles.Title}>Welcome to the new React DevTools!</div>
49 + <div>
50 + <a
51 + className={styles.ReleaseNotesLink}
52 + target="_blank"
53 + rel="noopener noreferrer"
54 + href={CHANGE_LOG_URL}
55 + >
56 + Learn more
57 + </a>{' '}
58 + about changes in this version.
59 + </div>
60 + </div>
61 + </div>
62 + </Fragment>
63 + );
64 +}
src/devtools/views/hooks.js
+11 -8
@@ -95,20 +95,21 @@ export function useLocalStorage<T>(
95
96 export function useModalDismissSignal(
97 modalRef: { current: HTMLDivElement | null },
98 - dismissCallback: () => void
98 + dismissCallback: () => void,
99 + dismissOnClickOutside?: boolean = true
100 ): void {
101 useEffect(() => {
102 if (modalRef.current === null) {
103 return () => {};
104 }
105
105 - const handleKeyDown = ({ key }: any) => {
106 + const handleDocumentKeyDown = ({ key }: any) => {
107 if (key === 'Escape') {
108 dismissCallback();
109 }
110 };
111
111 - const handleClick = (event: any) => {
112 + const handleDocumentClick = (event: any) => {
113 // $FlowFixMe
114 if (
115 modalRef.current !== null &&
@@ -125,14 +126,16 @@ export function useModalDismissSignal(
126 // Here we use portals to render individual tabs (e.g. Profiler),
127 // and the root document might belong to a different window.
128 const ownerDocument = modalRef.current.ownerDocument;
128 - ownerDocument.addEventListener('keydown', handleKeyDown);
129 - ownerDocument.addEventListener('click', handleClick);
129 + ownerDocument.addEventListener('keydown', handleDocumentKeyDown);
130 + if (dismissOnClickOutside) {
131 + ownerDocument.addEventListener('click', handleDocumentClick);
132 + }
133
134 return () => {
132 - ownerDocument.removeEventListener('keydown', handleKeyDown);
133 - ownerDocument.removeEventListener('click', handleClick);
135 + ownerDocument.removeEventListener('keydown', handleDocumentKeyDown);
136 + ownerDocument.removeEventListener('click', handleDocumentClick);
137 };
135 - }, [modalRef, dismissCallback]);
138 + }, [modalRef, dismissCallback, dismissOnClickOutside]);
139 }
140
141 // Copied from https://github.com/facebook/react/pull/15022