Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

a11y(form designer): a keyboard drag never reaches a drop target — collisionDetection pointerWithin answers nothing without pointer coordinates #11871

Closed

Description

@objectstack-fleet

Filing gate ①: a measured defect, class (a): an accessibility path that does nothing. Reach: Studio → an object → the Form tab's designer (ObjectFormDesigner), by keyboard. Measured by the objectui#11802 dev in real Chromium, on a dev-only harness that mounts the same ObjectFormDesigner the Form tab mounts (readOnly: false, sensors on; Studio itself not booted). The harness was not committed.

Who acts on it: objectui triage, grade and route. ⛔ Not a claim. Filed by the domain:ui execution seat 1 (session_01DrKzdPdyLLBW3qpZ4vtk7z) from the objectui#11802 dev report (out-of-scope finding 1), on the seat's ruling on that card's open question (option A: the screen-reader instructions stay faithful to the keyboard sensor, and the keyboard path is fixed here).

What happens

  • Steps: focus the Industry field card, press Space, press ArrowDown three times, then press Space.
  • Result: the draft is unchanged. No field moves, within its group or between groups.
  • What the base live region said: "Picked up draggable item f:industry." and then "Draggable item f:industry was dropped.", with no droppable.
  • Who is affected: keyboard-only and screen-reader users cannot arrange a form in Studio. The pointer path works.

Where it comes from (read in source)

  • The designer's DndContext uses collisionDetection={pointerWithin}.
  • pointerWithin returns no collision when pointerCoordinates is null. dnd-kit derives pointer coordinates from the activator event, and returns null for a KeyboardEvent.
  • So over stays null for every keyboard drag, and onDragEnd returns without committing.

Done when

  • A keyboard drag (Space, the arrow keys, Space) moves a field within its group and into another group, and the draft records the move.
    • One fix shape: a collision detection that answers when there are no pointer coordinates, such as closestCenter.
  • The pointer path is unchanged. The existing move tests and objectui#11780's drop write-back stay green.
  • The localized announcements from objectui#11802 name the keyboard drop's place, the same as a pointer drop.

Duplicate check

  • Semantic issue search on objectstack-ai/objectui, form designer keyboard drag no drop target pointerWithin keyboard sensor reorder fields: 1 hit.
  • That hit is objectui#11802 (open, the source: its announcements, not the keyboard path).
  • Positive control in the same session: vendor-markdown chunk eager first load streamdown rehype returned objectui#10065.

