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_SHOULD_BREAK_ON_CONSOLE_ERRORS,
    
  24.   LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
    
  25.   LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
    
  26.   LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
    
  27.   LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
    
  28. } from 'react-devtools-shared/src/constants';
    
  29. import {
    
  30.   COMFORTABLE_LINE_HEIGHT,
    
  31.   COMPACT_LINE_HEIGHT,
    
  32. } from 'react-devtools-shared/src/devtools/constants';
    
  33. import {useLocalStorage} from '../hooks';
    
  34. import {BridgeContext} from '../context';
    
  35. import {logEvent} from 'react-devtools-shared/src/Logger';
    
  36. 
    
  37. import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';
    
  38. 
    
  39. export type DisplayDensity = 'comfortable' | 'compact';
    
  40. export type Theme = 'auto' | 'light' | 'dark';
    
  41. 
    
  42. type Context = {
    
  43.   displayDensity: DisplayDensity,
    
  44.   setDisplayDensity(value: DisplayDensity): void,
    
  45. 
    
  46.   // Derived from display density.
    
  47.   // Specified as a separate prop so it can trigger a re-render of FixedSizeList.
    
  48.   lineHeight: number,
    
  49. 
    
  50.   appendComponentStack: boolean,
    
  51.   setAppendComponentStack: (value: boolean) => void,
    
  52. 
    
  53.   breakOnConsoleErrors: boolean,
    
  54.   setBreakOnConsoleErrors: (value: boolean) => void,
    
  55. 
    
  56.   parseHookNames: boolean,
    
  57.   setParseHookNames: (value: boolean) => void,
    
  58. 
    
  59.   hideConsoleLogsInStrictMode: boolean,
    
  60.   setHideConsoleLogsInStrictMode: (value: boolean) => void,
    
  61. 
    
  62.   showInlineWarningsAndErrors: boolean,
    
  63.   setShowInlineWarningsAndErrors: (value: boolean) => void,
    
  64. 
    
  65.   theme: Theme,
    
  66.   setTheme(value: Theme): void,
    
  67. 
    
  68.   browserTheme: Theme,
    
  69. 
    
  70.   traceUpdatesEnabled: boolean,
    
  71.   setTraceUpdatesEnabled: (value: boolean) => void,
    
  72. };
    
  73. 
    
  74. const SettingsContext: ReactContext<Context> = createContext<Context>(
    
  75.   ((null: any): Context),
    
  76. );
    
  77. SettingsContext.displayName = 'SettingsContext';
    
  78. 
    
  79. function useLocalStorageWithLog<T>(
    
  80.   key: string,
    
  81.   initialValue: T | (() => T),
    
  82. ): [T, (value: T | (() => T)) => void] {
    
  83.   return useLocalStorage<T>(key, initialValue, (v, k) => {
    
  84.     logEvent({
    
  85.       event_name: 'settings-changed',
    
  86.       metadata: {
    
  87.         source: 'localStorage setter',
    
  88.         key: k,
    
  89.         value: v,
    
  90.       },
    
  91.     });
    
  92.   });
    
  93. }
    
  94. 
    
  95. type DocumentElements = Array<HTMLElement>;
    
  96. 
    
  97. type Props = {
    
  98.   browserTheme: BrowserTheme,
    
  99.   children: React$Node,
    
  100.   componentsPortalContainer?: Element,
    
  101.   profilerPortalContainer?: Element,
    
  102. };
    
  103. 
    
  104. function SettingsContextController({
    
  105.   browserTheme,
    
  106.   children,
    
  107.   componentsPortalContainer,
    
  108.   profilerPortalContainer,
    
  109. }: Props): React.Node {
    
  110.   const bridge = useContext(BridgeContext);
    
  111. 
    
  112.   const [displayDensity, setDisplayDensity] =
    
  113.     useLocalStorageWithLog<DisplayDensity>(
    
  114.       'React::DevTools::displayDensity',
    
  115.       'compact',
    
  116.     );
    
  117.   const [theme, setTheme] = useLocalStorageWithLog<Theme>(
    
  118.     LOCAL_STORAGE_BROWSER_THEME,
    
  119.     'auto',
    
  120.   );
    
  121.   const [appendComponentStack, setAppendComponentStack] =
    
  122.     useLocalStorageWithLog<boolean>(
    
  123.       LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
    
  124.       true,
    
  125.     );
    
  126.   const [breakOnConsoleErrors, setBreakOnConsoleErrors] =
    
  127.     useLocalStorageWithLog<boolean>(
    
  128.       LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
    
  129.       false,
    
  130.     );
    
  131.   const [parseHookNames, setParseHookNames] = useLocalStorageWithLog<boolean>(
    
  132.     LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
    
  133.     false,
    
  134.   );
    
  135.   const [hideConsoleLogsInStrictMode, setHideConsoleLogsInStrictMode] =
    
  136.     useLocalStorageWithLog<boolean>(
    
  137.       LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
    
  138.       false,
    
  139.     );
    
  140.   const [showInlineWarningsAndErrors, setShowInlineWarningsAndErrors] =
    
  141.     useLocalStorageWithLog<boolean>(
    
  142.       LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
    
  143.       true,
    
  144.     );
    
  145.   const [traceUpdatesEnabled, setTraceUpdatesEnabled] =
    
  146.     useLocalStorageWithLog<boolean>(
    
  147.       LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
    
  148.       false,
    
  149.     );
    
  150. 
    
  151.   const documentElements = useMemo<DocumentElements>(() => {
    
  152.     const array: Array<HTMLElement> = [
    
  153.       ((document.documentElement: any): HTMLElement),
    
  154.     ];
    
  155.     if (componentsPortalContainer != null) {
    
  156.       array.push(
    
  157.         ((componentsPortalContainer.ownerDocument
    
  158.           .documentElement: any): HTMLElement),
    
  159.       );
    
  160.     }
    
  161.     if (profilerPortalContainer != null) {
    
  162.       array.push(
    
  163.         ((profilerPortalContainer.ownerDocument
    
  164.           .documentElement: any): HTMLElement),
    
  165.       );
    
  166.     }
    
  167.     return array;
    
  168.   }, [componentsPortalContainer, profilerPortalContainer]);
    
  169. 
    
  170.   useLayoutEffect(() => {
    
  171.     switch (displayDensity) {
    
  172.       case 'comfortable':
    
  173.         updateDisplayDensity('comfortable', documentElements);
    
  174.         break;
    
  175.       case 'compact':
    
  176.         updateDisplayDensity('compact', documentElements);
    
  177.         break;
    
  178.       default:
    
  179.         throw Error(`Unsupported displayDensity value "${displayDensity}"`);
    
  180.     }
    
  181.   }, [displayDensity, documentElements]);
    
  182. 
    
  183.   useLayoutEffect(() => {
    
  184.     switch (theme) {
    
  185.       case 'light':
    
  186.         updateThemeVariables('light', documentElements);
    
  187.         break;
    
  188.       case 'dark':
    
  189.         updateThemeVariables('dark', documentElements);
    
  190.         break;
    
  191.       case 'auto':
    
  192.         updateThemeVariables(browserTheme, documentElements);
    
  193.         break;
    
  194.       default:
    
  195.         throw Error(`Unsupported theme value "${theme}"`);
    
  196.     }
    
  197.   }, [browserTheme, theme, documentElements]);
    
  198. 
    
  199.   useEffect(() => {
    
  200.     bridge.send('updateConsolePatchSettings', {
    
  201.       appendComponentStack,
    
  202.       breakOnConsoleErrors,
    
  203.       showInlineWarningsAndErrors,
    
  204.       hideConsoleLogsInStrictMode,
    
  205.       browserTheme,
    
  206.     });
    
  207.   }, [
    
  208.     bridge,
    
  209.     appendComponentStack,
    
  210.     breakOnConsoleErrors,
    
  211.     showInlineWarningsAndErrors,
    
  212.     hideConsoleLogsInStrictMode,
    
  213.     browserTheme,
    
  214.   ]);
    
  215. 
    
  216.   useEffect(() => {
    
  217.     bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
    
  218.   }, [bridge, traceUpdatesEnabled]);
    
  219. 
    
  220.   const value = useMemo(
    
  221.     () => ({
    
  222.       appendComponentStack,
    
  223.       breakOnConsoleErrors,
    
  224.       displayDensity,
    
  225.       lineHeight:
    
  226.         displayDensity === 'compact'
    
  227.           ? COMPACT_LINE_HEIGHT
    
  228.           : COMFORTABLE_LINE_HEIGHT,
    
  229.       parseHookNames,
    
  230.       setAppendComponentStack,
    
  231.       setBreakOnConsoleErrors,
    
  232.       setDisplayDensity,
    
  233.       setParseHookNames,
    
  234.       setTheme,
    
  235.       setTraceUpdatesEnabled,
    
  236.       setShowInlineWarningsAndErrors,
    
  237.       showInlineWarningsAndErrors,
    
  238.       setHideConsoleLogsInStrictMode,
    
  239.       hideConsoleLogsInStrictMode,
    
  240.       theme,
    
  241.       browserTheme,
    
  242.       traceUpdatesEnabled,
    
  243.     }),
    
  244.     [
    
  245.       appendComponentStack,
    
  246.       breakOnConsoleErrors,
    
  247.       displayDensity,
    
  248.       parseHookNames,
    
  249.       setAppendComponentStack,
    
  250.       setBreakOnConsoleErrors,
    
  251.       setDisplayDensity,
    
  252.       setParseHookNames,
    
  253.       setTheme,
    
  254.       setTraceUpdatesEnabled,
    
  255.       setShowInlineWarningsAndErrors,
    
  256.       showInlineWarningsAndErrors,
    
  257.       setHideConsoleLogsInStrictMode,
    
  258.       hideConsoleLogsInStrictMode,
    
  259.       theme,
    
  260.       browserTheme,
    
  261.       traceUpdatesEnabled,
    
  262.     ],
    
  263.   );
    
  264. 
    
  265.   return (
    
  266.     <SettingsContext.Provider value={value}>
    
  267.       {children}
    
  268.     </SettingsContext.Provider>
    
  269.   );
    
  270. }
    
  271. 
    
  272. export function updateDisplayDensity(
    
  273.   displayDensity: DisplayDensity,
    
  274.   documentElements: DocumentElements,
    
  275. ): void {
    
  276.   // Sizes and paddings/margins are all rem-based,
    
  277.   // so update the root font-size as well when the display preference changes.
    
  278.   const computedStyle = getComputedStyle((document.body: any));
    
  279.   const fontSize = computedStyle.getPropertyValue(
    
  280.     `--${displayDensity}-root-font-size`,
    
  281.   );
    
  282.   const root = ((document.querySelector(':root'): any): HTMLElement);
    
  283.   root.style.fontSize = fontSize;
    
  284. }
    
  285. 
    
  286. export function updateThemeVariables(
    
  287.   theme: Theme,
    
  288.   documentElements: DocumentElements,
    
  289. ): void {
    
  290.   // Update scrollbar color to match theme.
    
  291.   // this CSS property is currently only supported in Firefox,
    
  292.   // but it makes a significant UI improvement in dark mode.
    
  293.   // https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
    
  294.   documentElements.forEach(documentElement => {
    
  295.     // $FlowFixMe[prop-missing] scrollbarColor is missing in CSSStyleDeclaration
    
  296.     documentElement.style.scrollbarColor = `var(${`--${theme}-color-scroll-thumb`}) var(${`--${theme}-color-scroll-track`})`;
    
  297.   });
    
  298. }
    
  299. 
    
  300. export {SettingsContext, SettingsContextController};