feat(base): keyboard navigation for status cell dropdown

This commit is contained in:
Philipinho
2026-04-18 15:05:30 +01:00
parent 836a25cdbf
commit 88c906cdcd
@@ -7,6 +7,8 @@ import {
} from "@/features/base/types/base.types"; } from "@/features/base/types/base.types";
import { choiceColor } from "@/features/base/components/cells/choice-color"; import { choiceColor } from "@/features/base/components/cells/choice-color";
import cellClasses from "@/features/base/styles/cells.module.css"; import cellClasses from "@/features/base/styles/cells.module.css";
import clsx from "clsx";
import { useListKeyboardNav } from "@/features/base/hooks/use-list-keyboard-nav";
type CellStatusProps = { type CellStatusProps = {
value: unknown; value: unknown;
@@ -73,6 +75,19 @@ export function CellStatus({
return result; return result;
}, [choices, search]); }, [choices, search]);
const flatChoices = useMemo(
() => groups.flatMap((g) => g.choices),
[groups],
);
const choiceIdxMap = useMemo(() => {
const m = new Map<string, number>();
flatChoices.forEach((c, i) => m.set(c.id, i));
return m;
}, [flatChoices]);
const { activeIndex, setActiveIndex, handleNavKey, setOptionRef } =
useListKeyboardNav(flatChoices.length, [search, isEditing]);
const handleSelect = useCallback( const handleSelect = useCallback(
(choice: Choice) => { (choice: Choice) => {
onCommit(choice.id === selectedId ? null : choice.id); onCommit(choice.id === selectedId ? null : choice.id);
@@ -85,9 +100,16 @@ export function CellStatus({
if (e.key === "Escape") { if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
onCancel(); onCancel();
return;
}
if (handleNavKey(e)) return;
if (e.key === "Enter") {
if (activeIndex < 0 || activeIndex >= flatChoices.length) return;
e.preventDefault();
handleSelect(flatChoices[activeIndex]);
} }
}, },
[onCancel], [onCancel, handleNavKey, activeIndex, flatChoices, handleSelect],
); );
if (isEditing) { if (isEditing) {
@@ -129,24 +151,33 @@ export function CellStatus({
<div className={cellClasses.selectCategoryLabel}> <div className={cellClasses.selectCategoryLabel}>
{group.label} {group.label}
</div> </div>
{group.choices.map((choice) => ( {group.choices.map((choice) => {
<div const idx = choiceIdxMap.get(choice.id) ?? -1;
key={choice.id} const isSelected = choice.id === selectedId;
className={`${cellClasses.selectOption} ${ return (
choice.id === selectedId <div
? cellClasses.selectOptionActive key={choice.id}
: "" ref={setOptionRef(idx)}
}`} className={clsx(
onClick={() => handleSelect(choice)} cellClasses.selectOption,
> isSelected && cellClasses.selectOptionActive,
<span idx === activeIndex && cellClasses.selectOptionKeyboardActive,
className={cellClasses.badge} )}
style={choiceColor(choice.color)} onMouseEnter={() => setActiveIndex(idx)}
onMouseDown={(e) => {
e.preventDefault();
}}
onClick={() => handleSelect(choice)}
> >
{choice.name} <span
</span> className={cellClasses.badge}
</div> style={choiceColor(choice.color)}
))} >
{choice.name}
</span>
</div>
);
})}
</div> </div>
))} ))}
</div> </div>