Improve keyboard navigation (#234)

* update ticket in the panel as we toggle through the tasks

* shortcut to delete task, fix enter key triggering multiple events

* cleanup
This commit is contained in:
Anastasiia Solop
2025-07-17 11:21:49 +02:00
committed by GitHub
parent d57e44efe1
commit ea77edcda2
5 changed files with 37 additions and 7 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { KeyboardEvent, useCallback, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -48,6 +48,21 @@ export function TaskCard({
}
}, [isFocused]);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Backspace') {
onDelete(task.id);
} else if (e.key === 'Enter' || e.key === ' ') {
onViewDetails(task);
}
},
[task, onDelete, onViewDetails]
);
const handleClick = useCallback(() => {
onViewDetails(task);
}, [task, onViewDetails]);
return (
<KanbanCard
key={task.id}
@@ -55,9 +70,10 @@ export function TaskCard({
name={task.title}
index={index}
parent={status}
onClick={() => onViewDetails(task)}
onClick={handleClick}
tabIndex={tabIndex}
forwardedRef={localRef}
onKeyDown={handleKeyDown}
>
<div className="space-y-2">
<div className="flex items-start justify-between">
@@ -82,6 +98,7 @@ export function TaskCard({
onPointerDown={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>

View File

@@ -23,6 +23,7 @@ interface TaskKanbanBoardProps {
onEditTask: (task: Task) => void;
onDeleteTask: (taskId: string) => void;
onViewTaskDetails: (task: Task) => void;
isPanelOpen: boolean;
}
const allTaskStatuses: TaskStatus[] = [
@@ -56,6 +57,7 @@ function TaskKanbanBoard({
onEditTask,
onDeleteTask,
onViewTaskDetails,
isPanelOpen,
}: TaskKanbanBoardProps) {
const { projectId, taskId } = useParams<{
projectId: string;
@@ -130,13 +132,20 @@ function TaskKanbanBoard({
// Keyboard navigation handler
useKanbanKeyboardNavigation({
focusedTaskId,
setFocusedTaskId: (id) => setFocusedTaskId(id as string | null),
setFocusedTaskId: (id) => {
setFocusedTaskId(id as string | null);
if (isPanelOpen) {
const task = filteredTasks.find((t: any) => t.id === id);
if (task) {
onViewTaskDetails(task);
}
}
},
focusedStatus,
setFocusedStatus: (status) => setFocusedStatus(status as TaskStatus | null),
groupedTasks,
filteredTasks,
allTaskStatuses,
onViewTaskDetails,
});
return (

View File

@@ -12,7 +12,7 @@ import {
useSensor,
useSensors,
} from '@dnd-kit/core';
import type { ReactNode, Ref } from 'react';
import type { ReactNode, Ref, KeyboardEvent } from 'react';
export type { DragEndEvent } from '@dnd-kit/core';
@@ -61,6 +61,7 @@ export type KanbanCardProps = Pick<Feature, 'id' | 'name'> & {
onClick?: () => void;
tabIndex?: number;
forwardedRef?: Ref<HTMLDivElement>;
onKeyDown?: (e: KeyboardEvent) => void;
};
export const KanbanCard = ({
@@ -73,6 +74,7 @@ export const KanbanCard = ({
onClick,
tabIndex,
forwardedRef,
onKeyDown,
}: KanbanCardProps) => {
const { attributes, listeners, setNodeRef, transform, isDragging } =
useDraggable({
@@ -108,6 +110,7 @@ export const KanbanCard = ({
ref={combinedRef}
tabIndex={tabIndex}
onClick={onClick}
onKeyDown={onKeyDown}
>
{children ?? <p className="m-0 font-medium text-sm">{name}</p>}
</Card>

View File

@@ -174,7 +174,7 @@ export function useKanbanKeyboardNavigation({
groupedTasks: Record<string, any[]>;
filteredTasks: any[];
allTaskStatuses: string[];
onViewTaskDetails: (task: any) => void;
onViewTaskDetails?: (task: any) => void;
preserveIndexOnColumnSwitch?: boolean;
}) {
useEffect(() => {
@@ -237,7 +237,7 @@ export function useKanbanKeyboardNavigation({
break;
}
}
} else if (e.key === 'Enter' || e.key === ' ') {
} else if ((e.key === 'Enter' || e.key === ' ') && onViewTaskDetails) {
const task = filteredTasks.find((t: any) => t.id === focusedTaskId);
if (task) {
onViewTaskDetails(task);

View File

@@ -495,6 +495,7 @@ export function ProjectTasks() {
onEditTask={handleEditTask}
onDeleteTask={handleDeleteTask}
onViewTaskDetails={handleViewTaskDetails}
isPanelOpen={isPanelOpen}
/>
</div>
</div>