Skip to content

Commit 83123fc

Browse files
msoginclaude
andcommitted
fix: keyboard navigation for developer filter dropdown
Arrow keys to navigate, Enter to select, Escape to close, Backspace to remove last chip. Highlight syncs between keyboard and mouse. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3c80b8a commit 83123fc

1 file changed

Lines changed: 55 additions & 35 deletions

File tree

src/app/page.tsx

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export default function Home() {
8989
const [filterLogins, setFilterLogins] = useState<Set<string>>(new Set());
9090
const [filterQuery, setFilterQuery] = useState('');
9191
const [filterOpen, setFilterOpen] = useState(false);
92+
const [filterHighlight, setFilterHighlight] = useState(0);
9293
const generationRef = useRef(0);
9394
const lastCompletedDevsRef = useRef(0);
9495

@@ -1046,43 +1047,62 @@ export default function Home() {
10461047
);
10471048
})}
10481049
<div className="relative">
1049-
<input
1050-
type="text"
1051-
value={filterQuery}
1052-
onChange={e => { setFilterQuery(e.target.value); setFilterOpen(true); }}
1053-
onFocus={() => setFilterOpen(true)}
1054-
placeholder={filterLogins.size > 0 ? 'Add more...' : 'Filter by developer...'}
1055-
className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-sm text-white placeholder-gray-600 focus:outline-none focus:border-blue-500 w-48"
1056-
/>
1057-
{filterOpen && filterQuery.length > 0 && (() => {
1050+
{(() => {
10581051
const q = filterQuery.toLowerCase();
1059-
const matches = developers.filter(d =>
1060-
!filterLogins.has(d.github_login) && (
1061-
d.github_login.toLowerCase().includes(q) ||
1062-
(d.github_name || '').toLowerCase().includes(q)
1063-
)
1064-
).slice(0, 8);
1065-
if (matches.length === 0) return null;
1052+
const matches = filterOpen && q.length > 0
1053+
? developers.filter(d =>
1054+
!filterLogins.has(d.github_login) && (
1055+
d.github_login.toLowerCase().includes(q) ||
1056+
(d.github_name || '').toLowerCase().includes(q)
1057+
)
1058+
).slice(0, 8)
1059+
: [];
1060+
const selectMatch = (login: string) => {
1061+
setFilterLogins(prev => new Set(prev).add(login));
1062+
setFilterQuery('');
1063+
setFilterOpen(false);
1064+
setFilterHighlight(0);
1065+
};
10661066
return (
1067-
<div className="absolute z-40 top-full mt-1 left-0 w-64 bg-gray-800 border border-gray-700 rounded-lg shadow-xl overflow-hidden">
1068-
{matches.map(d => (
1069-
<button
1070-
key={d.github_login}
1071-
className="w-full flex items-center gap-2.5 px-3 py-2 text-left text-sm hover:bg-gray-700 transition-colors"
1072-
onClick={() => {
1073-
setFilterLogins(prev => new Set(prev).add(d.github_login));
1074-
setFilterQuery('');
1075-
setFilterOpen(false);
1076-
}}
1077-
>
1078-
{d.avatar_url && <img src={d.avatar_url} alt="" className="w-5 h-5 rounded-full" />}
1079-
<div>
1080-
<span className="text-white">{d.github_name || d.github_login}</span>
1081-
{d.github_name && <span className="text-gray-500 ml-1.5">@{d.github_login}</span>}
1082-
</div>
1083-
</button>
1084-
))}
1085-
</div>
1067+
<>
1068+
<input
1069+
type="text"
1070+
value={filterQuery}
1071+
onChange={e => { setFilterQuery(e.target.value); setFilterOpen(true); setFilterHighlight(0); }}
1072+
onFocus={() => { setFilterOpen(true); setFilterHighlight(0); }}
1073+
onBlur={() => setTimeout(() => setFilterOpen(false), 150)}
1074+
onKeyDown={e => {
1075+
if (e.key === 'ArrowDown') { e.preventDefault(); setFilterHighlight(h => Math.min(h + 1, matches.length - 1)); }
1076+
else if (e.key === 'ArrowUp') { e.preventDefault(); setFilterHighlight(h => Math.max(h - 1, 0)); }
1077+
else if (e.key === 'Enter' && matches.length > 0) { e.preventDefault(); selectMatch(matches[filterHighlight]?.github_login); }
1078+
else if (e.key === 'Escape') { setFilterOpen(false); }
1079+
else if (e.key === 'Backspace' && filterQuery === '' && filterLogins.size > 0) {
1080+
const last = [...filterLogins].pop()!;
1081+
setFilterLogins(prev => { const n = new Set(prev); n.delete(last); return n; });
1082+
}
1083+
}}
1084+
placeholder={filterLogins.size > 0 ? 'Add more...' : 'Filter by developer...'}
1085+
className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-sm text-white placeholder-gray-600 focus:outline-none focus:border-blue-500 w-48"
1086+
/>
1087+
{matches.length > 0 && (
1088+
<div className="absolute z-40 top-full mt-1 left-0 w-64 bg-gray-800 border border-gray-700 rounded-lg shadow-xl overflow-hidden">
1089+
{matches.map((d, idx) => (
1090+
<button
1091+
key={d.github_login}
1092+
className={`w-full flex items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors ${idx === filterHighlight ? 'bg-gray-700' : 'hover:bg-gray-700'}`}
1093+
onMouseEnter={() => setFilterHighlight(idx)}
1094+
onClick={() => selectMatch(d.github_login)}
1095+
>
1096+
{d.avatar_url && <img src={d.avatar_url} alt="" className="w-5 h-5 rounded-full" />}
1097+
<div>
1098+
<span className="text-white">{d.github_name || d.github_login}</span>
1099+
{d.github_name && <span className="text-gray-500 ml-1.5">@{d.github_login}</span>}
1100+
</div>
1101+
</button>
1102+
))}
1103+
</div>
1104+
)}
1105+
</>
10861106
);
10871107
})()}
10881108
</div>

0 commit comments

Comments
 (0)