button: Fix loading buttons reacting to the pointer and dimming unevenly (#2668)
Two problems with the loading state, both of which made it behave differently from what `loading` implies. ## 1. A loading button still reacted to the pointer It highlighted on hover, showed the active style when pressed, and kept the pointer cursor on the link and text variants — even though clicking it does nothing. `clickable()` and `hoverable()` already excluded `loading`, but they only gate the `on_click` and `on_hover` listeners. Three other places were keyed off `disabled` alone: - the `hover` and `active` styling - the `cursor_pointer` of link and text variants - the `mouse_down` handler, whose comment says it exists to keep a dropdown menu from opening — which applies just as much to loading Add `Button::interactive()` (`!(disabled || loading)`) and use it in all of those places, so loading is as inert as disabled. `clickable()` and `hoverable()` are now that plus their listener check. The normal background and border stay outside the check: only the `hover` and `active` blocks are gated, so a loading button keeps its own colors instead of falling back to an unstyled background. Loading deliberately keeps its own look rather than taking the disabled styling: it is a temporary state caused by the click the user just made, and graying the button out would weaken that feedback. ## 2. Only the Primary variant looked like it was loading The loading styling faded `bg`, `border` and `fg` one by one, which only lands on variants that have a background to begin with. `Ghost`, `Link` and `Text` are transparent, so an alpha on their background changes nothing, and the light background of `Default` barely shifts once blended with the page. Only a saturated fill with white text showed both layers, which is why Primary dimmed and the rest looked untouched. Fade the element instead — `Styled::opacity` applies to the whole subtree — so every variant dims by the same amount. Net 5 lines shorter. It sits after the `disabled` branch and before `refine_style`, so disabled wins when both are set, and a caller-supplied style can still override it. ## Notes `DropdownButton` is unaffected — it already passes `disabled(self.disabled || self.loading)` to its inner button. `ToggleButton` has no loading state. Spinner behavior is unchanged. ## Testing `test_button_loading_is_not_interactive` covers the four `interactive` combinations, plus a loading button with an `on_hover` listener still not being hoverable. Verified every variant in the story gallery with the loading checkbox. AI generated: the patch was written with Claude Code and reviewed by me. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
J
Jason Lee committed
424f24577bc7ac939f9cfc1331f470186bf19c4c
Parent: d0c469e
Committed by GitHub <noreply@github.com>
on 8/10/2026, 8:32:47 AM