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