Skip to content

feat(card): add accessible card component - #7

Open
LikithaYadavG wants to merge 2 commits into
mainfrom
feat/card-component
Open

LikithaYadavG wants to merge 2 commits into
mainfrom
feat/card-component

Conversation

@LikithaYadavG

Copy link
Copy Markdown
Collaborator

PR Template & Definition of Done

What does this PR do?

  • Adds a reusable, accessible Card component with multiple variants
  • Adds sub-components: CardHeader, CardBody, CardFooter, CardTitle, CardDescription, CardImage
  • Supports three card types: static, clickable (button), and link (anchor)

What steps does your reviewer have to take to test this PR manually?

  1. Import Card component: import { Card, CardBody, CardTitle } from "@/components/card/Card"
  2. Test static card: <Card variant="outlined"><CardBody>Content</CardBody></Card>
  3. Test clickable card: <Card onClick={handleClick} hover>Click me</Card>
  4. Test link card: <Card href="/page" hover>Navigate</Card>
  5. Verify keyboard navigation (Tab, Enter/Space) works on interactive cards
  6. Verify focus ring is visible on keyboard focus

Pull Request standards checklist - Please check off

  • This Branch will be carrying one single responsibility - feature/bugfix/style/refactor...
  • I have followed conventional commit messages and descriptive Branch naming.
  • My PR has descriptive folder/file names that I have worked on.

Testing checklist - Please check off

  • I have performed manual testing on my local to validate all changes.

If you have not followed and completed any of the above, please explain why below.
N/A

Definition of Done - Please check off

  • My code is well tested and I have confidence my code works as I expect in a variety of situations.
  • I have lint and format code is enabled when the file is saved and I have fixed all errors highlighted by lint.
  • I have deleted all non-descriptive comments and dead code from the files I've touched.
  • I have rebased my branch with the base branch I want to merge into and all the commits in this PR are my own.

@LikithaYadavG

Copy link
Copy Markdown
Collaborator Author

@cw-pr-agent review

@mergemitra

mergemitra Bot commented Jan 20, 2026

Copy link
Copy Markdown

Change Summary

This PR adds a reusable, accessible Card component and its sub-components (CardHeader, CardBody, CardFooter, CardTitle, CardDescription, CardImage). The Card supports static, clickable (button), and link (anchor) modes, includes keyboard focus styles for accessibility, and exposes variant and padding options. A comprehensive Vitest + React Testing Library test suite verifies rendering, interactivity, variants, padding, and accessibility behaviors.

File Changes

File Summary
src/components/card/Card.tsx Adds accessible Card component with variants, static/clickable/link modes, and subcomponents
src/components/card/Card.test.tsx Adds Vitest + RTL tests covering rendering, click/link behaviors, variants, padding, and subcomponents

@mergemitra

mergemitra Bot commented Jan 20, 2026

Copy link
Copy Markdown

PR Scorecard

Score

Communication Quality Code Correctness & Design Quality Test Quality & Coverage Code Readability & Maintainability
Scoring Methodology

Communication Scoring Framework

The overall communication score is a weighted average:

Dimension Weight Evaluates
PR Description Quality 60% Title format (conventional commits) + Description clarity (what changed & why)
PR Size & Scope 25% Appropriate sizing, scope cohesion, and justification for size
Commit Messages 15% Conventional commits format, atomic & descriptive changes

Formula: (Description x 0.6) + (PR Size x 0.25) + (Commits x 0.15)

Code Scoring Framework

The scorecard evaluates code using 3 key reviewer questions:

Reviewer Question Category
Is this the right solution, implemented the right way? Code Correctness
Would this catch bugs if the code broke tomorrow? Test Quality
Can someone new understand and safely modify this in 6 months? Maintainability
PR Communication Notes

Description Quality

  • ✅ Title follows conventional commits format with clear scope and descriptive summary.
  • ✅ Description follows the project PR template, explains what and why, and includes manual testing steps.

PR Size & Scope

  • ✅ PR is well-scoped and within the ideal size (379 lines across 2 files) for a new component and tests.
  • ✅ Tests cover variants and interactive behaviors, which justify the added lines and keep scope focused.

Commit Messages

  • ✅ Commit message follows conventional commits format and clearly summarizes the feature and subcomponents.

Notes

Code Correctness & Design Quality

  • 🟠 Wrapping arbitrary children in <button>/<a> at src/components/card/Card.tsx:88 can create invalid nested interactive markup and accessibility failures
  • 🟠 Spreading buttonProps after explicit props at src/components/card/Card.tsx:90 allows overriding type="button", risking accidental form submissions

