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
32 changes: 30 additions & 2 deletions packages/react-native/Libraries/Components/StatusBar/StatusBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export type StatusBarStyle = keyof {
...
};

/**
* The edge where the system places the vertical bar that holds the status bar,
* such as the side rail on iPhone Duo.
*/
export type StatusBarVerticalBarEdgeIOS = 'leading' | 'trailing';

/**
* Status bar animation
*/
Expand Down Expand Up @@ -72,7 +78,8 @@ export type StatusBarPropsIOS = Readonly<{

type StatusBarBaseProps = Readonly<{
/**
* If the status bar is hidden.
* If the status bar is hidden. Has no effect on iPhone Duo, where the
* status bar is in a vertical bar.
*
* @default `false`
*/
Expand Down Expand Up @@ -273,7 +280,8 @@ class StatusBar extends React.Component<StatusBarProps> {
// See the corresponding prop for more detail.

/**
* Show or hide the status bar
* Show or hide the status bar. Has no effect on iPhone Duo, where the status
* bar is in a vertical bar.
* @param hidden Hide the status bar.
* @param animation Optional animation when
* changing the status bar hidden property.
Expand Down Expand Up @@ -304,6 +312,22 @@ class StatusBar extends React.Component<StatusBarProps> {
}
}

/**
* Get the edge where the system places the vertical bar that holds the
* status bar beside the content, such as the side rail on iPhone Duo.
* Returns `null` when the system places no vertical bar, such as on other
* devices, in orientations that keep the status bar at the top, or before
* iOS 27.1.
*
* @platform ios
*/
static getVerticalBarEdgeIOS(): ?StatusBarVerticalBarEdgeIOS {
if (Platform.OS !== 'ios') {
return null;
}
return toVerticalBarEdge(NativeStatusBarManagerIOS.getVerticalBarEdge());
}

/**
* Push a StatusBar entry onto the stack.
* The return value should be passed to `popStackEntry` when complete.
Expand Down Expand Up @@ -448,4 +472,8 @@ class StatusBar extends React.Component<StatusBarProps> {

export type StatusBarInstance = StatusBar;

function toVerticalBarEdge(edge: ?string): ?StatusBarVerticalBarEdgeIOS {
return edge === 'leading' || edge === 'trailing' ? edge : null;
}

export default StatusBar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

'use strict';

jest.mock('../NativeStatusBarManagerIOS', () => ({
__esModule: true,
default: {
addListener: jest.fn(),
removeListeners: jest.fn(),
getVerticalBarEdge: jest.fn(() => 'trailing'),
},
}));

const useVerticalBarEdgeIOS = require('../useVerticalBarEdgeIOS').default;
const {create} = require('@react-native/jest-preset/jest/renderer');
const React = require('react');

test('returns the vertical bar edge from the native module', async () => {
let edge;
function Probe() {
edge = useVerticalBarEdgeIOS();
return null;
}

await create(<Probe />);

expect(edge).toBe('trailing');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import type {StatusBarVerticalBarEdgeIOS} from './StatusBar';

import NativeEventEmitter from '../../EventEmitter/NativeEventEmitter';
import Platform from '../../Utilities/Platform';
import NativeStatusBarManagerIOS from './NativeStatusBarManagerIOS';
import StatusBar from './StatusBar';
import {useSyncExternalStore} from 'react';

// Read once and refreshed by events, so renders don't each call into native.
let cachedEdge: ?StatusBarVerticalBarEdgeIOS;

function subscribe(onStoreChange: () => void): () => void {
if (Platform.OS !== 'ios') {
return () => {};
}
cachedEdge = StatusBar.getVerticalBarEdgeIOS();
const subscription = new NativeEventEmitter<{
verticalBarEdgeDidChange: [{edge: ?string}],
}>(NativeStatusBarManagerIOS).addListener('verticalBarEdgeDidChange', () => {
cachedEdge = StatusBar.getVerticalBarEdgeIOS();
onStoreChange();
});
return () => subscription.remove();
}

function getSnapshot(): ?StatusBarVerticalBarEdgeIOS {
if (cachedEdge === undefined) {
cachedEdge = StatusBar.getVerticalBarEdgeIOS();
}
return cachedEdge;
}

/**
* React hook that returns the edge where the system places the vertical bar
* that holds the status bar, such as the side rail on iPhone Duo, and updates
* when it changes. Returns `null` when the system places no vertical bar.
*
* @platform ios
*/
export default function useVerticalBarEdgeIOS(): ?StatusBarVerticalBarEdgeIOS {
return useSyncExternalStore(subscribe, getSnapshot);
}
68 changes: 67 additions & 1 deletion packages/react-native/React/CoreModules/RCTStatusBarManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@

static NSString *const kStatusBarFrameDidChange = @"statusBarFrameDidChange";
static NSString *const kStatusBarFrameWillChange = @"statusBarFrameWillChange";
static NSString *const kVerticalBarEdgeDidChange = @"verticalBarEdgeDidChange";

static NSString *_Nullable RCTVerticalBarEdge(UITraitCollection *traitCollection)
{
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 270100 /* __IPHONE_27_1 */
if (@available(iOS 27.1, tvOS 27.1, *)) {
switch (traitCollection.verticalBarEdge) {
case UIVerticalBarEdgeLeading:
return @"leading";
case UIVerticalBarEdgeTrailing:
return @"trailing";
default:
return nil;
}
}
#endif
return nil;
}

@implementation RCTConvert (UIStatusBar)

Expand Down Expand Up @@ -53,6 +71,8 @@ @interface RCTStatusBarManager () <NativeStatusBarManagerIOSSpec, RCTInitializin

@implementation RCTStatusBarManager {
facebook::react::ModuleConstants<JS::NativeStatusBarManagerIOS::Constants> _constants;
__weak UIWindow *_verticalBarEdgeWindow;
id _verticalBarEdgeRegistration;
}

static BOOL RCTViewControllerBasedStatusBarAppearance()
Expand Down Expand Up @@ -85,7 +105,7 @@ - (void)initialize

- (NSArray<NSString *> *)supportedEvents
{
return @[ kStatusBarFrameDidChange, kStatusBarFrameWillChange ];
return @[ kStatusBarFrameDidChange, kStatusBarFrameWillChange, kVerticalBarEdgeDidChange ];
}

- (void)startObserving
Expand All @@ -102,11 +122,48 @@ - (void)startObserving
name:UIApplicationWillChangeStatusBarFrameNotification
object:nil];
#pragma clang diagnostic pop

dispatch_async(dispatch_get_main_queue(), ^{
[self observeVerticalBarEdge];
});
}

- (void)stopObserving
{
[[NSNotificationCenter defaultCenter] removeObserver:self];

dispatch_async(dispatch_get_main_queue(), ^{
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 270100 /* __IPHONE_27_1 */
if (self->_verticalBarEdgeRegistration != nil) {
[self->_verticalBarEdgeWindow unregisterForTraitChanges:self->_verticalBarEdgeRegistration];
}
#endif
self->_verticalBarEdgeRegistration = nil;
self->_verticalBarEdgeWindow = nil;
});
}

- (void)observeVerticalBarEdge
{
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 270100 /* __IPHONE_27_1 */
if (@available(iOS 27.1, tvOS 27.1, *)) {
UIWindow *window = RCTKeyWindow();
if (_verticalBarEdgeRegistration != nil || window == nil) {
return;
}
__weak __typeof(self) weakSelf = self;
_verticalBarEdgeWindow = window;
_verticalBarEdgeRegistration = [window
registerForTraitChanges:UITraitCollection.systemTraitsAffectingVerticalBarEdge
withHandler:^(id<UITraitEnvironment> traitEnvironment, UITraitCollection *previousTraitCollection) {
NSString *edge = RCTVerticalBarEdge(traitEnvironment.traitCollection);
NSString *previousEdge = RCTVerticalBarEdge(previousTraitCollection);
if (edge != previousEdge && ![edge isEqualToString:previousEdge]) {
[weakSelf sendEventWithName:kVerticalBarEdgeDidChange body:@{@"edge" : edge ?: [NSNull null]}];
}
}];
}
#endif
}

- (void)emitEvent:(NSString *)eventName forNotification:(NSNotification *)notification
Expand Down Expand Up @@ -143,6 +200,15 @@ - (void)getHeight:(RCTResponseSenderBlock)callback
} ]);
}

