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
264 changes: 245 additions & 19 deletions frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const Tasks = (
setSelectedIndex((prev) => Math.max(prev - 1, 0));
}

if (e.key === 'e') {
if (e.key === 'Enter') {
e.preventDefault();
const task = currentTasks[selectedIndex];
if (task) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { UseTaskDialogFocusMapProps } from '@/components/utils/types';
import React from 'react';

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

element.focus();

if (
field === 'due' ||
field === 'start' ||
field === 'end' ||
field === 'wait' ||
field === 'entry'
) {
element.click();
}
},
[fields, inputRefs]
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { UseTaskDialogKeyboardProps } from '@/components/utils/types';
import React from 'react';

export function useTaskDialogKeyboard<F extends readonly string[]>({
fields,
focusedFieldIndex,
setFocusedFieldIndex,
isEditingAny,
triggerEditForField,
stopEditing,
}: UseTaskDialogKeyboardProps<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) return;

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

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

case 'Enter':
if (isEditingAny) return;
e.preventDefault();
triggerEditForField(fields[focusedFieldIndex]);
break;

case 'Escape':
e.preventDefault();
stopEditing();
break;
}
},
[
fields,
focusedFieldIndex,
isEditingAny,
setFocusedFieldIndex,
stopEditing,
triggerEditForField,
]
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -689,4 +689,106 @@ describe('TaskDialog Component', () => {
expect(screen.getByText('O')).toBeInTheDocument();
});
});

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

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

const dialog = await screen.findByRole('dialog');

fireEvent.keyDown(dialog, { key: 'ArrowDown' });

// description → due
const dueRow = screen.getByText('Due:').closest('tr');
expect(dueRow).toHaveClass('bg-black/15');
});

test('Enter opens edit mode for focused field', async () => {
const editStateWithEditingOn = {
...defaultProps.editState,
isEditing: true,
editedDescription: defaultProps.task.description,
};

render(
<TaskDialog
{...defaultProps}
isOpen
editState={editStateWithEditingOn}
/>
);

await screen.findByRole('dialog');

const descriptionInput = screen.getByLabelText('description');
expect(descriptionInput).toBeInTheDocument();
expect(descriptionInput).toHaveAttribute('type', 'text');
expect(descriptionInput).toHaveValue(defaultProps.task.description);
});

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

const dialog = screen.getByRole('dialog');

fireEvent.keyDown(dialog, { key: 'Enter' });

fireEvent.keyDown(dialog, { key: 'ArrowDown' });

const descriptionRow = screen.getByText('Description:').closest('tr');
expect(descriptionRow).toBeInTheDocument();
});

test('Escape exits edit mode before closing dialog', () => {
const editStateWithEditingOn = {
...defaultProps.editState,
isEditing: true,
editedDescription: defaultProps.task.description,
};

render(
<TaskDialog
{...defaultProps}
isOpen
editState={editStateWithEditingOn}
/>
);

const dialog = screen.getByRole('dialog');

fireEvent.keyDown(dialog, { key: 'Enter' });
const descriptionInput = screen.getByLabelText('description');
expect(descriptionInput).toBeInTheDocument();

fireEvent.keyDown(dialog, { key: 'Escape' });

expect(dialog).toBeInTheDocument();
});

test('DateTimePicker is visible when any date field is in edit mode', async () => {
const editStateWithDueDateEditing = {
...defaultProps.editState,
isEditingDueDate: true,
editedDueDate: defaultProps.task.due || '',
};

render(
<TaskDialog
{...defaultProps}
isOpen
editState={editStateWithDueDateEditing}
/>
);

await screen.findByRole('dialog');

expect(
await screen.findByRole('button', { name: /calender-button/i })
).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { act, renderHook } from '@testing-library/react';
import { useTaskDialogFocusMap } from '../UseTaskDialogFocusMap';

describe('UseTaskDialogFocusMap', () => {
test('focus is being called for any field with an element', () => {
const focus = jest.fn();
const click = jest.fn();

const inputrefs = {
current: {
description: { focus, click },
},
};

const { result } = renderHook(() =>
useTaskDialogFocusMap({
fields: ['description'],
inputRefs: inputrefs as any,
})
);

act(() => {
result.current('description');
});

expect(focus).toHaveBeenCalled();
});

test('click is being called for date field', () => {
const focus = jest.fn();
const click = jest.fn();

const inputrefs = {
current: {
due: { focus, click },
},
};

const { result } = renderHook(() =>
useTaskDialogFocusMap({
fields: ['due'],
inputRefs: inputrefs as any,
})
);

act(() => {
result.current('due');
});

expect(focus).toHaveBeenCalled();
expect(click).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { act, renderHook } from '@testing-library/react';
import { useTaskDialogKeyboard } from '../UseTaskDialogKeyboard';

const FIELDS = ['Description', 'start', 'due'] as const;
const setFocusedFieldIndex = jest.fn();
const triggerEditForField = jest.fn();
const stopEditing = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

describe('UseTaskDialogKeyboard', () => {
test('ArrowDown increments focusFieldsIndex', () => {
const { result } = renderHook(() =>
useTaskDialogKeyboard({
fields: FIELDS,
focusedFieldIndex: 0,
setFocusedFieldIndex,
stopEditing,
isEditingAny: false,
triggerEditForField,
})
);

act(() => {
result.current({
key: 'ArrowDown',
preventDefault: jest.fn(),
target: document.body,
} as any);
});

expect(setFocusedFieldIndex).toHaveBeenCalled();
});

test('ArrowUp decrement focusFieldsIndex', () => {
const { result } = renderHook(() =>
useTaskDialogKeyboard({
fields: FIELDS,
focusedFieldIndex: 1,
setFocusedFieldIndex,
stopEditing,
isEditingAny: false,
triggerEditForField,
})
);

act(() => {
result.current({
key: 'ArrowUp',
preventDefault: jest.fn(),
target: document.body,
} as any);
});

expect(setFocusedFieldIndex).toHaveBeenCalled();
});

test('Enter key trigger edit for focusfield', () => {
const { result } = renderHook(() =>
useTaskDialogKeyboard({
fields: FIELDS,
focusedFieldIndex: 2,
setFocusedFieldIndex: jest.fn(),
stopEditing: jest.fn(),
isEditingAny: false,
triggerEditForField,
})
);

act(() => {
result.current({
key: 'Enter',
preventDefault: jest.fn(),
target: document.body,
} as any);
});

expect(triggerEditForField).toHaveBeenCalledWith('due');
});

test('Escape key exit editing mode', () => {
const { result } = renderHook(() =>
useTaskDialogKeyboard({
fields: FIELDS,
focusedFieldIndex: 1,
setFocusedFieldIndex: jest.fn(),
stopEditing,
isEditingAny: false,
triggerEditForField: jest.fn(),
})
);

act(() => {
result.current({
key: 'Escape',
preventDefault: jest.fn(),
target: document.body,
} as any);
});

expect(stopEditing).toHaveBeenCalled();
});

test('prevent navigating when editing', () => {
const { result } = renderHook(() =>
useTaskDialogKeyboard({
fields: FIELDS,
focusedFieldIndex: 0,
setFocusedFieldIndex,
stopEditing,
isEditingAny: true,
triggerEditForField,
})
);

act(() => {
result.current({
key: 'ArrowDown',
preventDefault: jest.fn(),
target: document.createElement('div'),
} as any);
});

expect(setFocusedFieldIndex).not.toHaveBeenCalled();
});
});
14 changes: 14 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const FIELDS = [
'description',
'due',
'start',
'end',
'wait',
'depends',
'priority',
'project',
'tags',
'entry',
'recur',
'annotations',
] as const;
Loading
Loading