Repository navigation
button: clear hover style when a focused button is disabled (fixes screenshot flake) - #342255
Merged
Alexandru Dima (alexdima) merged 1 commit intoOct 17, 2026
Merged
Conversation
The blur handler only resets the hover background while the button is enabled, so a button that was focused (or hovered) and then disabled kept its hover background indefinitely. This made the PluginMarketplaceMigrationDisabled screenshot fixture flaky, depending on whether the dashboard's deferred auto-focus ran before or after the fixture deselected the item. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The narrowly scoped fix has regression coverage and no unresolved findings.
No open issues 🎉
What changed in this PR
Fixes stale hover styling on disabled buttons in the shared Button widget, stabilizing the migration dashboard screenshot.
Changes:
- Tracks applied hover styling and clears it when disabling a button.
- Adds regression coverage for disabling a hovered button and ignoring subsequent hover events.
| File | Description |
|---|---|
| src/vs/base/test/browser/ui/button/button.test.ts | Tests hover-background reset when disabled. |
| src/vs/base/browser/ui/button/button.ts | Clears active hover styling without rewriting untouched styles. |
Review effort: Balanced
Alexandru Dima (alexdima)
marked this pull request as ready for review
October 17, 2026 09:59
Alexandru Dima (alexdima)
enabled auto-merge (squash)
October 17, 2026 09:59
Ladislau Szomoru (lszomoru)
approved these changes
Oct 17, 2026
Alexandru Dima (alexdima)
deleted the
agents/stabilize-screenshot-flake-issue
branch
October 17, 2026 10:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stabilizes the flaky
chat/aiCustomizations/aiCustomizationManagementEditor/PluginMarketplaceMigrationDisabled/Lightscreenshot. The flake showed up on #342242 (screenshot report), even though that PR only changes a unit test file. The root cause is a small bug in the baseButtonwidget, which this PR fixes.What changed in the screenshot
The only difference is the background of the disabled Migrate 0 button. In the base screenshot it is pure white. In the flaky one it is very slightly gray. 1,312 pixels changed, all inside the button.
Changed pixels in red (the rest of the page is faded so they stand out):
Zoomed 6×: before, after, and the delta:
Original full-size CI screenshots
The fill goes from
rgb(255,255,255)torgb(250,250,250). That matcheslist.hoverBackgroundshowing through the disabled button'sopacity: 0.4. So the disabled button was captured while still using its hover background.Why it happens: a race between focus and disable
The fixture does two things whose order isn't deterministic:
showCustomizationMigrationDashboard()focuses the first Migrate button in arequestAnimationFrame. In the fixture, that callback runs on virtual time.pluginMarketplaceMigrationDeselectedoption waits for the checkbox using real animation frames (waitForFixtureCondition), then clicks it. This setsselectedCount = 0, which runsmigrateButton.enabled = false.Because one side runs on virtual time and the other on real frames, either can go first under CI load:
ButtondoesonDidFocuschecksif (this.enabled). The button is already disabled, so hover styling is skipped.onDidFocusappliesbuttonSecondaryHoverBackground. Then the button is disabled, and nothing removes that background.The bug is in
Button:Both the focus and blur handlers check
enabled, and theenabledsetter only toggles a class and ARIA attributes. So once a focused button is disabled, its hover background stays until it is re-enabled, even after focus moves away. This isn't limited to fixtures. In the real UI: open the migration dashboard (Migrate gets auto-focused), then uncheck the only item. The disabled Migrate 0 button keeps the hover tint, and moving focus away doesn't clear it.The fix
Buttonnow records whether it has applied its hover style, and theenabled = falsesetter resets to the normal style when it has:The reset only runs when hover styling is actually applied, so disabling a button that isn't hovered or focused leaves its inline styles alone. This matters for callers that set a custom inline background, such as
promptCodingAgentActionOverlay. Re-enabling and focus behavior are unchanged.With this change, both orderings render the baseline (white) button, so the screenshot is deterministic again and the baseline doesn't need to change.
How it was verified
New unit test in
src/vs/base/test/browser/ui/button/button.test.ts: hover the button, disable it, then check the background goes back to the normal color and that mouse-over while disabled doesn't re-apply hover. The test fails without the fix ('blue'instead of'red') and passes with it../scripts/test.sh --run src/vs/base/test/browser/ui/button/button.test.ts: passedForced repro of the race with the component explorer: I temporarily added
.focus()on the Migrate button just before the fixture clicks the checkbox, which forces the "focus → disable" order, then renderedPluginMarketplaceMigrationDisabled:rgb(250,250,250), the same as the flaky CI imagergb(255,255,255), the same as the CI baselineI removed that temporary fixture change afterward; this PR doesn't modify the fixture.
Normal local renders: 8 runs with 8 workers, plus single-worker and headed runs. All produced the baseline locally, both with and without the fix. That's expected: the race depends on timing that I couldn't trigger locally without the forced focus.
npm run typecheck-client: passednpm run eslint -- src/vs/base/browser/ui/button/button.ts src/vs/base/test/browser/ui/button/button.test.ts: passed