Skip to content

Modal : Added Titlebar , title , X handling and movable for windows #14636

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

Merged
merged 13 commits into from
May 12, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "addded titlebar , title handling , X handling , resizable",
"packageName": "react-native-windows",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
m_reactNativeIsland.Island().Close();
}

// Add AppWindow closing token cleanup
if (m_appWindow && m_appWindowClosingToken) {
m_appWindow.Closing(m_appWindowClosingToken);
m_appWindowClosingToken.value = 0;
}

if (m_popUp) {
if (m_departFocusToken && !m_popUp.IsClosed()) {
// WASDK BUG: InputFocusNavigationHost::GetForSiteBridge fails on a DesktopPopupSiteBridge
Expand Down Expand Up @@ -68,7 +74,13 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
const winrt::Microsoft::ReactNative::ComponentView &view,
const winrt::com_ptr<::Microsoft::ReactNativeSpecs::ModalHostViewProps> &newProps,
const winrt::com_ptr<::Microsoft::ReactNativeSpecs::ModalHostViewProps> &oldProps) noexcept override {
if (!oldProps || newProps->visible != oldProps->visible) {
// Store the props locally
m_localProps = newProps;

const auto &oldViewProps = *oldProps;
const auto &newViewProps = *newProps;

if (!oldProps || newViewProps.visible != oldViewProps.visible) {
if (newProps->visible.value_or(true)) {
m_visible = true;
// We do not immediately show the window, since we want to resize/position
Expand All @@ -79,6 +91,15 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
CloseWindow();
}
}

// Update Title if changed and AppWindow exists
if (m_appWindow && (!oldProps || newViewProps.title != oldViewProps.title)) {
// Use empty string if title is not set
winrt::hstring titleValue =
newViewProps.title.has_value() ? winrt::to_hstring(newViewProps.title.value()) : winrt::hstring();
m_appWindow.Title(titleValue);
}

::Microsoft::ReactNativeSpecs::BaseModalHostView<ModalHostView>::UpdateProps(view, newProps, oldProps);
}

Expand Down Expand Up @@ -142,28 +163,34 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
}

void AdjustWindowSize(const winrt::Microsoft::ReactNative::LayoutMetrics &layoutMetrics) noexcept {
if (!m_popUp) {
if (!m_appWindow) {
return;
}

if (layoutMetrics.Frame.Width == 0 && layoutMetrics.Frame.Height == 0) {
return;
}

// get Modal's position based on parent
// Calculate physical pixels from DIPs
int32_t clientWidthPx = static_cast<int32_t>(layoutMetrics.Frame.Width * layoutMetrics.PointScaleFactor);
int32_t clientHeightPx = static_cast<int32_t>(layoutMetrics.Frame.Height * layoutMetrics.PointScaleFactor);

// Ensure minimum size for the window
clientWidthPx = std::max(100, clientWidthPx);
clientHeightPx = std::max(100, clientHeightPx);

// Size the client area directly
m_appWindow.ResizeClient({clientWidthPx, clientHeightPx});

// Center the window on its parent
RECT parentRC;
GetWindowRect(m_parentHwnd, &parentRC);
int32_t xCor = static_cast<int32_t>(
(parentRC.left + parentRC.right - layoutMetrics.Frame.Width * layoutMetrics.PointScaleFactor) / 2);
int32_t yCor = static_cast<int32_t>(
(parentRC.top + parentRC.bottom - layoutMetrics.Frame.Height * layoutMetrics.PointScaleFactor) / 2);

winrt::Windows::Graphics::RectInt32 rect2{
(int)xCor,
(int)yCor,
static_cast<int32_t>(layoutMetrics.Frame.Width * (layoutMetrics.PointScaleFactor)),
static_cast<int32_t>(layoutMetrics.Frame.Height * (layoutMetrics.PointScaleFactor))};
m_popUp.MoveAndResize(rect2);
auto outerSize = m_appWindow.Size();

int32_t xCor = parentRC.left + (parentRC.right - parentRC.left - outerSize.Width) / 2;
int32_t yCor = parentRC.top + (parentRC.bottom - parentRC.top - outerSize.Height) / 2;

m_appWindow.Move({xCor, yCor});
};

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

// Unregister closing event handler
if (m_appWindow && m_appWindowClosingToken) {
m_appWindow.Closing(m_appWindowClosingToken);
m_appWindowClosingToken.value = 0;
}

// dispatch onDismiss event
if (auto eventEmitter = EventEmitter()) {
::Microsoft::ReactNativeSpecs::ModalHostViewEventEmitter::OnDismiss eventArgs;
Expand Down Expand Up @@ -237,6 +270,42 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
.Island());
m_popUp.Connect(contentIsland);

// Get AppWindow and configure presenter
m_appWindow = winrt::Microsoft::UI::Windowing::AppWindow::GetFromWindowId(m_popUp.WindowId());
if (m_appWindow) {
auto overlappedPresenter = winrt::Microsoft::UI::Windowing::OverlappedPresenter::Create();

// Configure presenter for modal behavior
overlappedPresenter.IsModal(true);
overlappedPresenter.SetBorderAndTitleBar(true, true);

// Apply the presenter to the window
m_appWindow.SetPresenter(overlappedPresenter);

// Set initial title using the stored local props
if (m_localProps && m_localProps->title.has_value()) {
winrt::hstring titleValue = winrt::to_hstring(m_localProps->title.value());
m_appWindow.Title(titleValue);
} else {
m_appWindow.Title(L""); // Empty title if not provided
}

// Handle close request ('X' button)
m_appWindowClosingToken =
m_appWindow.Closing([wkThis = get_weak()](
const winrt::Microsoft::UI::Windowing::AppWindow & /*sender*/,
const winrt::Microsoft::UI::Windowing::AppWindowClosingEventArgs &args) {
args.Cancel(true); // Prevent default close
if (auto strongThis = wkThis.get()) {
// Dispatch onRequestClose event
if (auto eventEmitter = strongThis->EventEmitter()) {
::Microsoft::ReactNativeSpecs::ModalHostViewEventEmitter::OnRequestClose eventArgs;
eventEmitter->onRequestClose(eventArgs);
}
}
});
}

// set the top-level windows as the new hwnd
winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId(
view.ReactContext().Properties(),
Expand Down Expand Up @@ -320,6 +389,9 @@ struct ModalHostView : public winrt::implements<ModalHostView, winrt::Windows::F
winrt::Microsoft::ReactNative::IComponentState m_state{nullptr};
winrt::Microsoft::ReactNative::ReactNativeIsland m_reactNativeIsland{nullptr};
winrt::Microsoft::UI::Content::DesktopPopupSiteBridge m_popUp{nullptr};
winrt::Microsoft::UI::Windowing::AppWindow m_appWindow{nullptr};
winrt::event_token m_appWindowClosingToken;
winrt::com_ptr<::Microsoft::ReactNativeSpecs::ModalHostViewProps> m_localProps{nullptr};
};

void RegisterWindowsModalHostNativeComponent(
Expand Down
6 changes: 5 additions & 1 deletion vnext/codegen/react/components/rnwcore/ModalHostView.g.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ struct ModalHostViewProps : winrt::implements<ModalHostViewProps, winrt::Microso
visible = cloneFromProps->visible;
animated = cloneFromProps->animated;
supportedOrientations = cloneFromProps->supportedOrientations;
identifier = cloneFromProps->identifier;
identifier = cloneFromProps->identifier;
title = cloneFromProps->title;
}
}

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

REACT_FIELD(title)
std::optional<std::string> title;

const winrt::Microsoft::ReactNative::ViewProps ViewProps;
};

