fix(panel): a plan starts editing on a second tap on touch

Merged
DA
6d ago
+328-17

Summary

Fixes the mobile audit's P1: tapping the plan on a phone raised the keyboard. Now, on a device with a finger as its main pointer ((pointer: coarse), checked at the moment of the tap as the panel does elsewhere), editing starts on a second tap, as in Notion. There is no Edit button.

What the editor looks like. The read view is drawn sections, with nothing editable. One click puts the whole text into TipTap in place, with focus (DocumentBlock's onClick → editsOnClick → editing.open). A press on the empty room around the text does the same (useTextRoom). So the plan was not "always contenteditable", as the finding says. Instead, one tap opened and focused the editor. The title, though, was always contenteditable="plaintext-only", so any tap on it raised the keyboard.

The change sits in that existing click path, not in a new mode:

  • PlanEditing.tapped(place, timeStamp) (use-plan-editing.ts) answers whether a click opens the editor. A mouse's click always does. A finger's tap does only if it is the second tap on the same place within SECOND_TAP_WINDOW (1500 ms, by the event's timeStamp). The first tap arms the place. A place is a section block, the title, or the room around the text.
  • Sections: DocumentBlock's onClick asks editsOnClick(target) && editing.tapped(host, …). The first tap only focuses the block's role=group div, which raises no keyboard.
  • A scroll of the plan, or a tap on a control inside a block, lets go of the armed first tap, so the next tap is a first again (from review).
  • Room: a second tap is needed too. A plan with no text yet still opens on one tap, because there is nothing to read.
  • Title: on a coarse pointer it renders contenteditable="false" with no tabindex, so the first tap focuses only its block. A second tap makes it editable, focuses it inside that tap, so iOS raises the keyboard, and places the caret at the tapped point (caretRangeFromPoint, or Firefox's caretPositionFromPoint). Leaving it (blur) returns it to reading. Enter, ↑ from the text, and Rename from ⋯ make it editable before they place the caret, so a hardware keyboard still works.
  • Unchanged: taps on links, mentions, checkboxes and their labels, bylines, buttons, and diagrams (the picture stops its own click), because editsOnClick runs first. A scroll or a long press fires no click, and a press that ends with text selected is excluded as before, so selecting text and Comment/Reply-to-selection never start editing. Once editing has started, the TipTap editor stays open until blur, as before. Desktop and mouse behave as before.

Before/after (on a phone; emulation can't show the keyboard):

  • Before: one tap anywhere in the plan's text, under it, or on the title placed a caret and raised the keyboard.
  • After: one tap places no caret and raises no keyboard. Tap the same block again within 1.5 s to edit at that point, with caret and keyboard. Taps on links, chips, checkboxes and diagrams act as they did.

Evidence

  • pnpm check:affected: lint, knip, discarded-effect and every panel test shard pass (2,681 tests). Two failures are down to the sandbox, not this change. The desktop typecheck was OOM-killed in 8 GB, and app-menu.test.ts can't load the Electron binary, which isn't installed here. Panel typecheck passes alone. CI is green on every job, typecheck and all 4 test shards included.
  • pnpm exec vitest run packages/panel/src/tasks/plan-document-editor.test.ts packages/panel/src/tasks/section-links.test.ts: 40 tests pass. Without the fix, the 4 new finger tests fail (one tap opens the editor, and the title is editable).
  • Panel tasks, desktop, styles, plan-diagram suites: 181 files pass.
  • Real Chromium under Playwright, with iPhone 13 emulation (hasTouch) against pnpm --filter @luke/web dev and the mobile-audit fake:
    • First tap on a section: focus is on div.plan-document-block and no editor opens.
    • Second tap: div.tiptap is focused and editable.
    • After blur: back to reading.
    • Title: the first tap focuses its block, and the second focuses the editable heading.
    • Desktop 1280×800 with a mouse: one click opens TipTap at once.

New tests (test count +8):

  • plan-document-editor.test.ts:
    • One finger tap on a section opens nothing, and a second opens the editor at that point.
    • Taps on two different sections, or two taps further apart than the window, open nothing.
    • Taps on a link open it and a tap on a checkbox ticks it, without opening an editor.
    • An empty plan opens on one tap, while a plan with text needs a second tap in its room.
    • The title reads until a second tap and goes back to reading on blur.
    • A scroll or a tap on a control between two taps lets the first go.
    • A second tap on the title puts the caret at the point a Firefox-style caretPositionFromPoint answers.
  • section-links.test.ts: taps on a mention chip open what it names, without opening an editor.
  • A mouse click opening at once stays covered by the existing tests, such as "a click into a section opens the whole text in place…", which now run with (pointer: coarse) answering false.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged.
  • Every behavior change has the test that fails without it; no change-detector added. The link/checkbox/mention tests guard that behavior stays the same, as the brief asked.
  • Gateway envelope goldens unchanged.
  • No new as type assertion.
  • No Effect.run* outside the edges.
  • No new raw async primitives.
  • No dropped Effect statements.
  • Test count: +8, listed above.
  • Nothing hand-rolled is replaced.
  • No AGENTS.md sentence names the changed part.
  • PRIVACY.md unchanged.
  • Deps unchanged.
  • Barrel/door rule unaffected.

Notes

  • The brief says to key on "(pointer: coarse), or the narrow room plus touch". This keys on (pointer: coarse) alone, checked at the tap (coarse-pointer.ts), like the panel's other finger affordances. A tablet with a trackpad that still reports a coarse primary pointer would need two clicks. Keying on the event's pointerType was ruled out, because iOS Safari still reports "mouse" on touch-made clicks (WebKit bug 282988).
  • Still to check by hand: the keyboard rising on the second tap on a real iPhone and Android, since emulation has no on-screen keyboard.

Made with Luke