Skip to content

Improve VisualState order and prevent sticky Focused visual state#27477

Open
mattleibow wants to merge 9 commits into
mainfrom
dev/focus-visual-states
Open

Improve VisualState order and prevent sticky Focused visual state#27477
mattleibow wants to merge 9 commits into
mainfrom
dev/focus-visual-states

Conversation

@mattleibow

@mattleibow mattleibow commented Jan 30, 2025

Copy link
Copy Markdown
Member

Description of Change

Alternative to #19752

While reviewing @MartyIX's PR #19812 I discovered that the code for switching to the Focused and Unfocused states was all dependent on the control being enabled. What this results in that if you have a focused button and the visual state was some sort of border, disabling the button will not actually switch to unfocused and the visual state will remain with the border that was added when it got focused.

This PR originally copied the code logic from WinUI: https://github.com/microsoft/microsoft-ui-xaml/blob/ffe33f9b7d0e9f5a2ca3330d0ce329f09dff092b/src/dxaml/xcp/dxaml/lib/Button_Partial.cpp#L29-L60 but I have updated it to follow maybe a better visual state order. This new way is to make sure the unfocus happens first and the pointer over happens last.

For the issue in #19752, the actual reason things are wrong is not because the states are set wrong, but rather because the focus states are in the same group as the pointer over state. This means that the button can either be focused or be pointer over.

The correct way to have all these states working is to use multiple groups:

<VisualStateManager.VisualStateGroups>
  <VisualStateGroupList>
    <VisualStateGroup x:Name="CommonStates">
      <VisualState x:Name="Normal" />
      <VisualState x:Name="PointerOver" />
      <VisualState x:Name="Pressed" />
      <VisualState x:Name="Disabled" />
    </VisualStateGroup>
    <VisualStateGroup x:Name="FocusStates">
      <VisualState x:Name="Focused" />
      <VisualState x:Name="Unfocused" />
    </VisualStateGroup>
  </VisualStateGroupList>
</VisualStateManager.VisualStateGroups>

This can also be seen in other controls such as the WinUI combo box (the Button does not use a state but rather the OS focus border): https://github.com/microsoft/microsoft-ui-xaml/blob/ffe33f9b7d0e9f5a2ca3330d0ce329f09dff092b/src/controls/dev/ComboBox/ComboBox_themeresources.xaml#L472 It is also in the docs: https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.control.usesystemfocusvisuals?view=winrt-26100#examples

This is the docs for WinUI to do focus states:

To define custom focus visuals for a control, you need to provide a custom ControlTemplate. In the ControlTemplate, do the following:

  • If you're modifying a default ControlTemplate, be sure to set the UseSystemFocusVisuals property to false to turn off the system focus visuals. When set to false, the focus states in the VisualStateManager are called.
  • Define a VisualStateGroup for FocusStates.
  • In the FocusStates group, define VisualStates for Focused, Unfocused, and PointerFocused.
  • Define the focus visuals.

Another result of not having multiple groups is that sometimes unexpected things happen. If you are missing the focus states, then nothing happens when you change states. And, if you have the focus states in the same group as normal, the normal state will never apply since it will either be focused or unfocused and normal will be overwritten.

Issues Fixed

I was not able to find an open issue with the focus states "sticking" when disabling. And the issues that I have seen are just VSM improperly configured.

Maybe these:

Copilot AI review requested due to automatic review settings January 30, 2025 14:45
@mattleibow mattleibow requested a review from a team as a code owner January 30, 2025 14:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 5 out of 7 changed files in this pull request and generated no comments.

Files not reviewed (2)
  • src/Controls/tests/TestCases.HostApp/Issues/Issue19752.xaml: Language not supported
  • src/Controls/src/Core/VisualElement/VisualElement.cs: Evaluated as low risk
Comments suppressed due to low confidence (2)

src/TestUtils/src/UITest.Appium/Actions/AppiumMouseActions.cs:221

  • Ensure that the new MoveCursor and MoveCursorCoordinates commands are covered by tests.
CommandResponse MoveCursor(IDictionary<string, object> parameters)

