diff --git a/packages/react-native/Libraries/Components/StatusBar/StatusBar.d.ts b/packages/react-native/Libraries/Components/StatusBar/StatusBar.d.ts index 53144c375edc..83db72e05022 100644 --- a/packages/react-native/Libraries/Components/StatusBar/StatusBar.d.ts +++ b/packages/react-native/Libraries/Components/StatusBar/StatusBar.d.ts @@ -10,7 +10,11 @@ import type * as React from 'react'; import {ColorValue} from '../../StyleSheet/StyleSheet'; -export type StatusBarStyle = 'default' | 'light-content' | 'dark-content'; +export type StatusBarStyle = + | 'default' + | 'auto' + | 'light-content' + | 'dark-content'; export type StatusBarAnimation = 'none' | 'fade' | 'slide'; diff --git a/packages/react-native/Libraries/Components/StatusBar/StatusBar.js b/packages/react-native/Libraries/Components/StatusBar/StatusBar.js index 5df3131bdcf0..5a391305b6b3 100644 --- a/packages/react-native/Libraries/Components/StatusBar/StatusBar.js +++ b/packages/react-native/Libraries/Components/StatusBar/StatusBar.js @@ -9,8 +9,10 @@ */ import type {ColorValue} from '../../StyleSheet/StyleSheet'; +import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; import processColor from '../../StyleSheet/processColor'; +import * as Appearance from '../../Utilities/Appearance'; import Platform from '../../Utilities/Platform'; import NativeStatusBarManagerAndroid from './NativeStatusBarManagerAndroid'; import NativeStatusBarManagerIOS from './NativeStatusBarManagerIOS'; @@ -25,6 +27,11 @@ export type StatusBarStyle = keyof { * Default status bar style (dark for iOS, light for Android) */ default: string, + /** + * Automatically picks `light-content` or `dark-content` based on the current + * color scheme. Updates whenever the color scheme changes. + */ + auto: string, /** * Dark background, white texts and icons */ @@ -105,7 +112,7 @@ type StatusBarBaseProps = Readonly<{ /** * Sets the color of the status bar text. */ - barStyle?: ?('default' | 'light-content' | 'dark-content'), + barStyle?: ?('default' | 'auto' | 'light-content' | 'dark-content'), }>; export type StatusBarProps = Readonly<{ @@ -133,13 +140,24 @@ type StackProps = { }; /** - * Merges the prop stack with the default values. + * Returns the bar style to use when `barStyle` is `'auto'`, picked against + * the current color scheme. + */ +function getAutoBarStyle(): 'light-content' | 'dark-content' { + return Appearance.getColorScheme() === 'dark' + ? 'light-content' + : 'dark-content'; +} + +/** + * Merges the prop stack with the default values, resolving the `'auto'` + * barStyle to a concrete value. */ function mergePropsStack( propsStack: Array, defaultValues: Object, ): Object { - return propsStack.reduce( + const merged: StackProps = propsStack.reduce( (prev, cur) => { for (const prop in cur) { if (cur[prop] != null) { @@ -150,6 +168,12 @@ function mergePropsStack( }, {...defaultValues}, ); + + if (merged.barStyle?.value === 'auto') { + merged.barStyle = {...merged.barStyle, value: getAutoBarStyle()}; + } + + return merged; } /** @@ -247,9 +271,16 @@ class StatusBar extends React.Component { // Timer for updating the native module values at the end of the frame. static _updateImmediate: ?number = null; - // The current merged values from the props stack. + // The current merged values from the props stack. `barStyle.value` is stored + // in its resolved form (never `'auto'`), so diff comparisons reflect what + // was actually sent to the native module. static _currentValues: ?StackProps = null; + // Number of mounted `StatusBar` instances. Used to lazily subscribe to color + // scheme changes only while at least one instance is on screen. + static _mountedCount: number = 0; + static _appearanceSubscription: ?EventSubscription = null; + // TODO(janic): Provide a real API to deal with status bar height. See the // discussion in #6195. /** @@ -289,10 +320,11 @@ class StatusBar extends React.Component { static setBarStyle(style: StatusBarStyle, animated?: boolean) { animated = animated || false; StatusBar._defaultProps.barStyle.value = style; + const resolvedStyle = style === 'auto' ? getAutoBarStyle() : style; if (Platform.OS === 'ios') { - NativeStatusBarManagerIOS.setStyle(style, animated); + NativeStatusBarManagerIOS.setStyle(resolvedStyle, animated); } else if (Platform.OS === 'android') { - NativeStatusBarManagerAndroid.setStyle(style); + NativeStatusBarManagerAndroid.setStyle(resolvedStyle); } } @@ -407,6 +439,16 @@ class StatusBar extends React.Component { // stack. This allows having multiple StatusBar components and the one that is // added last or is deeper in the view hierarchy will have priority. this._stackEntry = StatusBar.pushStackEntry(this.props); + + if (StatusBar._mountedCount === 0) { + // Re-run the native update when the system color scheme changes so any + // `barStyle: 'auto'` entries resolve to the new appropriate value. + StatusBar._appearanceSubscription = Appearance.addChangeListener(() => { + StatusBar._updatePropsStack(); + }); + } + + StatusBar._mountedCount++; } componentWillUnmount() { @@ -415,6 +457,16 @@ class StatusBar extends React.Component { if (this._stackEntry != null) { StatusBar.popStackEntry(this._stackEntry); } + + StatusBar._mountedCount--; + + if ( + StatusBar._appearanceSubscription != null && + StatusBar._mountedCount === 0 + ) { + StatusBar._appearanceSubscription.remove(); + StatusBar._appearanceSubscription = null; + } } componentDidUpdate() { diff --git a/packages/react-native/Libraries/Components/StatusBar/__tests__/StatusBar-test.js b/packages/react-native/Libraries/Components/StatusBar/__tests__/StatusBar-test.js index 640a33a21d2a..3cbda80d885d 100644 --- a/packages/react-native/Libraries/Components/StatusBar/__tests__/StatusBar-test.js +++ b/packages/react-native/Libraries/Components/StatusBar/__tests__/StatusBar-test.js @@ -47,6 +47,33 @@ describe('StatusBar', () => { false, ); }); + it('resolves auto barStyle against the current color scheme', () => { + const Appearance = require('../../../Utilities/Appearance'); + const Platform = require('../../../Utilities/Platform').default; + + const nativeStatusBarManager = + Platform.OS === 'ios' + ? require('../NativeStatusBarManagerIOS').default + : require('../NativeStatusBarManagerAndroid').default; + + const appearanceSpy = jest.spyOn(Appearance, 'getColorScheme'); + const setStyleSpy = jest.spyOn(nativeStatusBarManager, 'setStyle'); + + appearanceSpy.mockReturnValue('light'); + setStyleSpy.mockClear(); + + StatusBar.setBarStyle('auto'); + expect(setStyleSpy.mock.calls[0][0]).toBe('dark-content'); + + appearanceSpy.mockReturnValue('dark'); + setStyleSpy.mockClear(); + + StatusBar.setBarStyle('auto'); + expect(setStyleSpy.mock.calls[0][0]).toBe('light-content'); + + appearanceSpy.mockRestore(); + setStyleSpy.mockRestore(); + }); it('renders the statusbar but should not be visible', async () => { const component = await create(