Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(android): Edge-to-edge Modal (navigationBarTranslucent prop) #47254

Closed
Closed
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
5 changes: 5 additions & 0 deletions packages/react-native/Libraries/Modal/Modal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
Expand Down
17 changes: 17 additions & 0 deletions packages/react-native/Libraries/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -176,6 +184,14 @@ function confirmProps(props: Props) {
`Modal with '${props.presentationStyle}' presentation style and 'transparent' value is not supported.`,
);
}
if (
props.navigationBarTranslucent === true &&
props.statusBarTranslucent !== true
) {
console.warn(
'Modal with translucent navigation bar and without translucent status bar is not supported.',
);
}
}
}

Expand Down Expand Up @@ -301,6 +317,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6411,6 +6411,7 @@ export type Props = $ReadOnly<{|
),
transparent?: ?boolean,
statusBarTranslucent?: ?boolean,
navigationBarTranslucent?: ?boolean,
hardwareAccelerated?: ?boolean,
visible?: ?boolean,
onRequestClose?: ?DirectEventHandler<null>,
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/React/Views/RCTModalHostView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 22 additions & 1 deletion packages/rn-tester/js/examples/Modal/ModalPresentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function ModalPresentation() {
transparent: false,
hardwareAccelerated: false,
statusBarTranslucent: false,
navigationBarTranslucent: false,
presentationStyle: Platform.select({
ios: 'fullScreen',
default: undefined,
Expand All @@ -72,6 +73,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;

Expand All @@ -92,10 +94,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 🟢
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function ScrollViewIndicatorInsetsExample() {
onRequestClose={() => setModalVisible(false)}
presentationStyle="fullScreen"
statusBarTranslucent={false}
navigationBarTranslucent={false}
supportedOrientations={['portrait', 'landscape']}>
<View style={styles.modal}>
<ScrollView
Expand Down
Loading