Clicks land the instant they happen — the empty-space gestures move behind the masonry
The measured defect: LaneView's empty-space double-click was a second sequential .onTapGesture(count: 2) stacked over the single tap, and that recogniser held every click in the lane — its own empty space and every card face alike — hostage to the system double-click interval while it disambiguated (~475 ms click-to-selection on a hosted board). The fix is structural: the empty-space surfaces live on a background layer behind the masonry, so a card's click never shares a gesture path with a lane recogniser; one .onTapGesture branches on PointerClick.count (AppKit's own clickCount, read the way ClickModifier reads the keyboard) — first click selects, second creates, Finder's cadence with nothing to disambiguate; and the layer carries a load-bearing empty .onDrag, because without a drag source macOS holds primary clicks pending multi-click disambiguation (measured: never fires alone, ~90 ms with one present). A measured viewport floor makes each lane's blank space actually belong to the layer — a ScrollView proposes nothing along its scroll axis, so only an explicit minimum stretches the content — with the trash column as its twin, less the padding that sits inside its scroll content. The template chooser's stacked pair collapses to the same one-recogniser branch, and the attachment rows move their double-click to a simultaneous gesture (instant there, because the row's real .onDrag forces immediate delivery). PointerLatencyTests pins the recovery with synthetic pointer events on a hosted board. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -183,10 +183,16 @@ struct TemplateChooserView: View {
|
||||
) {
|
||||
ForEach(rows) { row in
|
||||
TemplateCard(row: row, isSelected: row.id == selected?.id)
|
||||
.onTapGesture { selection = row.id }
|
||||
// The list convention welcome's recents use, for the same reason: a
|
||||
// double click is how a chooser is answered without reaching for a button.
|
||||
.onTapGesture(count: 2) { choose() }
|
||||
// A double click is how a chooser is answered without reaching for a
|
||||
// button (welcome's list convention). One recogniser branching on
|
||||
// `PointerClick.count`, never a second two-tap one — stacked, it delays
|
||||
// the single click by the whole double-click interval; simultaneous, it
|
||||
// still holds clicks on a view with no drag source (`PointerClick`). The
|
||||
// first click of the pair selects the tile, which is also what aims
|
||||
// `choose()` at the clicked row.
|
||||
.onTapGesture {
|
||||
if PointerClick.count > 1 { choose() } else { selection = row.id }
|
||||
}
|
||||
// **Tab-reachable, and a button to the accessibility tree** — the tile is
|
||||
// the chooser's one act of choosing, so it has to be a control rather than a
|
||||
// decorated rectangle that happens to answer clicks (10-accessibility.md ▸
|
||||
|
||||
Reference in New Issue
Block a user