feat(drill): DrillableCell — hover-reveal button wrapper

This commit is contained in:
Tyler
2026-06-21 11:47:05 -06:00
parent be93dbff72
commit 7dd6a5d025
2 changed files with 76 additions and 0 deletions
@@ -0,0 +1,37 @@
// @vitest-environment happy-dom
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
import { afterEach, describe, it, expect, vi } from "vitest";
import { cleanup, render, screen, fireEvent } from "@testing-library/react";
import { DrillableCell } from "@/components/drill/DrillableCell";
// happy-dom keeps `document.body` between tests; without cleanup,
// `screen.getByRole("button")` finds buttons from earlier renders.
afterEach(() => cleanup());
describe("DrillableCell", () => {
it("renders children, applies hover affordance classes, calls onClick", () => {
const onClick = vi.fn();
render(
<DrillableCell onClick={onClick}>
<span>CLM-114</span>
</DrillableCell>,
);
const btn = screen.getByRole("button") as HTMLButtonElement;
expect(btn.classList.contains("drillable")).toBe(true);
fireEvent.click(btn);
expect(onClick).toHaveBeenCalledOnce();
});
it("disabled state hides affordance and blocks click", () => {
const onClick = vi.fn();
render(
<DrillableCell onClick={onClick} disabled>
<span>unavailable</span>
</DrillableCell>,
);
const btn = screen.getByRole("button") as HTMLButtonElement;
expect(btn.disabled).toBe(true);
expect(btn.classList.contains("drillable")).toBe(false);
});
});
+39
View File
@@ -0,0 +1,39 @@
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
interface Props {
children: ReactNode;
onClick: () => void;
disabled?: boolean;
/** Optional aria-label; defaults to the visible text content. */
ariaLabel?: string;
}
/**
* Wrap any clickable cell with hover-reveal affordance:
* cursor: pointer + accent background tint + trailing "" chevron,
* applied via the `drillable` class on hover (see `src/index.css`
* in Task 1.4).
*
* Renders as a <button> (disabled when `disabled`) so it gets keyboard
* activation (Enter/Space) and the standard disabled-button semantics.
* The `drillable` affordance class is omitted when disabled.
*/
export function DrillableCell({ children, onClick, disabled, ariaLabel }: Props) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
aria-label={ariaLabel}
className={cn(
!disabled && "drillable",
"inline-flex items-center gap-0 rounded-sm border-0 bg-transparent p-0 text-left",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
disabled && "text-muted-foreground cursor-not-allowed",
)}
>
{children}
</button>
);
}