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