diff --git a/packages/react-native/Libraries/Utilities/Appearance.d.ts b/packages/react-native/Libraries/Utilities/Appearance.d.ts index fe02b80b972d..153f0386fdf1 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.d.ts +++ b/packages/react-native/Libraries/Utilities/Appearance.d.ts @@ -9,35 +9,46 @@ import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter'; -type ColorSchemeName = 'light' | 'dark' | 'unspecified'; +type ColorSchemeName = 'light' | 'dark'; + +type ColorSchemeOverride = + | ColorSchemeName + | 'auto' + /** @deprecated Use 'auto' instead */ + | 'unspecified'; export namespace Appearance { type AppearancePreferences = { - colorScheme: ColorSchemeName; + colorScheme: ColorSchemeName | null; }; type AppearanceListener = (preferences: AppearancePreferences) => void; /** - * Note: Although color scheme is available immediately, it may change at any - * time. Any rendering logic or styles that depend on this should try to call - * this function on every render, rather than caching the value (for example, - * using inline styles rather than setting a value in a `StyleSheet`). + * Returns the active color scheme (`'light'` or `'dark'`). This value may + * change at runtime, either at the system level (e.g. scheduled color scheme + * change at sunrise or sunset) or when overridden at the app level via + * `setColorScheme()`. + * + * Prefer `useColorScheme()` in React components. * - * Example: `const colorScheme = Appearance.getColorScheme();` + * Notes: + * - `null` will only be returned if the native Appearance module is + * unavailable (out of tree platforms). */ - export function getColorScheme(): ColorSchemeName | null | undefined; + export function getColorScheme(): ColorSchemeName | null; /** - * Set the color scheme preference. This is useful for overriding the default - * color scheme preference for the app. Note that this will not change the - * appearance of the system UI, only the appearance of the app. - * Only available on iOS 13+ and Android 10+. + * Force the application to always adopt a light or dark interface style. Pass + * `'auto'` to reset and follow the system default (removes any override). + * This does not affect the system UI, only the application. */ - export function setColorScheme(scheme: ColorSchemeName): void; + export function setColorScheme(scheme: ColorSchemeOverride): void; /** - * Add an event handler that is fired when appearance preferences change. + * Subscribe to color scheme changes. The listener receives the new appearance + * preferences whenever the color scheme changes, whether from a system event + * or a call to `setColorScheme()`. */ export function addChangeListener( listener: AppearanceListener, @@ -45,7 +56,11 @@ export namespace Appearance { } /** - * A new useColorScheme hook is provided as the preferred way of accessing - * the user's preferred color scheme (e.g. Dark Mode). + * Returns the active color scheme (`'light'` or `'dark'`). Automatically + * re-renders the component when the color scheme changes. + * + * Notes: + * - `null` will only be returned if the native Appearance module is unavailable + * (out of tree platforms). */ -export function useColorScheme(): ColorSchemeName; +export function useColorScheme(): ColorSchemeName | null; diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index 8b2bdba03c83..936b03f93852 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -9,16 +9,24 @@ */ import type {EventSubscription} from '../vendor/emitter/EventEmitter'; -import type {AppearancePreferences, ColorSchemeName} from './NativeAppearance'; +import type { + AppearancePreferences as NativeAppearancePreferences, + ColorSchemeName, + ColorSchemeOverride, +} from './NativeAppearance'; import typeof INativeAppearance from './NativeAppearance'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import EventEmitter from '../vendor/emitter/EventEmitter'; -export type {AppearancePreferences}; +export type {ColorSchemeName, ColorSchemeOverride}; + +export type AppearancePreferences = { + colorScheme: ColorSchemeName | null, +}; type Appearance = { - colorScheme: ?ColorSchemeName, + colorScheme: ColorSchemeName | null, }; let lazyState: ?{ @@ -56,7 +64,7 @@ function getState(): NonNullable { eventEmitter, }; new NativeEventEmitter<{ - appearanceChanged: [AppearancePreferences], + appearanceChanged: [NativeAppearancePreferences], }>(NativeAppearance).addListener('appearanceChanged', newAppearance => { state.appearance = { colorScheme: newAppearance.colorScheme, @@ -69,11 +77,18 @@ function getState(): NonNullable { } /** - * Returns the current color scheme preference. This value may change, so the - * value should not be cached without either listening to changes or using - * the `useColorScheme` hook. + * Returns the active color scheme (`'light'` or `'dark'`). This value may + * change at runtime, either at the system level (e.g. scheduled color scheme + * change at sunrise or sunset) or when overridden at the app level via + * `setColorScheme()`. + * + * Prefer `useColorScheme()` in React components. + * + * Notes: + * - `null` will only be returned if the native Appearance module is unavailable + * (out of tree platforms). */ -export function getColorScheme(): ?ColorSchemeName { +export function getColorScheme(): ColorSchemeName | null { let colorScheme = null; const state = getState(); const {NativeAppearance} = state; @@ -91,29 +106,31 @@ export function getColorScheme(): ?ColorSchemeName { } /** - * Updates the current color scheme to the supplied value. + * Force the application to always adopt a light or dark interface style. Pass + * `'auto'` to reset and follow the system default (removes any override). + * This does not affect the system UI, only the application. */ -export function setColorScheme(colorScheme: ColorSchemeName): void { +export function setColorScheme(colorScheme: ColorSchemeOverride): void { const state = getState(); const {NativeAppearance} = state; if (NativeAppearance != null) { NativeAppearance.setColorScheme(colorScheme); state.appearance = { - // When setting to 'unspecified', get the actual system color scheme. - // Fall back to the passed value if getColorScheme() returns null. colorScheme: - colorScheme === 'unspecified' - ? (NativeAppearance.getColorScheme() ?? colorScheme) + colorScheme === 'auto' || colorScheme === 'unspecified' + ? NativeAppearance.getColorScheme() : colorScheme, }; } } /** - * Add an event handler that is fired when appearance preferences change. + * Subscribe to color scheme changes. The listener receives the new appearance + * preferences whenever the color scheme changes, whether from a system event + * or a call to `setColorScheme()`. */ export function addChangeListener( - listener: ({colorScheme: ?ColorSchemeName}) => void, + listener: (preferences: AppearancePreferences) => void, ): EventSubscription { const {eventEmitter} = getState(); return eventEmitter.addListener('change', listener); diff --git a/packages/react-native/Libraries/Utilities/useColorScheme.js b/packages/react-native/Libraries/Utilities/useColorScheme.js index ab5f6de5256f..39cec85a3031 100644 --- a/packages/react-native/Libraries/Utilities/useColorScheme.js +++ b/packages/react-native/Libraries/Utilities/useColorScheme.js @@ -20,6 +20,14 @@ const subscribe = (onStoreChange: () => void) => { return () => appearanceSubscription.remove(); }; -export default function useColorScheme(): ?ColorSchemeName { +/** + * Returns the active color scheme (`'light'` or `'dark'`). Automatically + * re-renders the component when the color scheme changes. + * + * Notes: + * - `null` will only be returned if the native Appearance module is unavailable + * (out of tree platforms). + */ +export default function useColorScheme(): ColorSchemeName | null { return useSyncExternalStore(subscribe, getColorScheme); } diff --git a/packages/react-native/React/Base/RCTConvert.mm b/packages/react-native/React/Base/RCTConvert.mm index 0aacbd856107..9400163dd813 100644 --- a/packages/react-native/React/Base/RCTConvert.mm +++ b/packages/react-native/React/Base/RCTConvert.mm @@ -494,6 +494,7 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC RCT_ENUM_CONVERTER( UIUserInterfaceStyle, (@{ + @"auto" : @(UIUserInterfaceStyleUnspecified), @"unspecified" : @(UIUserInterfaceStyleUnspecified), @"light" : @(UIUserInterfaceStyleLight), @"dark" : @(UIUserInterfaceStyleDark), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt index 65f1088ef51c..4028e2a9a20d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt @@ -87,6 +87,7 @@ constructor( when (style) { "dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) "light" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + "auto", "unspecified" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) } diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 13c53f3f59ab..6e7dd111ae46 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1c8637ab03a5fec9d39704d1ae305595>> + * @generated SignedSource<> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1218,7 +1218,7 @@ declare type ActivityIndicatorProps = Readonly< > declare type add = typeof add declare function addChangeListener( - listener: ($$PARAM_0$$: { colorScheme: ColorSchemeName | undefined }) => void, + listener: (preferences: AppearancePreferences) => void, ): EventSubscription declare class Alert { static alert( @@ -1629,11 +1629,13 @@ declare namespace Appearance { getColorScheme, setColorScheme, addChangeListener, + ColorSchemeName, + ColorSchemeOverride, AppearancePreferences, } } declare type AppearancePreferences = { - colorScheme?: ColorSchemeName + colorScheme: ColorSchemeName | null } declare type AppParameters = { initialProps: { @@ -1856,7 +1858,8 @@ declare namespace CodegenTypes { } } declare type ColorListenerCallback = (value: ColorValue) => unknown -declare type ColorSchemeName = "dark" | "light" | "unspecified" +declare type ColorSchemeName = "dark" | "light" +declare type ColorSchemeOverride = "auto" | "dark" | "light" | "unspecified" declare type ColorValue = ____ColorValue_Internal declare type ComponentProvider = () => React.ComponentType declare type ComponentProviderInstrumentationHook = ( @@ -2465,7 +2468,7 @@ declare function get_2( name: string, ): null | T | undefined declare function getAppKeys(): ReadonlyArray -declare function getColorScheme(): ColorSchemeName | null | undefined +declare function getColorScheme(): ColorSchemeName | null declare function getEnforcing(name: string): T declare function getRegistry(): Registry declare function getRunnable(appKey: string): null | Runnable | undefined @@ -4709,7 +4712,7 @@ declare type Separators = { updateProps: (select: "leading" | "trailing", newProps: Object) => void } declare type sequence = typeof sequence -declare function setColorScheme(colorScheme: ColorSchemeName): void +declare function setColorScheme(colorScheme: ColorSchemeOverride): void declare function setComponentProviderInstrumentationHook( hook: ComponentProviderInstrumentationHook, ): void @@ -5735,7 +5738,7 @@ declare function useAnimatedValueXY( }, config?: Animated.AnimatedConfig | null | undefined, ): Animated.ValueXY -declare function useColorScheme(): ColorSchemeName | null | undefined +declare function useColorScheme(): ColorSchemeName | null declare function usePressability( config: null | PressabilityConfig | undefined, ): null | PressabilityEventHandlers @@ -6070,7 +6073,7 @@ export { AppState, // 12012be5 AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 - Appearance, // 00cbaa0a + Appearance, // 83e9641a AutoCapitalize, // c0e857a0 BackHandler, // f139fc69 BackPressEventName, // 4620fb76 @@ -6080,7 +6083,7 @@ export { ButtonProps, // 0df9cb59 Clipboard, // 41addb89 CodegenTypes, // 0b8108a8 - ColorSchemeName, // 31a4350e + ColorSchemeName, // 6615edd6 ColorValue, // 98989a8f ComponentProvider, // b5c60ddd ComponentProviderInstrumentationHook, // 9f640048 @@ -6336,7 +6339,7 @@ export { useAnimatedColor, // e3511f81 useAnimatedValue, // b18adb63 useAnimatedValueXY, // c7ee2332 - useColorScheme, // c216d6f7 + useColorScheme, // d585efdb usePressability, // b4e21b46 useWindowDimensions, // bb4b683f } diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js index 29296fe87921..24293ff900ae 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js @@ -12,15 +12,17 @@ import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; -export type ColorSchemeName = 'light' | 'dark' | 'unspecified'; +export type ColorSchemeName = 'light' | 'dark'; + +export type ColorSchemeOverride = 'light' | 'dark' | 'auto' | 'unspecified'; export type AppearancePreferences = { - colorScheme?: ?ColorSchemeName, + colorScheme: ColorSchemeName, }; export interface Spec extends TurboModule { - +getColorScheme: () => ?ColorSchemeName; - +setColorScheme: (colorScheme: ColorSchemeName) => void; + +getColorScheme: () => ColorSchemeName; + +setColorScheme: (colorScheme: ColorSchemeOverride) => void; // RCTEventEmitter +addListener: (eventName: string) => void; diff --git a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js index 7285806793a8..62bc5f9c2970 100644 --- a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js +++ b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js @@ -18,13 +18,13 @@ import {useEffect, useState} from 'react'; import {Appearance, Button, Text, View, useColorScheme} from 'react-native'; function ColorSchemeSubscription() { - const [colorScheme, setColorScheme] = useState( + const [colorScheme, setColorScheme] = useState( Appearance.getColorScheme(), ); useEffect(() => { const subscription = Appearance.addChangeListener( - ({colorScheme: newColorScheme}: {colorScheme: ?ColorSchemeName}) => { + ({colorScheme: newColorScheme}) => { setColorScheme(newColorScheme); }, ); @@ -135,8 +135,9 @@ const ColorShowcase = (props: {themeName: string}) => ( ); const ToggleNativeAppearance = () => { - const [nativeColorScheme, setNativeColorScheme] = - useState('unspecified'); + const [nativeColorScheme, setNativeColorScheme] = useState< + ColorSchemeName | 'auto', + >('auto'); const colorScheme = useColorScheme(); useEffect(() => { @@ -155,10 +156,7 @@ const ToggleNativeAppearance = () => { title="Set to dark" onPress={() => setNativeColorScheme('dark')} /> -