Skip to content

Commit ec1b2ef

Browse files
iamAbhi-916Abhijeet Jha
and
Abhijeet Jha
authored
Modal : Added Titlebar , title , X handling and movable for windows (#14636)
* addded titlebar , title handling , X handling , resizable * Change files * lint and format fix * removed debug logs and resizable property * removed unused imports from modalHostView .cpp file * lint and format fix * Update Modal.windows.js syntax after resolve conflict * updated overrides * updated rctmodalhostview file override structure --------- Co-authored-by: Abhijeet Jha <[email protected]>
1 parent cc126e4 commit ec1b2ef

File tree

9 files changed

+400
-17
lines changed

9 files changed

+400
-17
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "addded titlebar , title handling , X handling , resizable",
4+
"packageName": "react-native-windows",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
3939
m_reactNativeIsland.Island().Close();
4040
}
4141

42+
// Add AppWindow closing token cleanup
43+
if (m_appWindow && m_appWindowClosingToken) {
44+
m_appWindow.Closing(m_appWindowClosingToken);
45+
m_appWindowClosingToken.value = 0;
46+
}
47+
4248
if (m_popUp) {
4349
if (m_departFocusToken && !m_popUp.IsClosed()) {
4450
// WASDK BUG: InputFocusNavigationHost::GetForSiteBridge fails on a DesktopPopupSiteBridge
@@ -68,7 +74,13 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
6874
const winrt::Microsoft::ReactNative::ComponentView &view,
6975
const winrt::com_ptr<::Microsoft::ReactNativeSpecs::ModalHostViewProps> &newProps,
7076
const winrt::com_ptr<::Microsoft::ReactNativeSpecs::ModalHostViewProps> &oldProps) noexcept override {
71-
if (!oldProps || newProps->visible != oldProps->visible) {
77+
// Store the props locally
78+
m_localProps = newProps;
79+
80+
const auto &oldViewProps = *oldProps;
81+
const auto &newViewProps = *newProps;
82+
83+
if (!oldProps || newViewProps.visible != oldViewProps.visible) {
7284
if (newProps->visible.value_or(true)) {
7385
m_visible = true;
7486
// We do not immediately show the window, since we want to resize/position
@@ -79,6 +91,15 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
7991
CloseWindow();
8092
}
8193
}
94+
95+
// Update Title if changed and AppWindow exists
96+
if (m_appWindow && (!oldProps || newViewProps.title != oldViewProps.title)) {
97+
// Use empty string if title is not set
98+
winrt::hstring titleValue =
99+
newViewProps.title.has_value() ? winrt::to_hstring(newViewProps.title.value()) : winrt::hstring();
100+
m_appWindow.Title(titleValue);
101+
}
102+
82103
::Microsoft::ReactNativeSpecs::BaseModalHostView<ModalHostView>::UpdateProps(view, newProps, oldProps);
83104
}
84105

@@ -142,28 +163,34 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
142163
}
143164

144165
void AdjustWindowSize(const winrt::Microsoft::ReactNative::LayoutMetrics &layoutMetrics) noexcept {
145-
if (!m_popUp) {
166+
if (!m_appWindow) {
146167
return;
147168
}
148169

149170
if (layoutMetrics.Frame.Width == 0 && layoutMetrics.Frame.Height == 0) {
150171
return;
151172
}
152173

153-
// get Modal's position based on parent
174+
// Calculate physical pixels from DIPs
175+
int32_t clientWidthPx = static_cast<int32_t>(layoutMetrics.Frame.Width * layoutMetrics.PointScaleFactor);
176+
int32_t clientHeightPx = static_cast<int32_t>(layoutMetrics.Frame.Height * layoutMetrics.PointScaleFactor);
177+
178+
// Ensure minimum size for the window
179+
clientWidthPx = std::max(100, clientWidthPx);
180+
clientHeightPx = std::max(100, clientHeightPx);
181+
182+
// Size the client area directly
183+
m_appWindow.ResizeClient({clientWidthPx, clientHeightPx});
184+
185+
// Center the window on its parent
154186
RECT parentRC;
155187
GetWindowRect(m_parentHwnd, &parentRC);
156-
int32_t xCor = static_cast<int32_t>(
157-
(parentRC.left + parentRC.right - layoutMetrics.Frame.Width * layoutMetrics.PointScaleFactor) / 2);
158-
int32_t yCor = static_cast<int32_t>(
159-
(parentRC.top + parentRC.bottom - layoutMetrics.Frame.Height * layoutMetrics.PointScaleFactor) / 2);
160-
161-
winrt::Windows::Graphics::RectInt32 rect2{
162-
(int)xCor,
163-
(int)yCor,
164-
static_cast<int32_t>(layoutMetrics.Frame.Width * (layoutMetrics.PointScaleFactor)),
165-
static_cast<int32_t>(layoutMetrics.Frame.Height * (layoutMetrics.PointScaleFactor))};
166-
m_popUp.MoveAndResize(rect2);
188+
auto outerSize = m_appWindow.Size();
189+
190+
int32_t xCor = parentRC.left + (parentRC.right - parentRC.left - outerSize.Width) / 2;
191+
int32_t yCor = parentRC.top + (parentRC.bottom - parentRC.top - outerSize.Height) / 2;
192+
193+
m_appWindow.Move({xCor, yCor});
167194
};
168195

169196
void ShowOnUIThread(const winrt::Microsoft::ReactNative::ComponentView &view) {
@@ -199,6 +226,12 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
199226
m_popUp.Hide();
200227
}
201228

229+
// Unregister closing event handler
230+
if (m_appWindow && m_appWindowClosingToken) {
231+
m_appWindow.Closing(m_appWindowClosingToken);
232+
m_appWindowClosingToken.value = 0;
233+
}
234+
202235
// dispatch onDismiss event
203236
if (auto eventEmitter = EventEmitter()) {
204237
::Microsoft::ReactNativeSpecs::ModalHostViewEventEmitter::OnDismiss eventArgs;
@@ -237,6 +270,42 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
237270
.Island());
238271
m_popUp.Connect(contentIsland);
239272

273+
// Get AppWindow and configure presenter
274+
m_appWindow = winrt::Microsoft::UI::Windowing::AppWindow::GetFromWindowId(m_popUp.WindowId());
275+
if (m_appWindow) {
276+
auto overlappedPresenter = winrt::Microsoft::UI::Windowing::OverlappedPresenter::Create();
277+
278+
// Configure presenter for modal behavior
279+
overlappedPresenter.IsModal(true);
280+
overlappedPresenter.SetBorderAndTitleBar(true, true);
281+
282+
// Apply the presenter to the window
283+
m_appWindow.SetPresenter(overlappedPresenter);
284+
285+
// Set initial title using the stored local props
286+
if (m_localProps && m_localProps->title.has_value()) {
287+
winrt::hstring titleValue = winrt::to_hstring(m_localProps->title.value());
288+
m_appWindow.Title(titleValue);
289+
} else {
290+
m_appWindow.Title(L""); // Empty title if not provided
291+
}
292+
293+
// Handle close request ('X' button)
294+
m_appWindowClosingToken =
295+
m_appWindow.Closing([wkThis = get_weak()](
296+
const winrt::Microsoft::UI::Windowing::AppWindow & /*sender*/,
297+
const winrt::Microsoft::UI::Windowing::AppWindowClosingEventArgs &args) {
298+
args.Cancel(true); // Prevent default close
299+
if (auto strongThis = wkThis.get()) {
300+
// Dispatch onRequestClose event
301+
if (auto eventEmitter = strongThis->EventEmitter()) {
302+
::Microsoft::ReactNativeSpecs::ModalHostViewEventEmitter::OnRequestClose eventArgs;
303+
eventEmitter->onRequestClose(eventArgs);
304+
}
305+
}
306+
});
307+
}
308+
240309
// set the top-level windows as the new hwnd
241310
winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId(
242311
view.ReactContext().Properties(),
@@ -320,6 +389,9 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
320389
winrt::Microsoft::ReactNative::IComponentState m_state{nullptr};
321390
winrt::Microsoft::ReactNative::ReactNativeIsland m_reactNativeIsland{nullptr};
322391
winrt::Microsoft::UI::Content::DesktopPopupSiteBridge m_popUp{nullptr};
392+
winrt::Microsoft::UI::Windowing::AppWindow m_appWindow{nullptr};
393+
winrt::event_token m_appWindowClosingToken;
394+
winrt::com_ptr<::Microsoft::ReactNativeSpecs::ModalHostViewProps> m_localProps{nullptr};
323395
};
324396

325397
void RegisterWindowsModalHostNativeComponent(

vnext/codegen/react/components/rnwcore/ModalHostView.g.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ struct ModalHostViewProps : winrt::implements<ModalHostViewProps, winrt::Microso
3434
visible = cloneFromProps->visible;
3535
animated = cloneFromProps->animated;
3636
supportedOrientations = cloneFromProps->supportedOrientations;
37-
identifier = cloneFromProps->identifier;
37+
identifier = cloneFromProps->identifier;
38+
title = cloneFromProps->title;
3839
}
3940
}
4041

@@ -72,6 +73,9 @@ struct ModalHostViewProps : winrt::implements<ModalHostViewProps, winrt::Microso
7273
REACT_FIELD(identifier)
7374
std::optional<int32_t> identifier{};
7475

76+
REACT_FIELD(title)
77+
std::optional<std::string> title;
78+
7579
const winrt::Microsoft::ReactNative::ViewProps ViewProps;
7680
};
7781

