Skip to content

Commit 3681722

Browse files
authored
Add hideTitleBar and hideBorder to Modal (#16108)
* Add hideTitleBar and hideBorder to Modal * Change files * format * lint fix (type fixes) * snapshot
1 parent f4eb730 commit 3681722

7 files changed

Lines changed: 230 additions & 16 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Add hideTitleBar and hideBorder to Modal",
4+
"packageName": "react-native-windows",
5+
"email": "30809111+acoates-ms@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {RNTesterThemeContext} from '../../components/RNTesterTheme';
1919
import RNTOption from '../../components/RNTOption';
2020
import * as React from 'react';
2121
import {useCallback, useContext, useState} from 'react';
22-
import {Modal, Platform, StyleSheet, Switch, Text, View} from 'react-native';
22+
import {Modal, Platform, StyleSheet, Switch, Text, TextInput, View} from 'react-native';
2323

2424
const animationTypes = ['slide', 'none', 'fade'] as const;
2525
const presentationStyles = [
@@ -70,6 +70,9 @@ function ModalPresentation() {
7070
ios: ['portrait'],
7171
}),
7272
transparent: false,
73+
hideBorder: false, // Windows
74+
hideTitleBar: false, // Windows
75+
title: 'Modal Presentation', // Windows
7376
visible: false,
7477
});
7578
const presentationStyle = props.presentationStyle;
@@ -190,6 +193,37 @@ function ModalPresentation() {
190193
}
191194
/>
192195
</View>
196+
{/* [Windows] - HideTitleBar is a Windows only prop. It is not supported on iOS or Android. */}
197+
<View style={styles.rowWithSpaceBetween}>
198+
<RNTesterText style={styles.title}>HideTitleBar</RNTesterText>
199+
<Switch
200+
value={props.hideTitleBar}
201+
onValueChange={enabled =>
202+
setProps(prev => ({...prev, hideTitleBar: enabled}))
203+
}
204+
/>
205+
</View>
206+
{/* [Windows] - HideBorder is a Windows only prop. It is not supported on iOS or Android. */}
207+
<View style={styles.rowWithSpaceBetween}>
208+
<RNTesterText style={styles.title}>HideBorder</RNTesterText>
209+
<Switch
210+
value={props.hideBorder}
211+
onValueChange={enabled =>
212+
setProps(prev => ({...prev, hideBorder: enabled}))
213+
}
214+
/>
215+
</View>
216+
{/* [Windows] - Title is a Windows only prop. It is not supported on iOS or Android. */}
217+
<View style={{flexDirection: 'row'}}>
218+
<RNTesterText style={styles.title}>Title</RNTesterText>
219+
<TextInput
220+
style={{flex: 1, borderWidth: 1, marginLeft: 10, padding: 5}}
221+
value={props.title}
222+
onChangeText={text =>
223+
setProps(prev => ({...prev, title: text}))
224+
}
225+
/>
226+
</View>
193227
{Platform.OS === 'ios' && presentationStyle !== 'overFullScreen' ? (
194228
<RNTesterText style={styles.warning}>
195229
iOS Modal can only be transparent with 'overFullScreen' Presentation

packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31656,6 +31656,119 @@ exports[`snapshotAllPages Modal 1`] = `
3165631656
value={false}
3165731657
/>
3165831658
</View>
31659+
<View
31660+
style={
31661+
{
31662+
"flexDirection": "row",
31663+
"justifyContent": "space-between",
31664+
}
31665+
}
31666+
>
31667+
<Text
31668+
style={
31669+
[
31670+
{
31671+
"color": "#000000ff",
31672+
},
31673+
{
31674+
"fontWeight": "bold",
31675+
"margin": 3,
31676+
},
31677+
]
31678+
}
31679+
>
31680+
HideTitleBar
31681+
</Text>
31682+
<RCTSwitch
31683+
accessibilityRole="switch"
31684+
accessible={true}
31685+
focusable={true}
31686+
onChange={[Function]}
31687+
onResponderTerminationRequest={[Function]}
31688+
onStartShouldSetResponder={[Function]}
31689+
style={
31690+
{
31691+
"height": 31,
31692+
"width": 51,
31693+
}
31694+
}
31695+
value={false}
31696+
/>
31697+
</View>
31698+
<View
31699+
style={
31700+
{
31701+
"flexDirection": "row",
31702+
"justifyContent": "space-between",
31703+
}
31704+
}
31705+
>
31706+
<Text
31707+
style={
31708+
[
31709+
{
31710+
"color": "#000000ff",
31711+
},
31712+
{
31713+
"fontWeight": "bold",
31714+
"margin": 3,
31715+
},
31716+
]
31717+
}
31718+
>
31719+
HideBorder
31720+
</Text>
31721+
<RCTSwitch
31722+
accessibilityRole="switch"
31723+
accessible={true}
31724+
focusable={true}
31725+
onChange={[Function]}
31726+
onResponderTerminationRequest={[Function]}
31727+
onStartShouldSetResponder={[Function]}
31728+
style={
31729+
{
31730+
"height": 31,
31731+
"width": 51,
31732+
}
31733+
}
31734+
value={false}
31735+
/>
31736+
</View>
31737+
<View
31738+
style={
31739+
{
31740+
"flexDirection": "row",
31741+
}
31742+
}
31743+
>
31744+
<Text
31745+
style={
31746+
[
31747+
{
31748+
"color": "#000000ff",
31749+
},
31750+
{
31751+
"fontWeight": "bold",
31752+
"margin": 3,
31753+
},
31754+
]
31755+
}
31756+
>
31757+
Title
31758+
</Text>
31759+
<TextInput
31760+
onChangeText={[Function]}
31761+
style={
31762+
{
31763+
"borderWidth": 1,
31764+
"flex": 1,
31765+
"marginLeft": 10,
31766+
"padding": 5,
31767+
}
31768+
}
31769+
value="Modal Presentation"
31770+
/>
31771+
</View>
3165931772
</View>
3166031773
<View
3166131774
style={

vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "../../../codegen/react/components/rnwcore/ModalHostView.g.h"
99
#include <ComponentView.Experimental.interop.h>
10+
#include <dwmapi.h>
1011
#include <winrt/Microsoft.UI.Content.h>
1112
#include <winrt/Microsoft.UI.Input.h>
1213
#include <winrt/Microsoft.UI.Windowing.h>
@@ -112,6 +113,12 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
112113
m_rnWindow.AppWindow().Title(titleValue);
113114
}
114115

116+
if (m_rnWindow &&
117+
(newViewProps.hideTitleBar != oldViewProps.hideTitleBar ||
118+
newViewProps.hideBorder != oldViewProps.hideBorder)) {
119+
UpdateTitleBarAndBorder();
120+
}
121+
115122
::Microsoft::ReactNativeSpecs::BaseModalHostView<ModalHostView>::UpdateProps(view, newProps, oldProps);
116123
}
117124

@@ -258,6 +265,61 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
258265
}
259266
}
260267

268+
void UpdateTitleBarAndBorder() noexcept {
269+
if (!m_rnWindow) {
270+
return;
271+
}
272+
273+
auto overlappedPresenter = winrt::Microsoft::UI::Windowing::OverlappedPresenter::Create();
274+
275+
// Configure presenter for modal behavior
276+
overlappedPresenter.IsModal(true);
277+
278+
// modal should only have close button
279+
overlappedPresenter.IsMinimizable(false);
280+
overlappedPresenter.IsMaximizable(false);
281+
282+
// Apply the presenter to the window
283+
m_rnWindow.AppWindow().SetPresenter(overlappedPresenter);
284+
285+
// We only hide the borders if the titlebar is also hidden.
286+
const bool hideBorders = m_localProps->hideTitleBar.value_or(false) && m_localProps->hideBorder.value_or(false);
287+
288+
auto titleBar = m_rnWindow.AppWindow().TitleBar();
289+
titleBar.ResetToDefault();
290+
overlappedPresenter.IsResizable(false);
291+
292+
overlappedPresenter.SetBorderAndTitleBar(!hideBorders, !m_localProps->hideTitleBar.value_or(false));
293+
294+
auto hwnd = GetWindowFromWindowId(m_rnWindow.AppWindow().Id());
295+
296+
if (hideBorders) {
297+
const DWMNCRENDERINGPOLICY ncPolicy = DWMNCRP_DISABLED;
298+
::DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &ncPolicy, sizeof(ncPolicy));
299+
300+
const DWM_WINDOW_CORNER_PREFERENCE cornerPref = DWMWCP_DEFAULT;
301+
::DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPref, sizeof(cornerPref));
302+
303+
const COLORREF zeroColor = 0;
304+
::DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, &zeroColor, sizeof(zeroColor));
305+
::DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, &zeroColor, sizeof(zeroColor));
306+
::DwmSetWindowAttribute(hwnd, DWMWA_TEXT_COLOR, &zeroColor, sizeof(zeroColor));
307+
} else {
308+
const DWMNCRENDERINGPOLICY ncPolicy = DWMNCRP_USEWINDOWSTYLE;
309+
::DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &ncPolicy, sizeof(ncPolicy));
310+
311+
const DWM_WINDOW_CORNER_PREFERENCE cornerPref = DWMWCP_DEFAULT;
312+
::DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPref, sizeof(cornerPref));
313+
314+
const COLORREF zeroColor = DWMWA_COLOR_DEFAULT;
315+
::DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, &zeroColor, sizeof(zeroColor));
316+
::DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, &zeroColor, sizeof(zeroColor));
317+
::DwmSetWindowAttribute(hwnd, DWMWA_TEXT_COLOR, &zeroColor, sizeof(zeroColor));
318+
319+
titleBar.IconShowOptions(winrt::Microsoft::UI::Windowing::IconShowOptions::HideIconAndSystemMenu);
320+
}
321+
}
322+
261323
// creates a new modal window
262324
void EnsureModalCreated(const winrt::Microsoft::ReactNative::ComponentView &view) {
263325
if (m_popUp) {
@@ -282,22 +344,8 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
282344
m_rnWindow = winrt::Microsoft::ReactNative::ReactNativeWindow::CreateFromContentSiteBridgeAndIsland(
283345
m_popUp, winrt::Microsoft::ReactNative::ReactNativeIsland::CreatePortal(portal));
284346
m_rnWindow.ResizePolicy(winrt::Microsoft::ReactNative::ContentSizePolicy::None);
285-
auto overlappedPresenter = winrt::Microsoft::UI::Windowing::OverlappedPresenter::Create();
286-
287-
// Configure presenter for modal behavior
288-
overlappedPresenter.IsModal(true);
289-
overlappedPresenter.SetBorderAndTitleBar(true, true);
290-
291-
// modal should only have close button
292-
overlappedPresenter.IsMinimizable(false);
293-
overlappedPresenter.IsMaximizable(false);
294-
295-
// Apply the presenter to the window
296-
m_rnWindow.AppWindow().SetPresenter(overlappedPresenter);
297347

298-
// Hide the title bar icon
299-
m_rnWindow.AppWindow().TitleBar().IconShowOptions(
300-
winrt::Microsoft::UI::Windowing::IconShowOptions::HideIconAndSystemMenu);
348+
UpdateTitleBarAndBorder();
301349

302350
// Set initial title using the stored local props
303351
if (m_localProps && m_localProps->title.has_value()) {

vnext/src-win/Libraries/Modal/Modal.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ export interface ModalWindowsProps {
117117
/* title for the modal, shown in the title bar */
118118
// [Windows
119119
title?: string | undefined;
120+
121+
hideTitleBar?: boolean | undefined;
122+
123+
hideBorder?: boolean | undefined;
120124
// Windows]
121125
}
122126

vnext/src-win/Libraries/Modal/Modal.windows.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ export type ModalPropsWindows = {
174174
* [Windows] The `title` prop sets the title of the modal window.
175175
*/
176176
title?: ?string,
177+
hideTitleBar?: ?boolean,
178+
hideBorder?: ?boolean,
177179
};
178180

179181
export type ModalProps = {
@@ -352,6 +354,8 @@ class Modal extends React.Component<ModalProps, ModalState> {
352354
onOrientationChange={this.props.onOrientationChange}
353355
allowSwipeDismissal={this.props.allowSwipeDismissal}
354356
testID={this.props.testID}
357+
hideTitleBar={this.props.hideTitleBar} // [Windows]
358+
hideBorder={this.props.hideBorder} // [Windows]
355359
title={this.props.title}>
356360
<VirtualizedListContextResetter>
357361
<ScrollView.Context.Provider value={null}>

vnext/src-win/src/private/specs_DEPRECATED/components/RCTModalHostViewNativeComponent.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ type RCTModalHostViewNativeProps = Readonly<{|
152152
*/
153153
// [Windows
154154
title?: WithDefault<string, null>,
155+
156+
hideTitleBar?: WithDefault<boolean, false>,
157+
158+
hideBorder?: WithDefault<boolean, false>,
155159
// Windows]
156160
|}>;
157161

0 commit comments

Comments
 (0)