(null);
+ const attachCard = useCallback(
+ (node: HTMLDivElement | null) => {
+ cardRef.current = node;
+ setNodeRef(node);
+ },
+ [setNodeRef]
+ );
+ useEffect(() => {
+ if (isSelected) {
+ cardRef.current?.focus({ preventScroll: true });
+ cardRef.current?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
+ }
+ }, [isSelected]);
+
const { isSelecting, toggleSelect, isSelected: isBulkSelected } = useBulkActions();
const [tooltipDismissed, setTooltipDismissed] = useState(false);
const [statusMenuOpen, setStatusMenuOpen] = useState(false);
@@ -362,7 +377,7 @@ export const TaskCard = memo(function TaskCard({
}
>
setTooltipDismissed(false)}
role="article"
tabIndex={0}
- aria-label={`Task: ${task.title}, Type: ${typeLabel}, Priority: ${task.priority}${readinessAria}${isBlockedState ? ', Blocked' : ''}${isAgentRunning ? ', Agent running' : ''}${isAttemptFailed ? ', Latest attempt failed' : ''}${isAwaitingReview ? ', Awaiting review' : ''}${isVerified ? ', Verified' : ''}`}
+ aria-label={`${isSelected ? 'Selected. ' : ''}Task: ${task.title}, Type: ${typeLabel}, Priority: ${task.priority}${readinessAria}${isBlockedState ? ', Blocked' : ''}${isAgentRunning ? ', Agent running' : ''}${isAttemptFailed ? ', Latest attempt failed' : ''}${isAwaitingReview ? ', Awaiting review' : ''}${isVerified ? ', Verified' : ''}`}
data-type-color-token={typeColorToken}
data-selected={isSelected ? 'true' : undefined}
data-dragging={isDragging || isCurrentlyDragging ? 'true' : undefined}
diff --git a/web/src/hooks/useKeyboard.tsx b/web/src/hooks/useKeyboard.tsx
index abd842ee..c29f4619 100644
--- a/web/src/hooks/useKeyboard.tsx
+++ b/web/src/hooks/useKeyboard.tsx
@@ -11,6 +11,7 @@ import {
import {
DEFAULT_FEATURE_SETTINGS,
normalizeBoardColumns,
+ sortTasksByBoardPosition,
type Task,
type TaskStatus,
} from '@veritas-kanban/shared';
@@ -38,8 +39,8 @@ interface KeyboardContextValue {
setTasks: (tasks: Task[]) => void;
// Callbacks (using refs to avoid re-render loops)
- setOnOpenTask: (fn: (task: Task) => void) => void;
- setOnMoveTask: (fn: (taskId: string, status: TaskStatus) => void) => void;
+ setOnOpenTask: (fn: ((task: Task) => void) | null) => void;
+ setOnMoveTask: (fn: ((taskId: string, status: TaskStatus) => void) | null) => void;
}
const KeyboardContext = createContext(null);
@@ -52,7 +53,11 @@ function getColumnForShortcut(key: string, columns: Array<{ id: TaskStatus }>):
export function KeyboardProvider({ children }: { children: ReactNode }) {
const [isHelpOpen, setIsHelpOpen] = useState(false);
const [selectedTaskId, setSelectedTaskId] = useState(null);
- const [tasks, setTasks] = useState([]);
+ const [tasks, updateTasks] = useState([]);
+ const setTasks = useCallback((next: Task[]) => {
+ updateTasks(next);
+ setSelectedTaskId((id) => (id && next.some((task) => task.id === id) ? id : null));
+ }, []);
const { settings } = useFeatureSettings();
const columns = useMemo(
() => normalizeBoardColumns(settings.board?.columns ?? DEFAULT_FEATURE_SETTINGS.board.columns),
@@ -89,24 +94,23 @@ export function KeyboardProvider({ children }: { children: ReactNode }) {
setIsHelpOpen(false);
}, []);
- const setOnOpenTask = useCallback((fn: (task: Task) => void) => {
+ const setOnOpenTask = useCallback((fn: ((task: Task) => void) | null) => {
onOpenTaskRef.current = fn;
}, []);
- const setOnMoveTask = useCallback((fn: (taskId: string, status: TaskStatus) => void) => {
+ const setOnMoveTask = useCallback((fn: ((taskId: string, status: TaskStatus) => void) | null) => {
onMoveTaskRef.current = fn;
}, []);
- // Get flat list of tasks sorted by column then position
- const getTaskList = useCallback(() => {
- const statusOrder = columns.map((column) => column.id);
- return [...tasks].sort((a, b) => {
- const aIndex = statusOrder.indexOf(a.status);
- const bIndex = statusOrder.indexOf(b.status);
- if (aIndex !== bIndex) return aIndex - bIndex;
- return a.title.localeCompare(b.title);
- });
- }, [columns, tasks]);
+ // Match the rendered column order and the board's canonical rank/position order.
+ // Compute once per snapshot, rather than sorting during each keystroke.
+ const taskList = useMemo(
+ () =>
+ columns.flatMap((column) =>
+ sortTasksByBoardPosition(tasks.filter((task) => task.status === column.id))
+ ),
+ [columns, tasks]
+ );
// Keyboard event handler
useEffect(() => {
@@ -141,7 +145,6 @@ export function KeyboardProvider({ children }: { children: ReactNode }) {
return;
}
- const taskList = getTaskList();
const currentIndex = selectedTaskId ? taskList.findIndex((t) => t.id === selectedTaskId) : -1;
// Cmd+Shift+C (or Ctrl+Shift+C on Windows/Linux) - Toggle chat panel
@@ -237,7 +240,7 @@ export function KeyboardProvider({ children }: { children: ReactNode }) {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
- }, [getTaskList, selectedTaskId, isHelpOpen, openCreateDialog, openChatPanel, columns]);
+ }, [taskList, selectedTaskId, isHelpOpen, openCreateDialog, openChatPanel, columns]);
const value = useMemo(
() => ({