diff --git a/change/react-native-windows-5a8be4f7-4d46-49db-bc56-5b82f2ea97d0.json b/change/react-native-windows-5a8be4f7-4d46-49db-bc56-5b82f2ea97d0.json new file mode 100644 index 00000000000..f6762eb5caf --- /dev/null +++ b/change/react-native-windows-5a8be4f7-4d46-49db-bc56-5b82f2ea97d0.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "addded titlebar , title handling , X handling , resizable", + "packageName": "react-native-windows", + "email": "abhijeetjha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp index 7aef66ea44a..99632acbcc9 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp @@ -39,6 +39,12 @@ struct ModalHostView : public winrt::implements &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 @@ -79,6 +91,15 @@ struct ModalHostView : public winrt::implements::UpdateProps(view, newProps, oldProps); } @@ -142,7 +163,7 @@ struct ModalHostView : public winrt::implements(layoutMetrics.Frame.Width * layoutMetrics.PointScaleFactor); + int32_t clientHeightPx = static_cast(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( - (parentRC.left + parentRC.right - layoutMetrics.Frame.Width * layoutMetrics.PointScaleFactor) / 2); - int32_t yCor = static_cast( - (parentRC.top + parentRC.bottom - layoutMetrics.Frame.Height * layoutMetrics.PointScaleFactor) / 2); - - winrt::Windows::Graphics::RectInt32 rect2{ - (int)xCor, - (int)yCor, - static_cast(layoutMetrics.Frame.Width * (layoutMetrics.PointScaleFactor)), - static_cast(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) { @@ -199,6 +226,12 @@ struct ModalHostView : public winrt::implementstitle.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(), @@ -320,6 +389,9 @@ struct ModalHostView : public winrt::implements m_localProps{nullptr}; }; void RegisterWindowsModalHostNativeComponent( diff --git a/vnext/codegen/react/components/rnwcore/ModalHostView.g.h b/vnext/codegen/react/components/rnwcore/ModalHostView.g.h index 2d7cf10c5d2..4d93b8d85b7 100644 --- a/vnext/codegen/react/components/rnwcore/ModalHostView.g.h +++ b/vnext/codegen/react/components/rnwcore/ModalHostView.g.h @@ -34,7 +34,8 @@ struct ModalHostViewProps : winrt::implementsvisible; animated = cloneFromProps->animated; supportedOrientations = cloneFromProps->supportedOrientations; - identifier = cloneFromProps->identifier; + identifier = cloneFromProps->identifier; + title = cloneFromProps->title; } } @@ -72,6 +73,9 @@ struct ModalHostViewProps : winrt::implements identifier{}; + REACT_FIELD(title) + std::optional title; + const winrt::Microsoft::ReactNative::ViewProps ViewProps; }; diff --git a/vnext/codegen/react/components/rnwcore/Props.cpp b/vnext/codegen/react/components/rnwcore/Props.cpp index a46bf56b211..3d33bfb0980 100644 --- a/vnext/codegen/react/components/rnwcore/Props.cpp +++ b/vnext/codegen/react/components/rnwcore/Props.cpp @@ -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(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, diff --git a/vnext/codegen/react/components/rnwcore/Props.h b/vnext/codegen/react/components/rnwcore/Props.h index 82909907d12..397bc1706cb 100644 --- a/vnext/codegen/react/components/rnwcore/Props.h +++ b/vnext/codegen/react/components/rnwcore/Props.h @@ -355,6 +355,7 @@ class ModalHostViewProps final : public ViewProps { bool animated{false}; ModalHostViewSupportedOrientationsMask supportedOrientations{static_cast(ModalHostViewSupportedOrientations::Portrait)}; int identifier{0}; + std::string title{}; }; class SafeAreaViewProps final : public ViewProps { diff --git a/vnext/overrides.json b/vnext/overrides.json index e8b8d2c17b9..fc26978b4e9 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -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", @@ -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" diff --git a/vnext/src-win/Libraries/Modal/Modal.d.ts b/vnext/src-win/Libraries/Modal/Modal.d.ts new file mode 100644 index 00000000000..db9ae6c1ce1 --- /dev/null +++ b/vnext/src-win/Libraries/Modal/Modal.d.ts @@ -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) => void) | undefined; + /** + * The `onShow` prop allows passing a function that will be called once the modal has been shown. + */ + onShow?: ((event: NativeSyntheticEvent) => 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) => 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 {} diff --git a/vnext/src-win/Libraries/Modal/Modal.windows.js b/vnext/src-win/Libraries/Modal/Modal.windows.js index 695818725f4..0e972774884 100644 --- a/vnext/src-win/Libraries/Modal/Modal.windows.js +++ b/vnext/src-win/Libraries/Modal/Modal.windows.js @@ -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) { @@ -329,7 +334,8 @@ class Modal extends React.Component { onStartShouldSetResponder={this._shouldSetResponder} supportedOrientations={this.props.supportedOrientations} onOrientationChange={this.props.onOrientationChange} - testID={this.props.testID}> + testID={this.props.testID} + title={this.props.title}> ; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + + /** + * The `animationType` prop controls how the modal animates. + * + * See https://reactnative.dev/docs/modal#animationtype + */ + animationType?: WithDefault<'none' | 'slide' | 'fade', 'none'>, + + /** + * The `presentationStyle` prop controls how the modal appears. + * + * See https://reactnative.dev/docs/modal#presentationstyle + */ + presentationStyle?: WithDefault< + 'fullScreen' | 'pageSheet' | 'formSheet' | 'overFullScreen', + 'fullScreen', + >, + + /** + * The `transparent` prop determines whether your modal will fill the + * entire view. + * + * See https://reactnative.dev/docs/modal#transparent + */ + transparent?: WithDefault, + + /** + * The `statusBarTranslucent` prop determines whether your modal should go under + * the system statusbar. + * + * See https://reactnative.dev/docs/modal#statusBarTranslucent + */ + statusBarTranslucent?: WithDefault, + + /** + * The `navigationBarTranslucent` prop determines whether your modal should go under + * the system navigationbar. + * + * See https://reactnative.dev/docs/modal#navigationBarTranslucent + */ + navigationBarTranslucent?: WithDefault, + + /** + * The `hardwareAccelerated` prop controls whether to force hardware + * acceleration for the underlying window. + * + * See https://reactnative.dev/docs/modal#hardwareaccelerated + */ + hardwareAccelerated?: WithDefault, + + /** + * 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. + * + * See https://reactnative.dev/docs/modal#onrequestclose + */ + onRequestClose?: ?DirectEventHandler, + + /** + * The `onShow` prop allows passing a function that will be called once the + * modal has been shown. + * + * See https://reactnative.dev/docs/modal#onshow + */ + onShow?: ?DirectEventHandler, + + /** + * The `onDismiss` prop allows passing a function that will be called once + * the modal has been dismissed. + * + * See https://reactnative.dev/docs/modal#ondismiss + */ + onDismiss?: ?DirectEventHandler, + + /** + * The `visible` prop determines whether your modal is visible. + * + * See https://reactnative.dev/docs/modal#visible + */ + visible?: WithDefault, + + /** + * Deprecated. Use the `animationType` prop instead. + */ + animated?: WithDefault, + + /** + * The `supportedOrientations` prop allows the modal to be rotated to any of the specified orientations. + * + * See https://reactnative.dev/docs/modal#supportedorientations + */ + supportedOrientations?: WithDefault< + $ReadOnlyArray< + | 'portrait' + | 'portrait-upside-down' + | 'landscape' + | 'landscape-left' + | 'landscape-right', + >, + 'portrait', + >, + + /** + * The `onOrientationChange` callback is called when the orientation changes while the modal is being displayed. + * + * See https://reactnative.dev/docs/modal#onorientationchange + */ + onOrientationChange?: ?DirectEventHandler, + + /** + * The `identifier` is the unique number for identifying Modal components. + */ + identifier?: WithDefault, + + /* + title for the modal + windows only + */ + // [Windows + title?: WithDefault, + // Windows] +|}>; + +export default (codegenNativeComponent('ModalHostView', { + interfaceOnly: true, + paperComponentName: 'RCTModalHostView', +}): HostComponent);