feat(web): tap-to-assign on board cards for mobile + a11y #1116

Merged
reviewer merged 2 commits from feat/board-tap-to-assign into main 2026-05-11 23:31:42 +00:00
Collaborator

Summary

  • Mobile/touch users had zero path to assign a ticket on the board: native HTML5 drag (draggable={!isMobile}) doesn't fire on touch, the keyboard a shortcut needs a keyboard, and the card had no tap affordance.
  • AgentSlot (default density) and a new icon-only CompactAssignButton (compact density) are now real <button>s that open the existing BoardAgentPicker dialog.
  • Reuses the keyboard path's state via a new openAgentPicker(card) returned from useBoardKeymap — single source of truth for picker open state.
  • e.stopPropagation() on onClick + onPointerDown keeps the card-click → side-panel path and drag-start untouched.

A11y

  • Unassigned slot renders UserPlus icon + "Assign…" label, clearly affording action.
  • aria-label="Assign agent" / "Reassign agent (currently @x)".
  • min-h-8 touch target, focus-visible:outline-accent.
  • Keyboard path (a shortcut) unchanged.

Test plan

  • bun run test — 1079/1079 web tests pass, including new board.test.tsx case "tap on AgentSlot opens agent picker".
  • Existing a shortcut test still green.
  • Existing card-click-opens-panel + drag-and-drop tests still green (stopPropagation verified).
  • just qa — lint + typecheck clean (paraglide fr.json warning is pre-existing, unrelated).
  • Manual: open board at <640px viewport, tap card body → drawer opens; tap agent slot → picker opens; pick agent → assign fires.

🤖 Generated with Claude Code

## Summary - Mobile/touch users had **zero path to assign a ticket** on the board: native HTML5 drag (`draggable={!isMobile}`) doesn't fire on touch, the keyboard `a` shortcut needs a keyboard, and the card had no tap affordance. - `AgentSlot` (default density) and a new icon-only `CompactAssignButton` (compact density) are now real `<button>`s that open the existing `BoardAgentPicker` dialog. - Reuses the keyboard path's state via a new `openAgentPicker(card)` returned from `useBoardKeymap` — single source of truth for picker open state. - `e.stopPropagation()` on `onClick` + `onPointerDown` keeps the card-click → side-panel path and drag-start untouched. ## A11y - Unassigned slot renders `UserPlus` icon + "Assign…" label, clearly affording action. - `aria-label="Assign agent"` / `"Reassign agent (currently @x)"`. - `min-h-8` touch target, `focus-visible:outline-accent`. - Keyboard path (`a` shortcut) unchanged. ## Test plan - [x] `bun run test` — 1079/1079 web tests pass, including new `board.test.tsx` case "tap on AgentSlot opens agent picker". - [x] Existing `a` shortcut test still green. - [x] Existing card-click-opens-panel + drag-and-drop tests still green (stopPropagation verified). - [x] `just qa` — lint + typecheck clean (paraglide `fr.json` warning is pre-existing, unrelated). - [ ] Manual: open board at <640px viewport, tap card body → drawer opens; tap agent slot → picker opens; pick agent → assign fires. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(web): tap-to-assign on board cards for mobile + a11y
All checks were successful
qa / sql-layer-check (pull_request) Successful in 11s
qa / i18n-string-check (pull_request) Successful in 14s
qa / dockerfile (pull_request) Successful in 14s
qa / db-schema (pull_request) Successful in 24s
qa / qa-1 (pull_request) Successful in 3m20s
qa / qa (pull_request) Successful in 0s
c614c700de
Cards on the Kanban board had no assign path on mobile: native HTML5 drag
doesn't fire on touch, the keyboard `a` shortcut needs a keyboard, and the
card had no tap affordance. Mobile users were stuck.

Make `AgentSlot` (and a compact-density icon button) a real `<button>`
that opens the existing `BoardAgentPicker` dialog. Reuses the keyboard
path's state via a new `openAgentPicker(card)` from `useBoardKeymap`.
`stopPropagation` keeps the card-click → side-panel path intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(web): close board agent picker on outside click
All checks were successful
qa / sql-layer-check (pull_request) Successful in 15s
qa / i18n-string-check (pull_request) Successful in 16s
qa / db-schema (pull_request) Successful in 17s
qa / dockerfile (pull_request) Successful in 24s
qa / qa-1 (pull_request) Successful in 3m30s
qa / qa (pull_request) Successful in 0s
978677f314
Dialog.Popup fills the viewport, so clicks in the empty space around the
inner panel never hit the Backdrop and Base UI's auto-dismiss didn't fire.
Forward outer-wrapper clicks (target === currentTarget) to `onClose`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reviewer approved these changes 2026-05-11 23:31:33 +00:00
reviewer left a comment

