Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 32 additions & 17 deletions packages/react-native/Libraries/Utilities/Appearance.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,58 @@

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,
): NativeEventSubscription;
}

/**
* 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;
49 changes: 33 additions & 16 deletions packages/react-native/Libraries/Utilities/Appearance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: ?{
Expand Down Expand Up @@ -56,7 +64,7 @@ function getState(): NonNullable<typeof lazyState> {
eventEmitter,
};
new NativeEventEmitter<{
appearanceChanged: [AppearancePreferences],
appearanceChanged: [NativeAppearancePreferences],
}>(NativeAppearance).addListener('appearanceChanged', newAppearance => {
state.appearance = {
colorScheme: newAppearance.colorScheme,
Expand All @@ -69,11 +77,18 @@ function getState(): NonNullable<typeof lazyState> {
}

/**
* 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;
Expand All @@ -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);
Expand Down
10 changes: 9 additions & 1 deletion packages/react-native/Libraries/Utilities/useColorScheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions packages/react-native/React/Base/RCTConvert.mm
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC
RCT_ENUM_CONVERTER(
UIUserInterfaceStyle,
(@{
@"auto" : @(UIUserInterfaceStyleUnspecified),
@"unspecified" : @(UIUserInterfaceStyleUnspecified),
@"light" : @(UIUserInterfaceStyleLight),
@"dark" : @(UIUserInterfaceStyleDark),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
23 changes: 13 additions & 10 deletions packages/react-native/ReactNativeApi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<<eeffa8d1c270f98debd70024251b4a1a>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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<any>
declare type ComponentProviderInstrumentationHook = (
Expand Down Expand Up @@ -2465,7 +2468,7 @@ declare function get_2<T extends TurboModule>(
name: string,
): null | T | undefined
declare function getAppKeys(): ReadonlyArray<string>
declare function getColorScheme(): ColorSchemeName | null | undefined
declare function getColorScheme(): ColorSchemeName | null
declare function getEnforcing<T extends TurboModule>(name: string): T
declare function getRegistry(): Registry
declare function getRunnable(appKey: string): null | Runnable | undefined
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -6070,7 +6073,7 @@ export {
AppState, // 12012be5
AppStateEvent, // 80f034c3
AppStateStatus, // 447e5ef2
Appearance, // 00cbaa0a
Appearance, // 83e9641a
AutoCapitalize, // c0e857a0
BackHandler, // f139fc69
BackPressEventName, // 4620fb76
Expand All @@ -6080,7 +6083,7 @@ export {
ButtonProps, // 0df9cb59
Clipboard, // 41addb89
CodegenTypes, // 0b8108a8
ColorSchemeName, // 31a4350e
ColorSchemeName, // 6615edd6
ColorValue, // 98989a8f
ComponentProvider, // b5c60ddd
ComponentProviderInstrumentationHook, // 9f640048
Expand Down Expand Up @@ -6336,7 +6339,7 @@ export {
useAnimatedColor, // e3511f81
useAnimatedValue, // b18adb63
useAnimatedValueXY, // c7ee2332
useColorScheme, // c216d6f7
useColorScheme, // d585efdb
usePressability, // b4e21b46
useWindowDimensions, // bb4b683f
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 6 additions & 8 deletions packages/rn-tester/js/examples/Appearance/AppearanceExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ import {useEffect, useState} from 'react';
import {Appearance, Button, Text, View, useColorScheme} from 'react-native';

function ColorSchemeSubscription() {
const [colorScheme, setColorScheme] = useState<?ColorSchemeName | string>(
const [colorScheme, setColorScheme] = useState<?ColorSchemeName>(
Appearance.getColorScheme(),
);

useEffect(() => {
const subscription = Appearance.addChangeListener(
({colorScheme: newColorScheme}: {colorScheme: ?ColorSchemeName}) => {
({colorScheme: newColorScheme}) => {
setColorScheme(newColorScheme);
},
);
Expand Down Expand Up @@ -135,8 +135,9 @@ const ColorShowcase = (props: {themeName: string}) => (
);

const ToggleNativeAppearance = () => {
const [nativeColorScheme, setNativeColorScheme] =
useState<ColorSchemeName>('unspecified');
const [nativeColorScheme, setNativeColorScheme] = useState<
ColorSchemeName | 'auto',
>('auto');
const colorScheme = useColorScheme();

useEffect(() => {
Expand All @@ -155,10 +156,7 @@ const ToggleNativeAppearance = () => {
title="Set to dark"
onPress={() => setNativeColorScheme('dark')}
/>
<Button
title="Unset"
onPress={() => setNativeColorScheme('unspecified')}
/>
<Button title="Auto" onPress={() => setNativeColorScheme('auto')} />
</View>
);
};
Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -2653,7 +2653,7 @@ protocol NativeAppStateSpec : public NSObjectRCTBridgeModule, public RCTTurboMod
}

protocol NativeAppearanceSpec : public NSObjectRCTBridgeModule, public RCTTurboModule {
public virtual NSString* _Nullable getColorScheme();
public virtual NSString* getColorScheme();
public virtual void addListener:(NSString* eventName);
public virtual void removeListeners:(double count);
public virtual void setColorScheme:(NSString* colorScheme);
Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -2653,7 +2653,7 @@ protocol NativeAppStateSpec : public NSObjectRCTBridgeModule, public RCTTurboMod
}

protocol NativeAppearanceSpec : public NSObjectRCTBridgeModule, public RCTTurboModule {
public virtual NSString* _Nullable getColorScheme();
public virtual NSString* getColorScheme();
public virtual void addListener:(NSString* eventName);
public virtual void removeListeners:(double count);
public virtual void setColorScheme:(NSString* colorScheme);
Expand Down
Loading