From 37317c052e09fc9c19d59c5c3e69292ac95839d6 Mon Sep 17 00:00:00 2001 From: vineethkuttan <66076509+vineethkuttan@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:30:22 +0530 Subject: [PATCH 1/5] Picker Xaml NativeComponent Added to Sample Fabric --- packages/sample-app-fabric/App.tsx | 38 ++- packages/sample-app-fabric/package.json | 3 +- .../packages.lock.json | 10 + .../windows/SampleAppFabric.sln | 14 + .../AutolinkedNativeModules.g.cpp | 6 +- .../AutolinkedNativeModules.g.targets | 4 + .../SampleAppFabric/packages.lock.json | 9 + .../src/PickerXamlNativeComponent.ts | 43 +++ packages/sample-custom-component/src/index.ts | 3 + .../SampleCustomComponent/PickerXaml.cpp | 212 +++++++++++++++ .../SampleCustomComponent/PickerXaml.h | 10 + .../ReactPackageProvider.cpp | 2 + .../SampleCustomComponent.vcxproj | 2 + .../SampleCustomComponent/PickerXaml.g.h | 247 ++++++++++++++++++ 14 files changed, 598 insertions(+), 5 deletions(-) create mode 100644 packages/sample-custom-component/src/PickerXamlNativeComponent.ts create mode 100644 packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp create mode 100644 packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.h create mode 100644 packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/PickerXaml.g.h diff --git a/packages/sample-app-fabric/App.tsx b/packages/sample-app-fabric/App.tsx index 3ac27a50420..a9abdcfaa67 100644 --- a/packages/sample-app-fabric/App.tsx +++ b/packages/sample-app-fabric/App.tsx @@ -5,20 +5,52 @@ * @format */ -import React from 'react'; -import {SafeAreaView, StatusBar, useColorScheme} from 'react-native'; +import React, {useState} from 'react'; +import { + SafeAreaView, + StatusBar, + useColorScheme, + Text, + View, +} from 'react-native'; import {NewAppScreen} from '@react-native/new-app-screen'; +import {PickerXaml} from 'sample-custom-component'; + +const pickerItems = [ + {label: 'JavaScript', value: 'js'}, + {label: 'TypeScript', value: 'ts'}, + {label: 'Python', value: 'py'}, + {label: 'C++', value: 'cpp'}, + {label: 'Rust', value: 'rs'}, +]; function App(): React.JSX.Element { const isDarkMode = useColorScheme() === 'dark'; + const [selectedIndex, setSelectedIndex] = useState(3); // Default to C++ + + const backgroundColor = isDarkMode ? '#1a1a1a' : '#f5f5f5'; + const textColor = isDarkMode ? '#ffffff' : '#000000'; return ( - + + + + Selected: {pickerItems[selectedIndex]?.label} + + { + setSelectedIndex(event.nativeEvent.itemIndex); + }} + /> + ); diff --git a/packages/sample-app-fabric/package.json b/packages/sample-app-fabric/package.json index 25c7f5d119e..0c0607b4731 100644 --- a/packages/sample-app-fabric/package.json +++ b/packages/sample-app-fabric/package.json @@ -18,7 +18,8 @@ "@typescript-eslint/parser": "^7.1.1", "react": "^19.1.1", "react-native": "0.82.0-nightly-20250902-9731e8ebc", - "react-native-windows": "^0.0.0-canary.1031" + "react-native-windows": "^0.0.0-canary.1031", + "sample-custom-component": "0.0.1" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/packages/sample-app-fabric/windows/SampleAppFabric.Package/packages.lock.json b/packages/sample-app-fabric/windows/SampleAppFabric.Package/packages.lock.json index d4f48cf99bc..c4416e656d3 100644 --- a/packages/sample-app-fabric/windows/SampleAppFabric.Package/packages.lock.json +++ b/packages/sample-app-fabric/windows/SampleAppFabric.Package/packages.lock.json @@ -185,6 +185,16 @@ "type": "Project", "dependencies": { "Microsoft.JavaScript.Hermes": "[0.0.0-2512.22001-bc3d0ed7, )", + "Microsoft.ReactNative": "[1.0.0, )", + "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", + "Microsoft.WindowsAppSDK": "[1.8.251106002, )", + "SampleCustomComponent": "[1.0.0, )", + "boost": "[1.83.0, )" + } + }, + "samplecustomcomponent": { + "type": "Project", + "dependencies": { "Microsoft.ReactNative": "[1.0.0, )", "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", "Microsoft.WindowsAppSDK": "[1.8.251106002, )", diff --git a/packages/sample-app-fabric/windows/SampleAppFabric.sln b/packages/sample-app-fabric/windows/SampleAppFabric.sln index 3335ca845cd..e562e1f033e 100644 --- a/packages/sample-app-fabric/windows/SampleAppFabric.sln +++ b/packages/sample-app-fabric/windows/SampleAppFabric.sln @@ -33,6 +33,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Mso", "..\..\..\vnext\Mso\M EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Include", "..\..\..\vnext\include\Include.vcxitems", "{EF074BA1-2D54-4D49-A28E-5E040B47CD2E}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SampleCustomComponent", "..\..\..\node_modules\sample-custom-component\windows\SampleCustomComponent\SampleCustomComponent.vcxproj", "{A8DA218C-4CB5-48CB-A9EE-9E6337165D07}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution ..\..\..\vnext\Shared\Shared.vcxitems*{2049dbe9-8d13-42c9-ae4b-413ae38fffd0}*SharedItemsImports = 9 @@ -150,6 +152,18 @@ Global {14B93DC8-FD93-4A6D-81CB-8BC96644501C}.Release|x86.ActiveCfg = Release|Win32 {14B93DC8-FD93-4A6D-81CB-8BC96644501C}.Release|x86.Build.0 = Release|Win32 {14B93DC8-FD93-4A6D-81CB-8BC96644501C}.Release|x86.Deploy.0 = Release|Win32 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|x64.ActiveCfg = Debug|x64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|x64.Build.0 = Debug|x64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|x86.ActiveCfg = Debug|Win32 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|x86.Build.0 = Debug|Win32 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|ARM64.Build.0 = Debug|ARM64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|x64.ActiveCfg = Release|x64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|x64.Build.0 = Release|x64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|x86.ActiveCfg = Release|Win32 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|x86.Build.0 = Release|Win32 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|ARM64.ActiveCfg = Release|ARM64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|ARM64.Build.0 = Release|ARM64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/packages/sample-app-fabric/windows/SampleAppFabric/AutolinkedNativeModules.g.cpp b/packages/sample-app-fabric/windows/SampleAppFabric/AutolinkedNativeModules.g.cpp index 5821cfddc55..b4f9b7ce9c8 100644 --- a/packages/sample-app-fabric/windows/SampleAppFabric/AutolinkedNativeModules.g.cpp +++ b/packages/sample-app-fabric/windows/SampleAppFabric/AutolinkedNativeModules.g.cpp @@ -3,12 +3,16 @@ #include "pch.h" #include "AutolinkedNativeModules.g.h" +// Includes from sample-custom-component +#include + namespace winrt::Microsoft::ReactNative { void RegisterAutolinkedNativeModulePackages(winrt::Windows::Foundation::Collections::IVector const& packageProviders) { - UNREFERENCED_PARAMETER(packageProviders); + // IReactPackageProviders from sample-custom-component + packageProviders.Append(winrt::SampleCustomComponent::ReactPackageProvider()); } } diff --git a/packages/sample-app-fabric/windows/SampleAppFabric/AutolinkedNativeModules.g.targets b/packages/sample-app-fabric/windows/SampleAppFabric/AutolinkedNativeModules.g.targets index 6a85ec51cd9..35f260ab39e 100644 --- a/packages/sample-app-fabric/windows/SampleAppFabric/AutolinkedNativeModules.g.targets +++ b/packages/sample-app-fabric/windows/SampleAppFabric/AutolinkedNativeModules.g.targets @@ -2,5 +2,9 @@ + + + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07} + diff --git a/packages/sample-app-fabric/windows/SampleAppFabric/packages.lock.json b/packages/sample-app-fabric/windows/SampleAppFabric/packages.lock.json index 8af9287d979..6c00291fc28 100644 --- a/packages/sample-app-fabric/windows/SampleAppFabric/packages.lock.json +++ b/packages/sample-app-fabric/windows/SampleAppFabric/packages.lock.json @@ -190,6 +190,15 @@ "Folly": "[1.0.0, )", "boost": "[1.83.0, )" } + }, + "samplecustomcomponent": { + "type": "Project", + "dependencies": { + "Microsoft.ReactNative": "[1.0.0, )", + "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", + "Microsoft.WindowsAppSDK": "[1.8.251106002, )", + "boost": "[1.83.0, )" + } } }, "native,Version=v0.0/win": { diff --git a/packages/sample-custom-component/src/PickerXamlNativeComponent.ts b/packages/sample-custom-component/src/PickerXamlNativeComponent.ts new file mode 100644 index 00000000000..48d1a7ebc21 --- /dev/null +++ b/packages/sample-custom-component/src/PickerXamlNativeComponent.ts @@ -0,0 +1,43 @@ +import {codegenNativeComponent} from 'react-native'; +import type { ViewProps } from 'react-native'; +import type { + Int32, + WithDefault, + BubblingEventHandler, +} from 'react-native/Libraries/Types/CodegenTypes'; + +/** + * Represents a single item in the PickerXaml. + */ +export type PickerXamlItem = Readonly<{ + label: string; + value?: string; +}>; + +/** + * Event payload when the picker selection changes. + */ +export type PickerXamlChangeEvent = Readonly<{ + value: string; + itemIndex: Int32; + text: string; +}>; + +export interface PickerXamlProps extends ViewProps { + /** + * Array of picker items to display. + */ + items: ReadonlyArray; + + /** + * The index of the currently selected item. + */ + selectedIndex?: WithDefault; + + /** + * Callback when selection changes. + */ + onPickerSelect?: BubblingEventHandler; +} + +export default codegenNativeComponent('PickerXaml'); diff --git a/packages/sample-custom-component/src/index.ts b/packages/sample-custom-component/src/index.ts index 49b2bd07d43..69288ff72c6 100644 --- a/packages/sample-custom-component/src/index.ts +++ b/packages/sample-custom-component/src/index.ts @@ -7,10 +7,13 @@ import CalendarView from './FabricXamlCalendarViewNativeComponent' import CustomAccessibility from './CustomAccessibilityNativeComponent'; +import PickerXaml from './PickerXamlNativeComponent'; + export { CustomAccessibility, DrawingIsland, MovingLight, MovingLightHandle, CalendarView, + PickerXaml }; \ No newline at end of file diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp new file mode 100644 index 00000000000..f76cca0b9c6 --- /dev/null +++ b/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "PickerXaml.h" + +#if defined(RNW_NEW_ARCH) + +#include "codegen/react/components/SampleCustomComponent/PickerXaml.g.h" + +#include +#include +#include +#include +#include +#include + +namespace winrt::SampleCustomComponent { + +// State to store the measured size +struct PickerXamlStateData : winrt::implements { + PickerXamlStateData(winrt::Windows::Foundation::Size ds) : desiredSize(ds) {} + winrt::Windows::Foundation::Size desiredSize; +}; + +struct PickerXamlComponentView : winrt::implements, + Codegen::BasePickerXaml { + void InitializeContentIsland( + const winrt::Microsoft::ReactNative::Composition::ContentIslandComponentView &islandView) { + // Create ComboBox for picker functionality + m_comboBox = winrt::Microsoft::UI::Xaml::Controls::ComboBox(); + m_comboBox.HorizontalAlignment(winrt::Microsoft::UI::Xaml::HorizontalAlignment::Stretch); + + // For editable ComboBox, trigger selection change always (not just on commit) + m_comboBox.SelectionChangedTrigger(winrt::Microsoft::UI::Xaml::Controls::ComboBoxSelectionChangedTrigger::Always); + + // Listen for size changes on the comboBox + m_comboBox.SizeChanged([this](auto const & /*sender*/, auto const & /*args*/) { RefreshSize(); }); + + // Listen for selection changes + m_selectionChangedRevoker = m_comboBox.SelectionChanged( + winrt::auto_revoke, [this](const auto & /*sender*/, const auto & /*args*/) { EmitPickerSelectEvent(); }); + + // Listen for text submitted (when user presses Enter in editable mode) + m_textSubmittedRevoker = m_comboBox.TextSubmitted( + winrt::auto_revoke, [this](const auto & /*sender*/, const auto & /*args*/) { EmitPickerSelectEvent(); }); + + m_island = winrt::Microsoft::UI::Xaml::XamlIsland{}; + m_island.Content(m_comboBox); + islandView.Connect(m_island.ContentIsland()); + m_islandView = winrt::make_weak(islandView); + } + + void UpdateProps( + const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::com_ptr &newProps, + const winrt::com_ptr &oldProps) noexcept override { + BasePickerXaml::UpdateProps(view, newProps, oldProps); + + // Suspend event handlers during programmatic updates to avoid triggering + // change events. Using RAII ensures handlers are always re-attached. + WithEventsSuspended([&]() { + // Always update items on first render, or when size changes + // (We can't compare items directly as the struct lacks operator==) + if (!oldProps || oldProps->items.size() != newProps->items.size() || m_comboBox.Items().Size() == 0) { + m_comboBox.Items().Clear(); + m_items.clear(); + + for (const auto &item : newProps->items) { + // Store item data + m_items.push_back(item); + + // Add item to ComboBox + auto comboBoxItem = winrt::Microsoft::UI::Xaml::Controls::ComboBoxItem(); + comboBoxItem.Content(winrt::box_value(winrt::to_hstring(item.label))); + m_comboBox.Items().Append(comboBoxItem); + } + } + + // Always update selected index on first render or when changed + if (!oldProps || oldProps->selectedIndex != newProps->selectedIndex || + m_comboBox.SelectedIndex() != newProps->selectedIndex) { + const int32_t selectedIndex = newProps->selectedIndex; + if (selectedIndex >= 0 && selectedIndex < static_cast(m_comboBox.Items().Size())) { + m_comboBox.SelectedIndex(selectedIndex); + } else { + m_comboBox.SelectedIndex(-1); + } + } + }); + + RefreshSize(); + } + + void UpdateState( + const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::IComponentState &newState) noexcept override { + m_state = newState; + } + + private: + void RefreshSize() { + m_comboBox.Measure(winrt::Windows::Foundation::Size{ + std::numeric_limits::infinity(), std::numeric_limits::infinity()}); + + const auto desiredSize = m_comboBox.DesiredSize(); + + if (m_state) { + const auto currentState = winrt::get_self(m_state.Data()); + if (desiredSize != currentState->desiredSize) { + m_state.UpdateStateWithMutation([desiredSize](winrt::Windows::Foundation::IInspectable /*data*/) { + return winrt::make(desiredSize); + }); + } + } + } + + void EmitPickerSelectEvent() { + if (auto eventEmitter = this->EventEmitter()) { + const int32_t selectedIndex = m_comboBox.SelectedIndex(); + + Codegen::PickerXaml_OnPickerSelect eventArgs; + eventArgs.itemIndex = selectedIndex; + + // Get the selected item value and text if available + if (selectedIndex >= 0 && selectedIndex < static_cast(m_items.size())) { + eventArgs.value = m_items[selectedIndex].value.value_or(""); + eventArgs.text = m_items[selectedIndex].label; + } else { + eventArgs.text = ""; + eventArgs.value = ""; + } + + eventEmitter->onPickerSelect(eventArgs); + } + } + + winrt::weak_ref m_islandView; + winrt::Microsoft::UI::Xaml::XamlIsland m_island{nullptr}; + winrt::Microsoft::UI::Xaml::Controls::ComboBox m_comboBox{nullptr}; + winrt::Microsoft::ReactNative::IComponentState m_state{nullptr}; + std::vector m_items; + winrt::Microsoft::UI::Xaml::Controls::ComboBox::SelectionChanged_revoker m_selectionChangedRevoker; + winrt::Microsoft::UI::Xaml::Controls::ComboBox::TextSubmitted_revoker m_textSubmittedRevoker; + + // RAII helper to temporarily suspend event handlers during programmatic updates. + // This avoids triggering change events when we're setting values from props. + template + void WithEventsSuspended(TAction action) { + m_selectionChangedRevoker.revoke(); + m_textSubmittedRevoker.revoke(); + + action(); + + m_selectionChangedRevoker = m_comboBox.SelectionChanged( + winrt::auto_revoke, [this](const auto & /*sender*/, const auto & /*args*/) { EmitPickerSelectEvent(); }); + m_textSubmittedRevoker = m_comboBox.TextSubmitted( + winrt::auto_revoke, [this](const auto & /*sender*/, const auto & /*args*/) { EmitPickerSelectEvent(); }); + } +}; + +} // namespace winrt::SampleCustomComponent + +void RegisterPickerXamlComponentView(winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) { + winrt::SampleCustomComponent::Codegen::RegisterPickerXamlNativeComponent< + winrt::SampleCustomComponent::PickerXamlComponentView>( + packageBuilder, + [](const winrt::Microsoft::ReactNative::Composition::IReactCompositionViewComponentBuilder &builder) { + builder.as().XamlSupport(true); + // Use SetContentIslandComponentViewInitializer + builder.SetContentIslandComponentViewInitializer( + [](const winrt::Microsoft::ReactNative::Composition::ContentIslandComponentView &islandView) noexcept { + auto userData = winrt::make_self(); + userData->InitializeContentIsland(islandView); + islandView.UserData(*userData); + }); + // Set up initial state with zero size + builder.as().SetInitialStateDataFactory( + [](const winrt::Microsoft::ReactNative::IComponentProps & /*props*/) noexcept { + return winrt::make( + winrt::Windows::Foundation::Size{0, 0}); + }); + + // Register the measure function - reads from state + builder.as().SetMeasureContentHandler( + [](winrt::Microsoft::ReactNative::ShadowNode const &shadowNode, + winrt::Microsoft::ReactNative::LayoutContext const &, + winrt::Microsoft::ReactNative::LayoutConstraints const &) noexcept { + const auto currentState = + winrt::get_self(shadowNode.StateData()); + + if (currentState && currentState->desiredSize.Width > 0) { + // Return the measured size from state + return currentState->desiredSize; + } + + // Return a default size if we don't have a measurement yet + return winrt::Windows::Foundation::Size{100, 32}; + }); + + // Handle state updates + builder.as().SetUpdateStateHandler( + [](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::IComponentState &newState) { + const auto islandView = view.as(); + const auto userData = islandView.UserData().as(); + userData->UpdateState(view, newState); + }); + }); +} + +#endif // defined(RNW_NEW_ARCH) diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.h b/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.h new file mode 100644 index 00000000000..dad8f0a44af --- /dev/null +++ b/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#if defined(RNW_NEW_ARCH) + +void RegisterPickerXamlComponentView(winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder); + +#endif // defined(RNW_NEW_ARCH) diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp index 29f8c8905e1..b9045b1f6e2 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp +++ b/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp @@ -11,6 +11,7 @@ #include "CustomAccessibility.h" #include "DrawingIsland.h" #include "MovingLight.h" +#include "PickerXaml.h" using namespace winrt::Microsoft::ReactNative; @@ -24,6 +25,7 @@ void ReactPackageProvider::CreatePackage(IReactPackageBuilder const &packageBuil RegisterMovingLightNativeComponent(packageBuilder); RegisterCalendarViewComponentView(packageBuilder); RegisterCustomAccessibilityComponentView(packageBuilder); + RegisterPickerXamlComponentView(packageBuilder); #endif // #ifdef RNW_NEW_ARCH } diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj b/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj index 2a9dae1e5fe..4cedd6c57e9 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj +++ b/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj @@ -102,6 +102,7 @@ + DrawingIsland.idl @@ -115,6 +116,7 @@ + Create diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/PickerXaml.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/PickerXaml.g.h new file mode 100644 index 00000000000..5c67b58b5e5 --- /dev/null +++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/PickerXaml.g.h @@ -0,0 +1,247 @@ + +/* + * This file is auto-generated from PickerXamlNativeComponent 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::SampleCustomComponent::Codegen { + +REACT_STRUCT(PickerXamlSpec_PickerXamlProps_items) +struct PickerXamlSpec_PickerXamlProps_items { + REACT_FIELD(label) + std::string label; + + REACT_FIELD(value) + std::optional value; +}; + +REACT_STRUCT(PickerXamlProps) +struct PickerXamlProps : winrt::implements { + PickerXamlProps(winrt::Microsoft::ReactNative::ViewProps props, const winrt::Microsoft::ReactNative::IComponentProps& cloneFrom) + : ViewProps(props) + { + if (cloneFrom) { + auto cloneFromProps = cloneFrom.as(); + items = cloneFromProps->items; + selectedIndex = cloneFromProps->selectedIndex; + } + } + + void SetProp(uint32_t hash, winrt::hstring propName, winrt::Microsoft::ReactNative::IJSValueReader value) noexcept { + winrt::Microsoft::ReactNative::ReadProp(hash, propName, value, *this); + } + + REACT_FIELD(items) + std::vector items; + + REACT_FIELD(selectedIndex) + int32_t selectedIndex{-1}; + + const winrt::Microsoft::ReactNative::ViewProps ViewProps; +}; + +REACT_STRUCT(PickerXaml_OnPickerSelect) +struct PickerXaml_OnPickerSelect { + REACT_FIELD(value) + std::string value; + + REACT_FIELD(itemIndex) + int32_t itemIndex{}; + + REACT_FIELD(text) + std::string text; +}; + +struct PickerXamlEventEmitter { + PickerXamlEventEmitter(const winrt::Microsoft::ReactNative::EventEmitter &eventEmitter) + : m_eventEmitter(eventEmitter) {} + + using OnPickerSelect = PickerXaml_OnPickerSelect; + + void onPickerSelect(OnPickerSelect &value) const { + m_eventEmitter.DispatchEvent(L"pickerSelect", [value](const winrt::Microsoft::ReactNative::IJSValueWriter writer) { + winrt::Microsoft::ReactNative::WriteValue(writer, value); + }); + } + + private: + winrt::Microsoft::ReactNative::EventEmitter m_eventEmitter{nullptr}; +}; + +template +struct BasePickerXaml { + + 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; + } + + + + 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 RegisterPickerXamlNativeComponent( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder, + std::function builderCallback) noexcept { + packageBuilder.as().AddViewComponent( + L"PickerXaml", [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 != &BasePickerXaml::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 != &BasePickerXaml::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); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::MountChildComponentView != &BasePickerXaml::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 != &BasePickerXaml::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 != &BasePickerXaml::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 != &BasePickerXaml::Initialize) { + userData->Initialize(view); + } + view.UserData(*userData); + }); + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateVisual != &BasePickerXaml::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::SampleCustomComponent::Codegen + +#endif // #ifdef RNW_NEW_ARCH From 01d866255711f507ba7025de516252750262265b Mon Sep 17 00:00:00 2001 From: vineethkuttan <66076509+vineethkuttan@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:32:24 +0530 Subject: [PATCH 2/5] Change Files --- ...ative-windows-c93ed00c-bbe5-46ff-b2a7-cacfd804e375.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-c93ed00c-bbe5-46ff-b2a7-cacfd804e375.json diff --git a/change/react-native-windows-c93ed00c-bbe5-46ff-b2a7-cacfd804e375.json b/change/react-native-windows-c93ed00c-bbe5-46ff-b2a7-cacfd804e375.json new file mode 100644 index 00000000000..41a6bca6dea --- /dev/null +++ b/change/react-native-windows-c93ed00c-bbe5-46ff-b2a7-cacfd804e375.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Picker Xaml NativeComponent added to Sample Fabric", + "packageName": "react-native-windows", + "email": "66076509+vineethkuttan@users.noreply.github.com", + "dependentChangeType": "patch" +} \ No newline at end of file From 9d9caeb99c566d2560e1f83e4b8ceb6a22ecbf7a Mon Sep 17 00:00:00 2001 From: vineethkuttan <66076509+vineethkuttan@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:43:27 +0530 Subject: [PATCH 3/5] using weak reference --- .../SampleCustomComponent/PickerXaml.cpp | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp index f76cca0b9c6..67e040cda23 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp +++ b/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp @@ -35,15 +35,28 @@ struct PickerXamlComponentView : winrt::implementsRefreshSize(); + } + }); // Listen for selection changes - m_selectionChangedRevoker = m_comboBox.SelectionChanged( - winrt::auto_revoke, [this](const auto & /*sender*/, const auto & /*args*/) { EmitPickerSelectEvent(); }); + m_selectionChangedRevoker = + m_comboBox.SelectionChanged(winrt::auto_revoke, [weakThis](const auto & /*sender*/, const auto & /*args*/) { + if (auto strongThis = weakThis.get()) { + strongThis->EmitPickerSelectEvent(); + } + }); // Listen for text submitted (when user presses Enter in editable mode) - m_textSubmittedRevoker = m_comboBox.TextSubmitted( - winrt::auto_revoke, [this](const auto & /*sender*/, const auto & /*args*/) { EmitPickerSelectEvent(); }); + m_textSubmittedRevoker = + m_comboBox.TextSubmitted(winrt::auto_revoke, [weakThis](const auto & /*sender*/, const auto & /*args*/) { + if (auto strongThis = weakThis.get()) { + strongThis->EmitPickerSelectEvent(); + } + }); m_island = winrt::Microsoft::UI::Xaml::XamlIsland{}; m_island.Content(m_comboBox); @@ -152,10 +165,19 @@ struct PickerXamlComponentView : winrt::implementsEmitPickerSelectEvent(); + } + }); + m_textSubmittedRevoker = + m_comboBox.TextSubmitted(winrt::auto_revoke, [weakThis](const auto & /*sender*/, const auto & /*args*/) { + if (auto strongThis = weakThis.get()) { + strongThis->EmitPickerSelectEvent(); + } + }); } }; From 02b0c88bab7e53641b9dfed05b4ce81742dc576e Mon Sep 17 00:00:00 2001 From: vineethkuttan <66076509+vineethkuttan@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:26:13 +0530 Subject: [PATCH 4/5] build fix --- .../windows/SampleCustomComponent/PickerXaml.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp index 67e040cda23..aaf653368e3 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp +++ b/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp @@ -35,7 +35,7 @@ struct PickerXamlComponentView : winrt::implementsRefreshSize(); @@ -165,7 +165,7 @@ struct PickerXamlComponentView : winrt::implements Date: Wed, 11 Feb 2026 15:57:15 +0530 Subject: [PATCH 5/5] fix build issues --- .../windows/SampleCustomComponent/PickerXaml.cpp | 2 +- .../react/components/SampleCustomComponent/PickerXaml.g.h | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp index aaf653368e3..dc44560655a 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp +++ b/packages/sample-custom-component/windows/SampleCustomComponent/PickerXaml.cpp @@ -132,7 +132,7 @@ struct PickerXamlComponentView : winrt::implementsEventEmitter()) { const int32_t selectedIndex = m_comboBox.SelectedIndex(); - Codegen::PickerXaml_OnPickerSelect eventArgs; + Codegen::PickerXamlSpec_onPickerSelect eventArgs; eventArgs.itemIndex = selectedIndex; // Get the selected item value and text if available diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/PickerXaml.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/PickerXaml.g.h index 5c67b58b5e5..22d5ea891c6 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/PickerXaml.g.h +++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/PickerXaml.g.h @@ -52,8 +52,8 @@ struct PickerXamlProps : winrt::implements