Test Quality & Coverage

  • 🟠 Missing keyboard focus/activation coverage at src/components/card/Card.test.tsx:47 could miss regressions against stated Tab/Enter/Space accessibility requirements
  • 🟠 Tailwind class assertions at src/components/card/Card.test.tsx:79 are brittle and may fail on non-behavioral styling refactors

Code Readability & Maintainability

  • 💬 getByRole("link") without an accessible name at src/components/card/Card.test.tsx:70 reduces test clarity if additional links are added later
  • 🟠 Reliance on global React.* type namespace at src/components/card/Card.tsx:17 may break in stricter TS setups and is inconsistent with explicit imports
  • 💬 Card section spacing is hardcoded across components at src/components/card/Card.tsx:140 and src/components/card/Card.tsx:205, making future spacing changes easy to miss

@@ -0,0 +1,213 @@
import type {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: 🟠 Major
Avoid relying on the global React namespace for types; import the needed types explicitly for consistency and stricter TS configs.

Suggested Change:

import type {
  AnchorHTMLAttributes,
  ButtonHTMLAttributes,
  HTMLAttributes,
  ImgHTMLAttributes,
  MouseEventHandler,
  ReactNode,
} from "react";

export type CardBaseProps = {
  // ...
  children?: ReactNode;
};

type CardImageProps = {
  src: string;
  alt: string;
  className?: string;
} & Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "className">;

children?: React.ReactNode;
};

export type ClickableCardProps = CardBaseProps &

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: 🟠 Major
Prevent consumers from overriding type="button" and make prop precedence explicit by spreading buttonProps first and omitting type from the public props.

Why this matters: A Card used inside a <form> can unexpectedly submit if type is overridden to submit.

Suggested Change:

export type ClickableCardProps = CardBaseProps &
  Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "type"> & {
    onClick: MouseEventHandler<HTMLButtonElement>;
    href?: never;
    "aria-label"?: string;
  };

// ...

<button
  {...buttonProps}
  type="button"
  onClick={onClick}
  aria-label={ariaLabel}
  className={`${baseStyles} text-left w-full ${focusStyles}`}
>
  {children}
</button>

const focusStyles =
"focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2";

export const Card = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: 🟠 Major
Document that interactive Cards render a native <button>/<a> and therefore must not contain nested interactive elements.

Why this matters: Nested buttons/links/inputs inside a button/anchor are invalid HTML and commonly break screen readers and keyboard interaction.

Suggested Change:

/**
 * When `onClick` is provided, Card renders a <button>; when `href` is provided, Card renders an <a>.
 * Avoid nesting interactive elements (buttons, links, inputs) inside interactive Cards.
 */
export const Card = ({
  variant = "elevated",
  hover = false,
  padding = "none",
  className = "",
  children,
  ...restProps
}: CardProps) => {
  // ...
};


const isInteractive = isClickable || isLink;

const baseStyles = [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: 💬 Minor [nitpick]
Disable hover affordances when the clickable Card is disabled, so visuals match behavior.

Suggested Change:

const isClickable = "onClick" in restProps && typeof restProps.onClick === "function";
const isLink = "href" in restProps && typeof restProps.href === "string";
const isDisabled =
  isClickable && "disabled" in restProps && Boolean((restProps as { disabled?: boolean }).disabled);

const baseStyles = [
  "rounded-lg transition-all",
  variantStyles[variant],
  paddingStyles[padding],
  hover && isInteractive && !isDisabled && "hover:shadow-lg hover:-translate-y-0.5",
  className,
]
  .filter(Boolean)
  .join(" ");

);
};

export const CardHeader = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: 💬 Minor [nitpick]
Extract the repeated section padding into a single constant so CardHeader/Body/Footer and CardImage stay in sync.

Suggested Change:

const SECTION_PADDING = "px-6 py-4";

export const CardHeader = ({ className = "", children, ...props }: HTMLAttributes<HTMLElement>) => (
  <header
    className={`${SECTION_PADDING} border-b border-gray-200 ${className}`}
    {...props}
  >
    {children}
  </header>
);

export const CardBody = ({ className = "", children, ...props }: HTMLAttributes<HTMLDivElement>) => (
  <div className={`${SECTION_PADDING} ${className}`} {...props}>
    {children}
  </div>
);

export const CardFooter = ({ className = "", children, ...props }: HTMLAttributes<HTMLElement>) => (
  <footer
    className={`${SECTION_PADDING} border-t border-gray-200 ${className}`}
    {...props}
  >
    {children}
  </footer>
);

).toBeInTheDocument();
});

