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 { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -48,6 +48,21 @@ export function TaskCard({
|
|||||||
}
|
}
|
||||||
}, [isFocused]);
|
}, [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 (
|
return (
|
||||||
<KanbanCard
|
<KanbanCard
|
||||||
key={task.id}
|
key={task.id}
|
||||||
@@ -55,9 +70,10 @@ export function TaskCard({
|
|||||||
name={task.title}
|
name={task.title}
|
||||||
index={index}
|
index={index}
|
||||||
parent={status}
|
parent={status}
|
||||||
onClick={() => onViewDetails(task)}
|
onClick={handleClick}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
forwardedRef={localRef}
|
forwardedRef={localRef}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
@@ -82,6 +98,7 @@ export function TaskCard({
|
|||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface TaskKanbanBoardProps {
|
|||||||
onEditTask: (task: Task) => void;
|
onEditTask: (task: Task) => void;
|
||||||
onDeleteTask: (taskId: string) => void;
|
onDeleteTask: (taskId: string) => void;
|
||||||
onViewTaskDetails: (task: Task) => void;
|
onViewTaskDetails: (task: Task) => void;
|
||||||
|
isPanelOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTaskStatuses: TaskStatus[] = [
|
const allTaskStatuses: TaskStatus[] = [
|
||||||
@@ -56,6 +57,7 @@ function TaskKanbanBoard({
|
|||||||
onEditTask,
|
onEditTask,
|
||||||
onDeleteTask,
|
onDeleteTask,
|
||||||
onViewTaskDetails,
|
onViewTaskDetails,
|
||||||
|
isPanelOpen,
|
||||||
}: TaskKanbanBoardProps) {
|
}: TaskKanbanBoardProps) {
|
||||||
const { projectId, taskId } = useParams<{
|
const { projectId, taskId } = useParams<{
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -130,13 +132,20 @@ function TaskKanbanBoard({
|
|||||||
// Keyboard navigation handler
|
// Keyboard navigation handler
|
||||||
useKanbanKeyboardNavigation({
|
useKanbanKeyboardNavigation({
|
||||||
focusedTaskId,
|
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,
|
focusedStatus,
|
||||||
setFocusedStatus: (status) => setFocusedStatus(status as TaskStatus | null),
|
setFocusedStatus: (status) => setFocusedStatus(status as TaskStatus | null),
|
||||||
groupedTasks,
|
groupedTasks,
|
||||||
filteredTasks,
|
filteredTasks,
|
||||||
allTaskStatuses,
|
allTaskStatuses,
|
||||||
onViewTaskDetails,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import type { ReactNode, Ref } from 'react';
|
import type { ReactNode, Ref, KeyboardEvent } from 'react';
|
||||||
|
|
||||||
export type { DragEndEvent } from '@dnd-kit/core';
|
export type { DragEndEvent } from '@dnd-kit/core';
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ export type KanbanCardProps = Pick<Feature, 'id' | 'name'> & {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
forwardedRef?: Ref<HTMLDivElement>;
|
forwardedRef?: Ref<HTMLDivElement>;
|
||||||
|
onKeyDown?: (e: KeyboardEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KanbanCard = ({
|
export const KanbanCard = ({
|
||||||
@@ -73,6 +74,7 @@ export const KanbanCard = ({
|
|||||||
onClick,
|
onClick,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
|
onKeyDown,
|
||||||
}: KanbanCardProps) => {
|
}: KanbanCardProps) => {
|
||||||
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
||||||
useDraggable({
|
useDraggable({
|
||||||
@@ -108,6 +110,7 @@ export const KanbanCard = ({
|
|||||||
ref={combinedRef}
|
ref={combinedRef}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
>
|
>
|
||||||
{children ?? <p className="m-0 font-medium text-sm">{name}</p>}
|
{children ?? <p className="m-0 font-medium text-sm">{name}</p>}
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export function useKanbanKeyboardNavigation({
|
|||||||
groupedTasks: Record<string, any[]>;
|
groupedTasks: Record<string, any[]>;
|
||||||
filteredTasks: any[];
|
filteredTasks: any[];
|
||||||
allTaskStatuses: string[];
|
allTaskStatuses: string[];
|
||||||
onViewTaskDetails: (task: any) => void;
|
onViewTaskDetails?: (task: any) => void;
|
||||||
preserveIndexOnColumnSwitch?: boolean;
|
preserveIndexOnColumnSwitch?: boolean;
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -237,7 +237,7 @@ export function useKanbanKeyboardNavigation({
|
|||||||
break;
|
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);
|
const task = filteredTasks.find((t: any) => t.id === focusedTaskId);
|
||||||
if (task) {
|
if (task) {
|
||||||
onViewTaskDetails(task);
|
onViewTaskDetails(task);
|
||||||
|
|||||||
@@ -495,6 +495,7 @@ export function ProjectTasks() {
|
|||||||
onEditTask={handleEditTask}
|
onEditTask={handleEditTask}
|
||||||
onDeleteTask={handleDeleteTask}
|
onDeleteTask={handleDeleteTask}
|
||||||
onViewTaskDetails={handleViewTaskDetails}
|
onViewTaskDetails={handleViewTaskDetails}
|
||||||
|
isPanelOpen={isPanelOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user