- (NSString *)getVerticalBarEdge
{
__block NSString *edge;
RCTUnsafeExecuteOnMainQueueSync(^{
edge = RCTVerticalBarEdge(RCTKeyWindow().traitCollection);
});
return edge;
}

- (void)setStyle:(NSString *)style animated:(BOOL)animated
{
dispatch_async(dispatch_get_main_queue(), ^{
Expand Down
12 changes: 9 additions & 3 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<<29785e09db994cf0d43e7426f47c1b30>>
* @generated SignedSource<<db1b0fb76a53495a8f7e9eb50a3813a3>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
Expand Down Expand Up @@ -4753,6 +4753,7 @@ declare class StatusBar extends React.Component<StatusBarProps> {
componentDidMount(): void
componentDidUpdate(): void
componentWillUnmount(): void
static getVerticalBarEdgeIOS(): null | StatusBarVerticalBarEdgeIOS | undefined
static popStackEntry(entry: StackProps): void
static pushStackEntry(props: StatusBarProps): StackProps
render(): React.ReactNode
Expand Down Expand Up @@ -4781,6 +4782,7 @@ declare type StatusBarStyle = keyof {
default: string
"light-content": string
}
declare type StatusBarVerticalBarEdgeIOS = "leading" | "trailing"
declare type StickyHeaderComponentType = (
props: ScrollViewStickyHeaderProps & {
ref?: React.Ref<{
Expand Down Expand Up @@ -5454,6 +5456,8 @@ declare function useColorScheme(): ColorSchemeName | null
declare function usePressability(
config: null | PressabilityConfig | undefined,
): null | PressabilityEventHandlers
declare function useVerticalBarEdgeIOS():
null | StatusBarVerticalBarEdgeIOS | undefined
declare function useWindowDimensions(): DisplayMetrics | DisplayMetricsAndroid
declare type UTFSequence = typeof UTFSequence
declare type Value = null | {
Expand Down Expand Up @@ -6007,11 +6011,12 @@ export {
ShareActionSheetIOSOptions, // eff574f5
ShareContent, // 7c627896
ShareOptions, // 800c3a4e
StatusBar, // 5748d574
StatusBar, // 8bda633d
StatusBarAnimation, // 7fd047e6
StatusBarInstance, // de13e4a1
StatusBarInstance, // 6696fd7f
StatusBarProps, // c2a44d88
StatusBarStyle, // 78f53eea
StatusBarVerticalBarEdgeIOS, // f0ffbe54
StyleProp, // fa0e9b4a
StyleSheet, // 8bb13f55
SubmitBehavior, // c4ddf490
Expand Down Expand Up @@ -6088,5 +6093,6 @@ export {
useAppState, // 9338748a
useColorScheme, // d585efdb
usePressability, // af291ad3
useVerticalBarEdgeIOS, // a0670270
useWindowDimensions, // bb4b683f
}
4 changes: 4 additions & 0 deletions packages/react-native/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ module.exports = {
get usePressability() {
return require('./Libraries/Pressability/usePressability').default;
},
get useVerticalBarEdgeIOS() {
return require('./Libraries/Components/StatusBar/useVerticalBarEdgeIOS')
.default;
},
get useWindowDimensions() {
return require('./Libraries/Utilities/useWindowDimensions').default;
},
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export type {
StatusBarInstance,
StatusBarProps,
StatusBarStyle,
StatusBarVerticalBarEdgeIOS,
} from './Libraries/Components/StatusBar/StatusBar';
export {default as StatusBar} from './Libraries/Components/StatusBar/StatusBar';

Expand Down Expand Up @@ -461,6 +462,7 @@ export type {
export {default as usePressability} from './Libraries/Pressability/usePressability';
export {default as useAppState} from './Libraries/AppState/useAppState';
export {default as useColorScheme} from './Libraries/Utilities/useColorScheme';
export {default as useVerticalBarEdgeIOS} from './Libraries/Components/StatusBar/useVerticalBarEdgeIOS';
export {default as useWindowDimensions} from './Libraries/Utilities/useWindowDimensions';
export {default as UTFSequence} from './Libraries/UTFSequence';
export {default as Vibration} from './Libraries/Vibration/Vibration';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export interface Spec extends TurboModule {
* - withAnimation can be: 'none' | 'fade' | 'slide'
*/
readonly setHidden: (hidden: boolean, withAnimation: string) => void;
/**
* - returns 'leading', 'trailing', or null when there is no vertical bar
*/
readonly getVerticalBarEdge?: () => ?string;
}

const NativeModule = TurboModuleRegistry.getEnforcing<Spec>('StatusBarManager');
Expand Down Expand Up @@ -79,6 +83,13 @@ const NativeStatusBarManager = {
setHidden(hidden: boolean, withAnimation: string): void {
NativeModule.setHidden(hidden, withAnimation);
},

/**
* - returns 'leading', 'trailing', or null when there is no vertical bar
*/
getVerticalBarEdge(): ?string {
return NativeModule.getVerticalBarEdge?.() ?? null;
},
};

export default NativeStatusBarManager;
Loading
Loading