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:
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -495,6 +495,7 @@ export function ProjectTasks() {
|
||||
onEditTask={handleEditTask}
|
||||
onDeleteTask={handleDeleteTask}
|
||||
onViewTaskDetails={handleViewTaskDetails}
|
||||
isPanelOpen={isPanelOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user