Skip to content
Open
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
47 changes: 47 additions & 0 deletions docs/frontend-ui-audit-2026-07-24/SharedComponentAccessibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Frontend UI Audit — Shared Component Accessibility

**Files:** `src/components/DateRangeSelector/index.tsx`, `src/components/Image/index.tsx`, `src/components/SearchInput/index.tsx`, `src/components/TrafficLights/index.tsx`, `src/components/Virtualized/VirtualizedSessionList.tsx`
**Date:** 2026-07-24

## D1 — Raw HTML vs Design System

| Line | Element | Verdict | Reason | Suggested change |
| --- | --- | --- | --- | --- |
| `DateRangeSelector/index.tsx:96` | date-range trigger | fixed | A clickable `<div>` duplicated native activation and focus behavior. | Replaced with a native `button` carrying dialog expanded state. |
| `SearchInput/index.tsx:213` | replace-mode chevron | fixed | The clickable wrapper was not keyboard- or screen-reader-operable. | Replaced with a labelled native `button`. |
| `TrafficLights/index.tsx:106-128` | window controls | fixed | Three clickable `<div>` elements reimplemented keyboard activation and disabled behavior. | Replaced with native buttons; maximize uses the native `disabled` contract. |
| `Image/index.tsx:229` | preview close control | keep with reason | The close button uses preview-specific absolute geometry owned by `image-preview-close`; a general Button/IconButton would add incompatible box styling. It remains a native labelled button. | — |
| `VirtualizedSessionList.tsx:54` | virtual session row | keep with reason | The virtualizer expects a block row and measures that node directly. The row now supplies button role, focus, accessible name, and Enter/Space activation without changing the measured element type. | — |

## D2 — Arbitrary Tailwind Value vs Token

| Line | Value | Verdict | Reason | Suggested change |
| --- | --- | --- | --- | --- |
| `DateRangeSelector/index.tsx:90-104` | `rounded-[8px]`, `py-[1px]`, `text-[14px]`, `font-[400]` | fixed | Each value has a project spacing/typography equivalent. | Replaced with `rounded-lg`, `py-px`, `text-sm`, and `font-normal`. |
| `TrafficLights/index.tsx:108-126` | 14px circles and 0.5px borders | keep with reason | These optical values reproduce the macOS traffic-light control rather than a general product spacing pattern. | — |

## D3 — Hardcoded Sizes / Colors

| Line | Value | Verdict | Reason | Suggested change |
| --- | --- | --- | --- | --- |
| `TrafficLights/index.tsx:108-126` | macOS red/yellow/green hex colors | keep with reason | The colors are platform-native control colors, not product semantic state colors, and are isolated to this dedicated component. | — |

## D4 — Accessibility

| Line | Element | Verdict | Reason | Suggested change |
| --- | --- | --- | --- | --- |
| `Image/index.tsx:154-239` | image preview dialog | fixed | The preview now exposes dialog/modal semantics, an accessible name, a labelled close button, and Escape dismissal with listener cleanup. | — |
| `SearchInput/index.tsx:213-227` | expand/collapse control | fixed | The control exposes a translated label and `aria-expanded` state while inheriting native keyboard activation. | — |
| `VirtualizedSessionList.tsx:40-66` | selectable session row | fixed | Rows are focusable, named, and activate through Enter or Space in addition to pointer input. | — |

## D5 — Visual Patterns Observed

- Native button semantics replace local keyboard emulation where the element has ordinary button geometry.
- The virtualized session row is the only changed exception because its measured block node is part of the virtualization contract.
- No new design-system component or repo-wide visual sweep is required.

## Summary

- 7 fixes applied
- 4 patterns kept with documented reason
- 0 remaining abstraction candidates
19 changes: 6 additions & 13 deletions src/components/DateRangeSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,29 +89,22 @@ const DateRangeSelector: React.FC<DateRangeSelectorProps> = ({
};

const defaultClassName =
"flex items-center border w-fit border-border-2 border-solid rounded-[8px] px-3 py-[1px] gap-1 text-text-2 text-[14px] bg-bg-3 cursor-pointer hover:bg-fill-2 transition-colors";
"flex w-fit cursor-pointer items-center gap-1 rounded-lg border border-solid border-border-2 bg-bg-3 px-3 py-px text-sm text-text-2 transition-colors hover:bg-fill-2";

return (
<div className={`date-range-selector ${className}`}>
<div
<button
type="button"
className={`${defaultClassName}`}
role="button"
tabIndex={0}
aria-haspopup="dialog"
aria-expanded={isOpen}
onClick={() => setIsOpen(!isOpen)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setIsOpen((prev) => !prev);
}
}}
>
<Calendar className="text-[14px] text-text-2" size={14} />
<span className="text-[14px] font-[400] text-text-2">
<Calendar className="text-sm text-text-2" size={14} />
<span className="text-sm font-normal text-text-2">
{dateRange || placeholder}
</span>
</div>
</button>

