Skip to content

feat(widgets): add hover and press animation to Button component#1761

Open
pixeltannu wants to merge 3 commits into
Karanjot786:mainfrom
pixeltannu:feature/button-hover-animation
Open

feat(widgets): add hover and press animation to Button component#1761
pixeltannu wants to merge 3 commits into
Karanjot786:mainfrom
pixeltannu:feature/button-hover-animation

Conversation

@pixeltannu

@pixeltannu pixeltannu commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Closes #1735

Changes

  • Added startHover() / endHover() / startPress() methods to Button for hover/press animation states
  • Hover triggers a smooth background-color transition (configurable via animationMs, default 200ms)
  • Press triggers a brief color-invert effect as visual feedback (terminal equivalent of a "press-down" effect, since scale transforms aren't possible in a terminal grid)
  • Focus changes are auto-detected in _renderSelf() and drive the hover animation, since isFocused is set directly by the focus manager
  • Respects prefersReducedMotion() — no animation timers run if the user has reduced motion enabled
  • Added test coverage for hover/press state and re-render behavior

Testing

  • All existing Button tests pass
  • Added 4 new tests covering hover/press/focus behavior

Summary by CodeRabbit

  • New Features
    • Added hover/press animations to buttons, including an animationMs timing option.
    • Introduced startHover() and endHover() to control hover animation.
    • Improved visual feedback for hover/press states, honoring reduced-motion preferences.
  • Bug Fixes
    • Press animation now starts before onPress when activating via Enter/Space.
    • Button cleanup now cancels any pending hover/press and loading timers on unmount.
  • Tests
    • Expanded button animation and interaction test coverage.
  • Chores
    • Updated Vitest tooling versions in development dependencies.

- Add startHover/endHover/startPress methods with progress-based transitions
- Hover background color and press color-invert effect, animationMs configurable
- Auto-detect focus changes in _renderSelf since isFocused is set directly
- Respects prefersReducedMotion
- Add tests for hover/press behavior

Closes Karanjot786#1735
@pixeltannu pixeltannu requested a review from Karanjot786 as a code owner June 22, 2026 14:22
@github-actions github-actions Bot added type:feature +10 pts. New feature. area:widgets @termuijs/widgets type:testing +10 pts. Tests. and removed type:feature +10 pts. New feature. labels Jun 22, 2026
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 70f5be5c-c927-4b8f-b938-fa5165226429

📥 Commits

Reviewing files that changed from the base of the PR and between a9a1e20 and 7b728f0.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • package.json
  • packages/widgets/src/input/Button.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/widgets/src/input/Button.test.ts

📝 Walkthrough

Walkthrough

The Button widget gains hover and press animation support. ButtonOptions adds an animationMs option and a HOVER_BG_COLORS map is introduced. New public startHover and endHover methods schedule markDirty calls via a setTimeout loop respecting reduced-motion preferences. handleKey now calls startPress before onPress. _renderSelf is rewritten to compute progress-based color transitions and uses string writes instead of cell-by-cell rendering. Tests cover the new animation callbacks. Package dependencies also pin vitest versions.

Changes

Button hover/press animation

Layer / File(s) Summary
ButtonOptions, color maps, and internal animation state
packages/widgets/src/input/Button.ts
ButtonOptions gains animationMs; HOVER_BG_COLORS map added; Button class adds _hoverStartTime, _pressStartTime, _animationTimer, _wasFocused, and _animationMs fields; constructor initializes _animationMs with default 200.
Animation methods, handleKey press trigger, and unmount cleanup
packages/widgets/src/input/Button.ts
setLoading simplified; startHover, endHover, and _scheduleAnimation added respecting prefersReducedMotion; handleKey calls startPress before onPress; unmount clears _animationTimer.
_renderSelf: hover/press progress and string-based drawing
packages/widgets/src/input/Button.ts
Detects focus transitions to trigger hover animation, computes hover/press progress, swaps fg/bg during press, derives border/text colors from state, centers spinner+label string, and replaces setCell-driven rendering with string writes.
Animation tests and core import
packages/widgets/src/input/Button.test.ts
Adds @termuijs/core import for motion-related mocking; new Hover/press animation block tests startHover async markDirty, endHover state clear, isFocused=true hover background on render, and startPress no-throw.

Vitest dependency version pinning

Layer / File(s) Summary
Vitest version pinning
package.json
@vitest/coverage-v8 and vitest versions changed from ^4.1.8 to 1.6.0 with caret-range specifiers switched to pinned versions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Karanjot786/TermUI#722: Original Button widget implementation that this PR extends with hover/press animation state, public methods, and rendering changes.

Suggested labels

type:feature, area:widgets, quality:clean

Suggested reviewers

  • Karanjot786

Poem

🐇 Buttons hop and dance with flair,
Hover, press—smooth as morning air!
animationMs ticks the rhythm true,
Color swaps with progress too.
From static clicks to lively grace,
A rabbit paints animation's face! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature addition: hover and press animations for the Button component.
Description check ✅ Passed The description includes required sections: closes reference (#1735), changes summary with implementation details, and testing notes. All critical sections are complete.
Linked Issues check ✅ Passed The PR implements the core requirements from #1735: smooth hover/press animations, respects prefersReducedMotion(), adds test coverage, and works across Button variants.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing hover/press animations. The Vitest version bump in package.json aligns with testing infrastructure needs for the new tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the type:feature +10 pts. New feature. label Jun 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/widgets/src/input/Button.ts (1)

260-264: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consolidate animation scheduling to avoid duplication.

This setTimeout pattern duplicates _scheduleAnimation(). Consider reusing the existing method.

♻️ Suggested refactor
-        if (isAnimating && this._animationTimer == null) {
-            this._animationTimer = setTimeout(() => {
-                this._animationTimer = null;
-                this.markDirty();
-            }, 16);
-        }
+        if (isAnimating) {
+            this._scheduleAnimation();
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/widgets/src/input/Button.ts` around lines 260 - 264, The animation
timer setup logic in the conditional block checking isAnimating and
_animationTimer is duplicating the functionality already present in the
_scheduleAnimation() method. Replace the inline setTimeout code that sets
_animationTimer, nullifies it after delay, and calls markDirty() with a direct
call to _scheduleAnimation() instead to consolidate the animation scheduling
logic into a single reusable method.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/widgets/src/input/Button.test.ts`:
- Around line 199-206: The assertion in the test case for focusing the button
(where isFocused = true) is too weak. Replace the
`expect(screen.back[1][1].bg).toBeDefined()` assertion with a stronger check
that verifies the actual hover background color value is applied correctly when
the button is focused. Instead of just checking if bg is defined, assert that bg
contains the expected hover background color value (not just any truthy value
like { type: 'none' }).
- Around line 208-212: The test `'startPress triggers re-render and does not
throw'` has a misleading name because it only verifies that `startPress()` does
not throw an exception, but does not actually verify that a re-render is
triggered. Either rename the test to accurately reflect what is being tested
(e.g., `'startPress does not throw'`), or add meaningful assertions that verify
the re-render behavior by checking that the press animation schedules a
re-render, following the same pattern used in the `startHover` test.
- Around line 179-197: The tests for `startHover()` and `endHover()` are failing
because `prefersReducedMotion()` returns true in the test environment, causing
both methods to return early before calling `markDirty()`. Fix this by mocking
`caps.motion` to true in both test cases (for 'startHover schedules a re-render
via markDirty after the animation tick' and 'endHover clears hover state and
calls markDirty') to enable animations and allow the methods to proceed past the
early return check. Apply the established mocking pattern that is already
documented in the terminal environment capabilities test suite.

---

Nitpick comments:
In `@packages/widgets/src/input/Button.ts`:
- Around line 260-264: The animation timer setup logic in the conditional block
checking isAnimating and _animationTimer is duplicating the functionality
already present in the _scheduleAnimation() method. Replace the inline
setTimeout code that sets _animationTimer, nullifies it after delay, and calls
markDirty() with a direct call to _scheduleAnimation() instead to consolidate
the animation scheduling logic into a single reusable method.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e84684cd-c26a-4039-811f-9d8a0dca53db

📥 Commits

Reviewing files that changed from the base of the PR and between 35c2213 and a9a1e20.

📒 Files selected for processing (2)
  • packages/widgets/src/input/Button.test.ts
  • packages/widgets/src/input/Button.ts

Comment thread packages/widgets/src/input/Button.test.ts
Comment on lines +199 to +206
it('focusing the button (isFocused = true) renders with hover background', () => {
const { button, screen } = renderButton('Click');

button.isFocused = true;
button.render(screen);

expect(screen.back[1][1].bg).toBeDefined();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen assertion to verify actual hover background color.

toBeDefined() passes for any non-undefined value, including { type: 'none' }. The test should verify the specific hover background color is applied when focused.

💡 Suggested improvement
 it('focusing the button (isFocused = true) renders with hover background', () => {
     const { button, screen } = renderButton('Click');

     button.isFocused = true;
     button.render(screen);

-    expect(screen.back[1][1].bg).toBeDefined();
+    // Verify hover background is applied (default variant uses white hover bg)
+    expect(screen.back[1][1].bg).toEqual({ type: 'named', name: 'white' });
 });

As per coding guidelines: "Tests must be real. expect(true).toBe(true) and expect(widget).toBeDefined() are not tests. Assert observable behavior or rendered output."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('focusing the button (isFocused = true) renders with hover background', () => {
const { button, screen } = renderButton('Click');
button.isFocused = true;
button.render(screen);
expect(screen.back[1][1].bg).toBeDefined();
});
it('focusing the button (isFocused = true) renders with hover background', () => {
const { button, screen } = renderButton('Click');
button.isFocused = true;
button.render(screen);
// Verify hover background is applied (default variant uses white hover bg)
expect(screen.back[1][1].bg).toEqual({ type: 'named', name: 'white' });
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/widgets/src/input/Button.test.ts` around lines 199 - 206, The
assertion in the test case for focusing the button (where isFocused = true) is
too weak. Replace the `expect(screen.back[1][1].bg).toBeDefined()` assertion
with a stronger check that verifies the actual hover background color value is
applied correctly when the button is focused. Instead of just checking if bg is
defined, assert that bg contains the expected hover background color value (not
just any truthy value like { type: 'none' }).

Source: Coding guidelines

Comment on lines +208 to +212
it('startPress triggers re-render and does not throw', () => {
const { button } = renderButton('Click');

expect(() => button.startPress()).not.toThrow();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test is a placeholder — does not verify re-render behavior.

The test name claims startPress triggers re-render but the assertion only checks that no exception is thrown. This violates the guideline against placeholder tests.

Either rename to reflect actual behavior tested, or add meaningful assertions that verify the press animation schedules a re-render (similar to the startHover test pattern).

💡 Suggested improvement
-it('startPress triggers re-render and does not throw', () => {
-    const { button } = renderButton('Click');
-
-    expect(() => button.startPress()).not.toThrow();
-});
+it('startPress schedules a re-render via markDirty after the animation tick', async () => {
+    // Mock caps.motion to enable animations
+    const { button } = renderButton('Click');
+    const markSpy = vi.spyOn(button, 'markDirty');
+
+    button.startPress();
+    await new Promise(resolve => setTimeout(resolve, 30));
+
+    expect(markSpy).toHaveBeenCalled();
+});

As per coding guidelines: "Tests must be real... Do not leave placeholder tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/widgets/src/input/Button.test.ts` around lines 208 - 212, The test
`'startPress triggers re-render and does not throw'` has a misleading name
because it only verifies that `startPress()` does not throw an exception, but
does not actually verify that a re-render is triggered. Either rename the test
to accurately reflect what is being tested (e.g., `'startPress does not
throw'`), or add meaningful assertions that verify the re-render behavior by
checking that the press animation schedules a re-render, following the same
pattern used in the `startHover` test.

Source: Coding guidelines

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

Labels

area:widgets @termuijs/widgets type:feature +10 pts. New feature. type:testing +10 pts. Tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] Add smooth hover animation for Button component

1 participant