Skip to content

Comments

Feat: Expand test coverage for Pagination component#178

Open
lairsinc wants to merge 1 commit intomainfrom
feat/pagination-test-coverage
Open

Feat: Expand test coverage for Pagination component#178
lairsinc wants to merge 1 commit intomainfrom
feat/pagination-test-coverage

Conversation

@lairsinc
Copy link
Collaborator

@lairsinc lairsinc commented Dec 4, 2025

Overview: This PR significantly expands the test coverage for the Pagination component.

Changes

  • Added tests for various edge cases, including single-page scenarios.
  • Ensured correct disabled states for first and last pages.
  • Improved robustness with new keyboard navigation tests.
  • All new tests are located in src/components/ui/pagination.test.tsx.

Summary by CodeRabbit

  • New Features

    • Enhanced keyboard navigation support for pagination with arrow key, Home, and End key support
    • Added ability to disable pagination links
    • Improved focus management and control behavior
  • Tests

    • Expanded test coverage for keyboard navigation edge cases and disabled pagination scenarios

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 4, 2025

Walkthrough

This pull request refactors the pagination component's keyboard navigation and focus management. It converts PaginationLink to a forwardRef component with disabled prop support, updates PaginationContent to filter disabled links when managing focus, rewrites the keydown handler to support ArrowRight, ArrowLeft, Home, and End key navigation, and expands test coverage for keyboard navigation and focus edge cases.

Changes

Cohort / File(s) Summary
Pagination component implementation
src/components/ui/pagination.tsx
Converted PaginationLink to forwardRef component with disabled prop; updated PaginationContent to filter disabled links when collecting focusable links; rewrote keydown handler to support ArrowRight/ArrowLeft/Home/End navigation with wrap-around; changed initial focusedIndex initialization from active-link-based to always 0; enhanced context-based tabIndex logic to account for disabled state.
Pagination test suite
src/components/ui/pagination.test.tsx
Removed isActive assertions for second page link; introduced consistent navigation element reference in keyboard event simulations; added new test scenarios for initial tabindex assignments, single-page rendering (totalPages = 1), disabled previous/next controls on boundary pages, and keyboard navigation behavior with disabled controls including Home/End keys and focus management.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • PaginationLink component signature change: Verify forwardRef implementation correctly exposes the HTMLAnchorElement ref and that disabled prop integration with tabIndex logic is sound.
  • Focus management refactor: Review the disabled-link filtering logic in PaginationContent and ensure focusedIndex calculations work correctly across all keyboard navigation scenarios (arrow keys, Home, End, wrap-around).
  • Test coverage expansion: Confirm new test scenarios accurately validate focus order, disabled state handling, and keyboard navigation edge cases.

Possibly related PRs

Suggested reviewers

  • metanodreamer

Poem

🐰 Hop through pages with keys so grand,
Arrow and Home at paw's command!
Disabled links? They step aside,
Focus flows with graceful guide,
ref forwarding, context bright—
Pagination hops just right!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title only mentions test coverage expansion, but the changeset includes significant production code changes to the Pagination component (API changes to PaginationLink, disabled prop handling, refactoring to forwardRef). Update the title to reflect both test coverage expansion AND the production code refactoring, such as 'Feat: Refactor Pagination keyboard navigation and expand test coverage' or similar.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pagination-test-coverage

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/components/ui/pagination.test.tsx (2)

90-146: Keyboard navigation tests are thorough; consider userEvent.keyboard as a minor improvement

The ArrowLeft/ArrowRight/Home/End scenarios cover the roving focus behavior well, including wrap‑around and previous/next controls. If you want slightly more realistic interaction simulation in future, you could switch from fireEvent.keyDown on the navigation container to userEvent.keyboard, but that’s purely optional here.

Also applies to: 148-186


188-337: Nice edge‑case coverage for disabled and single‑page scenarios

These tests do a good job validating:

  • Single‑page behavior with both prev/next disabled.
  • Disabled prev on first page and disabled next on last page.
  • Roving focus and Home/End behavior when prev is disabled but next is not.

If you later harden pointer behavior for disabled links (e.g., ignoring clicks), you might add a small test asserting that clicking a disabled control does not change focus or trigger navigation; current tests already cover the keyboard side well.

src/components/ui/pagination.tsx (2)

41-55: Link registration + DOM querying are duplicative and potentially brittle

linksRef is maintained both via registerLink/unregisterLink and via the useLayoutEffect that overwrites it with querySelectorAll('[data-slot="pagination-link"]:not([disabled])'). This dual source of truth makes index bookkeeping (linkIndex vs focusedIndex) harder to reason about, especially when children are added/removed or re‑ordered.