{isOpen && (
<div className="date-range-selector__picker">
Expand Down
48 changes: 14 additions & 34 deletions src/components/TrafficLights/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,54 +104,34 @@ const TrafficLights: React.FC<TrafficLightsProps> = ({
}
};

const handleKeyActivate = (
event: React.KeyboardEvent<HTMLDivElement>,
action: () => void
) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
action();
}
};

return (
<div className={`title-bar-buttons flex items-center ${className}`}>
{/* Red button - Close */}
<div
className="mr-1.5 h-[14px] w-[14px] cursor-pointer rounded-full border-[0.5px] border-solid border-[#CE5347] bg-[#ED6A5E]"
role="button"
tabIndex={0}
<button
type="button"
className="mr-1.5 h-[14px] w-[14px] cursor-pointer rounded-full border-[0.5px] border-solid border-[#CE5347] bg-[#ED6A5E] p-0"
aria-label="Close window"
onClick={handleClose}
onKeyDown={(event) => handleKeyActivate(event, handleClose)}
></div>
/>

{/* Yellow button - Minimize */}
<div
className="mr-1.5 h-[14px] w-[14px] cursor-pointer rounded-full border-[0.5px] border-solid border-[#D6A243] bg-[#F6BE4F]"
role="button"
tabIndex={0}
<button
type="button"
className="mr-1.5 h-[14px] w-[14px] cursor-pointer rounded-full border-[0.5px] border-solid border-[#D6A243] bg-[#F6BE4F] p-0"
aria-label="Minimize window"
onClick={handleMinimize}
onKeyDown={(event) => handleKeyActivate(event, handleMinimize)}
></div>
/>

{/* Green button - Maximize/restore, can be disabled */}
<div
className={`h-[14px] w-[14px] rounded-full border-[0.5px] border-solid border-[#58A942] bg-[#62C554] ${
<button
type="button"
className={`h-[14px] w-[14px] rounded-full border-[0.5px] border-solid border-[#58A942] bg-[#62C554] p-0 ${
disableMaximize ? "cursor-not-allowed opacity-50" : "cursor-pointer"
}`}
role="button"
tabIndex={disableMaximize ? -1 : 0}
aria-label="Maximize or restore window"
aria-disabled={disableMaximize}
onClick={disableMaximize ? undefined : handleMaximize}
onKeyDown={
disableMaximize
? undefined
: (event) => handleKeyActivate(event, handleMaximize)
}
></div>
disabled={disableMaximize}
onClick={handleMaximize}
/>
</div>
);
};
Expand Down
46 changes: 46 additions & 0 deletions src/components/sharedAccessibilityContracts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";

function componentSource(path: string): string {
return readFileSync(resolve(__dirname, path), "utf8");
}

describe("shared component accessibility contracts", () => {
it("uses native buttons for date-range and window controls", () => {
const dateRange = componentSource("DateRangeSelector/index.tsx");
const trafficLights = componentSource("TrafficLights/index.tsx");

expect(dateRange).toMatch(
/<button\s+type="button"[\s\S]*?aria-haspopup="dialog"[\s\S]*?aria-expanded=\{isOpen\}/
);
expect(trafficLights.match(/<button/g)).toHaveLength(3);
expect(trafficLights).toContain("disabled={disableMaximize}");
});

it("keeps image preview dismissal keyboard and screen-reader accessible", () => {
const image = componentSource("Image/index.tsx");

expect(image).toContain('event.key === "Escape"');
expect(image).toContain('document.addEventListener("keydown"');
expect(image).toContain('role="dialog"');
expect(image).toContain("aria-modal={true}");
expect(image).toContain('aria-label="Close preview"');
});

it("exposes keyboard semantics for search expansion and virtual rows", () => {
const searchInput = componentSource("SearchInput/index.tsx");
const virtualizedSessions = componentSource(
"Virtualized/VirtualizedSessionList.tsx"
);

expect(searchInput).toMatch(
/<button[\s\S]*?onClick=\{onExpandToggle\}[\s\S]*?aria-expanded=\{expanded\}/
);
expect(virtualizedSessions).toContain('role="button"');
expect(virtualizedSessions).toContain("tabIndex={0}");
expect(virtualizedSessions).toContain(
'event.key === "Enter" || event.key === " "'
);
});
});