Skip to content

Commit

Permalink
Handle keyboard input for improved accessibility
Browse files Browse the repository at this point in the history
  • Loading branch information
zahrabayatt committed Nov 2, 2024
1 parent 5afe6e5 commit 060c7cc
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 31 deletions.
25 changes: 17 additions & 8 deletions src/components/category/CategoryEditMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,33 @@ import { MdDone } from "react-icons/md";
interface Props {
category: Category;
onRestEdit: () => void;
isFocused: boolean;
}

const CategoryEditMode = ({ category, onRestEdit }: Props) => {
const CategoryEditMode = ({ category, onRestEdit, isFocused }: Props) => {
const editCategory = useCategoryStore((s) => s.editCategory);
const [newCategoryName, setNewCategoryName] = useState(category.name);

const handleSaveEdit = () => {
editCategory(category.id, newCategoryName);
handleResetEdit();
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (!isFocused) return;

if (event.key === "Enter") {
handleSaveEdit();
} else if (event.key === "Escape") {
onRestEdit();
}
};

const handleResetEdit = () => {
const handleSaveEdit = () => {
editCategory(category.id, newCategoryName);
onRestEdit();
setNewCategoryName("");
};

return (
<div className="flex items-center gap-x-2 rounded-lg bg-gray-200 p-2 shadow-sm dark:bg-gray-800">
<div
onKeyDown={handleKeyDown}
className="flex items-center gap-x-2 rounded-lg bg-gray-200 p-2 shadow-sm dark:bg-gray-800"
>
<input
type="text"
value={newCategoryName}
Expand All @@ -33,7 +42,7 @@ const CategoryEditMode = ({ category, onRestEdit }: Props) => {
className="w-full overflow-scroll bg-transparent text-sm outline-none dark:text-white"
/>
<MdDone onClick={handleSaveEdit} className="text-green-500" size={18} />
<FaXmark onClick={handleResetEdit} className="text-red-500" size={13} />
<FaXmark onClick={onRestEdit} className="text-red-500" size={13} />
</div>
);
};
Expand Down
16 changes: 13 additions & 3 deletions src/components/category/CategoryList.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import useKeyboardNavigation from "../../hooks/useKeyboardNavigation";
import useCategoryStore from "../../store/useCategoryStore";
import CategoryListItem from "./CategoryListItem";

const CategoryList = () => {
const categories = useCategoryStore((s) => s.categories);
const { focusedIndex, handleKeyDown } = useKeyboardNavigation(categories);

return (
<ul className="scrollbar-light dark:scrollbar-dark my-auto h-[calc(100vh-185px)] w-full flex-grow overflow-y-auto rounded-lg bg-white shadow-sm max-md:h-[calc(100vh-198px)] dark:bg-gray-700">
<ul
onKeyDown={handleKeyDown}
tabIndex={0}
className="scrollbar-light dark:scrollbar-dark my-auto h-[calc(100vh-185px)] w-full flex-grow overflow-y-auto rounded-lg bg-white shadow-sm max-md:h-[calc(100vh-198px)] dark:bg-gray-700"
>
{categories.length === 0 && (
<p className="w-full pt-20 text-center text-xl max-md:text-lg max-sm:text-base dark:text-white">
No Categories
</p>
)}
{categories.map((cat) => (
<CategoryListItem key={cat.id} category={cat} />
{categories.map((cat, index) => (
<CategoryListItem
key={cat.id}
category={cat}
isFocused={index === focusedIndex}
/>
))}
</ul>
);
Expand Down
16 changes: 12 additions & 4 deletions src/components/category/CategoryListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { useState } from "react";
import Category from "../../types/Category";
import CategoryDisplayMode from "./CategoryDisplayMode";
import CategoryEditMode from "./CategoryEditMode";
import useCategoryListItem from "../../hooks/useCategoryListItem";

interface Props {
category: Category;
isFocused: boolean;
}

const CategoryListItem = ({ category }: Props) => {
const [isEditMode, setEditMode] = useState(false);
const CategoryListItem = ({ category, isFocused }: Props) => {
const { isEditMode, setEditMode, handleKeyDown, categoryRef } =
useCategoryListItem(category.id, isFocused);

return (
<li className="m-3">
<li
ref={categoryRef}
tabIndex={0}
onKeyDown={handleKeyDown}
className="m-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{isEditMode ? (
<CategoryEditMode
category={category}
onRestEdit={() => setEditMode(false)}
isFocused={isFocused}
/>
) : (
<CategoryDisplayMode
Expand Down
25 changes: 17 additions & 8 deletions src/components/task/TaskEditMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,33 @@ import { FaXmark } from "react-icons/fa6";
interface Props {
task: Task;
onRestEdit: () => void;
isFocused: boolean;
}

const TaskEditMode = ({ task, onRestEdit }: Props) => {
const TaskEditMode = ({ task, onRestEdit, isFocused }: Props) => {
const editTask = useTaskStore((s) => s.editTask);
const [newTaskName, setNewTaskName] = useState(task.name);

const handleSaveEdit = () => {
editTask(task.id, newTaskName);
handleResetEdit();
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (!isFocused) return;

if (event.key === "Enter") {
handleSaveEdit();
} else if (event.key === "Escape") {
onRestEdit();
}
};

const handleResetEdit = () => {
const handleSaveEdit = () => {
editTask(task.id, newTaskName);
onRestEdit();
setNewTaskName("");
};

return (
<div className="flex items-center gap-x-2 rounded-lg bg-gray-200 p-2 shadow-sm dark:bg-gray-800">
<div
onKeyDown={handleKeyDown}
className="flex items-center gap-x-2 rounded-lg bg-gray-200 p-2 shadow-sm dark:bg-gray-800"
>
<input
type="text"
value={newTaskName}
Expand All @@ -34,7 +43,7 @@ const TaskEditMode = ({ task, onRestEdit }: Props) => {
/>

<MdDone onClick={handleSaveEdit} className="text-green-500" size={18} />
<FaXmark onClick={handleResetEdit} className="text-red-500" size={13} />
<FaXmark onClick={onRestEdit} className="text-red-500" size={13} />
</div>
);
};
Expand Down
16 changes: 13 additions & 3 deletions src/components/task/TaskList.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import useTasks from "../../hooks/useTasks";
import TaskListItem from "./TaskListItem";
import useKeyboardNavigation from "../../hooks/useKeyboardNavigation";

const TaskList = () => {
const tasks = useTasks();
const { focusedIndex, handleKeyDown } = useKeyboardNavigation(tasks);

return (
<ul className="scrollbar-light dark:scrollbar-dark my-auto h-[calc(100vh-185px)] w-full flex-grow overflow-y-auto rounded-lg bg-white shadow-sm max-md:h-[calc(100vh-241px)] dark:bg-gray-700">
<ul
onKeyDown={handleKeyDown}
tabIndex={0}
className="scrollbar-light dark:scrollbar-dark my-auto h-[calc(100vh-185px)] w-full flex-grow overflow-y-auto rounded-lg bg-white shadow-sm max-md:h-[calc(100vh-241px)] dark:bg-gray-700"
>
{tasks.length === 0 && (
<p className="w-full pt-20 text-center text-xl max-md:text-lg max-sm:text-base dark:text-white">
No tasks
</p>
)}
{tasks.map((task) => (
<TaskListItem key={task.id} task={task} />
{tasks.map((task, index) => (
<TaskListItem
key={task.id}
task={task}
isFocused={index === focusedIndex}
/>
))}
</ul>
);
Expand Down
23 changes: 18 additions & 5 deletions src/components/task/TaskListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import { useState } from "react";
import Task from "../../types/Task";
import TaskDisplayMode from "./TaskDisplayMode";
import TaskEditMode from "./TaskEditMode";
import useTaskListItem from "../../hooks/useTaskListItem";

interface TaskListItemProps {
task: Task;
isFocused: boolean;
}

const TaskListItem = ({ task }: TaskListItemProps) => {
const [isEditMode, setEditMode] = useState(false);
const TaskListItem = ({ task, isFocused }: TaskListItemProps) => {
const { isEditMode, setEditMode, handleKeyDown, taskRef } = useTaskListItem(
task.id,
isFocused,
);

return (
<li className="m-3">
<li
ref={taskRef}
tabIndex={0}
onKeyDown={handleKeyDown}
className="m-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{isEditMode ? (
<TaskEditMode task={task} onRestEdit={() => setEditMode(false)} />
<TaskEditMode
task={task}
onRestEdit={() => setEditMode(false)}
isFocused={isFocused}
/>
) : (
<TaskDisplayMode task={task} onEdit={() => setEditMode(true)} />
)}
Expand Down
29 changes: 29 additions & 0 deletions src/hooks/useCategoryListItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useState, useRef, useEffect } from "react";
import useDeleteCategoryWithTasks from "./useDeleteCategoryWithTasks";

const useCategoryListItem = (categoryId: string, isFocused: boolean) => {
const [isEditMode, setEditMode] = useState(false);
const deleteCategoryWithTasks = useDeleteCategoryWithTasks();
const categoryRef = useRef<HTMLLIElement>(null);

useEffect(() => {
if (isFocused && categoryRef.current) {
categoryRef.current.focus();
}
}, [isFocused]);

const handleKeyDown = (event: React.KeyboardEvent<HTMLLIElement>) => {
if (!isFocused || isEditMode) return;

if (event.ctrlKey && event.key === "e") {
event.preventDefault();
setEditMode(true);
} else if (event.key === "Delete") {
deleteCategoryWithTasks(categoryId);
}
};

return { isEditMode, setEditMode, handleKeyDown, categoryRef };
};

export default useCategoryListItem;
25 changes: 25 additions & 0 deletions src/hooks/useKeyboardNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useState } from "react";
import Task from "../types/Task";
import Category from "../types/Category";

const useKeyboardNavigation = (items: Task[] | Category[]) => {
const [focusedIndex, setFocusedIndex] = useState(0);

const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "ArrowDown") {
event.preventDefault();
setFocusedIndex((prevIndex) =>
prevIndex === items.length - 1 ? 0 : prevIndex + 1,
);
} else if (event.key === "ArrowUp") {
event.preventDefault();
setFocusedIndex((prevIndex) =>
prevIndex === 0 ? items.length - 1 : prevIndex - 1,
);
}
};

return { focusedIndex, handleKeyDown };
};

export default useKeyboardNavigation;
31 changes: 31 additions & 0 deletions src/hooks/useTaskListItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useRef, useEffect, useState } from "react";
import useTaskStore from "../store/useTaskStore";

const useTaskListItem = (taskId: string, isFocused: boolean) => {
const [isEditMode, setEditMode] = useState(false);
const { deleteTask, toggleCompletion } = useTaskStore();
const taskRef = useRef<HTMLLIElement>(null);

useEffect(() => {
if (isFocused && taskRef.current) {
taskRef.current.focus();
}
}, [isFocused]);

const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
if (!isFocused || isEditMode) return;

if (event.ctrlKey && event.key === "e") {
event.preventDefault();
setEditMode(true);
} else if (event.key === "Enter") {
toggleCompletion(taskId);
} else if (event.key === "Delete") {
deleteTask(taskId);
}
};

return { isEditMode, setEditMode, handleKeyDown, taskRef };
};

export default useTaskListItem;

0 comments on commit 060c7cc

Please sign in to comment.