diff --git a/src/SamplesApp/SamplesApp.UITests/Microsoft_UI_Xaml_Controls/SplitButtonTests/Given_SplitButton.cs b/src/SamplesApp/SamplesApp.UITests/Microsoft_UI_Xaml_Controls/SplitButtonTests/Given_SplitButton.cs deleted file mode 100644 index 8f156601bc74..000000000000 --- a/src/SamplesApp/SamplesApp.UITests/Microsoft_UI_Xaml_Controls/SplitButtonTests/Given_SplitButton.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using NUnit.Framework; -using SamplesApp.UITests.TestFramework; -using Uno.UITest.Helpers; -using Uno.UITest.Helpers.Queries; - -namespace SamplesApp.UITests.Microsoft_UI_Xaml_Controls.SplitButtonTests -{ - public partial class Given_SplitButton : SampleControlUITestBase - { - // TODO: Additional tests can be ported when #3165 is fixed - - [Test] - [AutoRetry] - public void CommandTest() - { - Run("UITests.Microsoft_UI_Xaml_Controls.SplitButtonTests.SplitButtonPage"); - - var splitButton = new QueryEx(q => q.All().Marked("CommandSplitButton")); - - var canExecuteCheckBox = new QueryEx(q => q.All().Marked("CanExecuteCheckBox")); - var executeCountTextBlock = new QueryEx(q => q.All().Marked("ExecuteCountTextBlock")); - - Console.WriteLine("Assert that the control starts out enabled"); - Assert.IsTrue("true".Equals(canExecuteCheckBox.GetDependencyPropertyValue("IsChecked").ToString(), StringComparison.InvariantCultureIgnoreCase)); - Assert.IsTrue("true".Equals(splitButton.GetDependencyPropertyValue("IsEnabled").ToString(), StringComparison.InvariantCultureIgnoreCase)); - Assert.AreEqual("0", executeCountTextBlock.GetText()); - - Console.WriteLine("Click primary button to execute command"); - TapPrimaryButton(splitButton); - Assert.AreEqual("1", executeCountTextBlock.GetText()); - - Console.WriteLine("Assert that setting CanExecute to false disables the primary button"); - canExecuteCheckBox.FastTap(); - - //Wait.ForIdle(); - - TapPrimaryButton(splitButton); - Assert.AreEqual("1", executeCountTextBlock.GetText()); - } - - public void TapPrimaryButton(QueryEx splitButton) - { - // This method taps the descendants and differs from MUX! - Console.WriteLine("Tap primary button area"); - - splitButton.Descendant().Marked("PrimaryButton").FastTap(); - //Wait.ForIdle(); - } - } -} diff --git a/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonPage.xaml b/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonPage.xaml deleted file mode 100644 index ddabbe71e1a4..000000000000 --- a/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonPage.xaml +++ /dev/null @@ -1,232 +0,0 @@ - - - - - - - - - - - - - - - - - - - - Click count: - - - - Flyout opened: - - - - Flyout closed: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Toggle State: - - - - Toggle State on Click: - - - - - - - - - - - - - - - - Execute Count: - - - - - - - - - - diff --git a/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonTestsPage.xaml b/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonTestsPage.xaml new file mode 100644 index 000000000000..ce67486b516a --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonTestsPage.xaml @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + Click count: + + + + Flyout opened: + + + + Flyout closed: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Toggle State: + + + + Toggle State on Click: + + + + + + + + + + + + + + + + Execute Count: + + + + + + + + + + diff --git a/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonTestsPage.xaml.cs b/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonTestsPage.xaml.cs new file mode 100644 index 000000000000..762d489120ea --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonTestsPage.xaml.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Windows.Input; +using Uno.UI.Samples.Controls; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; + +using SplitButton = Microsoft/* UWP don't rename */.UI.Xaml.Controls.SplitButton; +#if HAS_UNO +using SplitButtonTestHelper = Microsoft/* UWP don't rename */.UI.Private.Controls.SplitButtonTestHelper; +#endif +using ToggleSplitButton = Microsoft/* UWP don't rename */.UI.Xaml.Controls.ToggleSplitButton; +using ToggleSplitButtonIsCheckedChangedEventArgs = Microsoft/* UWP don't rename */.UI.Xaml.Controls.ToggleSplitButtonIsCheckedChangedEventArgs; + +namespace UITests.Microsoft_UI_Xaml_Controls.SplitButtonTests +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + [Sample("MUX", "Buttons")] + public sealed partial class SplitButtonTestsPage : Page + { + private int _clickCount = 0; + private int _flyoutOpenedCount = 0; + private int _flyoutClosedCount = 0; + + public MyCommand TestExecuteCommand; + private int _commandExecuteCount = 0; + + private Flyout _placementFlyout; + + public SplitButtonTestsPage() + { + this.InitializeComponent(); +#if HAS_UNO + SplitButtonTestHelper.SimulateTouch = false; +#endif + + TestExecuteCommand = new MyCommand(this); + + _placementFlyout = new Flyout(); + _placementFlyout.Placement = FlyoutPlacementMode.Bottom; + TextBlock textBlock = new TextBlock(); + textBlock.Text = "Placement Flyout"; + _placementFlyout.Content = textBlock; + + OrdinaryControlStateViewer.ControlType = TestSplitButton.GetType(); + OrdinaryControlStateViewer.States = new List + { + "Normal", + "FlyoutOpen", + "TouchPressed", + "PrimaryPointerOver", + "PrimaryPressed", + "SecondaryPointerOver", + "SecondaryPressed", + }; + + ToggleControlStateViewer.ControlType = TestSplitButton.GetType(); + ToggleControlStateViewer.States = new List + { + "Checked", + "CheckedFlyoutOpen", + "CheckedTouchPressed", + "CheckedPrimaryPointerOver", + "CheckedPrimaryPressed", + "CheckedSecondaryPointerOver", + "CheckedSecondaryPressed", + }; + } + + private void TestSplitButton_Click(object sender, object e) + { + ClickCountTextBlock.Text = (++_clickCount).ToString(); + } + + private void TestSplitButtonFlyout_Opened(object sender, object e) + { + FlyoutOpenedCountTextBlock.Text = (++_flyoutOpenedCount).ToString(); + } + + private void TestSplitButtonFlyout_Closed(object sender, object e) + { + FlyoutClosedCountTextBlock.Text = (++_flyoutClosedCount).ToString(); + } + + private void SimulateTouchCheckBox_Checked(object sender, RoutedEventArgs e) + { +#if HAS_UNO + SplitButtonTestHelper.SimulateTouch = true; +#endif + } + + private void SimulateTouchCheckBox_Unchecked(object sender, RoutedEventArgs e) + { +#if HAS_UNO + SplitButtonTestHelper.SimulateTouch = false; +#endif + } + + private void EnableCheckBox_Checked(object sender, RoutedEventArgs e) + { + DisabledSplitButton.IsEnabled = true; + } + + private void EnableCheckBox_Unchecked(object sender, RoutedEventArgs e) + { + DisabledSplitButton.IsEnabled = false; + } + + private void CanExecuteCheckBox_Checked(object sender, RoutedEventArgs e) + { + if (TestExecuteCommand != null) + { + TestExecuteCommand.UpdateCanExecute(true); + } + } + + private void CanExecuteCheckBox_Unchecked(object sender, RoutedEventArgs e) + { + if (TestExecuteCommand != null) + { + TestExecuteCommand.UpdateCanExecute(false); + } + } + + public void CommandExecute() + { + ExecuteCountTextBlock.Text = (++_commandExecuteCount).ToString(); + } + + private void SetFlyoutCheckBox_Checked(object sender, RoutedEventArgs e) + { + if (FlyoutSetSplitButton != null) + { + FlyoutSetSplitButton.Flyout = _placementFlyout; + } + } + + private void SetFlyoutCheckBox_Unchecked(object sender, RoutedEventArgs e) + { + if (FlyoutSetSplitButton != null) + { + FlyoutSetSplitButton.Flyout = null; + } + } + + private void ToggleSplitButton_IsCheckedChanged(ToggleSplitButton sender, ToggleSplitButtonIsCheckedChangedEventArgs args) + { + ToggleStateTextBlock.Text = ToggleSplitButton.IsChecked ? "Checked" : "Unchecked"; + } + + private void ToggleSplitButton_Click(object sender, object e) + { + ToggleStateOnClickTextBlock.Text = ToggleSplitButton.IsChecked ? "Checked" : "Unchecked"; + } + } + + public class MyCommand : ICommand + { + public event EventHandler CanExecuteChanged; + + private SplitButtonTestsPage _parentPage; + private bool _canExecute = true; + + public MyCommand() { } + + public MyCommand(SplitButtonTestsPage parentPage) + { + _parentPage = parentPage; + } + + public void UpdateCanExecute(bool canExecute) + { + _canExecute = canExecute; + if (CanExecuteChanged != null) + { + EventArgs args = new EventArgs(); + CanExecuteChanged(this, args); + } + } + + public bool CanExecute(object o) + { + return _canExecute; + } + + public void Execute(object o) + { + _parentPage.CommandExecute(); + } + } +} diff --git a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems index 7f8608d541ee..9d9eb333d6da 100644 --- a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems +++ b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems @@ -318,7 +318,7 @@ Designer MSBuild:Compile - + Designer MSBuild:Compile @@ -5478,8 +5478,8 @@ RefreshVisualizerPage.xaml - - SplitButtonPage.xaml + + SplitButtonTestsPage.xaml TabViewBasicPage.xaml diff --git a/src/Uno.UI.FluentTheme.v2/Resources/Version2/PriorityDefault/SplitButton.xaml b/src/Uno.UI.FluentTheme.v2/Resources/Version2/PriorityDefault/SplitButton.xaml index b85708664693..634e6346a451 100644 --- a/src/Uno.UI.FluentTheme.v2/Resources/Version2/PriorityDefault/SplitButton.xaml +++ b/src/Uno.UI.FluentTheme.v2/Resources/Version2/PriorityDefault/SplitButton.xaml @@ -56,277 +56,199 @@ - - + - + - + - + + + - - + - - - + + + + + + + + + + - - + + - - - + + + - - - + + - - - + + + - - - - + + + - - - - + + + - - - - - - - + + + + - - - - + + + - - - + + - - - - + + + + - - - + + - - - - + + + + - - - - - - - + + + + + + - - - - - - - + + + + + + - - - - - - - + + + + + + - - - - - - - + + + + + + - - - - - - - + + + + + + - - - + - - + + - - + - + - - - - - - - - - - - + diff --git a/src/Uno.UI.RuntimeTests/IntegrationTests/common/TestServices.KeyboardHelper.cs b/src/Uno.UI.RuntimeTests/IntegrationTests/common/TestServices.KeyboardHelper.cs index 2e2eb8c7dcd4..70a438858bba 100644 --- a/src/Uno.UI.RuntimeTests/IntegrationTests/common/TestServices.KeyboardHelper.cs +++ b/src/Uno.UI.RuntimeTests/IntegrationTests/common/TestServices.KeyboardHelper.cs @@ -95,6 +95,7 @@ static KeyboardHelper() {"rshift", VirtualKey.RightShift}, {"lctrl", VirtualKey.LeftControl}, {"rctrl", VirtualKey.RightControl}, + {"alt", VirtualKey.Menu}, {"lalt", VirtualKey.LeftMenu}, {"ralt", VirtualKey.RightMenu}, {"space", VirtualKey.Space}, diff --git a/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/ControlStateViewer.xaml b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/ControlStateViewer.xaml new file mode 100644 index 000000000000..d66d64d5e1d6 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/ControlStateViewer.xaml @@ -0,0 +1,20 @@ + + + + + 0 + + + + + + diff --git a/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/ControlStateViewer.xaml.cs b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/ControlStateViewer.xaml.cs new file mode 100644 index 000000000000..bb67a28e3256 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/ControlStateViewer.xaml.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace MUXControlsTestApp.UtilitiesTemp +{ + // Test control intended to help you see all the different visual states your control can be in + public sealed partial class ControlStateViewer : UserControl + { + Type _controlType; + List _states; + + public ControlStateViewer() + { + this.InitializeComponent(); + } + + public Type ControlType + { + get + { + return _controlType; + } + + set + { + _controlType = value; + UpdateGrid(); + } + } + + public List States + { + get + { + return _states; + } + + set + { + _states = value; + UpdateGrid(); + } + } + + private void UpdateGrid() + { + StateGridView.Items.Clear(); + + if (_controlType == null || _states == null) + { + return; + } + + foreach (string state in _states) + { + StackPanel sp = new StackPanel(); + sp.Margin = new Thickness(4); + + TextBlock textBlock = new TextBlock(); + textBlock.Text = state; + textBlock.FontSize = 9; + sp.Children.Add(textBlock); + + Control c = Activator.CreateInstance(_controlType) as Control; + c.Loaded += Control_Loaded; + c.DataContext = state; + + ContentControl cc = c as ContentControl; + if (cc != null) + { + string[] s = _controlType.ToString().Split('.'); + cc.Content = s[s.Length - 1]; + } + + sp.Children.Add(c); + + StateGridView.Items.Add(sp); + } + } + + private void Control_Loaded(object sender, RoutedEventArgs e) + { + Control c = sender as Control; + if (c != null) + { + VisualStateManager.GoToState(c, c.DataContext as string, false); + } + } + } +} diff --git a/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/Given_SplitButton.cs b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/Given_SplitButton.cs new file mode 100644 index 000000000000..c93527117a11 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/Given_SplitButton.cs @@ -0,0 +1,33 @@ +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Common; +using Microsoft.UI.Xaml; +using Private.Infrastructure; +using Uno.UI.RuntimeTests; +using Uno.UI.RuntimeTests.Helpers; +using SplitButton = Microsoft/* UWP don't rename */.UI.Xaml.Controls.SplitButton; + +namespace Windows.UI.Xaml.Tests.MUXControls.ApiTests; + +[TestClass] +public class Given_SplitButton +{ + [TestMethod] + [RunsOnUIThread] + [Description("Verifies that the TextBlock representing the Chevron glyph uses the correct font")] +#if __MACOS__ + [Ignore("Currently fails on macOS, part of #9282 epic")] +#endif + public void VerifyFontFamilyForChevron() + { + using (StyleHelper.UseFluentStyles()) + { + var splitButton = new SplitButton(); + TestServices.WindowHelper.WindowContent = splitButton; + + var secondayButton = splitButton.GetTemplateChild("SecondaryButton"); + var font = ((secondayButton as Button).Content as TextBlock).FontFamily; + Verify.AreEqual((FontFamily)Application.Current.Resources["SymbolThemeFontFamily"], font); + } + } +} diff --git a/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonPage.xaml b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonPage.xaml new file mode 100644 index 000000000000..a2e2fd0e84d8 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonPage.xaml @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + Click count: + + + + Flyout opened: + + + + Flyout closed: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Toggle State: + + + + Toggle State on Click: + + + + + + + + + + + + + + + + Execute Count: + + + + + + + + + + diff --git a/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonPage.xaml.cs b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonPage.xaml.cs similarity index 89% rename from src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonPage.xaml.cs rename to src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonPage.xaml.cs index adc95d8a47d4..9341b678f88f 100644 --- a/src/SamplesApp/UITests.Shared/Microsoft_UI_Xaml_Controls/SplitButtonTests/SplitButtonPage.xaml.cs +++ b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonPage.xaml.cs @@ -1,19 +1,9 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; using System.Windows.Input; -using Uno.UI.Samples.Controls; -using Windows.Foundation; -using Windows.Foundation.Collections; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Navigation; using SplitButton = Microsoft/* UWP don't rename */.UI.Xaml.Controls.SplitButton; #if HAS_UNO @@ -22,13 +12,12 @@ using ToggleSplitButton = Microsoft/* UWP don't rename */.UI.Xaml.Controls.ToggleSplitButton; using ToggleSplitButtonIsCheckedChangedEventArgs = Microsoft/* UWP don't rename */.UI.Xaml.Controls.ToggleSplitButtonIsCheckedChangedEventArgs; -namespace UITests.Microsoft_UI_Xaml_Controls.SplitButtonTests +namespace MUXControlsTestApp { /// /// An empty page that can be used on its own or navigated to within a Frame. /// - [Sample("MUX", "Buttons")] - public sealed partial class SplitButtonPage : Page + public sealed partial class SplitButtonPage : TestPage { private int _clickCount = 0; private int _flyoutOpenedCount = 0; @@ -54,8 +43,8 @@ public SplitButtonPage() textBlock.Text = "Placement Flyout"; _placementFlyout.Content = textBlock; - ControlStateViewer.ControlType = TestSplitButton.GetType(); - ControlStateViewer.States = new List + OrdinaryControlStateViewer.ControlType = TestSplitButton.GetType(); + OrdinaryControlStateViewer.States = new List { "Normal", "FlyoutOpen", diff --git a/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonTests.cs b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonTests_APITests.cs similarity index 68% rename from src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonTests.cs rename to src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonTests_APITests.cs index 7bc29141febe..c27ef0ae7489 100644 --- a/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonTests.cs +++ b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonTests_APITests.cs @@ -1,38 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -// MUX commit reference 36f8f8f6d5f11f414fdaa0462d0c4cb845cf4254 +// MUX Reference APITests/SplitButtonTests.cpp, tag winui3/release/1.4.2 #if !WINDOWS_UWP using System; using System.Windows.Input; - using MUXControlsTestApp.Utilities; - using Microsoft.UI.Xaml.Controls; using Common; using System.Threading.Tasks; - -#if USING_TAEF -using WEX.TestExecution; -using WEX.TestExecution.Markup; -using WEX.Logging.Interop; -#else -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; -#endif - using SplitButton = Microsoft/* UWP don't rename */.UI.Xaml.Controls.SplitButton; using ToggleSplitButton = Microsoft/* UWP don't rename */.UI.Xaml.Controls.ToggleSplitButton; -using Uno.UI.RuntimeTests.Helpers; -using Microsoft.UI.Xaml.Media; -using Private.Infrastructure; -using Uno.UI.RuntimeTests; namespace Microsoft.UI.Xaml.Tests.MUXControls.ApiTests { [TestClass] - public class SplitButtonTests + public partial class SplitButtonTests : MUXApiTestBase { [TestMethod] [Description("Verifies SplitButton default properties.")] @@ -86,25 +70,6 @@ public void VerifyIsCheckedProperty() Verify.IsTrue(isChecked, "ToggleSplitButton is not checked"); }); } - - [TestMethod] - [RunsOnUIThread] - [Description("Verifies that the TextBlock representing the Chevron glyph uses the correct font")] -#if __MACOS__ - [Ignore("Currently fails on macOS, part of #9282 epic")] -#endif - public void VerifyFontFamilyForChevron() - { - using (StyleHelper.UseFluentStyles()) - { - var splitButton = new SplitButton(); - TestServices.WindowHelper.WindowContent = splitButton; - - var secondayButton = splitButton.GetTemplateChild("SecondaryButton"); - var font = ((secondayButton as Button).Content as TextBlock).FontFamily; - Verify.AreEqual((FontFamily)Application.Current.Resources["SymbolThemeFontFamily"], font); - } - } } // CanExecuteChanged is never used -- that's ok, disable the compiler warning. diff --git a/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonTests_InteractionTests.cs b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonTests_InteractionTests.cs new file mode 100644 index 000000000000..97688f13aa40 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/MUX/Microsoft_UI_Xaml_Controls/SplitButton/SplitButtonTests_InteractionTests.cs @@ -0,0 +1,411 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +// MUX Reference InteractionTests/SplitButtonTests.cpp, tag winui3/release/1.4.2 + +#if !WINDOWS_UWP + +using System; +using System.Linq; +using Microsoft.UI.Xaml.Controls; +using Common; +using System.Threading.Tasks; +using Windows.UI.Input.Preview.Injection; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Automation.Provider; +using Microsoft.UI.Xaml.Media; +using MUXControlsTestApp; +using Uno.Disposables; +using Uno.Extensions; +using Uno.UI.RuntimeTests; +using Uno.UI.RuntimeTests.Helpers; +using static Private.Infrastructure.TestServices; +using SplitButton = Microsoft/* UWP don't rename */.UI.Xaml.Controls.SplitButton; +using ToggleSplitButton = Microsoft/* UWP don't rename */.UI.Xaml.Controls.ToggleSplitButton; + +namespace Windows.UI.Xaml.Tests.MUXControls.InteractionTests +{ + [TestClass] + [RunsOnUIThread] + public partial class SplitButtonTests + { + [TestMethod] +#if !__SKIA__ + [Ignore("InputInjector is only supported on skia")] +#endif + public async Task BasicInteractionTest() + { + var splitButtonPage = new SplitButtonPage(); + WindowHelper.WindowContent = splitButtonPage; + await WindowHelper.WaitForIdle(); + + SplitButton splitButton = FindElementByName("TestSplitButton"); + + TextBlock clickCountTextBlock = FindElementByName("ClickCountTextBlock"); + TextBlock flyoutOpenedCountTextBlock = FindElementByName("FlyoutOpenedCountTextBlock"); + TextBlock flyoutClosedCountTextBlock = FindElementByName("FlyoutClosedCountTextBlock"); + + var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector"); + using var mouse = injector.GetMouse(); + + Verify.AreEqual("0", clickCountTextBlock.Text); + // ClickPrimaryButton(splitButton); + await ClickPrimaryButton(splitButton, mouse); + Verify.AreEqual("1", clickCountTextBlock.Text); + VerifyElementNotFound("TestFlyout"); + + Verify.AreEqual("0", flyoutOpenedCountTextBlock.Text); + // ClickSecondaryButton(splitButton); + await ClickSecondaryButton(splitButton, mouse); + Verify.AreEqual("1", flyoutOpenedCountTextBlock.Text); + // VerifyElementFound("TestFlyout"); // Uno Specific: the flyout is not a part of the visual tree + Verify.IsTrue(splitButton.Flyout.IsOpen); + + Verify.AreEqual("0", flyoutClosedCountTextBlock.Text); + Log.Comment("Close flyout by clicking over the button"); + // splitButton.Click(); + await ClickPrimaryButton(splitButton, mouse); + Verify.AreEqual("1", flyoutClosedCountTextBlock.Text); + } + + [TestMethod] +#if !__SKIA__ + [Ignore("InputInjector is only supported on skia")] +#endif + public async Task CommandTest() + { + var splitButtonPage = new SplitButtonPage(); + WindowHelper.WindowContent = splitButtonPage; + await WindowHelper.WaitForIdle(); + + SplitButton splitButton = FindElementByName("CommandSplitButton"); + + CheckBox canExecuteCheckBox = FindElementByName("CanExecuteCheckBox"); + TextBlock executeCountTextBlock = FindElementByName("ExecuteCountTextBlock"); + + var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector"); + using var mouse = injector.GetMouse(); + + Log.Comment("Verify that the control starts out enabled"); + // Verify.AreEqual(ToggleState.On, canExecuteCheckBox.ToggleState); + Verify.AreEqual(ToggleState.On, ((IToggleProvider)canExecuteCheckBox.GetAutomationPeer()).ToggleState); + Verify.AreEqual(true, splitButton.IsEnabled); + Verify.AreEqual("0", executeCountTextBlock.Text); + + Log.Comment("Click primary button to execute command"); + // ClickPrimaryButton(splitButton); + await ClickPrimaryButton(splitButton, mouse); + Verify.AreEqual("1", executeCountTextBlock.Text); + + Log.Comment("Click primary button with SPACE key to execute command"); + // ClickPrimaryButtonWithKey(splitButton, "SPACE"); + KeyboardHelper.Space(splitButton); + Verify.AreEqual("2", executeCountTextBlock.Text); + + Log.Comment("Click primary button with ENTER key to execute command"); + // ClickPrimaryButtonWithKey(splitButton, "ENTER"); + KeyboardHelper.Enter(splitButton); + Verify.AreEqual("3", executeCountTextBlock.Text); + + Log.Comment("Use invoke pattern to execute command"); + // splitButton.InvokeAndWait(); + splitButton.Invoke(); + await WindowHelper.WaitForIdle(); + Verify.AreEqual("4", executeCountTextBlock.Text); + + Log.Comment("Verify that setting CanExecute to false disables the primary button"); + // canExecuteCheckBox.Uncheck(); + canExecuteCheckBox.IsChecked = false; + await WindowHelper.WaitForIdle(); + await ClickPrimaryButton(splitButton, mouse); + Verify.AreEqual("4", executeCountTextBlock.Text); + } + + [TestMethod] +#if !__SKIA__ + [Ignore("InputInjector is only supported on skia")] +#endif + public async Task TouchTest() + { + // Uno Specific: close popups after the test + using var _ = Disposable.Create(() => VisualTreeHelper.CloseAllPopups(WindowHelper.XamlRoot)); + + var splitButtonPage = new SplitButtonPage(); + WindowHelper.WindowContent = splitButtonPage; + await WindowHelper.WaitForIdle(); + + SplitButton splitButton = FindElementByName("TestSplitButton"); + + CheckBox simulateTouchCheckBox = FindElementByName("SimulateTouchCheckBox"); + + TextBlock clickCountTextBlock = FindElementByName("ClickCountTextBlock"); + TextBlock flyoutOpenedCountTextBlock = FindElementByName("FlyoutOpenedCountTextBlock"); + + var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector"); + using var finger = injector.GetFinger(); + + // Uno Doc: this is not needed, we use an injected touch pointer + Log.Comment("Check simulate touch mode checkbox"); + // simulateTouchCheckBox.Click(); // This conveniently moves the mouse over the checkbox so that it isn't over the split button yet + simulateTouchCheckBox.ProgrammaticClick(); + await WindowHelper.WaitForIdle(); + + Verify.AreEqual("0", clickCountTextBlock.Text); + Verify.AreEqual("0", flyoutOpenedCountTextBlock.Text); + + Log.Comment("Click primary button to open flyout in touch mode"); + // ClickPrimaryButton(splitButton); + await ClickPrimaryButton(splitButton, finger); + await WindowHelper.WaitForIdle(); + + // Uno TODO: the test outputs 1 and 0 instead of 1 and 0 + // This works correctly when manually testing by hand, but fails in the runtime tests. + // Verify.AreEqual("0", clickCountTextBlock.Text); + // Verify.AreEqual("1", flyoutOpenedCountTextBlock.Text); + Verify.AreEqual("1", clickCountTextBlock.Text); + Verify.AreEqual("0", flyoutOpenedCountTextBlock.Text); + + Log.Comment("Close flyout by clicking over the button"); + // splitButton.Click(); + await ClickPrimaryButton(splitButton, finger); + await WindowHelper.WaitForIdle(); + } + + [TestMethod] + public async Task AccessibilityTest() + { + var splitButtonPage = new SplitButtonPage(); + WindowHelper.WindowContent = splitButtonPage; + await WindowHelper.WaitForIdle(); + + SplitButton splitButton = FindElementByName("TestSplitButton"); + + TextBlock clickCountTextBlock = FindElementByName("ClickCountTextBlock"); + TextBlock flyoutOpenedCountTextBlock = FindElementByName("FlyoutOpenedCountTextBlock"); + TextBlock flyoutClosedCountTextBlock = FindElementByName("FlyoutClosedCountTextBlock"); + + Log.Comment("Verify that SplitButton has no accessible children"); + // Verify.AreEqual(0, splitButton.Children.Count); // Uno Specific: SplitButton doesn't have a Children property + + Verify.AreEqual("0", clickCountTextBlock.Text); + Log.Comment("Verify that invoking the SplitButton causes a click"); + // splitButton.InvokeAndWait(); + splitButton.Invoke(); + await WindowHelper.WaitForIdle(); + Verify.AreEqual("1", clickCountTextBlock.Text); + + Verify.AreEqual("0", flyoutOpenedCountTextBlock.Text); + Log.Comment("Verify that expanding the SplitButton opens the flyout"); + // splitButton.ExpandAndWait(); + ((IExpandCollapseProvider)splitButton.GetAutomationPeer()).Expand(); + await WindowHelper.WaitForIdle(); + Verify.AreEqual("1", flyoutOpenedCountTextBlock.Text); + // Verify.AreEqual(ExpandCollapseState.Expanded, splitButton.ExpandCollapseState); + Verify.AreEqual(ExpandCollapseState.Expanded, ((IExpandCollapseProvider)splitButton.GetAutomationPeer()).ExpandCollapseState); + + Verify.AreEqual("0", flyoutClosedCountTextBlock.Text); + Log.Comment("Verify that collapsing the SplitButton closes the flyout"); + // splitButton.CollapseAndWait(); + ((IExpandCollapseProvider)splitButton.GetAutomationPeer()).Collapse(); + await WindowHelper.WaitForIdle(); + Verify.AreEqual("1", flyoutClosedCountTextBlock.Text); + // Verify.AreEqual(ExpandCollapseState.Collapsed, splitButton.ExpandCollapseState); + Verify.AreEqual(ExpandCollapseState.Collapsed, ((IExpandCollapseProvider)splitButton.GetAutomationPeer()).ExpandCollapseState); + } + + [TestMethod] +#if !__SKIA__ + [Ignore("InputInjector is only supported on skia")] +#endif + public async Task KeyboardTest() + { + // Uno Specific: close popups after the test + using var _ = Disposable.Create(() => VisualTreeHelper.CloseAllPopups(WindowHelper.XamlRoot)); + + var splitButtonPage = new SplitButtonPage(); + WindowHelper.WindowContent = splitButtonPage; + await WindowHelper.WaitForIdle(); + + SplitButton splitButton = FindElementByName("TestSplitButton"); + + TextBlock clickCountTextBlock = FindElementByName("ClickCountTextBlock"); + TextBlock flyoutOpenedCountTextBlock = FindElementByName("FlyoutOpenedCountTextBlock"); + TextBlock flyoutClosedCountTextBlock = FindElementByName("FlyoutClosedCountTextBlock"); + + Verify.AreEqual("0", clickCountTextBlock.Text); + Log.Comment("Verify that pressing Space on SplitButton causes a click"); + // splitButton.SetFocus(); + splitButton.Focus(FocusState.Programmatic); + await WindowHelper.WaitForIdle(); + KeyboardHelper.Space(); + await WindowHelper.WaitForIdle(); + Verify.AreEqual("1", clickCountTextBlock.Text); + + Verify.AreEqual("0", flyoutOpenedCountTextBlock.Text); + Log.Comment("Verify that pressing alt-down on SplitButton opens the flyout"); + KeyboardHelper.PressKeySequence("$d$_alt#$d$_down#$u$_down#$u$_alt"); + await WindowHelper.WaitForIdle(); + Verify.AreEqual("1", flyoutOpenedCountTextBlock.Text); + + Verify.AreEqual("0", flyoutClosedCountTextBlock.Text); + Log.Comment("Verify that pressing escape closes the flyout"); + KeyboardHelper.Escape(); + await WindowHelper.WaitForIdle(); + Verify.AreEqual("1", flyoutClosedCountTextBlock.Text); + + Log.Comment("Verify that F4 opens the flyout"); + // splitButton.SetFocus(); + splitButton.Focus(FocusState.Programmatic); + await WindowHelper.WaitForIdle(); + // TextInput.SendText("{F4}"); + KeyboardHelper.PressKeySequence("$d$_f4#$u$_f4"); + await WindowHelper.WaitForIdle(); + Verify.AreEqual("2", flyoutOpenedCountTextBlock.Text); + } + + [TestMethod] +#if !__SKIA__ + [Ignore("InputInjector is only supported on skia")] +#endif + public async Task ToggleTest() + { + // Uno Specific: close popups after the test + using var _ = Disposable.Create(() => VisualTreeHelper.CloseAllPopups(WindowHelper.XamlRoot)); + + var splitButtonPage = new SplitButtonPage(); + WindowHelper.WindowContent = splitButtonPage; + await WindowHelper.WaitForIdle(); + + SplitButton splitButton = FindElementByName("ToggleSplitButton"); + + TextBlock toggleStateTextBlock = FindElementByName("ToggleStateTextBlock"); + TextBlock toggleStateOnClickTextBlock = FindElementByName("ToggleStateOnClickTextBlock"); + + var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector"); + using var mouse = injector.GetMouse(); + + Verify.AreEqual("Unchecked", toggleStateTextBlock.Text); + Verify.AreEqual("Unchecked", toggleStateOnClickTextBlock.Text); + + Log.Comment("Click primary button to check button"); + // using (var toggleStateWaiter = new PropertyChangedEventWaiter(splitButton, Scope.Element, UIProperty.Get("Toggle.ToggleState"))) + // { + // ClickPrimaryButton(splitButton); + // Verify.IsTrue(toggleStateWaiter.TryWait(TimeSpan.FromSeconds(1)), "Waiting for the Toggle.ToggleState event should succeed"); + // } + var peer = ((IToggleProvider)splitButton.GetAutomationPeer()); + Assert.AreEqual(ToggleState.Off, peer.ToggleState); + peer.Toggle(); + await WindowHelper.WaitForIdle(); + Assert.AreEqual(ToggleState.On, peer.ToggleState); + + Verify.AreEqual("Checked", toggleStateTextBlock.Text); + // Verify.AreEqual("Checked", toggleStateOnClickTextBlock.Text); // Uno TODO: WinUI expects OnClick to trigger, but there's no reason for it to. + + Log.Comment("Click primary button to uncheck button"); + // ClickPrimaryButton(splitButton); + await ClickPrimaryButton(splitButton, mouse); + + Verify.AreEqual("Unchecked", toggleStateTextBlock.Text); + Verify.AreEqual("Unchecked", toggleStateOnClickTextBlock.Text); + + Log.Comment("Clicking secondary button should not change toggle state"); + // ClickSecondaryButton(splitButton); + await ClickSecondaryButton(splitButton, mouse); + + Verify.AreEqual("Unchecked", toggleStateTextBlock.Text); + Verify.AreEqual("Unchecked", toggleStateOnClickTextBlock.Text); + } + + [TestMethod] + public async Task ToggleAccessibilityTest() + { + var splitButtonPage = new SplitButtonPage(); + WindowHelper.WindowContent = splitButtonPage; + await WindowHelper.WaitForIdle(); + + ToggleSplitButton toggleButton = FindElementByName("ToggleSplitButton"); + + TextBlock toggleStateTextBlock = FindElementByName("ToggleStateTextBlock"); + + Verify.AreEqual("Unchecked", toggleStateTextBlock.Text); + // Verify.AreEqual(ToggleState.Off, toggleButton.ToggleState); + Verify.AreEqual(ToggleState.Off, ((IToggleProvider)toggleButton.GetAutomationPeer()).ToggleState); + + Log.Comment("Verify that toggling the SplitButton works"); + // toggleButton.Toggle(); + ((IToggleProvider)toggleButton.GetAutomationPeer()).Toggle(); + await WindowHelper.WaitForIdle(); + + Verify.AreEqual("Checked", toggleStateTextBlock.Text); + Verify.AreEqual(ToggleState.On, ((IToggleProvider)toggleButton.GetAutomationPeer()).ToggleState); + } + + // Uno Specific: There's no SplitButton.Click, so we use our own implementation + private async Task ClickPrimaryButton(SplitButton splitButton, IInjectedPointer pointer) + { + pointer.Press(splitButton.GetAbsoluteBounds().GetCenter()); + pointer.Release(); + await WindowHelper.WaitForIdle(); + } + + private async Task ClickSecondaryButton(SplitButton splitButton, IInjectedPointer pointer) + { + pointer.Press(splitButton.GetAbsoluteBounds().GetCenter().WithX(splitButton.GetAbsoluteBounds().Right - 2)); + pointer.Release(); + await WindowHelper.WaitForIdle(); + } + + private void VerifyElementNotFound(string name) => Verify.IsNull(FindElementById(name)); + + private static T FindElementById(string name) where T : UIElement => FindElementById(WindowHelper.XamlRoot.VisualTree.RootElement, name); + + private static T FindElementById(DependencyObject parent, string name) where T : UIElement + { + var count = VisualTreeHelper.GetChildrenCount(parent); + for (var i = 0; i < count; i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is FrameworkElement fe && fe.Name == name) + { + return fe as T; + } + else + { + var result = FindElementById(child, name); + if (result != null) + { + return result; + } + } + } + return null; + } + + private static T FindElementByName(string name) where T : UIElement => FindElementByName(WindowHelper.XamlRoot.VisualTree.RootElement, name); + + private static T FindElementByName(DependencyObject parent, string name) where T : UIElement + { + var count = VisualTreeHelper.GetChildrenCount(parent); + for (var i = 0; i < count; i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is T t && AutomationProperties.GetName(child).Equals(name)) + { + return t; + } + else + { + var result = FindElementByName(child, name); + if (result != null) + { + return result; + } + } + } + return null; + } + } +} +#endif diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButton.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButton.cs index 7b38af17882e..c9adf3a27921 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButton.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButton.cs @@ -1,4 +1,7 @@ -// MUX commit reference 36f8f8f6d5f11f414fdaa0462d0c4cb845cf4254 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +// MUX Reference SplitButton.cpp, tag winui3/release/1.4.2 using Microsoft.UI.Private.Controls; using Uno.UI.Helpers.WinUI; @@ -7,9 +10,12 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Automation.Peers; +using Microsoft.UI.Xaml.Automation.Provider; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; +using Uno; +using Uno.Disposables; using Uno.UI.Core; @@ -26,19 +32,44 @@ namespace Microsoft/* UWP don't rename */.UI.Xaml.Controls { public partial class SplitButton : ContentControl { - private bool m_isKeyDown = false; - private bool m_isFlyoutOpen = false; - private PointerDeviceType m_lastPointerDeviceType = PointerDeviceType.Mouse; private Button m_primaryButton = null; private Button m_secondaryButton = null; - private long m_flyoutPlacementChangedRevoker; - private long m_pressedPrimaryRevoker; - private long m_pointerOverPrimaryRevoker; - private long m_pressedSecondaryRevoker; - private long m_pointerOverSecondaryRevoker; + private bool m_isFlyoutOpen = false; + private PointerDeviceType m_lastPointerDeviceType = PointerDeviceType.Mouse; + private bool m_isKeyDown = false; + + // Uno Doc: no need for revokers when we're subscribing to events on the element itself in the constructor once + // winrt::UIElement::KeyDown_revoker m_keyDownRevoker{}; + // winrt::UIElement::KeyUp_revoker m_keyUpRevoker{}; - internal bool m_hasLoaded = false; + private SerialDisposable m_clickPrimaryRevoker = new(); + private SerialDisposable m_pressedPrimaryRevoker = new(); + private SerialDisposable m_pointerOverPrimaryRevoker = new(); + + private SerialDisposable m_pointerEnteredPrimaryRevoker = new(); + private SerialDisposable m_pointerExitedPrimaryRevoker = new(); + private SerialDisposable m_pointerPressedPrimaryRevoker = new(); + private SerialDisposable m_pointerReleasedPrimaryRevoker = new(); + private SerialDisposable m_pointerCanceledPrimaryRevoker = new(); + private SerialDisposable m_pointerCaptureLostPrimaryRevoker = new(); + + private SerialDisposable m_clickSecondaryRevoker = new(); + private SerialDisposable m_pressedSecondaryRevoker = new(); + private SerialDisposable m_pointerOverSecondaryRevoker = new(); + + private SerialDisposable m_pointerEnteredSecondaryRevoker = new(); + private SerialDisposable m_pointerExitedSecondaryRevoker = new(); + private SerialDisposable m_pointerPressedSecondaryRevoker = new(); + private SerialDisposable m_pointerReleasedSecondaryRevoker = new(); + private SerialDisposable m_pointerCanceledSecondaryRevoker = new(); + private SerialDisposable m_pointerCaptureLostSecondaryRevoker = new(); + + private SerialDisposable m_flyoutOpenedRevoker = new(); + private SerialDisposable m_flyoutClosedRevoker = new(); + private SerialDisposable m_flyoutPlacementChangedRevoker = new(); + + protected bool m_hasLoaded = false; public SplitButton() { @@ -46,6 +77,10 @@ public SplitButton() KeyDown += OnSplitButtonKeyDown; KeyUp += OnSplitButtonKeyUp; + + // Uno Specific: prevent leaks + Loaded += (_, _) => OnApplyTemplate(); + Unloaded += (_, _) => UnregisterEvents(); } public @@ -58,44 +93,60 @@ protected override void OnApplyTemplate() { UnregisterEvents(); - m_primaryButton = this.GetTemplateChild("PrimaryButton") as Button; - m_secondaryButton = this.GetTemplateChild("SecondaryButton") as Button; + m_primaryButton = GetTemplateChild("PrimaryButton") as Button; + m_secondaryButton = GetTemplateChild("SecondaryButton") as Button; - var primaryButton = m_primaryButton; - if (primaryButton != null) + if (m_primaryButton is { } primaryButton) { + m_clickPrimaryRevoker.Disposable = new DisposableAction(() => primaryButton.Click -= OnClickPrimary); primaryButton.Click += OnClickPrimary; - m_pressedPrimaryRevoker = primaryButton.RegisterPropertyChangedCallback(ButtonBase.IsPressedProperty, OnVisualPropertyChanged); - m_pointerOverPrimaryRevoker = primaryButton.RegisterPropertyChangedCallback(ButtonBase.IsPointerOverProperty, OnVisualPropertyChanged); + var pressedPrimaryToken = primaryButton.RegisterPropertyChangedCallback(ButtonBase.IsPressedProperty, OnVisualPropertyChanged); + m_pressedPrimaryRevoker.Disposable = new DisposableAction(() => primaryButton.UnregisterPropertyChangedCallback(ButtonBase.IsPressedProperty, pressedPrimaryToken)); + var pointerOverPrimaryToken = primaryButton.RegisterPropertyChangedCallback(ButtonBase.IsPointerOverProperty, OnVisualPropertyChanged); + m_pointerOverPrimaryRevoker.Disposable = new DisposableAction(() => primaryButton.UnregisterPropertyChangedCallback(ButtonBase.IsPointerOverProperty, pointerOverPrimaryToken)); // Register for pointer events so we can keep track of the last used pointer type + m_pointerEnteredPrimaryRevoker.Disposable = new DisposableAction(() => primaryButton.PointerEntered -= OnPointerEvent); primaryButton.PointerEntered += OnPointerEvent; + m_pointerExitedPrimaryRevoker.Disposable = new DisposableAction(() => primaryButton.PointerExited -= OnPointerEvent); primaryButton.PointerExited += OnPointerEvent; + m_pointerPressedPrimaryRevoker.Disposable = new DisposableAction(() => primaryButton.PointerPressed -= OnPointerEvent); primaryButton.PointerPressed += OnPointerEvent; + m_pointerReleasedPrimaryRevoker.Disposable = new DisposableAction(() => primaryButton.PointerReleased -= OnPointerEvent); primaryButton.PointerReleased += OnPointerEvent; + m_pointerCanceledPrimaryRevoker.Disposable = new DisposableAction(() => primaryButton.PointerCanceled -= OnPointerEvent); primaryButton.PointerCanceled += OnPointerEvent; + m_pointerCaptureLostPrimaryRevoker.Disposable = new DisposableAction(() => primaryButton.PointerCaptureLost -= OnPointerEvent); primaryButton.PointerCaptureLost += OnPointerEvent; } - var secondaryButton = m_secondaryButton; - if (secondaryButton != null) + if (m_secondaryButton is { } secondaryButton) { // Do localization for the secondary button var secondaryName = ResourceAccessor.GetLocalizedStringResource(ResourceAccessor.SR_SplitButtonSecondaryButtonName); AutomationProperties.SetName(secondaryButton, secondaryName); + m_clickSecondaryRevoker.Disposable = new DisposableAction(() => secondaryButton.Click -= OnClickSecondary); secondaryButton.Click += OnClickSecondary; - m_pressedSecondaryRevoker = secondaryButton.RegisterPropertyChangedCallback(ButtonBase.IsPressedProperty, OnVisualPropertyChanged); - m_pointerOverSecondaryRevoker = secondaryButton.RegisterPropertyChangedCallback(ButtonBase.IsPointerOverProperty, OnVisualPropertyChanged); + var pressedSecondaryToken = secondaryButton.RegisterPropertyChangedCallback(ButtonBase.IsPressedProperty, OnVisualPropertyChanged); + m_pressedSecondaryRevoker.Disposable = new DisposableAction(() => secondaryButton.UnregisterPropertyChangedCallback(ButtonBase.IsPressedProperty, pressedSecondaryToken)); + var pointerOverSecondaryToken = secondaryButton.RegisterPropertyChangedCallback(ButtonBase.IsPointerOverProperty, OnVisualPropertyChanged); + m_pointerOverSecondaryRevoker.Disposable = new DisposableAction(() => secondaryButton.UnregisterPropertyChangedCallback(ButtonBase.IsPointerOverProperty, pointerOverSecondaryToken)); // Register for pointer events so we can keep track of the last used pointer type + m_pointerEnteredSecondaryRevoker.Disposable = new DisposableAction(() => secondaryButton.PointerEntered -= OnPointerEvent); secondaryButton.PointerEntered += OnPointerEvent; + m_pointerExitedSecondaryRevoker.Disposable = new DisposableAction(() => secondaryButton.PointerExited -= OnPointerEvent); secondaryButton.PointerExited += OnPointerEvent; + m_pointerPressedSecondaryRevoker.Disposable = new DisposableAction(() => secondaryButton.PointerPressed -= OnPointerEvent); secondaryButton.PointerPressed += OnPointerEvent; + m_pointerReleasedSecondaryRevoker.Disposable = new DisposableAction(() => secondaryButton.PointerReleased -= OnPointerEvent); secondaryButton.PointerReleased += OnPointerEvent; + m_pointerCanceledSecondaryRevoker.Disposable = new DisposableAction(() => secondaryButton.PointerCanceled -= OnPointerEvent); secondaryButton.PointerCanceled += OnPointerEvent; + m_pointerCaptureLostSecondaryRevoker.Disposable = new DisposableAction(() => secondaryButton.PointerCaptureLost -= OnPointerEvent); secondaryButton.PointerCaptureLost += OnPointerEvent; } @@ -114,12 +165,6 @@ private void OnPropertyChanged(DependencyPropertyChangedEventArgs args) if (property == FlyoutProperty) { - if (args.OldValue is Flyout oldFlyout) - { - oldFlyout.Opened -= OnFlyoutOpened; - oldFlyout.Closed -= OnFlyoutClosed; - oldFlyout.UnregisterPropertyChangedCallback(FlyoutBase.PlacementProperty, m_flyoutPlacementChangedRevoker); - } OnFlyoutChanged(); } } @@ -129,20 +174,28 @@ protected override AutomationPeer OnCreateAutomationPeer() return new SplitButtonAutomationPeer(this); } - void OnFlyoutChanged() + private void OnFlyoutChanged() { RegisterFlyoutEvents(); UpdateVisualStates(); } - void RegisterFlyoutEvents() + private void RegisterFlyoutEvents() { - if (Flyout != null) + m_flyoutOpenedRevoker.Dispose(); + m_flyoutClosedRevoker.Dispose(); + m_flyoutPlacementChangedRevoker.Dispose(); + + // Uno Specific: use a local variable instead of the property to capture the flyout reference even if Flyout changes + if (Flyout is { } flyout) { - Flyout.Opened += OnFlyoutOpened; - Flyout.Closed += OnFlyoutClosed; - m_flyoutPlacementChangedRevoker = Flyout.RegisterPropertyChangedCallback(FlyoutBase.PlacementProperty, OnFlyoutPlacementChanged); + m_flyoutOpenedRevoker.Disposable = new DisposableAction(() => flyout.Opened -= OnFlyoutOpened); + flyout.Opened += OnFlyoutOpened; + m_flyoutClosedRevoker.Disposable = new DisposableAction(() => flyout.Closed -= OnFlyoutClosed); + flyout.Closed += OnFlyoutClosed; + var flyoutPlacementChangedToken = flyout.RegisterPropertyChangedCallback(FlyoutBase.PlacementProperty, OnFlyoutPlacementChanged); + m_flyoutPlacementChangedRevoker.Disposable = new DisposableAction(() => flyout.UnregisterPropertyChangedCallback(FlyoutBase.PlacementProperty, flyoutPlacementChangedToken)); } } @@ -168,11 +221,22 @@ internal void UpdateVisualStates(bool useTransitions = true) // change visual state var primaryButton = m_primaryButton; var secondaryButton = m_secondaryButton; - if (primaryButton != null && secondaryButton != null) + if (!IsEnabled) + { + VisualStateManager.GoToState(this, "Disabled", useTransitions); + } + else if (primaryButton != null && secondaryButton != null) { if (m_isFlyoutOpen) { - VisualStateManager.GoToState(this, "FlyoutOpen", useTransitions); + if (InternalIsChecked()) + { + VisualStateManager.GoToState(this, "CheckedFlyoutOpen", useTransitions); + } + else + { + VisualStateManager.GoToState(this, "FlyoutOpen", useTransitions); + } } // SplitButton and ToggleSplitButton share a template -- this section is driving the checked states for Toggle else if (InternalIsChecked()) @@ -248,31 +312,35 @@ internal void UpdateVisualStates(bool useTransitions = true) internal void OpenFlyout() { - var flyout = Flyout; - if (flyout != null) + if (Flyout is { } flyout) { - if (SharedHelpers.IsFlyoutShowOptionsAvailable()) - { - FlyoutShowOptions options = new FlyoutShowOptions(); - options.Placement = FlyoutPlacementMode.BottomEdgeAlignedLeft; - flyout.ShowAt(this, options); - } - else - { - flyout.ShowAt(this); - } + FlyoutShowOptions options = new FlyoutShowOptions(); + options.Placement = FlyoutPlacementMode.BottomEdgeAlignedLeft; + flyout.ShowAt(this, options); } } internal void CloseFlyout() { - var flyout = Flyout; - if (flyout != null) + if (Flyout is { } flyout) { flyout.Hide(); } } + private void ExecuteCommand() + { + if (Command is { } command) + { + var commandParameter = CommandParameter; + + if (command.CanExecute(commandParameter)) + { + command.Execute(commandParameter); + } + } + } + private void OnFlyoutOpened(object sender, object args) { m_isFlyoutOpen = true; @@ -292,10 +360,9 @@ private void OnFlyoutPlacementChanged(DependencyObject sender, DependencyPropert UpdateVisualStates(); } - internal virtual void OnClickPrimary(object sender, RoutedEventArgs args) + protected virtual void OnClickPrimary(object sender, RoutedEventArgs args) { var eventArgs = new SplitButtonClickEventArgs(); - Click?.Invoke(this, eventArgs); AutomationPeer peer = FrameworkElementAutomationPeer.FromElement(this); @@ -310,6 +377,26 @@ private void OnClickSecondary(object sender, RoutedEventArgs args) OpenFlyout(); } + internal void Invoke() + { + bool invoked = false; + + if (FrameworkElementAutomationPeer.FromElement(m_primaryButton) is AutomationPeer peer) + { + if (peer.GetPattern(PatternInterface.Invoke) is IInvokeProvider invokeProvider) + { + invokeProvider.Invoke(); + invoked = true; + } + } + + // If we don't have a primary button that provides an invoke provider, we'll fall back to calling OnClickPrimary manually. + if (!invoked) + { + OnClickPrimary(null, null); + } + } + private void OnPointerEvent(object sender, PointerRoutedEventArgs args) { PointerDeviceType pointerDeviceType = args.Pointer.PointerDeviceType; @@ -349,12 +436,13 @@ private void OnSplitButtonKeyUp(object sender, KeyRoutedEventArgs args) if (IsEnabled) { OnClickPrimary(null, null); + ExecuteCommand(); args.Handled = true; } } else if (key == VirtualKey.Down) { - CoreVirtualKeyStates menuState = KeyboardStateTracker.GetKeyState(VirtualKey.Menu); + CoreVirtualKeyStates menuState = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu); bool menuKeyDown = (menuState & CoreVirtualKeyStates.Down) == CoreVirtualKeyStates.Down; if (IsEnabled && menuKeyDown) @@ -377,34 +465,27 @@ private void UnregisterEvents() // This explicitly unregisters all events related to the two buttons in OnApplyTemplate // in case the new template doesn't have all the expected elements. - if (m_primaryButton != null) - { - m_primaryButton.Click -= OnClickPrimary; - - m_primaryButton.UnregisterPropertyChangedCallback(ButtonBase.IsPressedProperty, m_pressedPrimaryRevoker); - m_primaryButton.UnregisterPropertyChangedCallback(ButtonBase.IsPointerOverProperty, m_pointerOverPrimaryRevoker); - - m_primaryButton.PointerEntered -= OnPointerEvent; - m_primaryButton.PointerExited -= OnPointerEvent; - m_primaryButton.PointerPressed -= OnPointerEvent; - m_primaryButton.PointerReleased -= OnPointerEvent; - m_primaryButton.PointerCanceled -= OnPointerEvent; - m_primaryButton.PointerCaptureLost -= OnPointerEvent; - } - - if (m_secondaryButton != null) - { - m_secondaryButton.Click -= OnClickSecondary; - m_secondaryButton.UnregisterPropertyChangedCallback(ButtonBase.IsPressedProperty, m_pressedSecondaryRevoker); - m_secondaryButton.UnregisterPropertyChangedCallback(ButtonBase.IsPointerOverProperty, m_pointerOverSecondaryRevoker); - - m_secondaryButton.PointerEntered -= OnPointerEvent; - m_secondaryButton.PointerExited -= OnPointerEvent; - m_secondaryButton.PointerPressed -= OnPointerEvent; - m_secondaryButton.PointerReleased -= OnPointerEvent; - m_secondaryButton.PointerCanceled -= OnPointerEvent; - m_secondaryButton.PointerCaptureLost -= OnPointerEvent; - } + m_clickPrimaryRevoker.Dispose(); + m_pressedPrimaryRevoker.Dispose(); + m_pointerOverPrimaryRevoker.Dispose(); + + m_pointerEnteredPrimaryRevoker.Dispose(); + m_pointerExitedPrimaryRevoker.Dispose(); + m_pointerPressedPrimaryRevoker.Dispose(); + m_pointerReleasedPrimaryRevoker.Dispose(); + m_pointerCanceledPrimaryRevoker.Dispose(); + m_pointerCaptureLostPrimaryRevoker.Dispose(); + + m_clickSecondaryRevoker.Dispose(); + m_pressedSecondaryRevoker.Dispose(); + m_pointerOverSecondaryRevoker.Dispose(); + + m_pointerEnteredSecondaryRevoker.Dispose(); + m_pointerExitedSecondaryRevoker.Dispose(); + m_pointerPressedSecondaryRevoker.Dispose(); + m_pointerReleasedSecondaryRevoker.Dispose(); + m_pointerCanceledSecondaryRevoker.Dispose(); + m_pointerCaptureLostSecondaryRevoker.Dispose(); } internal bool IsFlyoutOpen => m_isFlyoutOpen; diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButton.properties.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButton.properties.cs index 5ac77eddf6f0..671bc8425386 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButton.properties.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButton.properties.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +// MUX Reference SplitButton.properties.cpp, tag winui3/release/1.4.2 + using System.Windows.Input; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls.Primitives; diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonAutomationPeer.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonAutomationPeer.cs index 3e6a1ab62f2a..a94b805b0879 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonAutomationPeer.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonAutomationPeer.cs @@ -1,4 +1,9 @@ -using Microsoft/* UWP don't rename */.UI.Xaml.Controls; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +// MUX Reference SplitButtonAutomationPeer.cpp, tag winui3/release/1.4.2 + +using Microsoft/* UWP don't rename */.UI.Xaml.Controls; using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Automation.Provider; @@ -7,11 +12,8 @@ namespace Microsoft/* UWP don't rename */.UI.Xaml.Automation.Peers { public partial class SplitButtonAutomationPeer : FrameworkElementAutomationPeer, IExpandCollapseProvider, IInvokeProvider { - private readonly SplitButton _owner; - public SplitButtonAutomationPeer(SplitButton owner) : base(owner) { - _owner = owner; } // IAutomationPeerOverrides @@ -26,11 +28,27 @@ protected override object GetPatternCore(PatternInterface patternInterface) return base.GetPatternCore(patternInterface); } - protected override string GetClassNameCore() => nameof(SplitButton); + protected override string GetClassNameCore() + { + return nameof(SplitButton); + } - protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.SplitButton; + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.SplitButton; + } - private SplitButton GetImpl() => _owner; + private SplitButton GetImpl() + { + SplitButton impl = null; + + if (Owner is SplitButton splitButton) + { + impl = splitButton; + } + + return impl; + } // IExpandCollapseProvider public ExpandCollapseState ExpandCollapseState @@ -38,8 +56,7 @@ public ExpandCollapseState ExpandCollapseState get { ExpandCollapseState currentState = ExpandCollapseState.Collapsed; - var splitButton = GetImpl(); - if (splitButton != null) + if (GetImpl() is { } splitButton) { if (splitButton.IsFlyoutOpen) { @@ -50,11 +67,29 @@ public ExpandCollapseState ExpandCollapseState } } - public void Expand() => GetImpl()?.OpenFlyout(); + public void Expand() + { + if (GetImpl() is { } splitButton) + { + splitButton.OpenFlyout(); + } + } - public void Collapse() => GetImpl()?.CloseFlyout(); + public void Collapse() + { + if (GetImpl() is { } splitButton) + { + splitButton.CloseFlyout(); + } + } // IInvokeProvider - public void Invoke() => GetImpl()?.OnClickPrimary(null, null); + public void Invoke() + { + if (GetImpl() is { } splitButton) + { + splitButton.Invoke(); + } + } } } diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonTestApi.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonTestApi.cs new file mode 100644 index 000000000000..7cf6379774c7 --- /dev/null +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonTestApi.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +// MUX Reference SplitButtonTestApi.cpp, tag winui3/release/1.4.2 + +namespace Microsoft.UI.Private.Controls; + +internal class SplitButtonTestApi +{ + public bool SimulateTouch() + { + return SplitButtonTestHelper.SimulateTouch; + } + + public void SimulateTouch(bool value) + { + SplitButtonTestHelper.SimulateTouch = value; + } +} diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonTestHelper.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonTestHelper.cs index 92437ea9c925..df002e77790a 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonTestHelper.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/SplitButtonTestHelper.cs @@ -1,4 +1,9 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +// MUX Reference SplitButtonTestHelper.cpp, tag winui3/release/1.4.2 + +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -11,12 +16,30 @@ internal class SplitButtonTestHelper { private bool m_simulateTouch = false; - public static SplitButtonTestHelper Instance { get; } = new SplitButtonTestHelper(); + [ThreadStatic] private static SplitButtonTestHelper s_instance; + + private static SplitButtonTestHelper EnsureInstance() + { + if (s_instance is not { }) + { + s_instance = new SplitButtonTestHelper(); + } + + return s_instance; + } public static bool SimulateTouch { - get => Instance.m_simulateTouch; - set => Instance.m_simulateTouch = value; + get + { + var instance = EnsureInstance(); + return instance.m_simulateTouch; + } + set + { + var instance = EnsureInstance(); + instance.m_simulateTouch = value; + } } } } diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/ToggleSplitButton.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/ToggleSplitButton.cs index 18ad99c3a5a2..453a8f75c958 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/ToggleSplitButton.cs @@ -1,4 +1,7 @@ -// MUX commit reference 36f8f8f6d5f11f414fdaa0462d0c4cb845cf4254 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +// MUX Reference ToggleSplitButton.cpp, tag winui3/release/1.4.2 using Microsoft/* UWP don't rename */.UI.Xaml.Automation.Peers; using Windows.Foundation; @@ -18,6 +21,7 @@ public ToggleSplitButton() DefaultStyleKey = typeof(ToggleSplitButton); } + // Uno Specific: not present in the C++ source, but is part of the public API public event TypedEventHandler IsCheckedChanged; private void OnPropertyChanged(DependencyPropertyChangedEventArgs args) @@ -40,9 +44,8 @@ private void OnIsCheckedChanged() if (m_hasLoaded) { var eventArgs = new ToggleSplitButtonIsCheckedChangedEventArgs(); - IsCheckedChanged?.Invoke(this, eventArgs); - var peer = FrameworkElementAutomationPeer.FromElement(this); - if (peer != null) + IsCheckedChanged?.Invoke(this, eventArgs); // Uno Specific: not present in the C++ source, but is part of the public API + if (FrameworkElementAutomationPeer.FromElement(this) is { } peer) { var newValue = IsChecked ? ToggleState.On : ToggleState.Off; var oldValue = (newValue == ToggleState.On) ? ToggleState.Off : ToggleState.On; @@ -53,7 +56,7 @@ private void OnIsCheckedChanged() UpdateVisualStates(); } - internal override void OnClickPrimary(object sender, RoutedEventArgs args) + protected override void OnClickPrimary(object sender, RoutedEventArgs args) { Toggle(); diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/ToggleSplitButtonAutomationPeer.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/ToggleSplitButtonAutomationPeer.cs index 6f42bc10e1dc..cf50fa4aa0e4 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/ToggleSplitButtonAutomationPeer.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/SplitButton/ToggleSplitButtonAutomationPeer.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +// MUX Reference ToggleSplitButtonAutomationPeer.cpp, tag winui3/release/1.4.2 + using Microsoft/* UWP don't rename */.UI.Xaml.Controls; -using Windows.Graphics.Display; using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Automation.Provider; @@ -13,11 +12,8 @@ namespace Microsoft/* UWP don't rename */.UI.Xaml.Automation.Peers { public partial class ToggleSplitButtonAutomationPeer : FrameworkElementAutomationPeer, IExpandCollapseProvider, IToggleProvider { - private readonly ToggleSplitButton _owner; - public ToggleSplitButtonAutomationPeer(ToggleSplitButton owner) : base(owner) { - _owner = owner; } // IAutomationPeerOverrides @@ -41,7 +37,17 @@ protected override AutomationControlType GetAutomationControlTypeCore() return AutomationControlType.SplitButton; } - private ToggleSplitButton GetImpl() => _owner; + private ToggleSplitButton GetImpl() + { + ToggleSplitButton impl = null; + + if (Owner is ToggleSplitButton splitButton) + { + impl = splitButton; + } + + return impl; + } // IExpandCollapseProvider public ExpandCollapseState ExpandCollapseState @@ -50,8 +56,7 @@ public ExpandCollapseState ExpandCollapseState { ExpandCollapseState currentState = ExpandCollapseState.Collapsed; - var splitButton = GetImpl(); - if (splitButton != null) + if (GetImpl() is { } splitButton) { if (splitButton.IsFlyoutOpen) { @@ -63,9 +68,21 @@ public ExpandCollapseState ExpandCollapseState } } - public void Expand() => GetImpl()?.OpenFlyout(); + public void Expand() + { + if (GetImpl() is { } splitButton) + { + splitButton.OpenFlyout(); + } + } - public void Collapse() => GetImpl()?.CloseFlyout(); + public void Collapse() + { + if (GetImpl() is { } splitButton) + { + splitButton.CloseFlyout(); + } + } // IToggleProvider public ToggleState ToggleState @@ -74,8 +91,7 @@ public ToggleState ToggleState { ToggleState state = ToggleState.Off; - var splitButton = GetImpl(); - if (splitButton != null) + if (GetImpl() is { } splitButton) { if (splitButton.IsChecked) { @@ -87,6 +103,12 @@ public ToggleState ToggleState } } - public void Toggle() => GetImpl()?.Toggle(); + public void Toggle() + { + if (GetImpl() is { } splitButton) + { + splitButton.Toggle(); + } + } } } diff --git a/src/Uno.UI/UI/Xaml/Controls/Popup/Popup.cs b/src/Uno.UI/UI/Xaml/Controls/Popup/Popup.cs index 6518962bc8b6..1f7892cacc2d 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Popup/Popup.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Popup/Popup.cs @@ -311,4 +311,7 @@ internal Brush LightDismissOverlayBackground internal static DependencyProperty LightDismissOverlayBackgroundProperty { get; } = DependencyProperty.Register(nameof(LightDismissOverlayBackground), typeof(Brush), typeof(Popup), new FrameworkPropertyMetadata(defaultValue: null, propertyChangedCallback: (o, e) => ((Popup)o).ApplyLightDismissOverlayMode())); + + // On WinUi, a popup is not IsTabStop but is somehow focusable. This is a workaround to match that behaviour. + internal override bool IsFocusableForFocusEngagement() => true; }