Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BoundaryBadge, StatusPill } from "@/components/site/control-primitives";
import { TableScroll } from "@/components/site/data-table";
import { EmptyState } from "@/components/site/state-views";
import {
QUALITY_BAND_TONE,
Expand Down Expand Up @@ -30,13 +31,22 @@ export function ContributorQualityTable({
description="Quality bands appear once this maintainer's repos have cached open pull requests to shape."
/>
) : (
<div className="mt-4 overflow-x-auto">
<table className="w-full min-w-[420px] text-left text-token-sm">
<TableScroll className="mt-4" label="Top contributors by quality band">
<table className="w-full min-w-[420px] whitespace-nowrap text-left text-token-sm">
<caption className="sr-only">
Contributors with their quality band and open pull request count.
</caption>
<thead>
<tr className="border-b-hairline font-mono text-token-2xs uppercase tracking-wider text-muted-foreground">
<th className="py-2 pr-3 font-normal">Contributor</th>
<th className="py-2 pr-3 font-normal">Band</th>
<th className="py-2 font-normal">Open PRs</th>
<th scope="col" className="py-2 pr-3 font-normal">
Contributor
</th>
<th scope="col" className="py-2 pr-3 font-normal">
Band
</th>
<th scope="col" className="py-2 font-normal">
Open PRs
</th>
</tr>
</thead>
<tbody>
Expand All @@ -58,7 +68,7 @@ export function ContributorQualityTable({
))}
</tbody>
</table>
</div>
</TableScroll>
)}
</section>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { MaintainerSettings } from "@/components/site/app-panels/maintainer-sett
import { OnboardingPreviewCard } from "@/components/site/app-panels/onboarding-preview-card";
import { CheckRunReadinessTable } from "@/components/site/check-run-readiness-table";
import type { CheckRunReadinessTableData } from "@/components/site/check-run-readiness-model";
import { TableScroll } from "@/components/site/data-table";
import { StatCard } from "@/components/site/primitives";
import { RefreshMeta } from "@/components/site/refresh-meta";
import { EmptyState, LoadingState, StateBoundary } from "@/components/site/state-views";
Expand Down Expand Up @@ -340,52 +341,71 @@ function MaintainerDashboardView({
private
</span>
</div>
<table className="mt-4 w-full text-left text-token-sm">
<thead>
<tr className="border-b-hairline font-mono text-token-2xs uppercase tracking-wider text-muted-foreground">
<th className="py-2 pr-3 font-normal">PR</th>
<th className="py-2 pr-3 font-normal">Title</th>
<th className="py-2 pr-3 font-normal">Author</th>
<th className="py-2 pr-3 font-normal">Bucket</th>
<th className="py-2 pr-3 font-normal">Slop</th>
<th className="py-2 font-normal">Reason</th>
</tr>
</thead>
<tbody>
{data.reviewability.map((row) => (
<tr
key={row.pr}
className="border-b-hairline last:border-b-0 transition-colors hover:bg-muted/40"
>
<td className="py-2 pr-3 font-mono text-token-xs text-foreground/90">
{row.pr}
</td>
<td className="py-2 pr-3">{row.title}</td>
<td className="py-2 pr-3 text-token-xs text-muted-foreground">{row.author}</td>
<td className="py-2 pr-3">
<StatusPill status={BUCKET_TONE[row.bucket] ?? "info"}>
{row.bucket}
</StatusPill>
</td>
<td className="py-2 pr-3">
{row.slop ? (
<StatusPill status={SLOP_BAND_TONE[row.slop.band] ?? "info"}>
{row.slop.band} {row.slop.risk}
</StatusPill>
) : (
<span
className="text-token-xs text-muted-foreground"
title="Slop detection is off for this repo, or this PR has not been assessed yet."
>
</span>
)}
</td>
<td className="py-2 text-token-xs text-muted-foreground">{row.reason}</td>
<TableScroll className="mt-4" label="Reviewability queue">
<table className="w-full whitespace-nowrap text-left text-token-sm">
<caption className="sr-only">
Reviewable pull requests with bucket, slop band, and reason.
</caption>
<thead>
<tr className="border-b-hairline font-mono text-token-2xs uppercase tracking-wider text-muted-foreground">
<th scope="col" className="py-2 pr-3 font-normal">
PR
</th>
<th scope="col" className="py-2 pr-3 font-normal">
Title
</th>
<th scope="col" className="py-2 pr-3 font-normal">
Author
</th>
<th scope="col" className="py-2 pr-3 font-normal">
Bucket
</th>
<th scope="col" className="py-2 pr-3 font-normal">
Slop
</th>
<th scope="col" className="py-2 font-normal">
Reason
</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{data.reviewability.map((row) => (
<tr
key={row.pr}
className="border-b-hairline last:border-b-0 transition-colors hover:bg-muted/40"
>
<td className="py-2 pr-3 font-mono text-token-xs text-foreground/90">
{row.pr}
</td>
<td className="py-2 pr-3">{row.title}</td>
<td className="py-2 pr-3 text-token-xs text-muted-foreground">
{row.author}
</td>
<td className="py-2 pr-3">
<StatusPill status={BUCKET_TONE[row.bucket] ?? "info"}>
{row.bucket}
</StatusPill>
</td>
<td className="py-2 pr-3">
{row.slop ? (
<StatusPill status={SLOP_BAND_TONE[row.slop.band] ?? "info"}>
{row.slop.band} {row.slop.risk}
</StatusPill>
) : (
<span
className="text-token-xs text-muted-foreground"
title="Slop detection is off for this repo, or this PR has not been assessed yet."
>
</span>
)}
</td>
<td className="py-2 text-token-xs text-muted-foreground">{row.reason}</td>
</tr>
))}
</tbody>
</table>
</TableScroll>
</section>

<GateOutcomeCard breakdown={data.qualityDashboard.gateOutcomeBreakdown} />
Expand Down Expand Up @@ -717,24 +737,35 @@ export function PreviewResult({

{preview.installation?.permissionRemediation.length ? (
<div className="overflow-hidden rounded-token border-hairline">
<table className="w-full text-left text-token-xs">
<thead className="border-b-hairline font-mono uppercase tracking-wider text-muted-foreground">
<tr>
<th className="px-3 py-2 font-normal">Permission</th>
<th className="px-3 py-2 font-normal">Current</th>
<th className="px-3 py-2 font-normal">Required</th>
</tr>
</thead>
<tbody>
{preview.installation.permissionRemediation.map((row) => (
<tr key={row.permission} className="border-b-hairline last:border-b-0">
<td className="px-3 py-2 text-foreground">{row.permission}</td>
<td className="px-3 py-2 text-muted-foreground">{row.currentAccess}</td>
<td className="px-3 py-2 text-muted-foreground">{row.requiredAccess}</td>
<TableScroll label="Permission remediation">
<table className="w-full whitespace-nowrap text-left text-token-xs">
<caption className="sr-only">
GitHub App permission remediation: current versus required access per permission.
</caption>
<thead className="border-b-hairline font-mono uppercase tracking-wider text-muted-foreground">
<tr>
<th scope="col" className="px-3 py-2 font-normal">
Permission
</th>
<th scope="col" className="px-3 py-2 font-normal">
Current
</th>
<th scope="col" className="px-3 py-2 font-normal">
Required
</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{preview.installation.permissionRemediation.map((row) => (
<tr key={row.permission} className="border-b-hairline last:border-b-0">
<td className="px-3 py-2 text-foreground">{row.permission}</td>
<td className="px-3 py-2 text-muted-foreground">{row.currentAccess}</td>
<td className="px-3 py-2 text-muted-foreground">{row.requiredAccess}</td>
</tr>
))}
</tbody>
</table>
</TableScroll>
</div>
) : null}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ export function StatusPill({
return (
<span
className={cn(
"inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 font-mono text-token-2xs uppercase tracking-wider",
"inline-flex items-center gap-1 whitespace-nowrap rounded-full border px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wider",
STATUS_STYLES[status],
className,
)}
>
<span className="size-1.5 rounded-full bg-current" />
<span className="size-1 shrink-0 rounded-full bg-current" />
{children}
</span>
);
Expand All @@ -53,12 +53,12 @@ export function BoundaryBadge({ boundary, className }: { boundary: Boundary; cla
return (
<span
className={cn(
"inline-flex items-center gap-1.5 rounded-token border px-2 py-0.5 font-mono text-token-2xs uppercase tracking-wider",
"inline-flex items-center gap-1 whitespace-nowrap rounded-token border px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wider",
tone,
className,
)}
>
<span className="size-1 rounded-full bg-current" />
<span className="size-1 shrink-0 rounded-full bg-current" />
{label}
</span>
);
Expand Down
44 changes: 44 additions & 0 deletions apps/gittensory-ui/src/components/site/data-table.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import { TableScroll } from "@/components/site/data-table";

describe("TableScroll", () => {
it("wraps its table in a keyboard-focusable, labelled scroll region (WCAG 2.1.1)", () => {
render(
<TableScroll label="Example data">
<table>
<caption className="sr-only">Example data rows</caption>
<tbody>
<tr>
<td>cell</td>
</tr>
</tbody>
</table>
</TableScroll>,
);
const region = screen.getByRole("region", { name: "Example data" });
// A bare overflow-x-auto div is not a tab stop; this one is, so keyboard users can scroll it.
expect(region.tabIndex).toBe(0);
expect(region.className).toContain("overflow-x-auto");
// The inner table takes its accessible name from the caption, not the region label.
expect(screen.getByRole("table", { name: "Example data rows" })).toBeTruthy();
});

it("merges a caller className onto the scroll region without dropping the base classes", () => {
render(
<TableScroll label="Wide table" className="mt-4">
<table>
<tbody>
<tr>
<td>x</td>
</tr>
</tbody>
</table>
</TableScroll>,
);
const region = screen.getByRole("region", { name: "Wide table" });
expect(region.className).toContain("mt-4");
expect(region.className).toContain("overflow-x-auto");
});
});
33 changes: 33 additions & 0 deletions apps/gittensory-ui/src/components/site/data-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ReactNode } from "react";

import { cn } from "@/lib/utils";

/**
* Accessible horizontal-scroll container for a wide data table (#794). A table that overflows its
* column area on a narrow viewport can be scrolled by pointer/trackpad but NOT by keyboard alone:
* marking the scroll region focusable (`tabIndex={0}`) with `role="region"` + an `aria-label` gives
* keyboard users a real tab stop to scroll from (WCAG 2.1.1 Keyboard) and names the region for
* assistive tech. Pair with a `<caption>` and `scope="col"` headers on the table inside — the three
* together are the standard responsive-table a11y pattern this app's bare `overflow-x-auto` divs and
* caption-less tables were missing.
*/
export function TableScroll({
label,
className,
children,
}: {
label: string;
className?: string;
children: ReactNode;
}) {
return (
<div
role="region"
aria-label={label}
tabIndex={0}
className={cn("overflow-x-auto focus-ring", className)}
>
{children}
</div>
);
}
Loading