Expand Down
3 changes: 2 additions & 1 deletion vnext/codegen/react/components/rnwcore/Props.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ ModalHostViewProps::ModalHostViewProps(
visible(convertRawProp(context, rawProps, "visible", sourceProps.visible, {false})),
animated(convertRawProp(context, rawProps, "animated", sourceProps.animated, {false})),
supportedOrientations(convertRawProp(context, rawProps, "supportedOrientations", ModalHostViewSupportedOrientationsMaskWrapped{ .value = sourceProps.supportedOrientations }, {static_cast<ModalHostViewSupportedOrientationsMask>(ModalHostViewSupportedOrientations::Portrait)}).value),
identifier(convertRawProp(context, rawProps, "identifier", sourceProps.identifier, {0}))
identifier(convertRawProp(context, rawProps, "identifier", sourceProps.identifier, {0})),
title(convertRawProp(context, rawProps, "title", sourceProps.title, {}))
{}
SafeAreaViewProps::SafeAreaViewProps(
const PropsParserContext &context,
Expand Down
1 change: 1 addition & 0 deletions vnext/codegen/react/components/rnwcore/Props.h
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ class ModalHostViewProps final : public ViewProps {
bool animated{false};
ModalHostViewSupportedOrientationsMask supportedOrientations{static_cast<ModalHostViewSupportedOrientationsMask>(ModalHostViewSupportedOrientations::Portrait)};
int identifier{0};
std::string title{};
};

class SafeAreaViewProps final : public ViewProps {
Expand Down
14 changes: 14 additions & 0 deletions vnext/overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,13 @@
"baseFile": "packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js",
"baseHash": "663d3325298404d7c012a6aa53e833eb5fc2ec76"
},
{
"type": "patch",
"file": "src-win/Libraries/Modal/Modal.d.ts",
"baseFile": "packages/react-native/Libraries/Modal/Modal.d.ts",
"baseHash": "aeebd34b8cccade2637e310a63a1e9a41f149f64",
"issue": 0
},
{
"type": "derived",
"file": "src-win/Libraries/Modal/Modal.windows.js",
Expand Down Expand Up @@ -605,6 +612,13 @@
"baseFile": "packages/react-native/src/private/debugging/ReactDevToolsSettingsManager.android.js",
"baseHash": "df41b76dc3d2df9455fae588748261d7b0a22d01"
},
{
"type": "patch",
"file": "src-win/src/private/specs/components/RCTModalHostViewNativeComponent.js",
"baseFile": "packages/react-native/src/private/specs/components/RCTModalHostViewNativeComponent.js",
"baseHash": "dbda84f2de3e0aa3504e38bd4bbb687b1ea671b2",
"issue": 0
},
{
"type": "platform",
"file": "src-win/src/private/specs/modules/NativeAppTheme.js"
Expand Down
123 changes: 123 additions & 0 deletions vnext/src-win/Libraries/Modal/Modal.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* 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.
*
* @format
*/

import type * as React from 'react';
import {ViewProps} from '../Components/View/ViewPropTypes';
import {NativeSyntheticEvent} from '../Types/CoreEventTypes';
import {ColorValue} from '../StyleSheet/StyleSheet';

export interface ModalBaseProps {
/**
* @deprecated Use animationType instead
*/
animated?: boolean | undefined;
/**
* The `animationType` prop controls how the modal animates.
*
* - `slide` slides in from the bottom
* - `fade` fades into view
* - `none` appears without an animation
*/
animationType?: 'none' | 'slide' | 'fade' | undefined;
/**
* The `transparent` prop determines whether your modal will fill the entire view.
* Setting this to `true` will render the modal over a transparent background.
*/
transparent?: boolean | undefined;
/**
* The `visible` prop determines whether your modal is visible.
*/
visible?: boolean | undefined;
/**
* The `onRequestClose` callback is called when the user taps the hardware back button on Android or the menu button on Apple TV.
*
* This is required on Apple TV and Android.
*/
onRequestClose?: ((event: NativeSyntheticEvent<any>) => void) | undefined;
/**
* The `onShow` prop allows passing a function that will be called once the modal has been shown.
*/
onShow?: ((event: NativeSyntheticEvent<any>) => void) | undefined;

/**
* The `backdropColor` props sets the background color of the modal's container.
* Defaults to `white` if not provided and transparent is `false`. Ignored if `transparent` is `true`.
*/
backdropColor?: ColorValue | undefined;
}

export interface ModalPropsIOS {
/**
* The `presentationStyle` determines the style of modal to show
*/
presentationStyle?:
| 'fullScreen'
| 'pageSheet'
| 'formSheet'
| 'overFullScreen'
| undefined;

/**
* The `supportedOrientations` prop allows the modal to be rotated to any of the specified orientations.
* On iOS, the modal is still restricted by what's specified in your app's Info.plist's UISupportedInterfaceOrientations field.
*/
supportedOrientations?:
| Array<
| 'portrait'
| 'portrait-upside-down'
| 'landscape'
| 'landscape-left'
| 'landscape-right'
>
| undefined;

/**
* The `onDismiss` prop allows passing a function that will be called once the modal has been dismissed.
*/
onDismiss?: (() => void) | undefined;

/**
* The `onOrientationChange` callback is called when the orientation changes while the modal is being displayed.
* The orientation provided is only 'portrait' or 'landscape'. This callback is also called on initial render, regardless of the current orientation.
*/
onOrientationChange?:
| ((event: NativeSyntheticEvent<any>) => void)
| undefined;
}

export interface ModalPropsAndroid {
/**
* Controls whether to force hardware acceleration for the underlying window.
*/
hardwareAccelerated?: boolean | undefined;

/**
* 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 interface ModalWindowsProps {
/* title for the modal, shown in the title bar */
// [Windows
title?: string | undefined;
// Windows]
}

export type ModalProps = ModalBaseProps &
ModalPropsIOS &
ModalPropsAndroid &
ModalWindowsProps &
ViewProps;

export class Modal extends React.Component<ModalProps> {}
8 changes: 7 additions & 1 deletion vnext/src-win/Libraries/Modal/Modal.windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ export type Props = $ReadOnly<{
* Defaults to `white` if not provided and transparent is `false`. Ignored if `transparent` is `true`.
*/
backdropColor?: ?string,

/**
* [Windows] The `title` prop sets the title of the modal window.
*/
title?: ?string,
}>;

function confirmProps(props: Props) {
Expand Down Expand Up @@ -329,7 +334,8 @@ class Modal extends React.Component<Props, State> {
onStartShouldSetResponder={this._shouldSetResponder}
supportedOrientations={this.props.supportedOrientations}
onOrientationChange={this.props.onOrientationChange}
testID={this.props.testID}>
testID={this.props.testID}
title={this.props.title}>
<VirtualizedListContextResetter>
<ScrollView.Context.Provider value={null}>
<View
Expand Down
Loading
Loading