vnext/codegen/react/components/rnwcore/Props.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ ModalHostViewProps::ModalHostViewProps(
122122
visible(convertRawProp(context, rawProps, "visible", sourceProps.visible, {false})),
123123
animated(convertRawProp(context, rawProps, "animated", sourceProps.animated, {false})),
124124
supportedOrientations(convertRawProp(context, rawProps, "supportedOrientations", ModalHostViewSupportedOrientationsMaskWrapped{ .value = sourceProps.supportedOrientations }, {static_cast<ModalHostViewSupportedOrientationsMask>(ModalHostViewSupportedOrientations::Portrait)}).value),
125-
identifier(convertRawProp(context, rawProps, "identifier", sourceProps.identifier, {0}))
125+
identifier(convertRawProp(context, rawProps, "identifier", sourceProps.identifier, {0})),
126+
title(convertRawProp(context, rawProps, "title", sourceProps.title, {}))
126127
{}
127128
SafeAreaViewProps::SafeAreaViewProps(
128129
const PropsParserContext &context,

vnext/codegen/react/components/rnwcore/Props.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ class ModalHostViewProps final : public ViewProps {
355355
bool animated{false};
356356
ModalHostViewSupportedOrientationsMask supportedOrientations{static_cast<ModalHostViewSupportedOrientationsMask>(ModalHostViewSupportedOrientations::Portrait)};
357357
int identifier{0};
358+
std::string title{};
358359
};
359360

360361
class SafeAreaViewProps final : public ViewProps {

vnext/overrides.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,13 @@
486486
"baseFile": "packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js",
487487
"baseHash": "663d3325298404d7c012a6aa53e833eb5fc2ec76"
488488
},
489+
{
490+
"type": "patch",
491+
"file": "src-win/Libraries/Modal/Modal.d.ts",
492+
"baseFile": "packages/react-native/Libraries/Modal/Modal.d.ts",
493+
"baseHash": "aeebd34b8cccade2637e310a63a1e9a41f149f64",
494+
"issue": 0
495+
},
489496
{
490497
"type": "derived",
491498
"file": "src-win/Libraries/Modal/Modal.windows.js",
@@ -605,6 +612,13 @@
605612
"baseFile": "packages/react-native/src/private/debugging/ReactDevToolsSettingsManager.android.js",
606613
"baseHash": "df41b76dc3d2df9455fae588748261d7b0a22d01"
607614
},
615+
{
616+
"type": "patch",
617+
"file": "src-win/src/private/specs/components/RCTModalHostViewNativeComponent.js",
618+
"baseFile": "packages/react-native/src/private/specs/components/RCTModalHostViewNativeComponent.js",
619+
"baseHash": "dbda84f2de3e0aa3504e38bd4bbb687b1ea671b2",
620+
"issue": 0
621+
},
608622
{
609623
"type": "platform",
610624
"file": "src-win/src/private/specs/modules/NativeAppTheme.js"
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
import type * as React from 'react';
11+
import {ViewProps} from '../Components/View/ViewPropTypes';
12+
import {NativeSyntheticEvent} from '../Types/CoreEventTypes';
13+
import {ColorValue} from '../StyleSheet/StyleSheet';
14+
15+
export interface ModalBaseProps {
16+
/**
17+
* @deprecated Use animationType instead
18+
*/
19+
animated?: boolean | undefined;
20+
/**
21+
* The `animationType` prop controls how the modal animates.
22+
*
23+
* - `slide` slides in from the bottom
24+
* - `fade` fades into view
25+
* - `none` appears without an animation
26+
*/
27+
animationType?: 'none' | 'slide' | 'fade' | undefined;
28+
/**
29+
* The `transparent` prop determines whether your modal will fill the entire view.
30+
* Setting this to `true` will render the modal over a transparent background.
31+
*/
32+
transparent?: boolean | undefined;
33+
/**
34+
* The `visible` prop determines whether your modal is visible.
35+
*/
36+
visible?: boolean | undefined;
37+
/**
38+
* The `onRequestClose` callback is called when the user taps the hardware back button on Android or the menu button on Apple TV.
39+
*
40+
* This is required on Apple TV and Android.
41+
*/
42+
onRequestClose?: ((event: NativeSyntheticEvent<any>) => void) | undefined;
43+
/**
44+
* The `onShow` prop allows passing a function that will be called once the modal has been shown.
45+
*/
46+
onShow?: ((event: NativeSyntheticEvent<any>) => void) | undefined;
47+
48+
/**
49+
* The `backdropColor` props sets the background color of the modal's container.
50+
* Defaults to `white` if not provided and transparent is `false`. Ignored if `transparent` is `true`.
51+
*/
52+
backdropColor?: ColorValue | undefined;
53+
}
54+
55+
export interface ModalPropsIOS {
56+
/**
57+
* The `presentationStyle` determines the style of modal to show
58+
*/
59+
presentationStyle?:
60+
| 'fullScreen'
61+
| 'pageSheet'
62+
| 'formSheet'
63+
| 'overFullScreen'
64+
| undefined;
65+
66+
/**
67+
* The `supportedOrientations` prop allows the modal to be rotated to any of the specified orientations.
68+
* On iOS, the modal is still restricted by what's specified in your app's Info.plist's UISupportedInterfaceOrientations field.
69+
*/
70+
supportedOrientations?:
71+
| Array<
72+
| 'portrait'
73+
| 'portrait-upside-down'
74+
| 'landscape'
75+
| 'landscape-left'
76+
| 'landscape-right'
77+
>
78+
| undefined;
79+
80+
/**
81+
* The `onDismiss` prop allows passing a function that will be called once the modal has been dismissed.
82+
*/
83+
onDismiss?: (() => void) | undefined;
84+
85+
/**
86+
* The `onOrientationChange` callback is called when the orientation changes while the modal is being displayed.
87+
* The orientation provided is only 'portrait' or 'landscape'. This callback is also called on initial render, regardless of the current orientation.
88+
*/
89+
onOrientationChange?:
90+
| ((event: NativeSyntheticEvent<any>) => void)
91+
| undefined;
92+
}
93+
94+
export interface ModalPropsAndroid {
95+
/**
96+
* Controls whether to force hardware acceleration for the underlying window.
97+
*/
98+
hardwareAccelerated?: boolean | undefined;
99+
100+
/**
101+
* Determines whether your modal should go under the system statusbar.
102+
*/
103+
statusBarTranslucent?: boolean | undefined;
104+
105+
/**
106+
* Determines whether your modal should go under the system navigationbar.
107+
*/
108+
navigationBarTranslucent?: boolean | undefined;
109+
}
110+
export interface ModalWindowsProps {
111+
/* title for the modal, shown in the title bar */
112+
// [Windows
113+
title?: string | undefined;
114+
// Windows]
115+
}
116+
117+
export type ModalProps = ModalBaseProps &
118+
ModalPropsIOS &
119+
ModalPropsAndroid &
120+
ModalWindowsProps &
121+
ViewProps;
122+
123+
export class Modal extends React.Component<ModalProps> {}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ export type Props = $ReadOnly<{
174174
* Defaults to `white` if not provided and transparent is `false`. Ignored if `transparent` is `true`.
175175
*/
176176
backdropColor?: ?string,
177+
178+
/**
179+
* [Windows] The `title` prop sets the title of the modal window.
180+
*/
181+
title?: ?string,
177182
}>;
178183

179184
function confirmProps(props: Props) {
@@ -329,7 +334,8 @@ class Modal extends React.Component<Props, State> {
329334
onStartShouldSetResponder={this._shouldSetResponder}
330335
supportedOrientations={this.props.supportedOrientations}
331336
onOrientationChange={this.props.onOrientationChange}
332-
testID={this.props.testID}>
337+
testID={this.props.testID}
338+
title={this.props.title}>
333339
<VirtualizedListContextResetter>
334340
<ScrollView.Context.Provider value={null}>
335341
<View

0 commit comments

Comments
 (0)