fix(web): keep Create Task open on Escape (#2803)
This commit is contained in:
@@ -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.
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user