Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
904 changes: 513 additions & 391 deletions frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import CopyToClipboard from 'react-copy-to-clipboard';
import { formattedDate, handleCopy } from './tasks-utils';
import { useEffect, useRef, useState } from 'react';
import { useTaskDialogKeyboard } from './UseTaskDialogKeyboard';
import { FIELDS } from './constants';
import { EDITTASKDIALOG_FIELDS } from './constants';
import { useTaskDialogFocusMap } from './UseTaskDialogFocusMap';

export const TaskDialog = ({
Expand Down Expand Up @@ -97,7 +97,7 @@ export const TaskDialog = ({
editState.isEditingRecur ||
editState.isEditingAnnotations;

const focusedField = FIELDS[focusedFieldIndex];
const focusedField = EDITTASKDIALOG_FIELDS[focusedFieldIndex];

const stopEditing = () => {
onUpdateState({
Expand Down Expand Up @@ -145,12 +145,12 @@ export const TaskDialog = ({
]);

const focusMap = useTaskDialogFocusMap({
fields: FIELDS,
fields: EDITTASKDIALOG_FIELDS,
inputRefs: inputRefs,
});

const handleDialogKeyDown = useTaskDialogKeyboard({
fields: FIELDS,
fields: EDITTASKDIALOG_FIELDS,
focusedFieldIndex: focusedFieldIndex,
setFocusedFieldIndex: setFocusedFieldIndex,
isEditingAny: isEditingAny,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,7 @@ export const Tasks = (
/>
<div className="flex justify-center">
<AddTaskdialog
onOpenChange={handleDialogOpenChange}
isOpen={isAddTaskOpen}
setIsOpen={setIsAddTaskOpen}
newTask={newTask}
Expand Down Expand Up @@ -1429,6 +1430,7 @@ export const Tasks = (
<div className="flex items-center justify-left">
<div className="pr-2">
<AddTaskdialog
onOpenChange={handleDialogOpenChange}
isOpen={isAddTaskOpen}
setIsOpen={setIsAddTaskOpen}
newTask={newTask}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,18 @@ export function useTaskDialogFocusMap<F extends readonly string[]>({
[fields, inputRefs]
);
}

export function useAddTaskDialogFocusMap<F extends readonly string[]>({
fields,
inputRefs,
}: UseTaskDialogFocusMapProps<F>) {
return React.useCallback(
(field: F[number]) => {
const element = inputRefs.current[field];
if (!element) return;

element.focus();
},
[fields, inputRefs]
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { UseTaskDialogKeyboardProps } from '@/components/utils/types';
import {
AddTaskProps,
UseTaskDialogKeyboardProps,
} from '@/components/utils/types';
import React from 'react';

export function useTaskDialogKeyboard<F extends readonly string[]>({
Expand Down Expand Up @@ -55,3 +58,49 @@ export function useTaskDialogKeyboard<F extends readonly string[]>({
]
);
}

export function useAddTaskDialogKeyboard<F extends readonly string[]>({
fields,
focusedFieldIndex,
setFocusedFieldIndex,
onEnter,
closeDialog,
}: AddTaskProps<F>) {
return React.useCallback(
(e: React.KeyboardEvent) => {
const target = e.target as HTMLElement;

const isTyping =
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable;

if (isTyping && (e.key === 'Enter' || e.key === 'Escape')) return;

switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setFocusedFieldIndex((i) => Math.min(i + 1, fields.length - 1));
break;

case 'ArrowUp':
e.preventDefault();
setFocusedFieldIndex((i) => Math.max(i - 1, 0));
break;

case 'Enter':
e.preventDefault();
e.stopPropagation();
const field = fields[focusedFieldIndex];
onEnter(field);
break;

case 'Escape':
e.preventDefault();
closeDialog();
break;
}
},
[fields, focusedFieldIndex, setFocusedFieldIndex, onEnter, closeDialog]
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -927,4 +927,57 @@ describe('AddTaskDialog Component', () => {
});
});
});

describe('Testing Shortcuts', () => {
beforeEach(() => {
Element.prototype.scrollIntoView = jest.fn();
});

test('ArrowDown moves focus to next field', async () => {
render(<AddTaskdialog {...mockProps} isOpen />);

const dialog = await screen.findByRole('dialog');
fireEvent.keyDown(dialog, { key: 'ArrowDown' });

const prioritySelect = screen.getByLabelText(/priority/i);
const priorityRow = prioritySelect.closest('div.grid');
expect(priorityRow).toHaveClass('bg-black/15');
});

test('Enter focuses priority select when priority row is focused', async () => {
render(<AddTaskdialog {...mockProps} isOpen />);

const dialog = await screen.findByRole('dialog');
fireEvent.keyDown(dialog, { key: 'ArrowDown' });
fireEvent.keyDown(dialog, { key: 'Enter' });

const prioritySelect = screen.getByLabelText(/priority/i);
expect(prioritySelect).toHaveFocus();
});

test('Arrow keys do navigate while editing', () => {
render(<AddTaskdialog {...mockProps} isOpen />);

const dialog = screen.getByRole('dialog');
fireEvent.keyDown(dialog, { key: 'Enter' });
fireEvent.keyDown(dialog, { key: 'ArrowDown' });

const descriptionRow = screen.getByText(/priority/i);
expect(descriptionRow).toBeInTheDocument();
});

test('DateTimePicker is visible when any date field is in edit mode', async () => {
render(<AddTaskdialog {...mockProps} isOpen />);

const dialog = screen.getByRole('dialog');
fireEvent.keyDown(dialog, { key: 'ArrowDown' });
fireEvent.keyDown(dialog, { key: 'ArrowDown' });
fireEvent.keyDown(dialog, { key: 'ArrowDown' });
fireEvent.keyDown(dialog, { key: 'Enter' });

expect(
screen.getByPlaceholderText('Select due date and time')
).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,7 @@ describe('Tasks Component', () => {

render(<Tasks {...mockProps} />);

await waitFor(async () => {
expect(await screen.findByText('Task 1')).toBeInTheDocument();
});

expect(screen.getByLabelText('Show:')).toHaveValue('20');
expect(await screen.findByLabelText('Show:')).toHaveValue('20');
});

test('updates pagination when "Tasks per Page" is changed', async () => {
Expand Down
17 changes: 16 additions & 1 deletion frontend/src/components/HomeComponents/Tasks/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const FIELDS = [
export const EDITTASKDIALOG_FIELDS = [
'description',
'due',
'start',
Expand All @@ -12,3 +12,18 @@ export const FIELDS = [
'recur',
'annotations',
] as const;

export const ADDTASKDIALOG_FIELDS = [
'description',
'priority',
'project',
'due',
'start',
'end',
'entry',
'wait',
'recur',
'tags',
'annotations',
'depends',
] as const;
18 changes: 16 additions & 2 deletions frontend/src/components/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { FIELDS } from '../HomeComponents/Tasks/constants';
import {
ADDTASKDIALOG_FIELDS,
EDITTASKDIALOG_FIELDS,
} from '../HomeComponents/Tasks/constants';

export interface User {
name: string;
Expand Down Expand Up @@ -115,6 +118,7 @@ export interface TaskFormData {
}

export interface AddTaskDialogProps {
onOpenChange: (open: boolean) => void;
isOpen: boolean;
setIsOpen: (value: boolean) => void;
newTask: TaskFormData;
Expand Down Expand Up @@ -172,7 +176,17 @@ export interface UseTaskDialogKeyboardProps<F extends readonly string[]> {
stopEditing: () => void;
}

export type FieldKey = (typeof FIELDS)[number];
export type AddTaskProps<F extends readonly string[]> = {
fields: F;
focusedFieldIndex: number;
setFocusedFieldIndex: React.Dispatch<React.SetStateAction<number>>;
onEnter: (field: F[number]) => void;
closeDialog: () => void;
};

export type AddFieldKey = (typeof ADDTASKDIALOG_FIELDS)[number];

export type FieldKey = (typeof EDITTASKDIALOG_FIELDS)[number];

export type RefMap = Record<string, HTMLElement | null>;

Expand Down
Loading