Skip to content

fix(TextBox): clear button turns invisible on hover after a theme change - #443

Open
TonyStark172 wants to merge 1 commit into
iNKORE-NET:mainfrom
TonyStark172:fix/CloseButton_TextBox
Open

fix(TextBox): clear button turns invisible on hover after a theme change#443
TonyStark172 wants to merge 1 commit into
iNKORE-NET:mainfrom
TonyStark172:fix/CloseButton_TextBox

Conversation

@TonyStark172

Copy link
Copy Markdown

Bug

Steps to reproduce:

  1. Run the app in the Dark theme.
  2. Type into a TextBox so the inner clear button ("x") appears, and hover it at least once.
  3. Switch the app to the Light theme at runtime.
  4. Hover the clear button again.

The glyph and the hover background are now painted with the Dark theme brushes, so on the light background the button is practically invisible: it looks like the "x" vanishes as soon as the pointer touches it. It reappears as soon as the pointer leaves. The same happens in the other direction (Light -> Dark), it is just far less noticeable there.

Cause

DeleteButtonStyle drove its hover/pressed visuals through a VisualStateManager Storyboard whose keyframes carried DynamicResource values:

<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground">
    <DiscreteObjectKeyFrame KeyTime="0"
        Value="{DynamicResource TextControlButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>

A Storyboard is a Freezable and gets frozen when the state is applied, so the DynamicResource inside the keyframe is resolved once and is not kept live against the resource dictionaries. ThemeManager swaps those dictionaries when the theme changes, but the frozen keyframe still holds the brushes of the theme that was active the first time the state ran.

Because an animation has higher precedence than the local value set on GlyphElement, the stale brush also wins over the correct TextControlButtonForeground the template assigns, for as long as the pointer stays over the button.

Fix

Replace the VisualStateManager block with ControlTemplate.Triggers (IsMouseOver / IsPressed / IsEnabled) that assign the brushes through Setters. Setters keep DynamicResource references live, so the button follows the active theme after any number of theme switches.

This is the same pattern already used by the equivalent buttons in PasswordBox.xaml and AutoSuggestBox.xaml, which is why those two controls never showed the bug.

Behaviour is otherwise unchanged: hover and pressed swap background, border and glyph brushes, and the disabled state still hides the button. The ButtonVisible / ButtonCollapsed states are untouched, so TextBoxHelper's VisualStateManager.GoToState call keeps working.

Verified with a small WPF harness that starts in Dark, switches to Light at runtime and hovers the button with a real injected mouse move: before the change the glyph is invisibl
after2hover
before2hover
comparisonzoom
comparison
e, after it renders correctly.

Bug
---
Steps to reproduce:
  1. Run the app in the Dark theme.
  2. Type into a TextBox so the inner clear button ("x") appears, and hover
     it at least once.
  3. Switch the app to the Light theme at runtime.
  4. Hover the clear button again.

The glyph and the hover background are now painted with the Dark theme
brushes, so on the light background the button is practically invisible:
it looks like the "x" vanishes as soon as the pointer touches it. It
reappears as soon as the pointer leaves. The same happens in the other
direction (Light -> Dark), it is just far less noticeable there.

Cause
---
DeleteButtonStyle drove its hover/pressed visuals through a
VisualStateManager Storyboard whose keyframes carried DynamicResource
values:

    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground">
        <DiscreteObjectKeyFrame KeyTime="0"
            Value="{DynamicResource TextControlButtonForegroundPointerOver}" />
    </ObjectAnimationUsingKeyFrames>

A Storyboard is a Freezable and gets frozen when the state is applied, so
the DynamicResource inside the keyframe is resolved once and is not kept
live against the resource dictionaries. ThemeManager swaps those
dictionaries when the theme changes, but the frozen keyframe still holds
the brushes of the theme that was active the first time the state ran.

Because an animation has higher precedence than the local value set on
GlyphElement, the stale brush also wins over the correct
TextControlButtonForeground the template assigns, for as long as the
pointer stays over the button.

Fix
---
Replace the VisualStateManager block with ControlTemplate.Triggers
(IsMouseOver / IsPressed / IsEnabled) that assign the brushes through
Setters. Setters keep DynamicResource references live, so the button
follows the active theme after any number of theme switches.

This is the same pattern already used by the equivalent buttons in
PasswordBox.xaml and AutoSuggestBox.xaml, which is why those two controls
never showed the bug.

Behaviour is otherwise unchanged: hover and pressed swap background,
border and glyph brushes, and the disabled state still hides the button.
The ButtonVisible / ButtonCollapsed states are untouched, so
TextBoxHelper's VisualStateManager.GoToState call keeps working.

Verified with a small WPF harness that starts in Dark, switches to Light
at runtime and hovers the button with a real injected mouse move: before
the change the glyph is invisible, after it renders correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant