Skip to content

Commit b2b46cb

Browse files
committed
fix(a11y): improve log tab keyboard copy feedback
1 parent c44d644 commit b2b46cb

1 file changed

Lines changed: 55 additions & 24 deletions

File tree

frontend/src/components/hud/IntelTabs/LogTab.tsx

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useMemo, useState } from "react";
44
import { useLocale } from "next-intl";
55
import { useSimulationStore } from "@/stores/simulation";
66
import { AgentAvatar } from "@/components/AgentAvatar";
@@ -30,44 +30,72 @@ const TYPE_COLOR: Record<string, string> = {
3030

3131
function useCopy() {
3232
const [copiedIdx, setCopiedIdx] = useState<number | null>(null);
33-
const copy = async (text: string, idx: number) => {
33+
const [copiedLabel, setCopiedLabel] = useState<string>("");
34+
35+
const copy = async (text: string, idx: number, label: string) => {
3436
await navigator.clipboard.writeText(text);
3537
setCopiedIdx(idx);
36-
setTimeout(() => setCopiedIdx(null), 1200);
38+
setCopiedLabel(label);
39+
setTimeout(() => {
40+
setCopiedIdx(null);
41+
setCopiedLabel("");
42+
}, 1200);
3743
};
38-
return { copiedIdx, copy };
44+
45+
return { copiedIdx, copiedLabel, copy };
3946
}
4047

4148
export function LogTab() {
4249
const locale = useLocale();
4350
const { events, agents, factions } = useSimulationStore();
4451
const [filter, setFilter] = useState<string>("all");
45-
const { copiedIdx, copy } = useCopy();
52+
const { copiedIdx, copiedLabel, copy } = useCopy();
4653

4754
const filtered = filter === "all"
4855
? events
4956
: events.filter((e) => e.type === filter);
5057

5158
// Show most recent first
5259
const displayed = [...filtered].reverse().slice(0, 100);
60+
const filterResultLabel = useMemo(() => {
61+
const filterName = filter === "all" ? "all events" : `${filter} events`;
62+
return `Showing ${displayed.length} of ${filtered.length} ${filterName}`;
63+
}, [displayed.length, filter, filtered.length]);
5364

5465
return (
5566
<div className="flex flex-col h-full">
67+
<div className="sr-only" role="status" aria-live="polite">
68+
{filterResultLabel}
69+
{copiedLabel ? `, copied ${copiedLabel}` : ""}
70+
</div>
71+
5672
{/* Filter bar */}
57-
<div className="flex flex-wrap gap-1 p-2 border-b border-hud-border">
58-
{EVENT_TYPES.map((type) => (
59-
<button
60-
key={type}
61-
onClick={() => setFilter(type)}
62-
className={`font-mono text-sm uppercase px-1.5 py-0.5 border transition-colors ${
63-
filter === type
64-
? "border-accent text-accent"
65-
: "border-hud-border text-hud-muted hover:text-hud-text"
66-
}`}
67-
>
68-
{type === "all" ? "ALL" : type.split(".")[1]}
69-
</button>
70-
))}
73+
<div className="flex flex-wrap gap-1 p-2 border-b border-hud-border" role="tablist" aria-label="Log event filters">
74+
{EVENT_TYPES.map((type) => {
75+
const isActive = filter === type;
76+
const shortLabel = type === "all" ? "ALL" : type.split(".")[1];
77+
const resultCount = type === "all"
78+
? events.length
79+
: events.filter((event) => event.type === type).length;
80+
81+
return (
82+
<button
83+
key={type}
84+
type="button"
85+
onClick={() => setFilter(type)}
86+
role="tab"
87+
aria-selected={isActive}
88+
aria-label={`${shortLabel} filter, ${resultCount} events`}
89+
className={`font-mono text-sm uppercase px-1.5 py-0.5 border transition-colors ${
90+
isActive
91+
? "border-accent text-accent"
92+
: "border-hud-border text-hud-muted hover:text-hud-text"
93+
}`}
94+
>
95+
{shortLabel}
96+
</button>
97+
);
98+
})}
7199
</div>
72100

73101
{/* Log entries */}
@@ -85,12 +113,15 @@ export function LogTab() {
85113
second: "2-digit",
86114
});
87115
const summary = getSummary(event, locale);
116+
const copyLabel = `${event.type} event at ${time}`;
88117

89118
return (
90-
<div
119+
<button
91120
key={i}
92-
className="font-mono text-sm flex gap-2 group cursor-pointer hover:bg-accent/5"
93-
onClick={() => copy(`[${time}] ${event.type}: ${summary}`, i)}
121+
type="button"
122+
className="w-full text-left font-mono text-sm flex gap-2 group cursor-pointer hover:bg-accent/5 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent"
123+
onClick={() => copy(`[${time}] ${event.type}: ${summary}`, i, copyLabel)}
124+
aria-label={`Copy ${copyLabel}: ${summary}`}
94125
>
95126
{event.type === "agent.message" && (() => {
96127
const agentId = event.payload.agent_id as string;
@@ -103,10 +134,10 @@ export function LogTab() {
103134
{event.type.split(".")[1]}
104135
</span>
105136
<span className="text-hud-muted truncate flex-1">{summary}</span>
106-
<span className="text-sm text-hud-label opacity-0 group-hover:opacity-100 flex-shrink-0">
137+
<span className="text-sm text-hud-label opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 flex-shrink-0" aria-hidden="true">
107138
{copiedIdx === i ? "COPIED" : "📋"}
108139
</span>
109-
</div>
140+
</button>
110141
);
111142
})}
112143
</div>

0 commit comments

Comments
 (0)