diff --git a/packages/react-native/Libraries/Modal/Modal.d.ts b/packages/react-native/Libraries/Modal/Modal.d.ts index 9548d81de09624..21fca65479c1f1 100644 --- a/packages/react-native/Libraries/Modal/Modal.d.ts +++ b/packages/react-native/Libraries/Modal/Modal.d.ts @@ -101,6 +101,11 @@ export interface ModalPropsAndroid { * Determines whether your modal should go under the system statusbar. */ statusBarTranslucent?: boolean | undefined; + + /** + * Determines whether your modal should go under the system navigationbar. + */ + navigationBarTranslucent?: boolean | undefined; } export type ModalProps = ModalBaseProps & diff --git a/packages/react-native/Libraries/Modal/Modal.js b/packages/react-native/Libraries/Modal/Modal.js index cc0a0622ff6b36..2dd7885a2b17d5 100644 --- a/packages/react-native/Libraries/Modal/Modal.js +++ b/packages/react-native/Libraries/Modal/Modal.js @@ -95,6 +95,14 @@ export type Props = $ReadOnly<{| */ statusBarTranslucent?: ?boolean, + /** + * The `navigationBarTranslucent` prop determines whether your modal should go under + * the system navigationbar. + * + * See https://reactnative.dev/docs/modal.html#navigationbartranslucent-android + */ + navigationBarTranslucent?: ?boolean, + /** * The `hardwareAccelerated` prop controls whether to force hardware * acceleration for the underlying window. @@ -301,6 +309,7 @@ class Modal extends React.Component<Props, State> { onDismiss={onDismiss} visible={this.props.visible} statusBarTranslucent={this.props.statusBarTranslucent} + navigationBarTranslucent={this.props.navigationBarTranslucent} identifier={this._identifier} style={styles.modal} // $FlowFixMe[method-unbinding] added when improving typing for this parameters diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index b3239871204a72..15b0de88cc476f 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -6411,6 +6411,7 @@ export type Props = $ReadOnly<{| ), transparent?: ?boolean, statusBarTranslucent?: ?boolean, + navigationBarTranslucent?: ?boolean, hardwareAccelerated?: ?boolean, visible?: ?boolean, onRequestClose?: ?DirectEventHandler<null>, diff --git a/packages/react-native/React/Views/RCTModalHostView.h b/packages/react-native/React/Views/RCTModalHostView.h index 2fcdcaea83f5b2..0469c23ca08493 100644 --- a/packages/react-native/React/Views/RCTModalHostView.h +++ b/packages/react-native/React/Views/RCTModalHostView.h @@ -27,6 +27,7 @@ // Android only @property (nonatomic, assign) BOOL statusBarTranslucent; +@property (nonatomic, assign) BOOL navigationBarTranslucent; @property (nonatomic, assign) BOOL hardwareAccelerated; @property (nonatomic, assign) BOOL animated; diff --git a/packages/react-native/React/Views/RCTModalHostViewManager.m b/packages/react-native/React/Views/RCTModalHostViewManager.m index eccf05f9cb6795..2e96194ac632e9 100644 --- a/packages/react-native/React/Views/RCTModalHostViewManager.m +++ b/packages/react-native/React/Views/RCTModalHostViewManager.m @@ -111,6 +111,7 @@ - (void)invalidate RCT_EXPORT_VIEW_PROPERTY(presentationStyle, UIModalPresentationStyle) RCT_EXPORT_VIEW_PROPERTY(transparent, BOOL) RCT_EXPORT_VIEW_PROPERTY(statusBarTranslucent, BOOL) +RCT_EXPORT_VIEW_PROPERTY(navigationBarTranslucent, BOOL) RCT_EXPORT_VIEW_PROPERTY(hardwareAccelerated, BOOL) RCT_EXPORT_VIEW_PROPERTY(animated, BOOL) RCT_EXPORT_VIEW_PROPERTY(onShow, RCTDirectEventBlock) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.kt index f8cadda3ab2dae..88157aea50294c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.kt @@ -57,6 +57,14 @@ public class ReactModalHostManager : view.statusBarTranslucent = statusBarTranslucent } + @ReactProp(name = "navigationBarTranslucent") + public override fun setNavigationBarTranslucent( + view: ReactModalHostView, + navigationBarTranslucent: Boolean + ) { + view.navigationBarTranslucent = navigationBarTranslucent + } + @ReactProp(name = "hardwareAccelerated") public override fun setHardwareAccelerated( view: ReactModalHostView, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt index ed607aa289c959..add085cf6e9813 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt @@ -48,6 +48,7 @@ import com.facebook.react.uimanager.events.EventDispatcher import com.facebook.react.views.common.ContextUtils import com.facebook.react.views.view.ReactViewGroup import com.facebook.react.views.view.setStatusBarTranslucency +import com.facebook.react.views.view.setSystemBarsTranslucency import java.util.Objects /** @@ -79,6 +80,12 @@ public class ReactModalHostView(context: ThemedReactContext) : createNewDialog = true } + public var navigationBarTranslucent: Boolean = false + set(value) { + field = value + createNewDialog = true + } + public var animationType: String? = null set(value) { field = value @@ -328,7 +335,12 @@ public class ReactModalHostView(context: ThemedReactContext) : } } - dialogWindow.setStatusBarTranslucency(statusBarTranslucent) + // Navigation bar cannot be translucent without status bar being translucent too + dialogWindow.setSystemBarsTranslucency(navigationBarTranslucent) + + if (!navigationBarTranslucent) { + dialogWindow.setStatusBarTranslucency(statusBarTranslucent) + } if (transparent) { dialogWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt index 4795564901c049..ff83490a83ecf9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt @@ -7,10 +7,14 @@ package com.facebook.react.views.view +import android.content.res.Configuration +import android.graphics.Color import android.os.Build import android.view.Window import android.view.WindowManager import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsControllerCompat @Suppress("DEPRECATION") public fun Window.setStatusBarTranslucency(isTranslucent: Boolean) { @@ -61,3 +65,39 @@ private fun Window.statusBarShow() { addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) } + +@Suppress("DEPRECATION") +public fun Window.setSystemBarsTranslucency(isTranslucent: Boolean) { + WindowCompat.setDecorFitsSystemWindows(this, !isTranslucent) + + if (isTranslucent) { + val isDarkMode = + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + isStatusBarContrastEnforced = false + isNavigationBarContrastEnforced = true + } + + statusBarColor = Color.TRANSPARENT + navigationBarColor = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Color.TRANSPARENT + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && !isDarkMode -> + Color.argb(0xe6, 0xFF, 0xFF, 0xFF) + else -> Color.argb(0x80, 0x1b, 0x1b, 0x1b) + } + + WindowInsetsControllerCompat(this, this.decorView).run { + isAppearanceLightNavigationBars = !isDarkMode + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + attributes.layoutInDisplayCutoutMode = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + else -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } + } + } +} diff --git a/packages/react-native/src/private/specs/components/RCTModalHostViewNativeComponent.js b/packages/react-native/src/private/specs/components/RCTModalHostViewNativeComponent.js index 86bf895d767526..58ec2940079bc8 100644 --- a/packages/react-native/src/private/specs/components/RCTModalHostViewNativeComponent.js +++ b/packages/react-native/src/private/specs/components/RCTModalHostViewNativeComponent.js @@ -58,6 +58,14 @@ type NativeProps = $ReadOnly<{| */ statusBarTranslucent?: WithDefault<boolean, false>, + /** + * The `navigationBarTranslucent` prop determines whether your modal should go under + * the system navigationbar. + * + * See https://reactnative.dev/docs/modal#navigationBarTranslucent + */ + navigationBarTranslucent?: WithDefault<boolean, false>, + /** * The `hardwareAccelerated` prop controls whether to force hardware * acceleration for the underlying window. diff --git a/packages/rn-tester/js/examples/Modal/ModalPresentation.js b/packages/rn-tester/js/examples/Modal/ModalPresentation.js index ee440e05fcc491..bd5a3f340c1064 100644 --- a/packages/rn-tester/js/examples/Modal/ModalPresentation.js +++ b/packages/rn-tester/js/examples/Modal/ModalPresentation.js @@ -19,7 +19,15 @@ import {RNTesterThemeContext} from '../../components/RNTesterTheme'; import RNTOption from '../../components/RNTOption'; import * as React from 'react'; import {useCallback, useContext, useState} from 'react'; -import {Modal, Platform, StyleSheet, Switch, Text, View} from 'react-native'; +import { + Modal, + Platform, + StatusBar, + StyleSheet, + Switch, + Text, + View, +} from 'react-native'; const animationTypes = ['slide', 'none', 'fade']; const presentationStyles = [ @@ -56,6 +64,7 @@ function ModalPresentation() { transparent: false, hardwareAccelerated: false, statusBarTranslucent: false, + navigationBarTranslucent: false, presentationStyle: Platform.select({ ios: 'fullScreen', default: undefined, @@ -72,6 +81,7 @@ function ModalPresentation() { const presentationStyle = props.presentationStyle; const hardwareAccelerated = props.hardwareAccelerated; const statusBarTranslucent = props.statusBarTranslucent; + const navigationBarTranslucent = props.navigationBarTranslucent; const backdropColor = props.backdropColor; const backgroundColor = useContext(RNTesterThemeContext).BackgroundColor; @@ -92,10 +102,29 @@ function ModalPresentation() { <Switch value={statusBarTranslucent} onValueChange={enabled => - setProps(prev => ({...prev, statusBarTranslucent: enabled})) + setProps(prev => ({ + ...prev, + statusBarTranslucent: enabled, + navigationBarTranslucent: false, + })) } /> </View> + <View style={styles.inlineBlock}> + <RNTesterText style={styles.title}> + Navigation Bar Translucent 🟢 + </RNTesterText> + <Switch + value={navigationBarTranslucent} + onValueChange={enabled => { + setProps(prev => ({ + ...prev, + statusBarTranslucent: enabled, + navigationBarTranslucent: enabled, + })); + }} + /> + </View> <View style={styles.inlineBlock}> <RNTesterText style={styles.title}> Hardware Acceleration 🟢 diff --git a/packages/rn-tester/js/examples/ScrollView/ScrollViewIndicatorInsetsIOSExample.js b/packages/rn-tester/js/examples/ScrollView/ScrollViewIndicatorInsetsIOSExample.js index d820dfe904c554..4e3bbb1302af65 100644 --- a/packages/rn-tester/js/examples/ScrollView/ScrollViewIndicatorInsetsIOSExample.js +++ b/packages/rn-tester/js/examples/ScrollView/ScrollViewIndicatorInsetsIOSExample.js @@ -32,6 +32,7 @@ export function ScrollViewIndicatorInsetsExample() { onRequestClose={() => setModalVisible(false)} presentationStyle="fullScreen" statusBarTranslucent={false} + navigationBarTranslucent={false} supportedOrientations={['portrait', 'landscape']}> <View style={styles.modal}> <ScrollView