From ec5a67c6c1fffe3452a2b4128c4546c7611d789b Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:41:09 -0700 Subject: [PATCH 1/8] Very initial framework for RNW fabric callout implementation --- packages/agentic-components/package.json | 1 + .../storybook/Directory.Build.props | 11 + .../Callout/react-native.config.cjs | 16 + .../Callout/src/Callout.stories.tsx | 6 +- .../Callout/windows/Callout/Callout.cpp | 558 ++++++++++++++++++ .../Callout/windows/Callout/Callout.def | 3 + .../Callout/windows/Callout/Callout.h | 25 + .../Callout/windows/Callout/Callout.vcxproj | 151 +++++ .../windows/Callout/ReactPackageProvider.cpp | 25 + .../windows/Callout/ReactPackageProvider.h | 25 + .../windows/Callout/ReactPackageProvider.idl | 12 + .../windows/Callout/codegen/.clang-format | 2 + .../components/FRNCalloutSpec/RCTCallout.g.h | 359 +++++++++++ .../windows/Callout/packages.lock.json | 151 +++++ .../Callout/windows/Callout/pch.cpp | 1 + .../components/Callout/windows/Callout/pch.h | 28 + .../components/Callout/windows/NuGet.Config | 13 + yarn.lock | 1 + 18 files changed, 1385 insertions(+), 3 deletions(-) create mode 100644 packages/agentic-components/storybook/Directory.Build.props create mode 100644 packages/components/Callout/react-native.config.cjs create mode 100644 packages/components/Callout/windows/Callout/Callout.cpp create mode 100644 packages/components/Callout/windows/Callout/Callout.def create mode 100644 packages/components/Callout/windows/Callout/Callout.h create mode 100644 packages/components/Callout/windows/Callout/Callout.vcxproj create mode 100644 packages/components/Callout/windows/Callout/ReactPackageProvider.cpp create mode 100644 packages/components/Callout/windows/Callout/ReactPackageProvider.h create mode 100644 packages/components/Callout/windows/Callout/ReactPackageProvider.idl create mode 100644 packages/components/Callout/windows/Callout/codegen/.clang-format create mode 100644 packages/components/Callout/windows/Callout/codegen/react/components/FRNCalloutSpec/RCTCallout.g.h create mode 100644 packages/components/Callout/windows/Callout/packages.lock.json create mode 100644 packages/components/Callout/windows/Callout/pch.cpp create mode 100644 packages/components/Callout/windows/Callout/pch.h create mode 100644 packages/components/Callout/windows/NuGet.Config diff --git a/packages/agentic-components/package.json b/packages/agentic-components/package.json index d2286f6be3d..4d67fc3713e 100644 --- a/packages/agentic-components/package.json +++ b/packages/agentic-components/package.json @@ -30,6 +30,7 @@ "update-snapshots": "fluentui-scripts jest -u" }, "dependencies": { + "@fluentui-react-native/callout": "workspace:*", "@fluentui-react-native/design": "workspace:*", "@fluentui-react-native/framework-base": "workspace:*" }, diff --git a/packages/agentic-components/storybook/Directory.Build.props b/packages/agentic-components/storybook/Directory.Build.props new file mode 100644 index 00000000000..037a06abb3b --- /dev/null +++ b/packages/agentic-components/storybook/Directory.Build.props @@ -0,0 +1,11 @@ + + + + + + yarn exec rnc-cli autolink-windows + false + yarn exec rnc-cli codegen-windows + + + \ No newline at end of file diff --git a/packages/components/Callout/react-native.config.cjs b/packages/components/Callout/react-native.config.cjs new file mode 100644 index 00000000000..3f13ca929e2 --- /dev/null +++ b/packages/components/Callout/react-native.config.cjs @@ -0,0 +1,16 @@ + +module.exports = { + dependency: { + platforms: { + windows: { + sourceDir: 'windows', + projects: [ + { + projectFile: 'Callout/Callout.vcxproj', + directDependency: true, + }, + ], + }, + }, + }, +} \ No newline at end of file diff --git a/packages/components/Callout/src/Callout.stories.tsx b/packages/components/Callout/src/Callout.stories.tsx index cec96831ad2..4220b45e0a7 100644 --- a/packages/components/Callout/src/Callout.stories.tsx +++ b/packages/components/Callout/src/Callout.stories.tsx @@ -1,6 +1,6 @@ /** @jsxImportSource @fluentui-react-native/framework-base */ import * as React from 'react'; -import { Pressable, StyleSheet, Text, View } from 'react-native'; +import { Button, Pressable, StyleSheet, Text, View } from 'react-native'; import type { Meta, StoryObj } from '@storybook/react-native'; @@ -66,7 +66,7 @@ const CalloutExample = ({ defaultVisible = false, onDismiss, onShow, showWindowC {visible && ( - + Fabric Callout This content is hosted in a separate native macOS window. {showWindowCommands && ( @@ -127,7 +127,7 @@ const PlacementExample = (props: CalloutProps) => { onDismiss={() => setSelectedIndex(undefined)} target={targetRefs[selectedIndex]} > - + {placements[selectedIndex].label} Click outside the native window to dismiss it. diff --git a/packages/components/Callout/windows/Callout/Callout.cpp b/packages/components/Callout/windows/Callout/Callout.cpp new file mode 100644 index 00000000000..367299f1869 --- /dev/null +++ b/packages/components/Callout/windows/Callout/Callout.cpp @@ -0,0 +1,558 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" + +#include "Callout.h" + +#include +#include +#include +#include +#include + +namespace winrt::FluentUI::Callout { + +enum class DirectionalHint { + LeftTopEdge, + LeftCenter, + LeftBottomEdge, + TopLeftEdge, + TopAutoEdge, + TopCenter, + TopRightEdge, + RightTopEdge, + RightCenter, + RightBottomEdge, + BottomLeftEdge, + BottomAutoEdge, + BottomCenter, + BottomRightEdge +}; + +DirectionalHint parseDirectionHint(std::string &directionHint) { + if (directionHint == "leftTopEdge") { + return DirectionalHint::LeftTopEdge; + } else if (directionHint == "leftCenter") { + return DirectionalHint::LeftCenter; + } else if (directionHint == "leftBottomEdge") { + return DirectionalHint::LeftBottomEdge; + } else if (directionHint == "topLeftEdge") { + return DirectionalHint::TopLeftEdge; + } else if (directionHint == "topAutoEdge") { + return DirectionalHint::TopAutoEdge; + } else if (directionHint == "topCenter") { + return DirectionalHint::TopCenter; + } else if (directionHint == "topRightEdge") { + return DirectionalHint::TopRightEdge; + } else if (directionHint == "rightTopEdge") { + return DirectionalHint::RightTopEdge; + } else if (directionHint == "rightCenter") { + return DirectionalHint::RightCenter; + } else if (directionHint == "rightBottomEdge") { + return DirectionalHint::RightBottomEdge; + } else if (directionHint == "bottomLeftEdge") { + return DirectionalHint::BottomLeftEdge; + } else if (directionHint == "bottomAutoEdge") { + return DirectionalHint::BottomAutoEdge; + } else if (directionHint == "bottomCenter") { + return DirectionalHint::BottomCenter; + } else if (directionHint == "bottomRightEdge") { + return DirectionalHint::BottomRightEdge; + } + + return DirectionalHint::LeftTopEdge; +} + +struct CalloutComponentView + : public winrt::implements, + Codegen::BaseRCTCallout { + ~CalloutComponentView() { + if (m_popup && !m_popup.IsClosed()) { + /* + // Unregister closing event handler + if (m_appWindowClosingToken) + { + m_rnWindow.AppWindow().Closing(m_appWindowClosingToken); + m_appWindowClosingToken = {}; + } + */ + + // Hide popup + if (m_popup.IsVisible()) { + m_popup.Hide(); + } + + if (m_rnWindow.AppWindow()) { + m_rnWindow.Close(); + m_rnWindow = nullptr; + } + + // Close bridge + m_popup.Close(); + m_popup = nullptr; + } + } + + void UpdateProps(const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::com_ptr &newProps, + const winrt::com_ptr + &oldProps) noexcept override { + if (!oldProps || newProps->directionalHint != oldProps->directionalHint) { + m_directionalHint = + newProps->directionalHint + ? parseDirectionHint(newProps->directionalHint.value()) + : DirectionalHint::LeftTopEdge; + + auto portal = view.as< + winrt::Microsoft::ReactNative::Composition::PortalComponentView>(); + if (portal.ContentRoot().Children().Size() != 0) { + AdjustWindowSize( + portal.ContentRoot().Children().GetAt(0).LayoutMetrics()); + } + } + + __super::UpdateProps(view, newProps, oldProps); + } + + void InitializePortalViewComponent( + const winrt::Microsoft::ReactNative::Composition::PortalComponentView + &portalComponentView) noexcept { + m_reactContext = portalComponentView.ReactContext(); + + portalComponentView.Mounted([](const auto & /*sender*/, const auto &view) { + view.UserData().as()->OnMounted(view); + }); + portalComponentView.Unmounted( + [](const auto & /*sender*/, const auto &view) { + view.UserData().as()->OnUnmounted(view); + }); + } + + void HandleFocusWindowCommand() noexcept override { + // nyi + } + + // You must provide an implementation of this method to handle the + // "blurWindow" command + void HandleBlurWindowCommand() noexcept override { + // nyi + } + + void MountChildComponentView( + const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::MountChildComponentViewArgs + &args) noexcept override { + AdjustWindowSize(args.Child().LayoutMetrics()); + // TODO These asserts are currently hit if the root view of the callout is collapsed. + // Need to handle this case + // assert(args.Index() == 0); + // assert(!m_childLayoutMetricsToken); + m_childLayoutMetricsToken = args.Child().LayoutMetricsChanged( + [wkThis = get_weak()]( + auto &/*sender*/, + const winrt::Microsoft::ReactNative::LayoutMetricsChangedArgs + &layoutMetricsChangedArgs) { + if (auto strongThis = wkThis.get()) { + strongThis->AdjustWindowSize( + layoutMetricsChangedArgs.NewLayoutMetrics()); + } + }); + } + + void UnmountChildComponentView( + const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs + &args) noexcept override { + // TODO These asserts are currently hit if the root view of the callout is collapsed. + // Need to handle this case + // assert(args.Index() == 0); + // assert(m_childLayoutMetricsToken); + args.Child().LayoutMetricsChanged(m_childLayoutMetricsToken); + // m_childLayoutMetricsToken.value = 0; + } + +private: + void + OnMounted(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + assert(!m_mounted); + m_mounted = true; + + CreatePopup(view); + + m_showQueued = true; + + m_reactContext.UIDispatcher().Post( + [wkThis = get_weak(), wkView = winrt::weak_ref(view)]() { + if (auto strongThis = wkThis.get()) { + strongThis->m_showQueued = false; + + if (!strongThis->m_mounted) { + return; + } + if (auto v = wkView.get()) { + strongThis->Show(); + } + } + }); + } + + void OnUnmounted( + const winrt::Microsoft::ReactNative::ComponentView & /*view*/) noexcept { + assert(m_mounted); + m_mounted = false; + } + + void OnLightDismissDismissed( + const winrt::Microsoft::UI::Input::InputLightDismissAction &, + const winrt::Microsoft::UI::Input::InputLightDismissEventArgs &) { + HidePopup(); + } + + void HidePopup() { + if (!m_popup) + return; + m_popup.Hide(); + if (auto eventEmitter = EventEmitter()) + eventEmitter->onDismiss({}); + } + + void CreatePopup( + const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + if (m_popup) + return; + + auto portal = view.as< + winrt::Microsoft::ReactNative::Composition::PortalComponentView>(); + m_popup = winrt::Microsoft::UI::Content::DesktopPopupSiteBridge::Create( + portal.Parent() + .as() + .Root() + .ReactNativeIsland() + .Island()); + + assert(!m_rnWindow); + m_rnWindow = winrt::Microsoft::ReactNative::ReactNativeWindow:: + CreateFromContentSiteBridgeAndIsland( + m_popup, + winrt::Microsoft::ReactNative::ReactNativeIsland::CreatePortal( + portal)); + m_rnWindow.ResizePolicy( + winrt::Microsoft::ReactNative::ContentSizePolicy::None); + + auto inputLightDismissAction = + winrt::Microsoft::UI::Input::InputLightDismissAction::GetForWindowId( + m_rnWindow.AppWindow().Id()); + inputLightDismissAction.Dismissed( + {get_weak(), &CalloutComponentView::OnLightDismissDismissed}); + + if (portal.ContentRoot().Children().Size()) { + AdjustWindowSize( + portal.ContentRoot().Children().GetAt(0).LayoutMetrics()); + } + } + + void RegisterLightDismissAction() noexcept {} + + void Show() noexcept { + m_popup.Show(); + + winrt::Microsoft::UI::Input::InputFocusController::GetForIsland( + m_rnWindow.ReactNativeIsland().Island()) + .TrySetFocus(); + m_rnWindow.ReactNativeIsland().NavigateFocus( + winrt::Microsoft::ReactNative::FocusNavigationRequest:: + FocusNavigationRequest( + winrt::Microsoft::ReactNative::FocusNavigationReason::First)); + + if (auto eventEmitter = EventEmitter()) { + eventEmitter->onShow({}); + } + } + + void AdjustWindowSize(const winrt::Microsoft::ReactNative::LayoutMetrics + &layoutMetrics) noexcept { + if (!m_rnWindow) { + return; + } + + if (layoutMetrics.Frame.Width == 0 && layoutMetrics.Frame.Height == 0) { + return; + } + + // Calculate physical pixels from DIPs + int32_t clientWidthPx = static_cast( + layoutMetrics.Frame.Width * layoutMetrics.PointScaleFactor); + int32_t clientHeightPx = static_cast( + layoutMetrics.Frame.Height * layoutMetrics.PointScaleFactor); + + // Size the client area directly + m_rnWindow.AppWindow().ResizeClient({clientWidthPx, clientHeightPx}); + + // Target can be either a view tag of a view to anchor to, or a string for + // an anchor id + if (Props()->target.Type() == + winrt::Microsoft::ReactNative::JSValueType::Int64) { + auto targetView = winrt::Microsoft::ReactNative::Composition:: + CompositionUIService::ComponentFromReactTag( + m_reactContext.Handle(), Props()->target.AsInt64()); + auto targetPos = ViewToScreenOffset(targetView); + + POINT anchorPoint{targetPos.X, targetPos.Y}; + SIZE windowSize{clientWidthPx, clientHeightPx}; + + RECT excludeRect{targetPos.X, targetPos.Y, + targetPos.X + targetView.LayoutMetrics().Frame.Width, + targetPos.Y + targetView.LayoutMetrics().Frame.Height}; + + UINT flags = 0; + + if (m_directionalHint == DirectionalHint::LeftTopEdge) { + flags = TPM_LEFTALIGN | TPM_TOPALIGN | TPM_HORIZONTAL; + } else if (m_directionalHint == DirectionalHint::LeftCenter) { + flags = TPM_LEFTALIGN | TPM_VCENTERALIGN | TPM_HORIZONTAL; + } else if (m_directionalHint == DirectionalHint::LeftBottomEdge) { + flags = TPM_LEFTALIGN | TPM_BOTTOMALIGN | TPM_HORIZONTAL; + } else if (m_directionalHint == DirectionalHint::TopLeftEdge) { + flags = TPM_LEFTALIGN | TPM_TOPALIGN | TPM_VERTICAL; + } else if (m_directionalHint == DirectionalHint::TopAutoEdge) { + flags = TPM_LEFTALIGN | TPM_TOPALIGN | TPM_VERTICAL; + } else if (m_directionalHint == DirectionalHint::TopCenter) { + flags = TPM_CENTERALIGN | TPM_TOPALIGN | TPM_VERTICAL; + } else if (m_directionalHint == DirectionalHint::TopRightEdge) { + flags = TPM_RIGHTALIGN | TPM_TOPALIGN | TPM_VERTICAL; + } else if (m_directionalHint == DirectionalHint::RightTopEdge) { + flags = TPM_RIGHTALIGN | TPM_TOPALIGN | TPM_HORIZONTAL; + } else if (m_directionalHint == DirectionalHint::RightCenter) { + flags = TPM_RIGHTALIGN | TPM_VCENTERALIGN | TPM_HORIZONTAL; + } else if (m_directionalHint == DirectionalHint::RightBottomEdge) { + flags = TPM_RIGHTALIGN | TPM_BOTTOMALIGN | TPM_HORIZONTAL; + } else if (m_directionalHint == DirectionalHint::BottomLeftEdge) { + flags = TPM_LEFTALIGN | TPM_BOTTOMALIGN | TPM_VERTICAL; + } else if (m_directionalHint == DirectionalHint::BottomAutoEdge) { + flags = TPM_LEFTALIGN | TPM_BOTTOMALIGN | TPM_VERTICAL; + } else if (m_directionalHint == DirectionalHint::BottomCenter) { + flags = TPM_CENTERALIGN | TPM_BOTTOMALIGN | TPM_VERTICAL; + } else if (m_directionalHint == DirectionalHint::BottomRightEdge) { + flags = TPM_RIGHTALIGN | TPM_BOTTOMALIGN | TPM_VERTICAL; + } + + flags |= TPM_WORKAREA; + + RECT finalPos; + + CalculatePopupWindowPosition(&anchorPoint, &windowSize, flags, + &excludeRect, &finalPos); + + m_rnWindow.AppWindow().Move({finalPos.left, finalPos.top}); + } else { + // TODO target named anchors + m_rnWindow.AppWindow().Move({0, 0}); + } + }; + + winrt::Windows::Graphics::PointInt32 + ViewToScreenOffset(const winrt::Microsoft::ReactNative::ComponentView &view) { + winrt::Windows::Foundation::Point pt{ + (view.LayoutMetrics().Frame.X) * view.LayoutMetrics().PointScaleFactor, + (view.LayoutMetrics().Frame.Y) * view.LayoutMetrics().PointScaleFactor}; + + for (auto p = view.Parent(); p; p = p.Parent()) { + pt.X += p.LayoutMetrics().Frame.X * p.LayoutMetrics().PointScaleFactor; + pt.Y += p.LayoutMetrics().Frame.Y * p.LayoutMetrics().PointScaleFactor; + } + + auto root = + view.as() + .Root(); + auto cc = root.ReactNativeIsland().Island().CoordinateConverter(); + return cc.ConvertLocalToScreen(pt); + } + + DirectionalHint m_directionalHint{DirectionalHint::LeftTopEdge}; + bool m_showQueued{false}; + bool m_mounted{false}; + winrt::event_token m_childLayoutMetricsToken; + winrt::Microsoft::UI::Content::DesktopPopupSiteBridge m_popup{nullptr}; + winrt::Microsoft::ReactNative::ReactNativeWindow m_rnWindow{nullptr}; + winrt::Microsoft::ReactNative::ReactContext m_reactContext{nullptr}; +}; + +} // namespace winrt::FluentUI::Callout + +namespace winrt::FluentUI::Callout::Codegen { + +// Copy/Paste of codegen'd RegisterRCTCalloutNativeComponent, but uses "Callout" +// to register the component instead of RCTCallout RN has special cased RCT* to +// be just *. But the codegen doesn't know that. We should probably eventually +// move away from using RCT prefixes everywhere too. +template +void RegisterRCTCalloutNativeComponent2( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder, + std::function + builderCallback) noexcept { + packageBuilder.as() + .AddViewComponent(L"Callout", [builderCallback]( + winrt::Microsoft::ReactNative:: + IReactViewComponentBuilder const + &builder) noexcept { + auto compBuilder = + builder.as(); + + builder.SetCreateProps( + [](winrt::Microsoft::ReactNative::ViewProps props, + const winrt::Microsoft::ReactNative::IComponentProps + &cloneFrom) noexcept { + return winrt::make(props, cloneFrom); + }); + + builder.SetUpdatePropsHandler( + [](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::IComponentProps &newProps, + const winrt::Microsoft::ReactNative::IComponentProps + &oldProps) noexcept { + auto userData = view.UserData().as(); + userData->UpdateProps( + view, newProps ? newProps.as() : nullptr, + oldProps ? oldProps.as() : nullptr); + }); + + compBuilder.SetUpdateLayoutMetricsHandler( + [](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::LayoutMetrics + &newLayoutMetrics, + const winrt::Microsoft::ReactNative::LayoutMetrics + &oldLayoutMetrics) noexcept { + auto userData = view.UserData().as(); + userData->UpdateLayoutMetrics(view, newLayoutMetrics, + oldLayoutMetrics); + }); + + builder.SetUpdateEventEmitterHandler( + [](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::EventEmitter + &eventEmitter) noexcept { + auto userData = view.UserData().as(); + userData->UpdateEventEmitter( + std::make_shared(eventEmitter)); + }); + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS ( + &TUserData::FinalizeUpdate != + &BaseRCTCallout::FinalizeUpdate) { + builder.SetFinalizeUpdateHandler( + [](const winrt::Microsoft::ReactNative::ComponentView &view, + winrt::Microsoft::ReactNative::ComponentViewUpdateMask + mask) noexcept { + auto userData = view.UserData().as(); + userData->FinalizeUpdate(view, mask); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::UpdateState != + &BaseRCTCallout< + TUserData>:: + UpdateState) { + builder.SetUpdateStateHandler( + [](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::IComponentState + &newState) noexcept { + auto userData = view.UserData().as(); + userData->UpdateState(view, newState); + }); + } + + builder.SetCustomCommandHandler( + [](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::HandleCommandArgs + &args) noexcept { + auto userData = view.UserData().as(); + userData->HandleCommand(view, args); + }); + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS ( + &TUserData::MountChildComponentView != + &BaseRCTCallout::MountChildComponentView) { + builder.SetMountChildComponentViewHandler( + [](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative:: + MountChildComponentViewArgs &args) noexcept { + auto userData = view.UserData().as(); + return userData->MountChildComponentView(view, args); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS ( + &TUserData::UnmountChildComponentView != + &BaseRCTCallout::UnmountChildComponentView) { + builder.SetUnmountChildComponentViewHandler( + [](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative:: + UnmountChildComponentViewArgs &args) noexcept { + auto userData = view.UserData().as(); + return userData->UnmountChildComponentView(view, args); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS ( + &TUserData::CreateAutomationPeer != + &BaseRCTCallout::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler( + [](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs + &args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + + compBuilder.SetViewComponentViewInitializer( + [](const winrt::Microsoft::ReactNative::ComponentView + &view) noexcept { + auto userData = winrt::make_self(); + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS ( + &TUserData::Initialize != + &BaseRCTCallout::Initialize) { + userData->Initialize(view); + } + view.UserData(*userData); + }); + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS ( + &TUserData::CreateVisual != + &BaseRCTCallout::CreateVisual) { + compBuilder.SetCreateVisualHandler( + [](const winrt::Microsoft::ReactNative::ComponentView + &view) noexcept { + auto userData = view.UserData().as(); + return userData->CreateVisual(view); + }); + } + + // Allow app to further customize the builder + if (builderCallback) { + builderCallback(compBuilder); + } + }); +} + +} // namespace winrt::FluentUI::Callout::Codegen + +void RegisterCalloutComponentView( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) { + winrt::FluentUI::Callout::Codegen::RegisterRCTCalloutNativeComponent2< + winrt::FluentUI::Callout::CalloutComponentView>( + packageBuilder, [](const winrt::Microsoft::ReactNative::Composition:: + IReactCompositionViewComponentBuilder &builder) { + builder.SetPortalComponentViewInitializer( + [](const winrt::Microsoft::ReactNative::Composition:: + PortalComponentView &portalComponentView) noexcept { + auto userData = winrt::make_self< + winrt::FluentUI::Callout::CalloutComponentView>(); + userData->InitializePortalViewComponent(portalComponentView); + portalComponentView.UserData(*userData); + }); + }); +}; \ No newline at end of file diff --git a/packages/components/Callout/windows/Callout/Callout.def b/packages/components/Callout/windows/Callout/Callout.def new file mode 100644 index 00000000000..24e7c1235c3 --- /dev/null +++ b/packages/components/Callout/windows/Callout/Callout.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE diff --git a/packages/components/Callout/windows/Callout/Callout.h b/packages/components/Callout/windows/Callout/Callout.h new file mode 100644 index 00000000000..b1f8e8d61f2 --- /dev/null +++ b/packages/components/Callout/windows/Callout/Callout.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#include "pch.h" + +#include "NativeModules.h" + +#include "../codegen/react/components/FRNCalloutSpec/RCTCallout.g.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace winrt::FluentUI::Callout { + +} // namespace winrt::FluentUI::Callout + +void RegisterCalloutComponentView( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder); diff --git a/packages/components/Callout/windows/Callout/Callout.vcxproj b/packages/components/Callout/windows/Callout/Callout.vcxproj new file mode 100644 index 00000000000..3799c9cb2a3 --- /dev/null +++ b/packages/components/Callout/windows/Callout/Callout.vcxproj @@ -0,0 +1,151 @@ + + + + + + true + true + true + {FE36524A-FE8C-456A-9368-08FB1EB16CF8} + FluentUICallout + FluentUI.Callout + en-US + 16.0 + false + Windows Store + 10.0 + 10.0.19041.0 + 10.0.17763.0 + + + $([MSBuild]::GetDirectoryNameOfFileAbove($(SolutionDir), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ + + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + DynamicLibrary + Unicode + false + + + true + true + + + false + true + false + + + + + + + + + + + + + + Use + pch.h + $(IntDir)pch.pch + Level4 + %(AdditionalOptions) /bigobj + 4453;28204 + _WINRT_DLL;%(PreprocessorDefinitions) + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + + + User32.lib;%(AdditionalDependencies) + Console + true + Callout.def + + + + + _DEBUG;%(PreprocessorDefinitions) + + + + + NDEBUG;%(PreprocessorDefinitions) + + + + + false + + + + + + ReactPackageProvider.idl + + + + + + + Create + + + ReactPackageProvider.idl + + + + + + + + + + + + + + + + + This project references targets in your node_modules\react-native-windows folder that are missing. The missing file is {0}. + + + + + diff --git a/packages/components/Callout/windows/Callout/ReactPackageProvider.cpp b/packages/components/Callout/windows/Callout/ReactPackageProvider.cpp new file mode 100644 index 00000000000..bac18811408 --- /dev/null +++ b/packages/components/Callout/windows/Callout/ReactPackageProvider.cpp @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" + +#include "ReactPackageProvider.h" +#if __has_include("ReactPackageProvider.g.cpp") +#include "ReactPackageProvider.g.cpp" +#endif + +#include "Callout.h" + +using namespace winrt::Microsoft::ReactNative; + +namespace winrt::FluentUI::Callout::implementation { + +void ReactPackageProvider::CreatePackage( + IReactPackageBuilder const &packageBuilder) noexcept { + AddAttributedModules(packageBuilder, true); + +#ifdef RNW_NEW_ARCH + RegisterCalloutComponentView(packageBuilder); +#endif // #ifdef RNW_NEW_ARCH +} + +} // namespace winrt::FluentUI::Callout::implementation diff --git a/packages/components/Callout/windows/Callout/ReactPackageProvider.h b/packages/components/Callout/windows/Callout/ReactPackageProvider.h new file mode 100644 index 00000000000..ee4a6091036 --- /dev/null +++ b/packages/components/Callout/windows/Callout/ReactPackageProvider.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#include "ReactPackageProvider.g.h" + +using namespace winrt::Microsoft::ReactNative; + +namespace winrt::FluentUI::Callout::implementation { + +struct ReactPackageProvider : ReactPackageProviderT { + ReactPackageProvider() = default; + + void CreatePackage(IReactPackageBuilder const &packageBuilder) noexcept; +}; + +} // namespace winrt::FluentUI::Callout::implementation + +namespace winrt::FluentUI::Callout::factory_implementation { + +struct ReactPackageProvider + : ReactPackageProviderT {}; + +} // namespace winrt::FluentUI::Callout::factory_implementation diff --git a/packages/components/Callout/windows/Callout/ReactPackageProvider.idl b/packages/components/Callout/windows/Callout/ReactPackageProvider.idl new file mode 100644 index 00000000000..cd726b6e617 --- /dev/null +++ b/packages/components/Callout/windows/Callout/ReactPackageProvider.idl @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace FluentUI.Callout +{ + [webhosthidden] + [default_interface] + runtimeclass ReactPackageProvider : Microsoft.ReactNative.IReactPackageProvider + { + ReactPackageProvider(); + }; +} diff --git a/packages/components/Callout/windows/Callout/codegen/.clang-format b/packages/components/Callout/windows/Callout/codegen/.clang-format new file mode 100644 index 00000000000..a43d914ec38 --- /dev/null +++ b/packages/components/Callout/windows/Callout/codegen/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: false \ No newline at end of file diff --git a/packages/components/Callout/windows/Callout/codegen/react/components/FRNCalloutSpec/RCTCallout.g.h b/packages/components/Callout/windows/Callout/codegen/react/components/FRNCalloutSpec/RCTCallout.g.h new file mode 100644 index 00000000000..be4153ee8f6 --- /dev/null +++ b/packages/components/Callout/windows/Callout/codegen/react/components/FRNCalloutSpec/RCTCallout.g.h @@ -0,0 +1,359 @@ + +/* + * This file is auto-generated from RCTCalloutNativeComponent spec file in flow / TypeScript. + */ +// clang-format off +#pragma once + +#include + +#ifdef RNW_NEW_ARCH +#include + +#include +#include +#endif // #ifdef RNW_NEW_ARCH + +#ifdef RNW_NEW_ARCH + +namespace winrt::FluentUI::Callout::Codegen { + +REACT_STRUCT(RCTCalloutSpec_RCTCalloutProps_anchorRect) +struct RCTCalloutSpec_RCTCalloutProps_anchorRect { + REACT_FIELD(screenX) + double screenX{}; + + REACT_FIELD(screenY) + double screenY{}; + + REACT_FIELD(width) + double width{}; + + REACT_FIELD(height) + double height{}; +}; + +REACT_STRUCT(RCTCalloutProps) +struct RCTCalloutProps : winrt::implements { + RCTCalloutProps(winrt::Microsoft::ReactNative::ViewProps props, const winrt::Microsoft::ReactNative::IComponentProps& cloneFrom) + : ViewProps(props) + { + if (cloneFrom) { + auto cloneFromProps = cloneFrom.as(); + accessibilityLabel = cloneFromProps->accessibilityLabel; + accessibilityOnShowAnnouncement = cloneFromProps->accessibilityOnShowAnnouncement; + anchorRect = cloneFromProps->anchorRect; + dismissBehaviors = cloneFromProps->dismissBehaviors; + doNotTakePointerCapture = cloneFromProps->doNotTakePointerCapture; + focusable = cloneFromProps->focusable; + isBeakVisible = cloneFromProps->isBeakVisible; + maxHeight = cloneFromProps->maxHeight; + maxWidth = cloneFromProps->maxWidth; + setInitialFocus = cloneFromProps->setInitialFocus; + testID = cloneFromProps->testID; + directionalHint = cloneFromProps->directionalHint; + target = cloneFromProps->target.Copy(); + onRestoreFocus = cloneFromProps->onRestoreFocus; + onDismiss = cloneFromProps->onDismiss; + onShow = cloneFromProps->onShow; + } + } + + void SetProp(uint32_t hash, winrt::hstring propName, winrt::Microsoft::ReactNative::IJSValueReader value) noexcept { + winrt::Microsoft::ReactNative::ReadProp(hash, propName, value, *this); + } + + REACT_FIELD(accessibilityLabel) + std::optional accessibilityLabel; + + REACT_FIELD(accessibilityOnShowAnnouncement) + std::optional accessibilityOnShowAnnouncement; + + REACT_FIELD(anchorRect) + std::optional anchorRect; + + REACT_FIELD(dismissBehaviors) + std::optional> dismissBehaviors; + + REACT_FIELD(doNotTakePointerCapture) + std::optional doNotTakePointerCapture{}; + + REACT_FIELD(focusable) + std::optional focusable{}; + + REACT_FIELD(isBeakVisible) + std::optional isBeakVisible{}; + + REACT_FIELD(maxHeight) + std::optional maxHeight{}; + + REACT_FIELD(maxWidth) + std::optional maxWidth{}; + + REACT_FIELD(setInitialFocus) + std::optional setInitialFocus{}; + + REACT_FIELD(testID) + std::optional testID; + + REACT_FIELD(directionalHint) + std::optional directionalHint; + + REACT_FIELD(target) + winrt::Microsoft::ReactNative::JSValue target{nullptr}; + + // These fields can be used to determine if JS has registered for this event + REACT_FIELD(onRestoreFocus) + bool onRestoreFocus{false}; + + REACT_FIELD(onDismiss) + bool onDismiss{false}; + + REACT_FIELD(onShow) + bool onShow{false}; + + const winrt::Microsoft::ReactNative::ViewProps ViewProps; +}; + +REACT_STRUCT(RCTCalloutSpec_onShow) +struct RCTCalloutSpec_onShow { + REACT_FIELD(target) + int32_t target{}; +}; + +REACT_STRUCT(RCTCalloutSpec_onDismiss) +struct RCTCalloutSpec_onDismiss { + REACT_FIELD(target) + int32_t target{}; +}; + +REACT_STRUCT(RCTCalloutSpec_onRestoreFocus) +struct RCTCalloutSpec_onRestoreFocus { + REACT_FIELD(target) + int32_t target{}; + + REACT_FIELD(containsFocus) + bool containsFocus{}; +}; + +struct RCTCalloutEventEmitter { + RCTCalloutEventEmitter(const winrt::Microsoft::ReactNative::EventEmitter &eventEmitter) + : m_eventEmitter(eventEmitter) {} + + using OnRestoreFocus = RCTCalloutSpec_onRestoreFocus; + using OnDismiss = RCTCalloutSpec_onDismiss; + using OnShow = RCTCalloutSpec_onShow; + + void onRestoreFocus(OnRestoreFocus &&value) const { + m_eventEmitter.DispatchEvent(L"restoreFocus", [value = std::move(value)](const winrt::Microsoft::ReactNative::IJSValueWriter writer) { + winrt::Microsoft::ReactNative::WriteValue(writer, value); + }); + } + + void onDismiss(OnDismiss &&value) const { + m_eventEmitter.DispatchEvent(L"dismiss", [value = std::move(value)](const winrt::Microsoft::ReactNative::IJSValueWriter writer) { + winrt::Microsoft::ReactNative::WriteValue(writer, value); + }); + } + + void onShow(OnShow &&value) const { + m_eventEmitter.DispatchEvent(L"show", [value = std::move(value)](const winrt::Microsoft::ReactNative::IJSValueWriter writer) { + winrt::Microsoft::ReactNative::WriteValue(writer, value); + }); + } + + private: + winrt::Microsoft::ReactNative::EventEmitter m_eventEmitter{nullptr}; +}; + +template +struct BaseRCTCallout { + + virtual void UpdateProps( + const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + const winrt::com_ptr &newProps, + const winrt::com_ptr &/*oldProps*/) noexcept { + m_props = newProps; + } + + // UpdateLayoutMetrics will only be called if this method is overridden + virtual void UpdateLayoutMetrics( + const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + const winrt::Microsoft::ReactNative::LayoutMetrics &/*newLayoutMetrics*/, + const winrt::Microsoft::ReactNative::LayoutMetrics &/*oldLayoutMetrics*/) noexcept { + } + + // UpdateState will only be called if this method is overridden + virtual void UpdateState( + const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + const winrt::Microsoft::ReactNative::IComponentState &/*newState*/) noexcept { + } + + virtual void UpdateEventEmitter(const std::shared_ptr &eventEmitter) noexcept { + m_eventEmitter = eventEmitter; + } + + // MountChildComponentView will only be called if this method is overridden + virtual void MountChildComponentView(const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + const winrt::Microsoft::ReactNative::MountChildComponentViewArgs &/*args*/) noexcept { + } + + // UnmountChildComponentView will only be called if this method is overridden + virtual void UnmountChildComponentView(const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs &/*args*/) noexcept { + } + + // Initialize will only be called if this method is overridden + virtual void Initialize(const winrt::Microsoft::ReactNative::ComponentView &/*view*/) noexcept { + } + + // CreateVisual will only be called if this method is overridden + virtual winrt::Microsoft::UI::Composition::Visual CreateVisual(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + return view.as().Compositor().CreateSpriteVisual(); + } + + // FinalizeUpdate will only be called if this method is overridden + virtual void FinalizeUpdate(const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { + } + + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + + // You must provide an implementation of this method to handle the "focusWindow" command + virtual void HandleFocusWindowCommand() noexcept = 0; + + // You must provide an implementation of this method to handle the "blurWindow" command + virtual void HandleBlurWindowCommand() noexcept = 0; + + void HandleCommand(const winrt::Microsoft::ReactNative::ComponentView &view, const winrt::Microsoft::ReactNative::HandleCommandArgs& args) noexcept { + auto userData = view.UserData().as(); + auto commandName = args.CommandName(); + if (commandName == L"focusWindow") { + + userData->HandleFocusWindowCommand(); + return; + } + + if (commandName == L"blurWindow") { + + userData->HandleBlurWindowCommand(); + return; + } + } + + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } + const winrt::com_ptr& Props() const { return m_props; } + +private: + winrt::com_ptr m_props; + std::shared_ptr m_eventEmitter; +}; + +template +void RegisterRCTCalloutNativeComponent( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder, + std::function builderCallback) noexcept { + packageBuilder.as().AddViewComponent( + L"Callout", [builderCallback](winrt::Microsoft::ReactNative::IReactViewComponentBuilder const &builder) noexcept { + auto compBuilder = builder.as(); + + builder.SetCreateProps([](winrt::Microsoft::ReactNative::ViewProps props, + const winrt::Microsoft::ReactNative::IComponentProps& cloneFrom) noexcept { + return winrt::make(props, cloneFrom); + }); + + builder.SetUpdatePropsHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::IComponentProps &newProps, + const winrt::Microsoft::ReactNative::IComponentProps &oldProps) noexcept { + auto userData = view.UserData().as(); + userData->UpdateProps(view, newProps ? newProps.as() : nullptr, oldProps ? oldProps.as() : nullptr); + }); + + compBuilder.SetUpdateLayoutMetricsHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::LayoutMetrics &newLayoutMetrics, + const winrt::Microsoft::ReactNative::LayoutMetrics &oldLayoutMetrics) noexcept { + auto userData = view.UserData().as(); + userData->UpdateLayoutMetrics(view, newLayoutMetrics, oldLayoutMetrics); + }); + + builder.SetUpdateEventEmitterHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::EventEmitter &eventEmitter) noexcept { + auto userData = view.UserData().as(); + userData->UpdateEventEmitter(std::make_shared(eventEmitter)); + }); + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::FinalizeUpdate != &BaseRCTCallout::FinalizeUpdate) { + builder.SetFinalizeUpdateHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + winrt::Microsoft::ReactNative::ComponentViewUpdateMask mask) noexcept { + auto userData = view.UserData().as(); + userData->FinalizeUpdate(view, mask); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::UpdateState != &BaseRCTCallout::UpdateState) { + builder.SetUpdateStateHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::IComponentState &newState) noexcept { + auto userData = view.UserData().as(); + userData->UpdateState(view, newState); + }); + } + + builder.SetCustomCommandHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::HandleCommandArgs& args) noexcept { + auto userData = view.UserData().as(); + userData->HandleCommand(view, args); + }); + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::MountChildComponentView != &BaseRCTCallout::MountChildComponentView) { + builder.SetMountChildComponentViewHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::MountChildComponentViewArgs &args) noexcept { + auto userData = view.UserData().as(); + return userData->MountChildComponentView(view, args); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::UnmountChildComponentView != &BaseRCTCallout::UnmountChildComponentView) { + builder.SetUnmountChildComponentViewHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs &args) noexcept { + auto userData = view.UserData().as(); + return userData->UnmountChildComponentView(view, args); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseRCTCallout::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + auto userData = winrt::make_self(); + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseRCTCallout::Initialize) { + userData->Initialize(view); + } + view.UserData(*userData); + }); + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateVisual != &BaseRCTCallout::CreateVisual) { + compBuilder.SetCreateVisualHandler([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + auto userData = view.UserData().as(); + return userData->CreateVisual(view); + }); + } + + // Allow app to further customize the builder + if (builderCallback) { + builderCallback(compBuilder); + } + }); +} + +} // namespace winrt::FluentUI::Callout::Codegen + +#endif // #ifdef RNW_NEW_ARCH diff --git a/packages/components/Callout/windows/Callout/packages.lock.json b/packages/components/Callout/windows/Callout/packages.lock.json new file mode 100644 index 00000000000..b5730bc2ce8 --- /dev/null +++ b/packages/components/Callout/windows/Callout/packages.lock.json @@ -0,0 +1,151 @@ +{ + "version": 1, + "dependencies": { + "native,Version=v0.0": { + "boost": { + "type": "Direct", + "requested": "[1.84.0, )", + "resolved": "1.84.0", + "contentHash": "4el2YP3cNJDVFPdzOso+LxGvdWP2rHxML4siq8VdonNypW2m4q503tHfCj6vK0L1UfxioE2hpFGb4ITEua73tg==" + }, + "Microsoft.ReactNative": { + "type": "Direct", + "requested": "[0.81.32, )", + "resolved": "0.81.32", + "contentHash": "Ks1M8X7hk/yFF0LV4PymQelr1A2PqselgqBIuJIvJgI0bnO0OMXDXqj6C1SDJCAaKUa5U3/G78h+yUPa4EKsJQ==" + }, + "Microsoft.ReactNative.Cxx": { + "type": "Direct", + "requested": "[0.81.32, )", + "resolved": "0.81.32", + "contentHash": "7sk1LmAne4vDt5BWUg2drRw85ktACDdBLBMGK2y0iXWwZjayhmzkMBmVhX4zoN6t+bYu/Z2/LfyOVqQJCok7Sg==", + "dependencies": { + "Microsoft.ReactNative": "0.81.32" + } + }, + "Microsoft.VCRTForwarders.140": { + "type": "Direct", + "requested": "[1.0.2-rc, )", + "resolved": "1.0.2-rc", + "contentHash": "/r+sjtEeCIGyDhobIZ5hSmYhC/dSyGZxf1SxYJpElUhB0LMCktOMFs9gXrauXypIFECpVynNyVjAmJt6hjJ5oQ==" + }, + "Microsoft.Windows.CppWinRT": { + "type": "Direct", + "requested": "[2.0.230706.1, )", + "resolved": "2.0.230706.1", + "contentHash": "l0D7oCw/5X+xIKHqZTi62TtV+1qeSz7KVluNFdrJ9hXsst4ghvqQ/Yhura7JqRdZWBXAuDS0G0KwALptdoxweQ==" + }, + "Microsoft.WindowsAppSDK": { + "type": "Direct", + "requested": "[1.8.260508005, )", + "resolved": "1.8.260508005", + "contentHash": "+aA+zrvqJKgsn/1TPOSR0Uy7dkMO5jI/+cDWPu2pWmXe4KQxYkR8gQFY9IrfbgCxxSd/yl1zMcAKD/4HBbNqaw==", + "dependencies": { + "Microsoft.WindowsAppSDK.AI": "[1.8.76]", + "Microsoft.WindowsAppSDK.Base": "[1.8.251216001]", + "Microsoft.WindowsAppSDK.DWrite": "[1.8.25122902]", + "Microsoft.WindowsAppSDK.Foundation": "[1.8.260505001]", + "Microsoft.WindowsAppSDK.InteractiveExperiences": "[1.8.260430001]", + "Microsoft.WindowsAppSDK.ML": "[1.8.2197]", + "Microsoft.WindowsAppSDK.Runtime": "[1.8.260508005]", + "Microsoft.WindowsAppSDK.Widgets": "[1.8.251231004]", + "Microsoft.WindowsAppSDK.WinUI": "[1.8.260505002]" + } + }, + "Microsoft.Web.WebView2": { + "type": "Transitive", + "resolved": "1.0.3179.45", + "contentHash": "3pokSH5CnN0G6rGhGFo1y87inxYhNxBQ2Vdf0wlvBj99KHxQJormjDACmqRnFeUsmuNFIhWwfAL1ztq7wD5qRA==" + }, + "Microsoft.Windows.SDK.BuildTools": { + "type": "Transitive", + "resolved": "10.0.26100.4654", + "contentHash": "2mgcOlj/t2RfSyyw+pVESfO+Tk1RkfQzto9Vrq42M1lUQIfQEwbi8QLha9GXWIOj+TFzeHIEJckIoF25mgiM8A==" + }, + "Microsoft.Windows.SDK.BuildTools.MSIX": { + "type": "Transitive", + "resolved": "1.7.20250829.1", + "contentHash": "IMdvRmCIZnBS5GkYnv0po1bcx6U1OF39pqA4TphQ9evDzpCRoSE19/PkDvlUNNrBavTsLIEJgd/TAIFner75ow==" + }, + "Microsoft.WindowsAppSDK.AI": { + "type": "Transitive", + "resolved": "1.8.76", + "contentHash": "Ayn9QybcwzH+c8eQlE7dm2oO3Jrcn2uohLcsHJpCbLOAfcisjzwtSBe0oyulbaJ86R4eDSX3RDS25tsjGpIqyQ==", + "dependencies": { + "Microsoft.WindowsAppSDK.Base": "1.8.251216001", + "Microsoft.WindowsAppSDK.Foundation": "1.8.260501000" + } + }, + "Microsoft.WindowsAppSDK.Base": { + "type": "Transitive", + "resolved": "1.8.251216001", + "contentHash": "PS1wriuFknz3W2F2P/e6RvOTM35w89Lsj/f0QmUEPrJjKnc+jM0JLX1vfdytI14y1gNRUTm9uclwP0aH/SVU5w==", + "dependencies": { + "Microsoft.Windows.SDK.BuildTools": "10.0.26100.4654", + "Microsoft.Windows.SDK.BuildTools.MSIX": "1.7.20250829.1" + } + }, + "Microsoft.WindowsAppSDK.DWrite": { + "type": "Transitive", + "resolved": "1.8.25122902", + "contentHash": "zFNn07i7Cyz62Y8FnPQAyzeZK7ww3m9t42i9pzy4C04pNbyUDQ4fG7pB6VSh6n4EyFuYtuFQuDzt4mKmXFrkrg==", + "dependencies": { + "Microsoft.WindowsAppSDK.Base": "1.8.251216001" + } + }, + "Microsoft.WindowsAppSDK.Foundation": { + "type": "Transitive", + "resolved": "1.8.260505001", + "contentHash": "41SSoEn3sKKCAVPA/w18zVJwV1C7aDEkPS2f6Zyp19m27tDEmteEp0XS3Ln3b0ElYR4FfPEPvIDbJPSA9vePGw==", + "dependencies": { + "Microsoft.WindowsAppSDK.Base": "1.8.251216001", + "Microsoft.WindowsAppSDK.InteractiveExperiences": "1.8.260430001" + } + }, + "Microsoft.WindowsAppSDK.InteractiveExperiences": { + "type": "Transitive", + "resolved": "1.8.260430001", + "contentHash": "fTPCnQb3ZarMh9khlEfbLDllcZzK0tSP+2S6W/T4cPyLLSAmARm8Gd3AC5kqubnDjzTvG+1lNZ1bZGFsIHplJQ==", + "dependencies": { + "Microsoft.WindowsAppSDK.Base": "1.8.251216001" + } + }, + "Microsoft.WindowsAppSDK.ML": { + "type": "Transitive", + "resolved": "1.8.2197", + "contentHash": "6Bc1SOLd5HicY3GbF+zr76YBfH4iZOKeEGxTK/lHKAK2ExZTWavOADxB2CKSi/irF4dWSngUdRFopWPmckJ6fA==", + "dependencies": { + "Microsoft.WindowsAppSDK.Base": "1.8.251216001", + "Microsoft.WindowsAppSDK.Foundation": "1.8.260505001" + } + }, + "Microsoft.WindowsAppSDK.Runtime": { + "type": "Transitive", + "resolved": "1.8.260508005", + "contentHash": "2JqXzA4heHSkkaXJNyEvYtpv2wsEUMhwQzgXmZsFmYSkUXSdeVc6hdHcWoBK1SY9l1Sjd1brLt7h16kIpgh2kA==", + "dependencies": { + "Microsoft.WindowsAppSDK.Base": "1.8.251216001" + } + }, + "Microsoft.WindowsAppSDK.Widgets": { + "type": "Transitive", + "resolved": "1.8.251231004", + "contentHash": "bIWqQYR8DCoB1SoPOMil5AtgtkTn438wJTdpsHgyO/6o7Eh7PMP5BzrR0KbDsFqy+4LhPWQ4vtwko5k93fECcA==", + "dependencies": { + "Microsoft.WindowsAppSDK.Base": "1.8.251216001" + } + }, + "Microsoft.WindowsAppSDK.WinUI": { + "type": "Transitive", + "resolved": "1.8.260505002", + "contentHash": "/hGl6EOmo8aeJR2bYbOmGhOicnJK4EY+u+cQI+jm8H1uXThfMX32DsfKOt9LkOcu8AKX3j5FpiBay8ObUSytIw==", + "dependencies": { + "Microsoft.Web.WebView2": "1.0.3179.45", + "Microsoft.WindowsAppSDK.Base": "1.8.251216001", + "Microsoft.WindowsAppSDK.Foundation": "1.8.260505001", + "Microsoft.WindowsAppSDK.InteractiveExperiences": "1.8.260430001" + } + } + } + } +} \ No newline at end of file diff --git a/packages/components/Callout/windows/Callout/pch.cpp b/packages/components/Callout/windows/Callout/pch.cpp new file mode 100644 index 00000000000..bcb5590be1b --- /dev/null +++ b/packages/components/Callout/windows/Callout/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/packages/components/Callout/windows/Callout/pch.h b/packages/components/Callout/windows/Callout/pch.h new file mode 100644 index 00000000000..4eac9041ed7 --- /dev/null +++ b/packages/components/Callout/windows/Callout/pch.h @@ -0,0 +1,28 @@ +// pch.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#define NOMINMAX 1 +//#define WIN32_LEAN_AND_MEAN 1 +//#define WINRT_LEAN_AND_MEAN 1 + +// Windows Header Files +#include +#undef GetCurrentTime +#include + +// WinRT Header Files +#include +#include +#include + +// C RunTime Header Files +#include +#include +#include +#include + +// Reference additional headers your project requires here diff --git a/packages/components/Callout/windows/NuGet.Config b/packages/components/Callout/windows/NuGet.Config new file mode 100644 index 00000000000..aee3137fd74 --- /dev/null +++ b/packages/components/Callout/windows/NuGet.Config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/yarn.lock b/yarn.lock index 660957ab8ee..e0490e18b98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3035,6 +3035,7 @@ __metadata: resolution: "@fluentui-react-native/components@workspace:packages/agentic-components" dependencies: "@babel/core": "catalog:" + "@fluentui-react-native/callout": "workspace:*" "@fluentui-react-native/design": "workspace:*" "@fluentui-react-native/framework-base": "workspace:*" "@fluentui-react-native/scripts": "workspace:*" From 02bc11286c2f9b8b739549f50ec202d89d8dc50d Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:49:06 -0700 Subject: [PATCH 2/8] fix check-publishing --- packages/agentic-components/package.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/agentic-components/package.json b/packages/agentic-components/package.json index 4d67fc3713e..d13a9a9ab42 100644 --- a/packages/agentic-components/package.json +++ b/packages/agentic-components/package.json @@ -56,7 +56,8 @@ "react-native": "^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.6", "react-native-macos": "^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.0", "react-native-svg": ">=15.0.0 <15.13.0 || >=15.4.0 <15.13.0 || ^15.11.2 || ^15.12.1", - "react-native-windows": "^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.0" + "react-native-windows": "^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.0", + "@office-iss/react-native-win32": "^0.74.0 || ^0.81.0" }, "peerDependenciesMeta": { "@types/react": { @@ -67,6 +68,9 @@ }, "react-native-windows": { "optional": true + }, + "@office-iss/react-native-win32": { + "optional": true } }, "furn": { @@ -83,4 +87,4 @@ }, "extends": "@fluentui-react-native/scripts/kit-config" } -} +} \ No newline at end of file From 4524294d843fdb6d557ff5ed95b054f13221f614 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:49:45 -0700 Subject: [PATCH 3/8] change file --- .changeset/whole-keys-cry.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/whole-keys-cry.md diff --git a/.changeset/whole-keys-cry.md b/.changeset/whole-keys-cry.md new file mode 100644 index 00000000000..c0705c8070e --- /dev/null +++ b/.changeset/whole-keys-cry.md @@ -0,0 +1,6 @@ +--- +"@fluentui-react-native/components": patch +"@fluentui-react-native/callout": patch +--- + +Very initial framework for RNW fabric callout implementation From 4c0369d03b455b631b34ab00d37b4e41ef78d66a Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:52:56 -0700 Subject: [PATCH 4/8] Update lock file --- packages/agentic-components/package.json | 12 ++++++------ yarn.lock | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/agentic-components/package.json b/packages/agentic-components/package.json index d13a9a9ab42..22fce2aa794 100644 --- a/packages/agentic-components/package.json +++ b/packages/agentic-components/package.json @@ -51,15 +51,18 @@ "test-renderer": "catalog:0.81" }, "peerDependencies": { + "@office-iss/react-native-win32": "^0.74.0 || ^0.81.0", "@types/react": "~18.2.0 || ~19.0.0 || ~19.1.4", "react": "18.2.0 || 19.0.0 || 19.1.4", "react-native": "^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.6", "react-native-macos": "^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.0", "react-native-svg": ">=15.0.0 <15.13.0 || >=15.4.0 <15.13.0 || ^15.11.2 || ^15.12.1", - "react-native-windows": "^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.0", - "@office-iss/react-native-win32": "^0.74.0 || ^0.81.0" + "react-native-windows": "^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.0" }, "peerDependenciesMeta": { + "@office-iss/react-native-win32": { + "optional": true + }, "@types/react": { "optional": true }, @@ -68,9 +71,6 @@ }, "react-native-windows": { "optional": true - }, - "@office-iss/react-native-win32": { - "optional": true } }, "furn": { @@ -87,4 +87,4 @@ }, "extends": "@fluentui-react-native/scripts/kit-config" } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index e0490e18b98..a0ae34bc95a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3052,6 +3052,7 @@ __metadata: react-native-svg: "npm:^15.12.1" test-renderer: "catalog:0.81" peerDependencies: + "@office-iss/react-native-win32": ^0.74.0 || ^0.81.0 "@types/react": ~18.2.0 || ~19.0.0 || ~19.1.4 react: 18.2.0 || 19.0.0 || 19.1.4 react-native: ^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.6 @@ -3059,6 +3060,8 @@ __metadata: react-native-svg: ">=15.0.0 <15.13.0 || >=15.4.0 <15.13.0 || ^15.11.2 || ^15.12.1" react-native-windows: ^0.73.0 || ^0.74.0 || ^0.78.0 || ^0.81.0 peerDependenciesMeta: + "@office-iss/react-native-win32": + optional: true "@types/react": optional: true react-native-macos: From 64dca80fd9b98fe9e9641f94c7e44a9fc6ebd345 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:32:49 -0700 Subject: [PATCH 5/8] fix --- packages/components/Callout/src/Callout.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/Callout/src/Callout.stories.tsx b/packages/components/Callout/src/Callout.stories.tsx index 4220b45e0a7..87ddaf51e91 100644 --- a/packages/components/Callout/src/Callout.stories.tsx +++ b/packages/components/Callout/src/Callout.stories.tsx @@ -1,6 +1,6 @@ /** @jsxImportSource @fluentui-react-native/framework-base */ import * as React from 'react'; -import { Button, Pressable, StyleSheet, Text, View } from 'react-native'; +import { Pressable, StyleSheet, Text, View } from 'react-native'; import type { Meta, StoryObj } from '@storybook/react-native'; From 9d9a41eec02276a910738bf9799c96a5d550b9df Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:42:39 -0700 Subject: [PATCH 6/8] fix --- packages/agentic-components/tsconfig.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/agentic-components/tsconfig.json b/packages/agentic-components/tsconfig.json index 14a3238c308..6c3e9912969 100644 --- a/packages/agentic-components/tsconfig.json +++ b/packages/agentic-components/tsconfig.json @@ -9,6 +9,9 @@ "include": ["src"], "exclude": ["**/*.stories.ts", "**/*.stories.tsx"], "references": [ + { + "path": "../components/Callout/tsconfig.json" + }, { "path": "../agentic-design/tsconfig.json" }, From e375a45de64301c16a2492e2fb42f6c621cb04a8 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:41:12 -0700 Subject: [PATCH 7/8] format --- packages/components/Callout/react-native.config.cjs | 5 ++--- .../components/Callout/windows/Callout/codegen/.clang-format | 2 +- .../components/Callout/windows/Callout/packages.lock.json | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/components/Callout/react-native.config.cjs b/packages/components/Callout/react-native.config.cjs index 3f13ca929e2..5046885ab4d 100644 --- a/packages/components/Callout/react-native.config.cjs +++ b/packages/components/Callout/react-native.config.cjs @@ -1,6 +1,5 @@ - module.exports = { - dependency: { + dependency: { platforms: { windows: { sourceDir: 'windows', @@ -13,4 +12,4 @@ module.exports = { }, }, }, -} \ No newline at end of file +}; diff --git a/packages/components/Callout/windows/Callout/codegen/.clang-format b/packages/components/Callout/windows/Callout/codegen/.clang-format index a43d914ec38..9d159247d51 100644 --- a/packages/components/Callout/windows/Callout/codegen/.clang-format +++ b/packages/components/Callout/windows/Callout/codegen/.clang-format @@ -1,2 +1,2 @@ DisableFormat: true -SortIncludes: false \ No newline at end of file +SortIncludes: false diff --git a/packages/components/Callout/windows/Callout/packages.lock.json b/packages/components/Callout/windows/Callout/packages.lock.json index b5730bc2ce8..17e459d2609 100644 --- a/packages/components/Callout/windows/Callout/packages.lock.json +++ b/packages/components/Callout/windows/Callout/packages.lock.json @@ -148,4 +148,4 @@ } } } -} \ No newline at end of file +} From d23fdadf75097e41c03fb488bc575f48df9a5ea6 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Tue, 18 Aug 2026 09:44:30 -0700 Subject: [PATCH 8/8] fix knip error in CI --- packages/agentic-components/package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/agentic-components/package.json b/packages/agentic-components/package.json index 22fce2aa794..618f15c3e1a 100644 --- a/packages/agentic-components/package.json +++ b/packages/agentic-components/package.json @@ -74,7 +74,12 @@ } }, "furn": { - "jestPlatform": "macos" + "jestPlatform": "macos", + "knip": { + "ignoreDependencies": [ + "@fluentui-react-native/callout" + ] + } }, "rnx-kit": { "kitType": "library",