diff --git a/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml.cs b/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml.cs index b7744fa262ea..6e467ed53a97 100644 --- a/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml.cs +++ b/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml.cs @@ -6,4 +6,4 @@ public MainPage() { InitializeComponent(); } -} \ No newline at end of file +} diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs index 6161fcd8a8c1..fdbf5e1e61d6 100644 --- a/src/Controls/src/Core/VisualElement/VisualElement.cs +++ b/src/Controls/src/Core/VisualElement/VisualElement.cs @@ -20,7 +20,6 @@ namespace Microsoft.Maui.Controls /// /// The base class for most .NET MAUI on-screen elements. Provides most properties, events, and methods for presenting an item on screen. /// - [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] public partial class VisualElement : NavigableElement, IAnimatable, IVisualElementController, IResourcesProvider, IStyleElement, IFlowDirectionController, IPropertyPropagationController, IVisualController, IWindowController, IView, IControlsVisualElement { @@ -1607,26 +1606,43 @@ private protected void SetPointerOver(bool value, bool callChangeVisualState = t /// protected internal virtual void ChangeVisualState() { + // A disabled control should never be in a focused state as part of the feature + // of being disabled is that it cannot receive focus. If it was in focus, then + // it has to go out of focus. + var shouldFocus = IsFocused && IsEnabled; + + // If the control cannot have focus, make sure it appears unfocused by moving to + // the Unfocused state. + if (!shouldFocus) + { + VisualStateManager.GoToState(this, VisualStateManager.FocusStates.Unfocused); + } + + // Set the Disabled or Normal states depending on the value of IsEnabled and + // IsPointerOver. We set the PointerOver state later, after the Focused state. if (!IsEnabled) { VisualStateManager.GoToState(this, VisualStateManager.CommonStates.Disabled); } - else if (IsPointerOver) + else if (!IsPointerOver) { - VisualStateManager.GoToState(this, VisualStateManager.CommonStates.PointerOver); + VisualStateManager.GoToState(this, VisualStateManager.CommonStates.Normal); } - else + + // Go to the Focus state after the Normal state, so that the Focus state can + // override the Normal state's properties if a control is both focused and + // hovered. + if (shouldFocus) { - VisualStateManager.GoToState(this, VisualStateManager.CommonStates.Normal); + VisualStateManager.GoToState(this, VisualStateManager.FocusStates.Focused); } - if (IsEnabled) + // The PointerOver state is applied last so that it can override all the states. Even + // though this state is separate here, it should still be part of the CommonStates + // visual state group. + if (IsPointerOver) { - // Focus needs to be handled independently; otherwise, if no actual Focus state is supplied - // in the control's visual states, the state can end up stuck in PointerOver after the pointer - // exits and the control still has focus. - VisualStateManager.GoToState(this, - IsFocused ? VisualStateManager.CommonStates.Focused : VisualStateManager.CommonStates.Unfocused); + VisualStateManager.GoToState(this, VisualStateManager.CommonStates.PointerOver); } } diff --git a/src/Controls/src/Core/VisualStateManager.cs b/src/Controls/src/Core/VisualStateManager.cs index e80baabda1b2..46d5aca6368e 100644 --- a/src/Controls/src/Core/VisualStateManager.cs +++ b/src/Controls/src/Core/VisualStateManager.cs @@ -15,10 +15,16 @@ public class CommonStates { public const string Normal = "Normal"; public const string Disabled = "Disabled"; - public const string Focused = "Focused"; + public const string Focused = FocusStates.Focused; public const string Selected = "Selected"; public const string PointerOver = "PointerOver"; - internal const string Unfocused = "Unfocused"; + } + + // TODO: .NET 10 - make public + internal class FocusStates + { + public const string Focused = "Focused"; + public const string Unfocused = "Unfocused"; } /// Bindable property for attached property VisualStateGroups. diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue19752.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue19752.xaml new file mode 100644 index 000000000000..db7586278000 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue19752.xaml @@ -0,0 +1,67 @@ + + + + + + + + +