main
js 243 lines 6.49 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {ReactContext} from 'shared/ReactTypes';
11
12 import * as React from 'react';
13 import {
14 createContext,
15 useContext,
16 useEffect,
17 useLayoutEffect,
18 useMemo,
19 } from 'react';
20 import {
21 LOCAL_STORAGE_BROWSER_THEME,
22 LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
23 LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
24 } from 'react-devtools-shared/src/constants';
25 import {
26 COMFORTABLE_LINE_HEIGHT,
27 COMPACT_LINE_HEIGHT,
28 } from 'react-devtools-shared/src/devtools/constants';
29 import {useLocalStorage} from '../hooks';
30 import {BridgeContext} from '../context';
31 import {logEvent} from 'react-devtools-shared/src/Logger';
32
33 import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';
34
35 export type DisplayDensity = 'comfortable' | 'compact';
36 export type Theme = 'auto' | 'light' | 'dark';
37
38 type Context = {
39 displayDensity: DisplayDensity,
40 setDisplayDensity(value: DisplayDensity): void,
41
42 // Derived from display density.
43 // Specified as a separate prop so it can trigger a re-render of FixedSizeList.
44 lineHeight: number,
45
46 parseHookNames: boolean,
47 setParseHookNames: (value: boolean) => void,
48
49 theme: Theme,
50 setTheme(value: Theme): void,
51
52 browserTheme: Theme,
53
54 traceUpdatesEnabled: boolean,
55 setTraceUpdatesEnabled: (value: boolean) => void,
56 };
57
58 const SettingsContext: ReactContext<Context> = createContext<Context>(
59 null as any as Context,
60 );
61 SettingsContext.displayName = 'SettingsContext';
62
63 function useLocalStorageWithLog<T>(
64 key: string,
65 initialValue: T | (() => T),
66 ): [T, (value: T | (() => T)) => void] {
67 return useLocalStorage<T>(key, initialValue, (v, k) => {
68 logEvent({
69 event_name: 'settings-changed',
70 metadata: {
71 source: 'localStorage setter',
72 key: k,
73 value: v,
74 },
75 });
76 });
77 }
78
79 type DocumentElements = Array<HTMLElement>;
80
81 type Props = {
82 browserTheme: BrowserTheme,
83 children: React$Node,
84 componentsPortalContainer?: Element,
85 profilerPortalContainer?: Element,
86 suspensePortalContainer?: Element,
87 };
88
89 function SettingsContextController({
90 browserTheme,
91 children,
92 componentsPortalContainer,
93 profilerPortalContainer,
94 suspensePortalContainer,
95 }: Props): React.Node {
96 const bridge = useContext(BridgeContext);
97
98 const [displayDensity, setDisplayDensity] =
99 useLocalStorageWithLog<DisplayDensity>(
100 'React::DevTools::displayDensity',
101 'compact',
102 );
103 const [theme, setTheme] = useLocalStorageWithLog<Theme>(
104 LOCAL_STORAGE_BROWSER_THEME,
105 'auto',
106 );
107 const [parseHookNames, setParseHookNames] = useLocalStorageWithLog<boolean>(
108 LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
109 false,
110 );
111 const [traceUpdatesEnabled, setTraceUpdatesEnabled] =
112 useLocalStorageWithLog<boolean>(
113 LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
114 false,
115 );
116
117 const documentElements = useMemo<DocumentElements>(() => {
118 const array: Array<HTMLElement> = [
119 document.documentElement as any as HTMLElement,
120 ];
121 if (componentsPortalContainer != null) {
122 array.push(
123 componentsPortalContainer.ownerDocument
124 .documentElement as any as HTMLElement,
125 );
126 }
127 if (profilerPortalContainer != null) {
128 array.push(
129 profilerPortalContainer.ownerDocument
130 .documentElement as any as HTMLElement,
131 );
132 }
133 if (suspensePortalContainer != null) {
134 array.push(
135 suspensePortalContainer.ownerDocument
136 .documentElement as any as HTMLElement,
137 );
138 }
139 return array;
140 }, [
141 componentsPortalContainer,
142 profilerPortalContainer,
143 suspensePortalContainer,
144 ]);
145
146 useLayoutEffect(() => {
147 switch (displayDensity) {
148 case 'comfortable':
149 updateDisplayDensity('comfortable', documentElements);
150 break;
151 case 'compact':
152 updateDisplayDensity('compact', documentElements);
153 break;
154 default:
155 throw Error(`Unsupported displayDensity value "${displayDensity}"`);
156 }
157 }, [displayDensity, documentElements]);
158
159 useLayoutEffect(() => {
160 switch (theme) {
161 case 'light':
162 updateThemeVariables('light', documentElements);
163 break;
164 case 'dark':
165 updateThemeVariables('dark', documentElements);
166 break;
167 case 'auto':
168 updateThemeVariables(browserTheme, documentElements);
169 break;
170 default:
171 throw Error(`Unsupported theme value "${theme}"`);
172 }
173 }, [browserTheme, theme, documentElements]);
174
175 useEffect(() => {
176 bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
177 }, [bridge, traceUpdatesEnabled]);
178
179 const value: Context = useMemo(
180 () => ({
181 displayDensity,
182 lineHeight:
183 displayDensity === 'compact'
184 ? COMPACT_LINE_HEIGHT
185 : COMFORTABLE_LINE_HEIGHT,
186 parseHookNames,
187 setDisplayDensity,
188 setParseHookNames,
189 setTheme,
190 setTraceUpdatesEnabled,
191 theme,
192 browserTheme,
193 traceUpdatesEnabled,
194 }),
195 [
196 displayDensity,
197 parseHookNames,
198 setDisplayDensity,
199 setParseHookNames,
200 setTheme,
201 setTraceUpdatesEnabled,
202 theme,
203 browserTheme,
204 traceUpdatesEnabled,
205 ],
206 );
207
208 return (
209 <SettingsContext.Provider value={value}>
210 {children}
211 </SettingsContext.Provider>
212 );
213 }
214
215 export function updateDisplayDensity(
216 displayDensity: DisplayDensity,
217 documentElements: DocumentElements,
218 ): void {
219 // Sizes and paddings/margins are all rem-based,
220 // so update the root font-size as well when the display preference changes.
221 const computedStyle = getComputedStyle(document.body as any);
222 const fontSize = computedStyle.getPropertyValue(
223 `--${displayDensity}-root-font-size`,
224 );
225 const root = document.querySelector(':root') as any as HTMLElement;
226 root.style.fontSize = fontSize;
227 }
228
229 export function updateThemeVariables(
230 theme: Theme,
231 documentElements: DocumentElements,
232 ): void {
233 // Update scrollbar color to match theme.
234 // this CSS property is currently only supported in Firefox,
235 // but it makes a significant UI improvement in dark mode.
236 // https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
237 documentElements.forEach(documentElement => {
238 // $FlowFixMe[prop-missing] scrollbarColor is missing in CSSStyleDeclaration
239 documentElement.style.scrollbarColor = `var(${`--${theme}-color-scroll-thumb`}) var(${`--${theme}-color-scroll-track`})`;
240 });
241 }
242
243 export {SettingsContext, SettingsContextController};