Skip to content

Commit 61c8b04

Browse files
zoontekmeta-codesync[bot]
authored andcommitted
Add StatusBar "auto" barStyle value (#56978)
Summary: Adds support for `barStyle="auto"` on `StatusBar`. When set, the bar style is resolved against the current color scheme: `light-content` in dark mode, `dark-content` in light mode. The resolution re-runs when the system color scheme changes, while at least one `StatusBar` is mounted. The behavior mirrors what `expo-status-bar` already exposes. The `'auto'` name is the one we use at Expo. It is not necessarily the right name for React Native core, happy to switch to something else if reviewers prefer. ## Changelog: [GENERAL] [ADDED] - StatusBar: support `barStyle="auto"` to follow the current color scheme Pull Request resolved: #56978 Test Plan: - `yarn jest packages/react-native/Libraries/Components/StatusBar` passes, including a new test that asserts `setBarStyle('auto')` calls the native module with `dark-content` in light mode and `light-content` in dark mode. - Play with it in the RNTester app Reviewed By: cortinico Differential Revision: D106535143 Pulled By: zeyap fbshipit-source-id: eff3dea37a317f22af3364345d355908104c8c5a
1 parent 833144f commit 61c8b04

5 files changed

Lines changed: 97 additions & 13 deletions

File tree

packages/react-native/Libraries/Components/StatusBar/StatusBar.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
import type * as React from 'react';
1111
import {ColorValue} from '../../StyleSheet/StyleSheet';
1212

13-
export type StatusBarStyle = 'default' | 'light-content' | 'dark-content';
13+
export type StatusBarStyle =
14+
| 'default'
15+
| 'auto'
16+
| 'light-content'
17+
| 'dark-content';
1418

1519
export type StatusBarAnimation = 'none' | 'fade' | 'slide';
1620

packages/react-native/Libraries/Components/StatusBar/StatusBar.js

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
*/
1010

1111
import type {ColorValue} from '../../StyleSheet/StyleSheet';
12+
import type {EventSubscription} from '../../vendor/emitter/EventEmitter';
1213

1314
import processColor from '../../StyleSheet/processColor';
15+
import * as Appearance from '../../Utilities/Appearance';
1416
import Platform from '../../Utilities/Platform';
1517
import NativeStatusBarManagerAndroid from './NativeStatusBarManagerAndroid';
1618
import NativeStatusBarManagerIOS from './NativeStatusBarManagerIOS';
@@ -25,6 +27,11 @@ export type StatusBarStyle = keyof {
2527
* Default status bar style (dark for iOS, light for Android)
2628
*/
2729
default: string,
30+
/**
31+
* Automatically picks `light-content` or `dark-content` based on the current
32+
* color scheme. Updates whenever the color scheme changes.
33+
*/
34+
auto: string,
2835
/**
2936
* Dark background, white texts and icons
3037
*/
@@ -105,7 +112,7 @@ type StatusBarBaseProps = Readonly<{
105112
/**
106113
* Sets the color of the status bar text.
107114
*/
108-
barStyle?: ?('default' | 'light-content' | 'dark-content'),
115+
barStyle?: ?('default' | 'auto' | 'light-content' | 'dark-content'),
109116
}>;
110117

111118
export type StatusBarProps = Readonly<{
@@ -133,13 +140,24 @@ type StackProps = {
133140
};
134141

135142
/**
136-
* Merges the prop stack with the default values.
143+
* Returns the bar style to use when `barStyle` is `'auto'`, picked against
144+
* the current color scheme.
145+
*/
146+
function getAutoBarStyle(): 'light-content' | 'dark-content' {
147+
return Appearance.getColorScheme() === 'dark'
148+
? 'light-content'
149+
: 'dark-content';
150+
}
151+
152+
/**
153+
* Merges the prop stack with the default values, resolving the `'auto'`
154+
* barStyle to a concrete value.
137155
*/
138156
function mergePropsStack(
139157
propsStack: Array<Object>,
140158
defaultValues: Object,
141159
): Object {
142-
return propsStack.reduce(
160+
const merged: StackProps = propsStack.reduce(
143161
(prev, cur) => {
144162
for (const prop in cur) {
145163
if (cur[prop] != null) {
@@ -150,6 +168,12 @@ function mergePropsStack(
150168
},
151169
{...defaultValues},
152170
);
171+
172+
if (merged.barStyle?.value === 'auto') {
173+
merged.barStyle = {...merged.barStyle, value: getAutoBarStyle()};
174+
}
175+
176+
return merged;
153177
}
154178

155179
/**
@@ -247,9 +271,16 @@ class StatusBar extends React.Component<StatusBarProps> {
247271
// Timer for updating the native module values at the end of the frame.
248272
static _updateImmediate: ?number = null;
249273

250-
// The current merged values from the props stack.
274+
// The current merged values from the props stack. `barStyle.value` is stored
275+
// in its resolved form (never `'auto'`), so diff comparisons reflect what
276+
// was actually sent to the native module.
251277
static _currentValues: ?StackProps = null;
252278

279+
// Number of mounted `StatusBar` instances. Used to lazily subscribe to color
280+
// scheme changes only while at least one instance is on screen.
281+
static _mountedCount: number = 0;
282+
static _appearanceSubscription: ?EventSubscription = null;
283+
253284
// TODO(janic): Provide a real API to deal with status bar height. See the
254285
// discussion in #6195.
255286
/**
@@ -289,10 +320,11 @@ class StatusBar extends React.Component<StatusBarProps> {
289320
static setBarStyle(style: StatusBarStyle, animated?: boolean) {
290321
animated = animated || false;
291322
StatusBar._defaultProps.barStyle.value = style;
323+
const resolvedStyle = style === 'auto' ? getAutoBarStyle() : style;
292324
if (Platform.OS === 'ios') {
293-
NativeStatusBarManagerIOS.setStyle(style, animated);
325+
NativeStatusBarManagerIOS.setStyle(resolvedStyle, animated);
294326
} else if (Platform.OS === 'android') {
295-
NativeStatusBarManagerAndroid.setStyle(style);
327+
NativeStatusBarManagerAndroid.setStyle(resolvedStyle);
296328
}
297329
}
298330

@@ -407,6 +439,16 @@ class StatusBar extends React.Component<StatusBarProps> {
407439
// stack. This allows having multiple StatusBar components and the one that is
408440
// added last or is deeper in the view hierarchy will have priority.
409441
this._stackEntry = StatusBar.pushStackEntry(this.props);
442+
443+
if (StatusBar._mountedCount === 0) {
444+
// Re-run the native update when the system color scheme changes so any
445+
// `barStyle: 'auto'` entries resolve to the new appropriate value.
446+
StatusBar._appearanceSubscription = Appearance.addChangeListener(() => {
447+
StatusBar._updatePropsStack();
448+
});
449+
}
450+
451+
StatusBar._mountedCount++;
410452
}
411453

412454
componentWillUnmount() {
@@ -415,6 +457,16 @@ class StatusBar extends React.Component<StatusBarProps> {
415457
if (this._stackEntry != null) {
416458
StatusBar.popStackEntry(this._stackEntry);
417459
}
460+
461+
StatusBar._mountedCount--;
462+
463+
if (
464+
StatusBar._appearanceSubscription != null &&
465+
StatusBar._mountedCount === 0
466+
) {
467+
StatusBar._appearanceSubscription.remove();
468+
StatusBar._appearanceSubscription = null;
469+
}
418470
}
419471

420472
componentDidUpdate() {

packages/react-native/Libraries/Components/StatusBar/__tests__/StatusBar-test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,33 @@ describe('StatusBar', () => {
4747
false,
4848
);
4949
});
50+
it('resolves auto barStyle against the current color scheme', () => {
51+
const Appearance = require('../../../Utilities/Appearance');
52+
const Platform = require('../../../Utilities/Platform').default;
53+
54+
const nativeStatusBarManager =
55+
Platform.OS === 'ios'
56+
? require('../NativeStatusBarManagerIOS').default
57+
: require('../NativeStatusBarManagerAndroid').default;
58+
59+
const appearanceSpy = jest.spyOn(Appearance, 'getColorScheme');
60+
const setStyleSpy = jest.spyOn(nativeStatusBarManager, 'setStyle');
61+
62+
appearanceSpy.mockReturnValue('light');
63+
setStyleSpy.mockClear();
64+
65+
StatusBar.setBarStyle('auto');
66+
expect(setStyleSpy.mock.calls[0][0]).toBe('dark-content');
67+
68+
appearanceSpy.mockReturnValue('dark');
69+
setStyleSpy.mockClear();
70+
71+
StatusBar.setBarStyle('auto');
72+
expect(setStyleSpy.mock.calls[0][0]).toBe('light-content');
73+
74+
appearanceSpy.mockRestore();
75+
setStyleSpy.mockRestore();
76+
});
5077
it('renders the statusbar but should not be visible', async () => {
5178
const component = await create(<StatusBar hidden={true} />);
5279
expect(component.toTree()?.props.hidden).toBe(true);

packages/react-native/ReactNativeApi.d.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<08dd369849273136812ea5edbda6e1df>>
7+
* @generated SignedSource<<a438af540f18c77572f05cef9481b7a6>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -4948,7 +4948,7 @@ declare type StatusBarAnimation = keyof {
49484948
}
49494949
declare type StatusBarBaseProps = {
49504950
readonly animated?: boolean
4951-
readonly barStyle?: "dark-content" | "default" | "light-content"
4951+
readonly barStyle?: "auto" | "dark-content" | "default" | "light-content"
49524952
readonly hidden?: boolean
49534953
}
49544954
declare type StatusBarProps = Readonly<
@@ -4963,6 +4963,7 @@ declare type StatusBarPropsIOS = {
49634963
readonly showHideTransition?: "fade" | "none" | "slide"
49644964
}
49654965
declare type StatusBarStyle = keyof {
4966+
auto: string
49664967
"dark-content": string
49674968
default: string
49684969
"light-content": string
@@ -6213,10 +6214,10 @@ export {
62136214
ShareContent, // 7c627896
62146215
ShareOptions, // 800c3a4e
62156216
SimpleTask, // 0e619d11
6216-
StatusBar, // 5e08d563
6217+
StatusBar, // 875b4eca
62176218
StatusBarAnimation, // 7fd047e6
6218-
StatusBarProps, // 06c98add
6219-
StatusBarStyle, // 986b2051
6219+
StatusBarProps, // b72a9127
6220+
StatusBarStyle, // 78f53eea
62206221
StyleProp, // fa0e9b4a
62216222
StyleSheet, // e77dd046
62226223
SubmitBehavior, // c4ddf490

packages/rn-tester/js/examples/StatusBar/StatusBarExample.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424

2525
const colors = ['#ff0000', '#00ff00', '#0000ff', 'rgba(0, 0, 0, 0.4)'];
2626

27-
const barStyles = ['default', 'light-content', 'dark-content'];
27+
const barStyles = ['default', 'auto', 'light-content', 'dark-content'];
2828

2929
const showHideTransitions = ['fade', 'slide'];
3030

0 commit comments

Comments
 (0)