it("should call onClick when button card is clicked", async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: 🟠 Major
Add keyboard interaction tests (Tab + Enter/Space) for interactive cards to match the PR’s stated accessibility requirements.

Suggested Change:

it("should be focusable via Tab and activate on Enter for clickable card", async () => {
  const user = userEvent.setup();
  const onClick = vi.fn();

  renderCard({
    additionalProps: {
      onClick,
      "aria-label": "Clickable card",
    },
  });

  await user.tab();
  expect(screen.getByRole("button", { name: /clickable card/i })).toHaveFocus();

  await user.keyboard("{Enter}");
  expect(onClick).toHaveBeenCalledTimes(1);
});

it("should be focusable via Tab when rendered as a link", async () => {
  const user = userEvent.setup();

  renderCard({ additionalProps: { href: "/test-link" } });

  await user.tab();
  expect(screen.getByRole("link", { name: /card content/i })).toHaveFocus();
});

Comment thread src/components/card/Card.test.tsx Outdated
expect(screen.getByRole("article")).toBeInTheDocument();
});

it("should apply elevated variant styles by default", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: 🟠 Major
Avoid asserting implementation-specific Tailwind classes; add stable data-* attributes and assert those instead.

Why this matters: It keeps tests resilient to style refactors while still validating the chosen variant/padding.

Suggested Change:

// Card.tsx: add stable attributes on the rendered interactive element
// static:
<article className={baseStyles} data-variant={variant} data-padding={padding} {...staticProps}>

// clickable: (on <button>)
<button ... data-variant={variant} data-padding={padding}>

// link: (on <a>)
<a ... data-variant={variant} data-padding={padding}>
// Card.test.tsx: assert data attributes instead of classnames
it("should apply elevated variant by default", () => {
  renderCard();
  expect(screen.getByRole("article")).toHaveAttribute("data-variant", "elevated");
});

it("should apply outlined variant", () => {
  renderCard({ additionalProps: { variant: "outlined" } });
  expect(screen.getByRole("article")).toHaveAttribute("data-variant", "outlined");
});

it("should apply padding when provided", () => {
  renderCard({ additionalProps: { padding: "md" } });
  expect(screen.getByRole("article")).toHaveAttribute("data-padding", "md");
});

@mergemitra

mergemitra Bot commented Jan 20, 2026

Copy link
Copy Markdown
PR Insights

Potential PR Improvements

  • Testing: Add keyboard and focus-visible behavior tests for interactive cards.
  • Testing: Add tests for flat variant and other padding sizes.
  • Robustness: Define behavior when aria-label is missing on button cards.
  • Documentation: Add usage docs for subcomponents and styling options.
  • Code Maintainability: Consider sharing wrapper markup between link and button paths.

Strengths

  • Description Quality: PR description gives clear manual test steps.
  • Testing: Tests cover rendering, variants, and interactive card roles.
  • Robustness: Props union prevents mixing href and onClick.
  • Best Practices: Uses semantic roles and focus-visible styles for accessibility.
  • Code Maintainability: Subcomponents keep card layout consistent across usages.

Comment thread src/components/card/Card.test.tsx Outdated
Comment on lines +15 to +17
type RenderOptions = {
additionalProps?: CardProps;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

we don't have to create it as separate type.

Comment on lines +106 to +110
render(
<Card>
<CardHeader>Header</CardHeader>
</Card>,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

check the possible way to render the children also in the renderCard function itself

Comment thread src/components/card/Card.test.tsx Outdated
Comment on lines +87 to +89
additionalProps: {
variant: "outlined",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

it could be directly a variant instead of additionalProps:{} and sending.

Comment thread src/components/card/Card.test.tsx Outdated
Comment on lines +85 to +93
it("should apply outlined variant styles", () => {
renderCard({
additionalProps: {
variant: "outlined",
},
});

expect(screen.getByRole("article")).toHaveClass("border-2");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Checking the Styles is redundant.

Comment on lines +139 to +144
<header
className={`px-6 py-4 border-b border-gray-200 ${className}`}
{...props}
>
{children}
</header>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good to see semantic tags✅

Comment thread src/components/card/Card.tsx Outdated
Comment on lines +190 to +197
type CardImageProps = {
src: string;
alt: string;
className?: string;
} & Omit<
React.ImgHTMLAttributes<HTMLImageElement>,
"src" | "alt" | "className"
>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

remove this omits. and check the source type itself.

@mergemitra

mergemitra Bot commented Feb 4, 2026

Copy link
Copy Markdown

Tip

Need another review?

Tag me and say rereview for re-analysis after you have fixed all the issues.

@cw-pr-agent rereview

@LikithaYadavG
LikithaYadavG changed the base branch from feat/empty-state to main February 4, 2026 11:08
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.

2 participants