fix(web): keep Create Task open on Escape (#2803)

This commit is contained in:
Carlos Florêncio
2026-08-19 17:03:03 +01:00
committed by GitHub
parent f53681e607
commit 61bde7ab9e
19 changed files with 586 additions and 17 deletions
@@ -0,0 +1,125 @@
---
spec: docs/specs/tasks/task-create-escape-dismissal.md
created: 2026-08-19
status: complete
---
# Implementation Plan: Create Task Escape Dismissal
## Overview
The Create Task dialog uses the default Escape dismissal from Radix Dialog.
Its autocomplete handler receives the event after Radix requests dialog closure.
The first fix blocked Radix dismissal only in create mode and relied on the event reaching the
autocomplete's later bubble handler. The follow-up makes autocomplete dismissal and focus retention
capture-safe across browser runtimes.
## Confirmed root cause
- PR #1155 added prompt autocomplete to the task-create prompt field.
- `TaskCreateDialog` does not pass `onEscapeKeyDown` to `DialogContent`.
- Radix handles Escape on the document capture phase and requests closure first.
- `useInlineMention` prevents the same event later in the input event path.
- The current E2E assertion checks visibility before the 100 ms close animation completes.
This timing lets a closing dialog satisfy the assertion.
- The first repair added a create-mode Radix guard but left autocomplete handling in the textarea's
React bubble handler. The PR preview can therefore keep the dialog open while a browser/runtime
moves focus before that later handler closes the menu.
- Nested Escape behavior must be claimed in capture phase with both `preventDefault()` and
`stopPropagation()`, then explicitly restore prompt focus after the event cycle.
The smallest reproduction opens Create Task and presses Escape in the prompt field.
The dialog enters `data-state="closed"` with or without an open autocomplete menu.
## Frontend
### Create Task dialog
Update `apps/web/components/task-create-dialog.tsx`.
Pass an `onEscapeKeyDown` handler to `DialogContent`.
Call `preventDefault()` only when `setup.isCreateMode` is true.
Do not stop propagation because the nested autocomplete handler must receive Escape.
Keep Edit Task and New Agent behavior unchanged.
Do not change the shared `@kandev/ui` dialog component.
### Prompt autocomplete
Update `apps/web/components/task-create-dialog-selectors.tsx` and
`apps/web/hooks/use-inline-mention.ts`.
Run the mention keyboard handler from the prompt textarea's capture phase while leaving the normal
form shortcut handler in bubble phase. When Escape closes an open mention menu, claim the event and
restore the prompt textarea's focus on the next animation frame.
## Tests
- **What:** Create mode prevents Radix Escape dismissal, while edit mode does not add the create-mode guard.
**File:** `apps/web/components/task-create-dialog.test.tsx`.
**How:** expose the mocked `DialogContent` Escape callback and call it with a `preventDefault` spy.
- **What:** Mention Escape runs even when a target listener prevents the later bubble phase.
**File:** `apps/web/components/task-create-dialog-selectors.test.tsx`.
**How:** stop propagation at the textarea target and assert the mention handler already ran in capture.
- **What:** Mention Escape claims the event and restores input focus.
**File:** `apps/web/hooks/use-inline-mention.test.ts`.
**How:** open a mention menu, press Escape, and flush the scheduled focus restoration.
## E2E Tests
- **Scenario:** An open `@` autocomplete menu closes on Escape while Create Task remains open.
**File:** `apps/web/e2e/tests/task/task-create-prompt-autocomplete-qa.spec.ts`.
**What to verify:** the menu disappears, the query remains, the dialog keeps `data-state="open"`,
the textarea stays focused, and typing continues.
- **Scenario:** Create Task remains open when no autocomplete menu is visible.
**File:** `apps/web/e2e/tests/task/task-create-prompt-autocomplete-qa.spec.ts`.
**What to verify:** Escape keeps `data-state="open"` and preserves the prompt value.
- **Scenario:** A phone user with a hardware keyboard presses Escape without an open menu.
**File:** `apps/web/e2e/tests/task/mobile-task-create-escape.spec.ts`.
**What to verify:** the full-height dialog keeps `data-state="open"` in the `mobile-chrome` project.
- **Scenario:** A phone user with a hardware keyboard closes an open `@` menu.
**File:** `apps/web/e2e/tests/task/mobile-task-create-escape.spec.ts`.
**What to verify:** the menu closes and prompt focus remains in the shared mobile dialog.
- **Scenario:** Task chat closes open `@` and `#` menus without losing editor focus.
**File:** `apps/web/e2e/tests/chat/entity-reference-composer.spec.ts`.
**What to verify:** typing continues in the same editor after Escape.
## Mobile parity
The existing mobile entry point is the Kanban task-creation action.
The nearest shipped exemplar is `mobile-create-task-webkit-rendering.spec.ts`.
The same `TaskCreateDialog` component owns desktop and mobile Escape behavior.
This fix does not change layout, touch targets, scroll ownership, safe areas, or the primary action.
The new mobile E2E scenario covers a hardware keyboard without changing the phone composition.
## Verification Results
- RED component test: 3 expected failures because `DialogContent` had no Escape handler.
- RED desktop E2E: 2 expected failures because the dialog changed to `data-state="closed"`.
- GREEN component test: 7 tests passed.
- GREEN desktop E2E: 2 Escape scenarios passed.
- GREEN mobile E2E: 1 hardware-keyboard Escape scenario passed.
- Frontend typecheck passed.
- Frontend lint passed.
- Follow-up RED unit run: 2 expected failures and 32 passing tests. The mention handler did not
receive target-stopped Escape during capture, and Escape did not stop propagation or restore focus.
- Follow-up GREEN unit run: 34 tests passed across the inline mention hook and Task Form Inputs.
- Follow-up desktop E2E: 1 Create Task autocomplete focus and continued-typing scenario passed.
- Follow-up mobile E2E: 2 hardware-keyboard Escape scenarios passed, including autocomplete focus.
- Follow-up task-chat E2E: 2 `@` and `#` dismissal focus and continued-typing scenarios passed.
- Follow-up frontend typecheck and changed-file ESLint passed.
## Implementation Waves And Parallel Candidates
Wave 1 (sequential):
- [x] [task-01-guard-dialog-escape](task-01-guard-dialog-escape.md)
Wave 2 (sequential):
- [x] [task-02-capture-autocomplete-escape](task-02-capture-autocomplete-escape.md)
This plan does not authorize subagents.
## Open Questions
None.
@@ -0,0 +1,112 @@
---
id: "01-guard-dialog-escape"
title: "Guard Create Task Escape dismissal"
status: complete
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/tasks/task-create-escape-dismissal.md"
---
# Task 01: Guard Create Task Escape Dismissal
## Intent
Keep the Create Task dialog open for every Escape press.
Keep nested autocomplete dismissal and non-create dialog behavior unchanged.
## Inputs
- `docs/specs/tasks/task-create-escape-dismissal.md`
- `docs/plans/task-create-escape-dismissal/plan.md`
- `apps/web/components/task-create-dialog.tsx`
- `apps/web/components/task-create-dialog.test.tsx`
- `apps/web/e2e/tests/task/task-create-prompt-autocomplete-qa.spec.ts`
- `apps/web/e2e/tests/task/mobile-create-task-webkit-rendering.spec.ts`
## Acceptance
- Escape never closes `TaskCreateDialog` in create mode.
- Escape closes an open prompt autocomplete menu and preserves the draft.
- Edit Task and New Agent keep their current Escape behavior.
- Desktop and mobile E2E tests prove the create-mode contract.
## TDD sequence
1. Add the component and E2E regression assertions.
2. Run the focused tests and record the expected failures.
3. Add the create-mode `onEscapeKeyDown` guard.
4. Run the same tests and record their successful results.
## Files likely touched
- `apps/web/components/task-create-dialog.tsx`
- `apps/web/components/task-create-dialog.test.tsx`
- `apps/web/e2e/tests/task/task-create-prompt-autocomplete-qa.spec.ts`
- `apps/web/e2e/tests/task/mobile-task-create-escape.spec.ts`
- `docs/plans/task-create-escape-dismissal/plan.md`
- `docs/plans/task-create-escape-dismissal/task-01-guard-dialog-escape.md`
## Dependencies
None.
## Parallelism
`sequential`. The test and production changes share the dialog contract.
## Verification
If workspace dependencies are absent, run this bootstrap once:
```bash
cd apps && pnpm install --frozen-lockfile
```
Run the component test:
```bash
cd apps && pnpm --filter @kandev/web test -- --run components/task-create-dialog.test.tsx
```
Run the desktop E2E scenarios with a fresh production build:
```bash
cd apps/web && pnpm e2e:run tests/task/task-create-prompt-autocomplete-qa.spec.ts -- --grep "Escape"
```
Run the mobile E2E scenario with a fresh production build:
```bash
cd apps/web && pnpm e2e:run --project mobile-chrome tests/task/mobile-task-create-escape.spec.ts
```
Run the frontend type and lint checks:
```bash
cd apps/web && pnpm run typecheck
cd apps && pnpm --filter @kandev/web lint
```
## Risks
- An unconditional shared-dialog guard can change Edit Task and New Agent behavior.
- Stopping propagation can keep the autocomplete menu open.
- A visibility-only E2E assertion can pass during the close animation.
## Output contract
Report the changed files, RED and GREEN results, blockers, and remaining risks.
Update this task status and the plan checkbox in the same conversation.
## Results
- RED: `task-create-dialog.test.tsx` failed 3 Escape contract tests as expected.
- RED: the focused desktop E2E run failed both Escape scenarios with
`data-state="closed"`.
- GREEN: `task-create-dialog.test.tsx` passed 7 tests.
- GREEN: the focused desktop E2E run passed 2 tests.
- GREEN: `mobile-task-create-escape.spec.ts` passed 1 test in `mobile-chrome`.
- GREEN: frontend typecheck passed.
- GREEN: frontend lint passed.
- Fresh desktop and mobile PR screenshots were captured with synthetic E2E data.
@@ -0,0 +1,82 @@
---
id: "02-capture-autocomplete-escape"
title: "Capture autocomplete Escape and retain focus"
status: complete
wave: 2
depends_on: ["01-guard-dialog-escape"]
plan: "plan.md"
spec: "../../specs/tasks/task-create-escape-dismissal.md"
---
# Task 02: Capture Autocomplete Escape And Retain Focus
## Intent
Close the Create Task prompt autocomplete before later browser or dialog handlers can move focus.
Keep the prompt textarea focused and preserve the equivalent `@` and `#` task-chat behavior.
## Acceptance
- Create Task handles an open mention menu's Escape during capture phase.
- The inner autocomplete claims Escape with `preventDefault()` and `stopPropagation()`.
- The menu closes, the dialog remains open, and the prompt textarea regains focus on the next frame.
- Task chat keeps editor focus after dismissing `@` and `#` autocomplete.
- Desktop and mobile hardware-keyboard scenarios prove continued typing after dismissal.
## TDD sequence
1. Add unit regressions for capture-phase delivery and scheduled focus restoration.
2. Run them and record the expected failures on the current implementation.
3. Move mention keyboard handling to textarea capture and add focus restoration.
4. Add desktop, mobile, and task-chat E2E focus/continued-typing assertions.
5. Run the focused unit and E2E commands with retries disabled.
## Files likely touched
- `apps/web/components/task-create-dialog-selectors.tsx`
- `apps/web/components/task-create-dialog-selectors.test.tsx`
- `apps/web/hooks/use-inline-mention.ts`
- `apps/web/hooks/use-inline-mention.test.ts`
- `apps/web/e2e/tests/task/task-create-prompt-autocomplete-qa.spec.ts`
- `apps/web/e2e/tests/task/mobile-task-create-escape.spec.ts`
- `apps/web/e2e/tests/chat/entity-reference-composer.spec.ts`
## Parallelism
`sequential`. Unit and E2E coverage exercise the same keyboard event contract.
## Verification
```bash
cd apps && pnpm --filter @kandev/web test -- --run \
hooks/use-inline-mention.test.ts \
components/task-create-dialog-selectors.test.tsx
cd apps/web && pnpm e2e:run tests/task/task-create-prompt-autocomplete-qa.spec.ts \
-- --grep "Escape closes the menu" --retries=0
cd apps/web && pnpm e2e:run --no-build --project mobile-chrome \
tests/task/mobile-task-create-escape.spec.ts -- --retries=0
cd apps/web && pnpm e2e:run --no-build tests/chat/entity-reference-composer.spec.ts \
-- --grep "keeps Kandev task suggestions under @|pasted hash text stays literal" --retries=0
cd apps/web && pnpm run typecheck
```
## Risks
- Stopping Escape too early could prevent the inner autocomplete from receiving it.
- Moving all textarea keyboard handling to capture could suppress form shortcuts; only mention
handling moves, while unclaimed keys continue to the existing bubble handler.
- A focus-only assertion can miss a menu that remained mounted; E2E must assert both menu removal
and continued typing.
## Results
- RED unit run: 2 expected failures and 32 passing tests.
- GREEN unit run: 34 tests passed.
- Desktop Create Task E2E: 1 autocomplete Escape scenario passed.
- Mobile Create Task E2E: 2 hardware-keyboard Escape scenarios passed.
- Task-chat E2E: 2 `@` and `#` Escape scenarios passed.
- Frontend typecheck and changed-file ESLint passed.
## Output contract
Report RED/GREEN results, exact commands, changed files, preview verification, and remaining risks.
+1
View File
@@ -104,6 +104,7 @@ Kandev's task model: documents, execution stages, labels, blocker escalation, su
| [agent-generated-titles](tasks/agent-generated-titles.md) | approved |
| [task-create-executor-default](tasks/task-create-executor-default.md) | approved |
| [task-create-workflow-memory](tasks/task-create-workflow-memory.md) | approved |
| [task-create-escape-dismissal](tasks/task-create-escape-dismissal.md) | complete |
| [repository-sets](repository-sets/spec.md) | building |
| [external-id-idempotency](tasks/external-id-idempotency/spec.md) | draft |
| [prompt-attachments](tasks/prompt-attachments.md) | draft |
@@ -0,0 +1,42 @@
---
status: complete
created: 2026-08-19
owner: kandev
---
# Create Task Escape Dismissal
## Why
Users can press Escape to close an autocomplete menu while they write a task prompt.
The same key must not close the Create Task dialog and discard the active task-creation flow.
## What
- The standard Create Task dialog remains open when the user presses Escape.
- This rule applies when an autocomplete menu is open and when no menu is open.
- If an autocomplete menu is open, Escape closes that menu, keeps the dialog open, and leaves
focus in the prompt textarea so typing can continue.
- Escape does not clear the prompt, the autocomplete query, attachments, or other form values.
- The footer Cancel action and other existing dismissal actions keep their current behavior.
- The rule applies to all standard task-creation entry points and all viewport sizes.
## Scenarios
- **GIVEN** the Create Task dialog is open without an autocomplete menu, **WHEN** the user presses Escape, **THEN** the dialog stays open with its form values unchanged.
- **GIVEN** the Create Task dialog has an open `@` autocomplete menu, **WHEN** the user presses Escape, **THEN** the menu closes, the dialog stays open, and the prompt textarea keeps focus.
- **GIVEN** a task-chat `@` or `#` autocomplete menu is open, **WHEN** the user presses Escape, **THEN** the menu closes and the chat editor keeps focus so typing can continue.
- **GIVEN** the Create Task dialog is open on a phone with a hardware keyboard, **WHEN** the user presses Escape, **THEN** the full-height dialog stays open.
- **GIVEN** the Create Task dialog is open, **WHEN** the user selects Cancel, **THEN** the dialog closes through the existing cancellation path.
## Out of scope
- Changing Escape behavior in Edit Task, New Agent, Quick Chat, or other dialogs.
- Changing autocomplete search, selection, or trigger characters.
- Changing pointer, outside-click, browser-back, or Cancel dismissal behavior.
- Changing the dialog layout, general focus order, scroll owner, or mobile composition beyond
retaining prompt focus after autocomplete dismissal.
## Implementation plan
- [Create Task Escape Dismissal](../../plans/task-create-escape-dismissal/plan.md)