Dedupe words: form designer keyboard drag no drop target · pointerWithin keyboard sensor · Studio keyboard reorder fields


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: write metadata — the form designer by keyboard and screen reader | 缺项 | P2

    Triage: first grade, bug · priority:p2 · domain:ui · area:studio · pm:queue. Direction: a collision detection that answers without pointer coordinates

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T03:56Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in the form designer's DndContext (collisionDetection={pointerWithin}, in ObjectFormDesigner) ⇒ domain:ui; rationale: a Studio or console surface in objectui. Read on objectui main 4e7dd56ed1.

  2. added
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 8, 2026
  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8
    Session: session_01DrKzdPdyLLBW3qpZ4vtk7z
    Account: huangyiirene
    Branch: claude/issue-11871-form-designer-keyboard-drop
    Worktree: objectui-issue-11871
    Domain: domain:ui
    Seat: domain:ui#1
    File surface (line numbers on 3035948):

    • packages/app-shell/src/views/studio-design/ObjectFormDesigner.tsx:
      • the DndContext's collisionDetection (pointerWithin, about :660);
      • one module-private collision helper beside the id helpers, which answers for a keyboard drag;
      • the drag handlers (onDragOver / onDragEnd), only where a keyboard drop must reach the same commit the pointer drop reaches. The dev names any such change.
    • The tests beside it, and .changeset/11871-*.md.

    ⛔ Not on it:

    • SortableField's aria-roledescription: objectui#11872, which stays queued and runs serial after this card in the same file.
    • objectui#11802's announcements (formDndAnnouncements.ts, landed): they must name the keyboard drop's place unchanged, and are read only.
    • isKeptOffLayout and the layout model; sensors other than their wiring; @dnd-kit/* versions and any package.json.
    • packages/components/src/ui/** and packages/i18n/**.

    Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report).
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui over these paths: no path-derived mandate; default tier)
    Clause-②: no
    Responsibility: objectui app-shell Studio form designer: collisionDetection pointerWithin answers nothing when a KeyboardEvent supplies no pointer coordinates, so a keyboard drag never reaches a drop target and keyboard-only and screen-reader users cannot arrange a form | the platform path is right: dnd-kit's KeyboardSensor is installed and its collision strategies answer without a pointer | every keyboard or screen-reader author in Studio's Form tab, measured in real Chromium by objectui#11802's dev
    Thread-read: 6051859405
    Serial constraints cleared: none blocking.

    • Triage's serial note ("objectui#11802 edits the same DndContext props; claim after it lands") holds. objectui#11802 closed completed, landing as 33e3583.
    • objectui#11872 (the same file, SortableField) stays queued, serial after this card.
    • area:studio in flight: seat 2's objectui#11803; seat 3's objectui#11786; this seat's objectui#11799, and objectui#11847 (PR objectui#11884, in the merge queue).
      • Their claims were read 2026-10-08T04:26Z. None holds ObjectFormDesigner.tsx.
      • Seat 2's objectui#11781 (the designer hint) has landed.
    • Open objectui PRs: none touches ObjectFormDesigner.tsx. Read 2026-10-08T04:26Z.
    • This seat: dispatch is parallel, batch 3 working devs, on the maintainer's word in this session's chat (「改为并行派发,batch 3」, 「继续派发」).
      • The hourly write budget (40) is shared with this seat's devs, so slots are refilled as it allows.
      • Working with this card: objectui#11799's dev and objectui#11842's.

    Why Clause-②: no: the collision strategy is internal to a module-internal component's DndContext. No export, prop, @object-ui/types member or accepted input changes.

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11871,
      "status": "done",
      "branch": "claude/issue-11871-form-designer-keyboard-drop",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11897",
      "session": "session_01DrKzdPdyLLBW3qpZ4vtk7z — the session the PM claim comment 6052216192 names (subagent run; the parent session id)",
      "premise_still_valid": true,
      "summary": "Studio's form designer DndContext now takes collisionDetection={formCollision}: a module-private helper beside the id helpers in ObjectFormDesigner.tsx that returns pointerWithin(args) when args.pointerCoordinates is set (the pointer path, unchanged) and closestCorners(args) otherwise (a keyboard drag, which has no pointer coordinates). A keyboard drag (Space, arrows, Space) now moves a field within its group and into another group, the draft records it, and objectui#11802's announcements (read only, untouched) name the drop's place. onDragOver / onDragEnd are unchanged: the keyboard reaches the pointer's commits through the existing arithmetic. Measured residual outside the fence: at multi-column widths the stock keyboard coordinate getter never targets an empty group (H3 falsified there), reported as finding 1; in a one-column canvas the empty group is reached and committed. Draft PR objectui#11897; zero label and assign writes.",
      "tests": "HEAD the gate union ran on: 56f3ad7 (git rev-parse --short HEAD; merge of origin/main 0aa8c0c into the branch, no rebase). Commits: 7455f4e (fix), 2c0c3e5 (pins + changeset), 56f3ad7 (merge). files_changed: packages/app-shell/src/views/studio-design/ObjectFormDesigner.tsx (+15 -1: imports closestCorners and type CollisionDetection, adds formCollision, swaps the DndContext prop); packages/app-shell/src/views/studio-design/ObjectFormDesigner.keyboardDrop-11871.test.tsx (new, 7 cases); .changeset/11871-form-designer-keyboard-drop.md (new, @object-ui/app-shell patch). GATES (command → exit → verdict line): pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build → 0 → 'VERDICT command-exit 0' (28 packages, tree clean after) | pnpm exec vitest run (DataPillar.hiddenSystemFields-11780, ObjectFormDesigner.dndAnnouncements-11802, ObjectFormDesigner.keyboardDrop-11871, ObjectFormDesigner.test, StudioDesignSurface.formFields, viewCacheInvalidation.guard; the six files git ls-files names for the PM's globs) → 0 → 'Test Files 6 passed (6) / Tests 52 passed (52)' | pnpm exec vitest run packages/app-shell/src/views/studio-design/ → 0 → 'Test Files 119 passed (119) / Tests 751 passed (751)' | pnpm --filter @object-ui/app-shell type-check → 0 (script name echoed; tsc -p tsconfig.test.json --listFilesOnly lists the new test once) | pnpm check:control-bytes → 0 → 'check-control-bytes: OK' | pnpm check:new-line-citations → 0 → 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0' | pnpm check:changeset-claims → 0 → 'No pending changeset names a file this change touches.' | pnpm check:pending-changeset-literals → 0 → 'No test source names a pending changeset.' | derived: pnpm check:test-path-roots, check:phantom-deps, check:unreferenced-sources, check:vi-mock-specifiers → 0 each (OK lines) | node scripts/check-changeset-presence.mjs → 0 → '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)' | check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite → 0 each | eslint --no-inline-config --format json on the 2 changed files → 0 → 2 files, 0 errors, 1 warning (react-hooks/set-state-in-effect on the untouched draft re-sync effect). Lint narrowing evidence: population = root eslint.config.js as eslint --print-config resolves it per file; count = JSON result length 2; invariance = no parserOptions.project / projectService, so no type-aware rule can move an untouched file. Every build and test ran through os-verify-lock (slot issue-11871). REVERSE CHECKS (objectstack scripts/ablation-replace.mjs, WRAP mode inside the lock; tests import ./ObjectFormDesigner from src, no dist leg): (1) formCollision → pointerWithin alone: mutation landed (anchor 1→0, blob 6c6d0b914ebb→9410599792af), 5 failed / 2 passed, every keyboard case red in the predicted direction with no commit ('Industry was dropped outside the groups and is back in Ungrouped, position 2 of 2.'), pointer cases green; restored (blob == HEAD, git diff HEAD empty). (2) closestCorners on every drag: first attempt refused by the tool as a no-op (replacement text already inside the anchor, count 1→1) and restored; rerun with a distinct replacement landed (blob →972d6a52697a), 1 failed / 6 passed: the pointer control 'a pointer released outside every group commits nothing' went red ('Name moved to New group, position 1 of 1.'); restored (blob == HEAD, diff empty). H1: holds — source (pointerWithin returns [] for null pointerCoordinates; getEventCoordinates returns null without clientX) and real Chromium on 3035948: 7 keyboard sequences, 0 commits, each 'was dropped outside the groups'; pointer drags committed. H2: holds in a one-column canvas (480px: steps land over f: cards, or the g: droppable of an empty group); partly falsified multi-column (1280px: the getter steps card to card and never targets a section droppable, a full-width section rect loses on corner distance). H3: holds one-column (Email ArrowDown: over g:new_group, onDragOver moves it, over becomes f:email, onDragEnd's same-container branch commits unchanged — the pointer path's two-step); falsified multi-column (Email ArrowDown lands Ungrouped 2 of 3; Name ArrowUp lands Contact 1 of 3; the empty group is never reached). closestCenter vs closestCorners: identical results on every measured sequence at both widths; closestCorners chosen because it is the measure sortableKeyboardCoordinates picks targets by. BROWSER (real Chromium /opt/pw-browsers/chromium, dev-only Vite page mounting ObjectFormDesigner with the 11802 fixture, Playwright keys; harness deleted, never committed; reading on 7455f4e, whose ObjectFormDesigner.tsx equals HEAD's): within a group — 480 Phone ArrowDown → 'Phone moved to Contact, position 2 of 2.' and 1280 Phone ArrowRight → same, commit Contact [email, phone]; across groups — 480 Email ArrowDown → 'Email moved to New group, position 1 of 1.' (empty group), 1280 Email ArrowDown → 'Email moved to Ungrouped, position 2 of 3.', 480 Name ArrowUp x3 → 'Name moved to Contact, position 2 of 3.', 1280 eleven-field canvas F6 ArrowUp → 'F6 moved to Contact, position 4 of 7.'; pointer control identical before and after (Name onto New group, Phone onto Email, Name onto Email). DEVIATIONS: (a) the route section's 'including an empty one' pin holds in a one-column canvas only; multi-column needs a coordinate getter, outside the fence → finding 1, not changed; (b) the test's first run failed on the instrument, not the product: dnd-kit measures the DragOverlay's single child (the label chip), so the geometry stub now models the chip at the height Chromium measured (34px); (c) declared narrowing: full @object-ui/app-shell suite and repo-wide pnpm lint left to CI, the studio-design directory (119 files) run instead; (d) one lock attempt ended 99 (queue-timeout) and was re-taken under the same slot; (e) temporary measurement probes in ObjectFormDesigner.tsx (a URL-param strategy switch and console.info of over ids) were never committed and were removed with git checkout HEAD, git diff HEAD empty; (f) commits carry objectui AGENTS.md's model-free trailer pair (Co-authored-by: Claude), not the harness's model-named trailer; the merge commit carries git's default message; (g) no handler change was needed, so none is named.",
      "mcp_calls": "0 — no MCP GitHub tool used",
      "api_writes": "2 relay writes plus pushes — (1) fleet-write/dispatch.mjs op pr_create → objectstack-ai/objectui#11897 (relay dispatch POST to objectstack-ai/objectstack, run 37731704837 success, read-back 9529 bytes identical); (2) post-stamped.mjs --comment=11871 --repo=objectstack-ai/objectui: this os-dev-report. git push of claude/issue-11871-form-designer-keyboard-drop three times (empty branch probe, fix, merge head) — one push item per the order. Zero label, assignee or PR-body PATCH writes. Reads: gh api REST single-card, its comments, PR 11897, and objectui#11802's comments.",
      "open_questions": [],
      "out_of_scope_findings": [
        "Finding 1 — class: a · reach: real Chromium, the Form tab's ObjectFormDesigner (dev harness mounting the same component, readOnly off, sensors on), keyboard only: at a 1280px viewport (two or three fields per row) Email Space ArrowDown Space lands in Ungrouped, position 2 of 3, skipping the empty New group between; ArrowUp from Name lands in Contact, skipping it too, so a keyboard author cannot put a field into an empty group at multi-column widths (one-column canvases reach it); on an eleven-field canvas the full-row Notes (textarea) does not move on ArrowUp; at 480px the first ArrowUp from a group's first field moves nothing; at 480px Phone ArrowDown x3 announces 'over Ungrouped, position 1 of 3' and drops at 'position 2 of 3' (the onDragEnd arithmetic objectui#11802 pins for a pointer drop still over the field it was carried onto). Evidence: sortableKeyboardCoordinates picks each step's target by closestCorners over every enabled droppable, and a section droppable spans the whole section, so in a multi-column grid its corner distance loses to the nearest card and its own section is the first target upward. Seam: renderer:ObjectFormDesigner KeyboardSensor coordinateGetter (sortableKeyboardCoordinates) and onDragEnd same-container branch → consumer: keyboard and screen-reader authors in Studio's Form tab. Not a sub-issue of objectui#11872 (aria-roledescription); same file, so serial with it. Dedupe words: form designer keyboard empty group unreachable · sortableKeyboardCoordinates section droppable multi-column · keyboard over position drop position mismatch",
        "carrier: 承接者:无 · noted, not filed — a keyboard pick-up and drop in place now commits the unchanged layout through onDragEnd's existing commit(prev), as a pointer released on its own card already does; recorded in the PR's Acceptance notes"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: ACCEPT — PR objectui#11897 (head 56f3ad7)

    domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · reviewed against the PR's file list and diff hunks, git merge-tree, check-governed-merges.mjs, its check runs and the dev's report on this card, read 2026-10-08T05:25Z.

    • Shape: draft → main. The first line is Fixes #11871, and Clause-②: no starts a later line.
    • Scope: 3 files, +277/−1, inside claim 6052216192's surface.
      • ObjectFormDesigner.tsx (+15/−1): a module-private formCollision answers pointerWithin(args) when args.pointerCoordinates is set, and closestCorners(args) otherwise. DndContext takes it in place of pointerWithin.
      • onDragOver, onDragEnd, the sensors, SortableField, formDndAnnouncements.ts, every package.json and packages/i18n/** are untouched.
      • A new pin, ObjectFormDesigner.keyboardDrop-11871.test.tsx (7 cases), and an @object-ui/app-shell patch changeset.
    • Clause-② re-read: nothing is added to the package entry, and no export, prop or type member changes. no holds.
    • Governed surface: check-governed-merges.mjs --pr objectstack-ai/objectui#11897 reports 0 of 3 paths governed, 278 changed lines. That is an ordinary queue landing.
    • Done-when, item by item:
      • A keyboard drag moves a field within its group and into another group, and the draft records it. Met. Real Chromium on 7455f4e (its ObjectFormDesigner.tsx is identical to the head's) commits each keyboard sequence at 480px and 1280px. Before the fix, 7 sequences on main made 0 commits.
      • The pointer path is unchanged. Met. The three pointer controls read the same before and after.
      • objectui#11802's localized announcements name the keyboard drop's place. Met, for example "Email moved to New group, position 1 of 1."
    • Deviation (a), accepted, and why the PR stays Fixes: the dispatch's "into an empty group" holds on a one-column canvas only. On a multi-column canvas, the keyboard coordinate getter sortableKeyboardCoordinates never steps onto an empty group's section droppable. That is the getter, outside this card's fence, and not the collision detection this card names. The card's Done-when does not name an empty group, so it is met. The gap is filed as its own card, objectui#11898, serial after this card and objectui#11872.
    • Deviation (b), accepted: a test-instrument fix. The geometry stub models the DragOverlay chip at the 34px Chromium measured.
    • Tests:
      • Local union on 56f3ad7 (with main 0aa8c0c merged in): the studio-design directory has 119 files and 751 tests, all green. The six files the order's globs match have 52 tests, all green, the *.guard.test.* view-cache guard among them. The type-check, eslint on the changed files (0 errors; 1 existing warning on an effect this diff does not touch) and the derived check:* set are green.

      • Reverse checks:

        • With plain pointerWithin, 5 failed and 2 passed. Every keyboard case went red and the pointer cases stayed green.
        • With closestCorners for every drag, 1 failed and 6 passed. The pointer control "a pointer released outside every group commits nothing" went red.

        Each restore was proven by blob hash.

    • Overlap: an in-memory git merge-tree against main is clean. No other open PR touches ObjectFormDesigner.tsx.
    • CI at 56f3ad7: converging at this read: 26 green, 3 expected skips, the test shards and the type-check still running. The PR is queued when every check completes.
    • Serial order: objectui#11872 (aria-roledescription, the same file) is released for dispatch after this PR merges, and objectui#11898 after that.
    • Noted, not filed (no defect): a keyboard pick-up dropped in place now commits the unchanged layout through onDragEnd's existing commit(prev), the same as a pointer released on its own card.
    • PR assignee: set by this seat to huangyiirene.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11897 → main a476dee

    domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · verified 2026-10-08T05:55Z.

    • Merged through the merge queue at 2026-10-08T05:53:23Z, after it was queued at 2026-10-08T05:36:36Z.
      • CI on head 56f3ad7 before queueing: 40 of 43 check runs passed, and the 3 skipped are the expected ones.
      • check-governed-merges.mjs: not governed, +277/−1.
    • Content check:
      • a476dee is one squash commit on parent 0855fc6. It changes 3 files, +277/−1, the same as the PR.
      • All 3 files are blob-identical to the reviewed head 56f3ad7, and the whole diff's -U0 patch-id matches (094bb8a1…).
    • Card: closed completed by the PR's Fixes. pm:dispatched was removed in the same pass and read back.
    • On record:
      • A pointer drag keeps pointerWithin. A keyboard drag, which has no pointer coordinates, reads closestCorners.
      • The multi-column gap (the arrow keys never reach an empty group) is objectui#11898.
      • objectui#11872 (the same file) is now released for dispatch (ACCEPT 6052983795).

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portalbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions