mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-19 09:25:02 +00:00
* fix(tasks): keep subtask input open after add
* fix(tasks): keep subtask input open after add
* perf(tasks): avoid task row effect reactivity
* fix(tasks): address subtask input review
* feat(tasks): ease in the inline subtask input instead of popping
* fix(tasks): consume inline subtask-input request to prevent stale focus-steal
Address multi-review findings on the inline subtask input:
- The open request was held in a signal that was never cleared, so a task row re-created with the same id (e.g. navigating away from a project and back) re-ran its open effect on init and re-opened the input, stealing focus with no user action. Reset via consume() once the row acts on it.
- Drop the redundant requestId counter (a fresh request value already re-fires the effect); request payload is now just the parentId string.
- Add an aria-label to the input (placeholder is not an accessible name).
- _commit() returns void (its boolean result was unused).
* feat(tasks): animate the inline subtask input out and tighten its top gap
- Use the expandFade animation (enter + leave) so the input collapses out instead of vanishing. Caveat: when the input is the only thing in .sub-tasks (adding the first subtask, then cancelling without creating any), the enclosing @if collapses in the same tick and Angular skips the child leave animation, so it's instant in that one case.
- Reduce the input's top margin from --s-half (4px) to --s-quarter (2px).
* feat(tasks): refocus the task row when the subtask draft is cancelled with Escape
The inline subtask input's 'closed' output now reports why it closed ('escape' vs 'blur'). On Escape (a keyboard cancel) the host task calls focusSelf() so keyboard navigation continues from that row; on blur it doesn't, since focus already moved elsewhere. focusSelf() is a no-op on touch.
Covered by unit tests (reason emitted, host refocus only on escape) and an e2e assertion that the task row is focused after Escape.
* feat(tasks): return focus to the task the subtask draft was opened from
Escape previously refocused the parent row that hosts the input. Capture the originating task (which may be a subtask) when opening the draft — before focus moves into the input and the parent row claims focusedTaskId — and restore that on Escape. Falls back to the host row when no origin was captured; no-op on touch.
Focus-by-id prefers the last #t-<id> instance (the detail side-panel one) to match the existing inline-edit focus convention when a task renders twice.
Covered by unit tests and an e2e proving focus returns to the originating subtask, not its parent.
* test(tasks): update subtask e2e for the inline draft input flow
The add-subtask UX changed from 'press a -> empty subtask title focused for edit' to an inline draft input (type + Enter to commit, stays open for rapid entry). Update every e2e site that added subtasks via the old textarea selector:
- WorkViewPage.addSubTask helper: wait for .e2e-add-subtask-input, fill + Enter, then Escape to close the draft for a clean post-state (fixes simple-subtask, add-to-today, drag-task-into-subtask, finish-day-quick-history, and the sync specs that use the helper).
- add-subtask-with-detail-panel-open.spec.ts: assert the draft input opens focused from panel/main-list focus; rewrite the multi-subtask case to repeated Enter (the new rapid-entry path).
- supersync-archive-subtasks + supersync-lww-conflict: same inline-draft flow (not runnable in-sandbox; fixed by analogy).
Verified locally: detail-panel-open 3/3, simple-subtask, add-to-today 5/5, drag-into-subtask, finish-day-quick-history all green.
---------
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
import { test, expect } from '../../fixtures/test.fixture';
|
|
|
|
test.describe('Subtask inline input', () => {
|
|
test('keeps the draft input focused for rapid subtask creation', async ({
|
|
page,
|
|
workViewPage,
|
|
taskPage,
|
|
}) => {
|
|
await workViewPage.waitForTaskList();
|
|
await workViewPage.addTask('Parent task');
|
|
|
|
const parentTask = taskPage.getTaskByText('Parent task');
|
|
await expect(parentTask).toBeVisible();
|
|
|
|
await parentTask.focus();
|
|
await page.keyboard.press('a');
|
|
|
|
const draftInput = parentTask.locator('.e2e-add-subtask-input');
|
|
await expect(draftInput).toBeVisible();
|
|
await expect(draftInput).toBeFocused();
|
|
|
|
await draftInput.fill('1 subtask');
|
|
await page.keyboard.press('Enter');
|
|
|
|
await expect(parentTask.locator('.sub-tasks task')).toHaveCount(1);
|
|
await expect(parentTask.locator('.sub-tasks task task-title')).toContainText([
|
|
'1 subtask',
|
|
]);
|
|
await expect(draftInput).toBeVisible();
|
|
await expect(draftInput).toBeFocused();
|
|
await expect(draftInput).toHaveValue('');
|
|
|
|
await draftInput.fill('2 subtask');
|
|
await page.keyboard.press('Enter');
|
|
|
|
await expect(parentTask.locator('.sub-tasks task')).toHaveCount(2);
|
|
await expect(parentTask.locator('.sub-tasks task task-title')).toContainText([
|
|
'1 subtask',
|
|
'2 subtask',
|
|
]);
|
|
await expect(draftInput).toBeFocused();
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
await expect(parentTask.locator('.e2e-add-subtask-input')).toHaveCount(0);
|
|
await expect(parentTask.locator('.sub-tasks task')).toHaveCount(2);
|
|
});
|
|
|
|
test('cancels a typed draft subtask on Escape', async ({
|
|
page,
|
|
workViewPage,
|
|
taskPage,
|
|
}) => {
|
|
await workViewPage.waitForTaskList();
|
|
await workViewPage.addTask('Parent task');
|
|
|
|
const parentTask = taskPage.getTaskByText('Parent task');
|
|
await expect(parentTask).toBeVisible();
|
|
|
|
await parentTask.focus();
|
|
await page.keyboard.press('a');
|
|
|
|
const draftInput = parentTask.locator('.e2e-add-subtask-input');
|
|
await expect(draftInput).toBeFocused();
|
|
|
|
await draftInput.fill('Canceled subtask');
|
|
await page.keyboard.press('Escape');
|
|
|
|
await expect(parentTask.locator('.e2e-add-subtask-input')).toHaveCount(0);
|
|
await expect(parentTask.locator('.sub-tasks task')).toHaveCount(0);
|
|
await expect(parentTask).not.toContainText('Canceled subtask');
|
|
// Escape returns focus to the task row so keyboard navigation continues.
|
|
await expect(parentTask).toBeFocused();
|
|
});
|
|
|
|
test('returns focus to the originating subtask on Escape', async ({
|
|
page,
|
|
workViewPage,
|
|
taskPage,
|
|
}) => {
|
|
await workViewPage.waitForTaskList();
|
|
await workViewPage.addTask('Parent task');
|
|
|
|
const parentTask = taskPage.getTaskByText('Parent task');
|
|
await expect(parentTask).toBeVisible();
|
|
|
|
// Create one subtask, then close the draft input.
|
|
await parentTask.focus();
|
|
await page.keyboard.press('a');
|
|
const draftInput = parentTask.locator('.e2e-add-subtask-input');
|
|
await draftInput.fill('First subtask');
|
|
await page.keyboard.press('Enter');
|
|
await expect(parentTask.locator('.sub-tasks task')).toHaveCount(1);
|
|
await page.keyboard.press('Escape');
|
|
await expect(parentTask.locator('.e2e-add-subtask-input')).toHaveCount(0);
|
|
|
|
// Now open the draft from the subtask, then cancel: focus must return to the
|
|
// subtask it was opened from, not the parent row.
|
|
const subTask = parentTask.locator('.sub-tasks task').first();
|
|
await subTask.focus();
|
|
await page.keyboard.press('a');
|
|
await expect(parentTask.locator('.e2e-add-subtask-input')).toBeFocused();
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
await expect(parentTask.locator('.e2e-add-subtask-input')).toHaveCount(0);
|
|
await expect(subTask).toBeFocused();
|
|
});
|
|
|
|
test('does not create a subtask when a typed draft loses focus', async ({
|
|
page,
|
|
workViewPage,
|
|
taskPage,
|
|
}) => {
|
|
await workViewPage.waitForTaskList();
|
|
await workViewPage.addTask('Parent task');
|
|
|
|
const parentTask = taskPage.getTaskByText('Parent task');
|
|
await expect(parentTask).toBeVisible();
|
|
|
|
await parentTask.focus();
|
|
await page.keyboard.press('a');
|
|
|
|
const draftInput = parentTask.locator('.e2e-add-subtask-input');
|
|
await expect(draftInput).toBeFocused();
|
|
|
|
await draftInput.fill('Blurred subtask');
|
|
await page.locator('body').click({ position: { x: 10, y: 10 } });
|
|
|
|
await expect(parentTask.locator('.e2e-add-subtask-input')).toHaveCount(0);
|
|
await expect(parentTask.locator('.sub-tasks task')).toHaveCount(0);
|
|
await expect(parentTask).not.toContainText('Blurred subtask');
|
|
});
|
|
});
|