src/TestUtils/src/UITest.Appium/HelperExtensions.cs:2282

  • Ensure that the new MoveCursor methods are covered by tests.
public static void MoveCursor(this IApp app, string element)

Comment thread src/TestUtils/src/UITest.Appium/HelperExtensions.cs Outdated
Comment thread src/TestUtils/src/UITest.Appium/HelperExtensions.cs Outdated
Comment thread src/TestUtils/src/UITest.Appium/HelperExtensions.cs Outdated
@mattleibow

Copy link
Copy Markdown
Member Author

@MartyIX had some wise words:

I just wonder what the precedence rules are for these two style groups. I mean I want "pointer-over" state to have higher precedence than "focused" state. Does it depend on the order here https://github.com/dotnet/maui/pull/27477/files#diff-f9f36395cbe8fbe5db0fb42d9eeda70e070ed5ced099553747d1236d4f05117fR16-R54 ?

@mattleibow

mattleibow commented Jan 30, 2025

Copy link
Copy Markdown
Member Author

Thanks for the wise words @MartyIX, maybe this is a better order:

var shouldFocus = IsFocused && IsEnabled;
			
// 1. unfocus first
if (!shouldFocus)
	VisualStateManager.GoToState(this, VisualStateManager.FocusStates.Unfocused);

// 2. set basic states (normal/disabled)
if (!IsEnabled)
	VisualStateManager.GoToState(this, VisualStateManager.CommonStates.Disabled);
else if (!IsPointerOver)
	VisualStateManager.GoToState(this, VisualStateManager.CommonStates.Normal);

// 3. focus
if (shouldFocus)
	VisualStateManager.GoToState(this, VisualStateManager.FocusStates.Focused);

// 4. end with pointer over
if (IsPointerOver)
	VisualStateManager.GoToState(this, VisualStateManager.CommonStates.PointerOver);

This is different to UWP/WPF/WinUI, so it may be better or it may cause people to get surprised coming from another XAML framework:

// 1. set basic states (normal/disabled/pointer over)
if (!IsEnabled)
	VisualStateManager.GoToState(this, VisualStateManager.CommonStates.Disabled);
else if (IsPointerOver)
	VisualStateManager.GoToState(this, VisualStateManager.CommonStates.PointerOver);
else
	VisualStateManager.GoToState(this, VisualStateManager.CommonStates.Normal);

// 2. override with focus
if (IsFocused && IsEnabled)
	VisualStateManager.GoToState(this, VisualStateManager.FocusStates.Focused);
else
	VisualStateManager.GoToState(this, VisualStateManager.FocusStates.Unfocused);

Any thoughts?

@mattleibow mattleibow added this to the .NET 9 SR4 milestone Jan 30, 2025
@mattleibow mattleibow added the area-xaml XAML, CSS, Triggers, Behaviors label Jan 30, 2025
@mattleibow mattleibow changed the title Always apply the Unfocued visual state when the element loses focus Improve VisualState order and prevent sticky focus Jan 30, 2025
@mattleibow mattleibow changed the title Improve VisualState order and prevent sticky focus Improve VisualState order and prevent sticky Focused visual state Jan 30, 2025
@MartyIX

MartyIX commented Jan 30, 2025

Copy link
Copy Markdown
Contributor

This is different to UWP/WPF/WinUI, so it may be better or it may cause people to get surprised coming from another XAML framework.

Could you explain how it is different exactly? I don't know the frameworks in detail.

@jsuarezruiz jsuarezruiz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to verify some related UITests checking the focused VisualState etc.
image

Example:
DisablingUnfocusedButtonMovesToDisabledState

Assert.That(App.FindElement("button2").GetText(), Is.EqualTo("Disabled"))
Expected string length 8 but was 11. Strings differ at index 0.
Expected: "Disabled"
But was:  "PointerOver"

@mattleibow

Copy link
Copy Markdown
Member Author

Could you explain how it is different exactly? I don't know the frameworks in detail.

@MartyIX I updated the comment with the WinUI way so it can be seen side-by-side

@MartyIX

MartyIX commented Jan 31, 2025

Copy link
Copy Markdown
Contributor

It looks good to me. :-)

I still wonder though how will one implement styling for a button like this:

  • red ~ the button is focused and the pointer is over the button (i.e. Focused && PointerOver styles at the same time)
  • green ~ the button is just focused
  • blue ~ pointer is over the button

I think that one can make it somehow work with triggers (doc). But not with visual styles (doc). Is that right?

It's not like the scenario is super-useful. The question is more about API design and perhaps even for user-defined visual styles and their composition.

@PureWeen

PureWeen commented Mar 2, 2025

Copy link
Copy Markdown
Member

/rebase

@PureWeen PureWeen moved this from Changes Requested to Ready To Review in MAUI SDK Ongoing Mar 2, 2025
@github-actions github-actions Bot force-pushed the dev/focus-visual-states branch from 4e36d9f to a1b0d56 Compare March 2, 2025 22:12
@mattleibow

Copy link
Copy Markdown
Member Author

/rebase

@github-actions github-actions Bot force-pushed the dev/focus-visual-states branch from a1b0d56 to 30161dc Compare March 3, 2025 18:51

@jsuarezruiz jsuarezruiz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image This failing tests are related with VisualStates, could you verify if are related with the changes?

@github-project-automation github-project-automation Bot moved this from Ready To Review to Changes Requested in MAUI SDK Ongoing Mar 4, 2025
@kubaflo

kubaflo commented May 10, 2026

Copy link
Copy Markdown
Contributor

Looks like it has been resolved

@kubaflo kubaflo closed this May 10, 2026
@github-project-automation github-project-automation Bot moved this from Changes Requested to Done in MAUI SDK Ongoing May 10, 2026
@MartyIX

MartyIX commented May 10, 2026

Copy link
Copy Markdown
Contributor

Looks like it has been resolved

How was it resolved?

@kubaflo

kubaflo commented May 10, 2026

Copy link
Copy Markdown
Contributor

@MartyIX do you think this PR is still needed? I reopened it, but it might be better to create a new PR so I can try to prioritize it. This one looks like it went stale and accumulated conflicts, so I closed it for now.

@kubaflo kubaflo reopened this May 10, 2026
@github-project-automation github-project-automation Bot moved this from Done to In Progress in MAUI SDK Ongoing May 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 27477

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 27477"

@MartyIX

MartyIX commented May 10, 2026

Copy link
Copy Markdown
Contributor

@MartyIX do you think this PR is still needed?

This PR adds new API for focused state: https://github.com/dotnet/maui/pull/27477/files#diff-6dd0e5f770ae939379098cbfe8654a00ebc4e9d0bf1c08c3bf9ce3e33a92de8fR24-R27 I don't know about any PR that does that.

But the PR is stale, that's true. Matt is probably out of time to finish it.

@kubaflo

kubaflo commented May 24, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/refactor-copilot-yml

@dotnet dotnet deleted a comment from MauiBot May 28, 2026
@dotnet dotnet deleted a comment from MauiBot May 28, 2026
@kubaflo

kubaflo commented May 28, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/refactor-copilot-yml

@MauiBot

This comment has been minimized.

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please resolve conflicts?

@github-project-automation github-project-automation Bot moved this from In Progress to Changes Requested in MAUI SDK Ongoing May 28, 2026
@kubaflo

kubaflo commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/enhanced-reviewer -p windows

@MauiBot

This comment has been minimized.

@kubaflo

kubaflo commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/enhanced-reviewer -p android

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jun 13, 2026
@MauiBot

MauiBot commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Merge Conflict Detected — This PR has merge conflicts with its target branch. Please rebase onto the target branch and resolve the conflicts.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jun 13, 2026
@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

/review rerun

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-xaml XAML, CSS, Triggers, Behaviors s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) stale Indicates a stale issue/pr and will be closed soon

Projects

Status: Changes Requested

Development

Successfully merging this pull request may close these issues.

Button does not behave properly when pointer hovers over the button because it's in focused state

9 participants