Review: APPROVED

CI green, mergeable, implementation is clean. A few nits worth noting for future cleanup — none are blockers.


What works well

  • Single source of truthopenAgentPicker surfaced from useBoardKeymap means the a shortcut and the tap path share the same state; no duplication.
  • stopPropagation on both onClick and onPointerDown — correctly prevents card-click-to-panel and drag-start from firing when the assign button is the intended target.
  • e.target === e.currentTarget backdrop dismiss — correct pattern; won't fire when clicking inside the inner panel.
  • aria-label coverage — both assigned (Reassign agent (currently @x)) and unassigned (Assign agent) states are annotated. Visible "Assign…" text plus UserPlus icon gives good sighted affordance.
  • Tests — two new cases cover the happy path (tap → picker → assign) and backdrop dismiss; they use data-testid selectors consistently.

🔸 Nits (non-blocking)

1. data-testid-assign-trigger="true" is never read

Both the interactive AgentSlot button and CompactAssignButton carry this attribute, but no test or application code queries for it. Either wire it up (e.g. as a selector in a future test) or drop it to avoid dead HTML.

2. Redundant agentInstance / assignee props on AgentSlot

AgentSlot now always receives card, so agentInstance={card.agent_instance} and assignee={card.assignee} at the call-site are redundant — the component could read them directly off card. Harmless, but the interface could be simplified to just { card, onRequestAssign? }.

3. IIFE for inner reduces readability

const inner: ReactElement = label ? (() => {
    const roleKey = ;
    return <span></span>;
})() : interactive ?  : ;

A tiny named helper (e.g. function AssignedLabel({ label })) or a useMemo-style block before the conditional would be easier to scan. Not urgent.

4. No test for compact density path

CompactAssignButton is exercised visually but not by a dedicated test. The default-density coverage is sufficient for now; just flagging in case the compact path diverges later.


Merging now.

## Review: APPROVED ✅ CI green, mergeable, implementation is clean. A few nits worth noting for future cleanup — none are blockers. --- ### ✅ What works well - **Single source of truth** — `openAgentPicker` surfaced from `useBoardKeymap` means the `a` shortcut and the tap path share the same state; no duplication. - **`stopPropagation` on both `onClick` and `onPointerDown`** — correctly prevents card-click-to-panel and drag-start from firing when the assign button is the intended target. - **`e.target === e.currentTarget` backdrop dismiss** — correct pattern; won't fire when clicking inside the inner panel. - **`aria-label` coverage** — both assigned (`Reassign agent (currently @x)`) and unassigned (`Assign agent`) states are annotated. Visible "Assign…" text plus `UserPlus` icon gives good sighted affordance. - **Tests** — two new cases cover the happy path (tap → picker → assign) and backdrop dismiss; they use `data-testid` selectors consistently. --- ### 🔸 Nits (non-blocking) **1. `data-testid-assign-trigger="true"` is never read** Both the interactive `AgentSlot` button and `CompactAssignButton` carry this attribute, but no test or application code queries for it. Either wire it up (e.g. as a selector in a future test) or drop it to avoid dead HTML. **2. Redundant `agentInstance` / `assignee` props on `AgentSlot`** `AgentSlot` now always receives `card`, so `agentInstance={card.agent_instance}` and `assignee={card.assignee}` at the call-site are redundant — the component could read them directly off `card`. Harmless, but the interface could be simplified to just `{ card, onRequestAssign? }`. **3. IIFE for `inner` reduces readability** ```ts const inner: ReactElement = label ? (() => { const roleKey = …; return <span>…</span>; })() : interactive ? … : …; ``` A tiny named helper (e.g. `function AssignedLabel({ label })`) or a `useMemo`-style block before the conditional would be easier to scan. Not urgent. **4. No test for compact density path** `CompactAssignButton` is exercised visually but not by a dedicated test. The default-density coverage is sufficient for now; just flagging in case the compact path diverges later. --- Merging now.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!1116
No description provided.