diff --git a/packages/react-native/Libraries/Utilities/Appearance.d.ts b/packages/react-native/Libraries/Utilities/Appearance.d.ts index fe02b80b972d..a271aa02caeb 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.d.ts +++ b/packages/react-native/Libraries/Utilities/Appearance.d.ts @@ -9,7 +9,9 @@ import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter'; -type ColorSchemeName = 'light' | 'dark' | 'unspecified'; +type ColorSchemeName = 'light' | 'dark'; + +type ColorSchemeOverride = ColorSchemeName | 'unspecified'; export namespace Appearance { type AppearancePreferences = { @@ -19,25 +21,30 @@ export namespace Appearance { 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; /** - * 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 + * `'unspecified'` 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 +52,7 @@ 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. */ export function useColorScheme(): ColorSchemeName; diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index 8b2bdba03c83..05b32bb2dbb6 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -9,13 +9,17 @@ */ import type {EventSubscription} from '../vendor/emitter/EventEmitter'; -import type {AppearancePreferences, ColorSchemeName} from './NativeAppearance'; +import type { + AppearancePreferences, + 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 {AppearancePreferences, ColorSchemeName, ColorSchemeOverride}; type Appearance = { colorScheme: ?ColorSchemeName, @@ -69,9 +73,16 @@ 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 { let colorScheme = null; @@ -91,26 +102,28 @@ 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 + * `'unspecified'` 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) + ? (NativeAppearance.getColorScheme() ?? null) : 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, diff --git a/packages/react-native/Libraries/Utilities/useColorScheme.js b/packages/react-native/Libraries/Utilities/useColorScheme.js index ab5f6de5256f..45eb00fa8cac 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(); }; +/** + * 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 { return useSyncExternalStore(subscribe, getColorScheme); } diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 13c53f3f59ab..c3f957f6688a 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<<4950f1efd16fed02b526f83325c8351d>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1630,6 +1630,8 @@ declare namespace Appearance { setColorScheme, addChangeListener, AppearancePreferences, + ColorSchemeName, + ColorSchemeOverride, } } declare type AppearancePreferences = { @@ -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 = "dark" | "light" | "unspecified" declare type ColorValue = ____ColorValue_Internal declare type ComponentProvider = () => React.ComponentType declare type ComponentProviderInstrumentationHook = ( @@ -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 @@ -6070,7 +6073,7 @@ export { AppState, // 12012be5 AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 - Appearance, // 00cbaa0a + Appearance, // df9545f9 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, // 29a517d5 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..dca2c6f8a08f 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js @@ -12,7 +12,9 @@ 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' | 'unspecified'; export type AppearancePreferences = { colorScheme?: ?ColorSchemeName, @@ -20,7 +22,7 @@ export type AppearancePreferences = { export interface Spec extends TurboModule { +getColorScheme: () => ?ColorSchemeName; - +setColorScheme: (colorScheme: ColorSchemeName) => void; + +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..7fd44d84f22b 100644 --- a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js +++ b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js @@ -18,7 +18,7 @@ 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(), ); @@ -135,8 +135,9 @@ const ColorShowcase = (props: {themeName: string}) => ( ); const ToggleNativeAppearance = () => { - const [nativeColorScheme, setNativeColorScheme] = - useState('unspecified'); + const [nativeColorScheme, setNativeColorScheme] = useState< + ColorSchemeName | 'unspecified', + >('unspecified'); const colorScheme = useColorScheme(); useEffect(() => {