Given that handleKeyDown already recomputes focusableLinks from the DOM, consider simplifying by:

  • Either relying solely on DOM queries for both focusLink and index derivation (e.g., focusLink uses a fresh querySelectorAll with the same selector), and dropping registerLink/unregisterLink/linkIndex.
  • Or, if you prefer context registration, stop overwriting linksRef.current from the DOM and base navigation solely on the registered list.

This will make focus management more predictable and reduce the chance of subtle index desynchronization bugs when the children change.

Also applies to: 56-69, 71-79


120-125: Disabled semantics on <a> could be improved for accessibility and click behavior

PaginationLink introduces a disabled prop and:

  • Filters links with :not([disabled]) to build the focusable list.
  • Sets tabIndex={-1} when disabled is true.
  • Passes disabled={disabled} directly to the <a> element.

This gives correct behavior for roving tabindex, but:

  • <a> elements don’t natively support disabled; screen readers and browsers won’t treat them as inert buttons.
  • Pointer clicks on a disabled link will still follow href / fire onClick unless you handle that explicitly.

If you want disabled pagination controls to behave like true disabled buttons, consider:

  • Adding aria-disabled={disabled || undefined} and possibly role="button" when appropriate; and/or
  • Preventing navigation when disabled (e.g., ignore onClick / call event.preventDefault() when disabled); or
  • Rendering a <button disabled> instead when disabled is true.

Tests already assert the presence of the disabled attribute and tabindex="-1", so any change should preserve those expectations while improving semantics.

Also applies to: 143-159

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84108d0 and 860b852.

📒 Files selected for processing (2)
  • src/components/ui/pagination.test.tsx (6 hunks)
  • src/components/ui/pagination.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/ui/pagination.test.tsx (1)
src/components/ui/pagination.tsx (6)
  • PaginationLink (220-220)
  • Pagination (218-218)
  • PaginationContent (219-219)
  • PaginationItem (221-221)
  • PaginationPrevious (222-222)
  • PaginationNext (223-223)
src/components/ui/pagination.tsx (2)
src/components/ui/button.tsx (2)
  • Button (53-53)
  • buttonVariants (53-53)
src/lib/utils.ts (1)
  • cn (9-11)
🔇 Additional comments (3)
src/components/ui/pagination.test.tsx (2)

47-52: Good coverage of initial roving tabindex behavior

Asserting that only the first focusable control (“Go to previous page”) starts with tabindex="0" and others -1 nicely verifies the roving tabindex pattern and aligns the test with the new focus-management behavior.


55-88: Solid regression test for children updates

This test usefully asserts that when the children set changes, the first focusable link after rerender (A) regains tabindex="0" while the others remain -1, which guards against regressions in focusedIndex reset logic when pagination items are dynamically updated.

src/components/ui/pagination.tsx (1)

126-163: Forwarded ref breaks internal registration when a consumer passes ref

In PaginationLink, you currently do:

const linkRef = React.useRef<HTMLAnchorElement>(null);

<a
  ref={ref || linkRef}
  ...
/>

and then register based on linkRef.current. When a consumer supplies a ref, ref || linkRef resolves to the external ref, so linkRef.current never gets populated and this link is never registered with the PaginationContext. That means such links won't participate correctly in the roving tabindex / keyboard navigation.

Always populate the internal linkRef, and then fan out the same node to the forwarded ref. For example:

-const PaginationLink = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(
-  ({ className, isActive, size = "icon", disabled, ...props }, ref) => {
+const PaginationLink = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(
+  ({ className, isActive, size = "icon", disabled, ...props }, ref) => {
     const context = React.useContext(PaginationContext);
     const linkRef = React.useRef<HTMLAnchorElement>(null);
@@
-    return (
-      <a
-        ref={ref || linkRef}
+    const mergedRef = React.useCallback(
+      (node: HTMLAnchorElement | null) => {
+        linkRef.current = node;
+        if (typeof ref === "function") {
+          ref(node);
+        } else if (ref && "current" in (ref as any)) {
+          (ref as React.MutableRefObject<HTMLAnchorElement | null>).current = node;
+        }
+      },
+      [ref]
+    );
+
+    return (
+      <a
+        ref={mergedRef}
@@
-        disabled={disabled}
+        disabled={disabled}
         tabIndex={tabIndex}
         {...props}
       />
     );
   }
 );

This keeps your internal registration working regardless of whether a parent supplies a ref, while still honoring the forwarded ref contract.

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