You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
a11y(form designer): on a multi-column canvas the arrow keys never reach an empty group, and a keyboard drop can commit a place other than the one announced #11898
Filing gate ①: a measured defect, class (a): an accessibility path that does not reach its target. Reach: Studio → an object → the Form tab's designer (ObjectFormDesigner), by keyboard, on a canvas that lays out two or more fields per row. Measured by the objectui#11871 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. The reading was taken with objectui#11871's fix (PR objectui#11897) in place.
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#11871 dev report (out-of-scope finding 1).
Serial note: this is the same file as objectui#11871 (PR objectui#11897, landing) and objectui#11872 (aria-roledescription). Run it after both, never alongside either.
What happens
The fixture has three groups: Contact (Phone, Email), New group (empty), Ungrouped (Name, Industry), measured at a 1280px viewport (two or three fields per row) unless noted.
An empty group cannot be reached by keyboard. Email, Space, ArrowDown, Space lands in Ungrouped at position 2 of 3. That skips the empty New group between them. Name, ArrowUp lands in Contact and also skips it. On a one-column canvas (480px) the same keys reach the empty group.
A full-row field does not move up. On an eleven-field canvas, ArrowUp on the full-row Notes field (a textarea) does nothing.
The first ArrowUp from a group's first field does nothing (480px).
The announced place and the committed place differ. At 480px, Phone, ArrowDown ×3 announces "over Ungrouped, position 1 of 3", but the drop commits at position 2 of 3.
Who is affected: keyboard-only and screen-reader authors on any form wider than one column. They cannot put a field into a new, empty group, and the live region can name a place the drop does not commit to. The pointer path is not affected.
Where it comes from (read in source and measured)
The keyboard sensor's coordinate getter is dnd-kit's stock sortableKeyboardCoordinates. It picks each arrow-key step's target by closestCorners over every enabled droppable.
A group's section droppable spans the whole section. In a multi-column grid, its corner distance loses to the nearest field card, so the getter steps from card to card and never onto an empty section. Moving up, the field's own section is the first target (symptom 3).
Symptom 4 is onDragEnd's same-container arithmetic, the one objectui#11802 pins for a pointer drop still over the card it was carried onto.
Done when
On a multi-column canvas, the arrow keys can carry a field into an empty group, and the drop commits it there.
A full-row field moves up and down like any other, and the first ArrowUp from a group's first field moves it.
The live region's "over" place and the committed place agree for a keyboard drop.
The pointer path is unchanged, and objectui#11871's keyboard pins stay green.
Duplicate check
Semantic issue search on objectstack-ai/objectui, form designer keyboard empty group unreachable multi-column drop position mismatch: 7 hits. objectui#11871 (open, the collision detection, the source of this finding) and objectui#11802 (closed, the announcements) are the nearest. Neither names the coordinate getter.
Dedupe words: form designer keyboard empty group unreachable · sortableKeyboardCoordinates section droppable multi-column · keyboard over position drop position mismatch
Path: write metadata — the form designer by keyboard and screen reader | 缺项 | P3
Triage: first grade, bug · priority:p3 · domain:ui · area:studio · pm:queue. Direction: keyboard moves reach every group on a multi-column canvas, and the drop commits where it was announced
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T05:58Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in ObjectFormDesigner's keyboard coordinates and collision handling ⇒ domain:ui; rationale: Studio's form designer in objectui.
the keyboard sensor's coordinate getter (useSensor(KeyboardSensor, { coordinateGetter: … }), about :547), replaced by a module-private getter where sortableKeyboardCoordinates cannot reach an empty group or move a full-row field;
formCollision (about :90), only for the keyboard branch;
onDragOver / onDragEnd (about :584–:640), only where the announced place and the committed place differ;
and nothing else in the file.
The tests beside it (a new ObjectFormDesigner.keyboardGrid-11898.test.tsx; objectui#11871's and objectui#11802's pins stay green), and .changeset/11898-*.md.
⛔ Not on it:
formDndAnnouncements.ts (objectui#11802's sentences). If the announced place itself is wrong, the dev reports it.
SortableField's attributes (objectui#11872, landed), and the read-only card's aria-describedby (objectui#11924, queued behind this card).
The pointer path's collision (pointerWithin) and its commits. These are unchanged and pinned as controls.
metadata-admin/i18n.ts, packages/i18n/** and packages/components/src/ui/**.
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 this path: no path-derived mandate; default tier)
Clause-②: no
Responsibility: objectui app-shell Studio form designer: on a multi-column canvas the arrow keys never reach an empty group or move a full-row field, and one keyboard drop commits a place other than the one announced | the platform path is right: dnd-kit's KeyboardSensor takes a coordinateGetter, and the designer owns its layout | every keyboard and screen-reader author arranging a form wider than one column
Thread-read: 6053422526
Serial constraints cleared: none blocking.
The same file, in order: objectui#11871 landed (a476dee), and objectui#11872 landed (fc82a31) through PR objectui#11920, merged 2026-10-08T07:10:07Z. Triage's grade 6053422526 asked for this card to run after both.
Open objectui PRs: none touches ObjectFormDesigner.tsx. Read 2026-10-08T07:22Z.
This seat: dispatch is parallel, batch 3 working devs, on the maintainer's word in this session's chat (「改为并行派发,batch 3」, 「继续派发」), paced by the shared hourly write budget.
objectui#11799's remainder dev has finished (PR objectui#11928), which freed this slot.
Working with this card: objectui#11895's dev and objectui#11902's.
Why Clause-②: no: keyboard geometry and drop arithmetic inside the designer. No export, prop, @object-ui/types member or accepted input changes.
{
"issue": 11898,
"status": "done",
"branch": "claude/issue-11898-form-keyboard-grid",
"pr": "https://github.com/objectstack-ai/objectui/pull/11940",
"session": "session_01DrKzdPdyLLBW3qpZ4vtk7z — the session the PM claim comment 6054823047 names (subagent run; the parent session id)",
"premise_still_valid": true,
"summary": "The Studio form designer's keyboard drag now steps through the designer's own layout in reading order (ArrowDown/ArrowRight one place later, ArrowUp/ArrowLeft one place earlier, across groups, an empty group one step like any other, whatever the column count): a module-private keyboardStep replaces dnd-kit's sortableKeyboardCoordinates as the KeyboardSensor's coordinateGetter, and formCollision's keyboard branch (was closestCorners) answers the droppable that place reads off the layout, which is the dragged card itself once onDragOver has carried it into another group. That fixes all four cases at 1280px and 480px, read in real Chromium and pinned in a new ObjectFormDesigner.keyboardGrid-11898.test.tsx. onDragOver, onDragEnd, dropPlaceIn, formDndAnnouncements.ts and the whole pointer path are unchanged; case 4 is fixed at the over, not in the commit arithmetic (H3 below), and the pointer drags read identically before and after. One dispatch conflict is reported, not silently chosen: objectui#11871's pin 'carries a field up through the empty group…' pinned case 3's no-op first ArrowUp, so its intermediate sentences now read one place per step (its final sentence and commit are unchanged). Draft PR objectui#11940; zero label and assign writes.",
"tests": "HEAD the gate union ran on: a06a2a4 (git rev-parse --short HEAD; merge commit of origin/main f1781be into the branch, no rebase). Commits: 2fb6b8b (fix), 8c1b262 (pins + changeset), a06a2a4 (merge). | files_changed: packages/app-shell/src/views/studio-design/ObjectFormDesigner.tsx (+118 −19: drops the closestCorners and sortableKeyboardCoordinates imports, adds type KeyboardCoordinateGetter; formCollision becomes a factory whose keyboard branch answers keyboardOverAt; adds KeyboardSlot, keyboardOverAt, stepSlot, STEP_BY_KEY, keyboardStep; the KeyboardSensor gets coordinateGetter keyboardStep and onActivation that resets the place; DndContext's collisionDetection prop passes the layout); ObjectFormDesigner.keyboardGrid-11898.test.tsx (new, 15 tests: 6 per width × 2 + 3 controls); ObjectFormDesigner.keyboardDrop-11871.test.tsx (+5 −4: the header's instrument line, and one pin's intermediate sentences); .changeset/11898-form-keyboard-grid.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, 29 of 47 projects in scope, tree clean after) ; pnpm exec vitest run over ObjectFormDesigner.dndAnnouncements-11802, .keyboardDrop-11871, .keyboardGrid-11898, .roleDescription-11872, ObjectFormDesigner.test, StudioDesignSurface.formFields, viewCacheInvalidation.guard → 0 → 'Test Files 7 passed (7) / Tests 60 passed (60)' ; pnpm exec vitest run packages/app-shell/src/views/studio-design/ → 0 → 'Test Files 136 passed (136) / Tests 854 passed (854)' ; pnpm --filter @object-ui/app-shell type-check → 0 (script name echoed; tsc -p tsconfig.test.json --listFilesOnly lists both pin files) ; 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 from the diff: check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:phantom-deps, check:unused-deps, check:unreferenced-sources, check:i18n-keys, check:shell-escape-residue → 0 each (OK lines) ; node scripts/check-changeset-presence.mjs → 0 → '3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/11898-form-keyboard-grid.md.' ; check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite → 0 each ; eslint --no-inline-config --format json on the 3 changed source/test files → 0 → 3 files, 0 errors, 1 warning (react-hooks/set-state-in-effect on the untouched draft re-sync effect, present on main). Lint narrowing evidence: population = root eslint.config.js as eslint --print-config resolves it; count = JSON result length 3; invariance = parserOptions empty (no project / projectService), so no type-aware rule can move an untouched file. Every build and test ran through os-verify-lock, slot issue-11898. CI on the PR head: read once after opening, 42 check runs, 37 in_progress, 1 queued, 3 skipped, 1 success — in_progress, not waited on. | H1 (holds in part; real Chromium on d52d908, a probe wrapping the stock getter recorded its direction-filtered candidates with closestCorners values): 1280px Email ArrowDown ranked f:industry 262, g:new_group 371, g:__ungrouped__ 451, f:name 630, so the card won (Ungrouped 2 of 3); Name ArrowUp ranked f:phone 230 first (Contact 1 of 3); 480px Name ArrowUp ranked its own section g:__ungrouped__ 69 first, the over stayed f:name, nothing moved (case 3). Correction for case 2: the full-row Notes' chip is as wide as its section, so Notes' OWN section won ArrowUp (g:contact 163, then f:email 410), the drag was over the section, and the same-group arithmetic moved Notes to the group's END ('Notes moved to Contact, position 6 of 6' from 5 of 6); a Notes that is already last reads as 'does nothing'. Case 3 also depends on geometry: on the eleven-field canvas at 480px the empty group (105) beat the own section (192). | H2 (holds, amended): the reading-order getter reaches every case at both widths; but a getter alone is not enough — reverse-check leg 2 shows that with closestCorners still judging the keyboard drop, the empty section loses on corner distance even with the chip placed on it (1280px Email ArrowDown over 'Contact, position 1 of 2'). | H3 (mechanism holds, fix location differs): at 480px Phone ArrowDown ×3, the third step was over f:name and announced 'Phone is over Ungrouped, position 1 of 3'; onDragOver carried Phone before Name; a probe at a no-op key right before the drop read the over still f:name; onDragEnd's same-group branch (old 0, new 1) committed 'position 2 of 3'. The announcement is right and the arithmetic is right for the over it is given; the over was wrong after the carry. With the keyboard over following the layout (the carried card itself, dnd-kit's own multi-container pattern) the existing arithmetic commits the announced place. onDragEnd and dropPlaceIn untouched; objectui#11802's pointer two-step pin untouched. | BROWSER (real Chromium /opt/pw-browsers/chromium, dev-only Vite page mounting ObjectFormDesigner, Playwright keys, MutationObserver on the live region; harness deleted, never committed; reading on HEAD blob c947f010ae64 of ObjectFormDesigner.tsx), identical at 1280px and 480px: case 1 Email ArrowDown → 'Email is over New group, position 1 of 1.' → 'Email moved to New group, position 1 of 1.', commit new_group [email]; case 1 Name ArrowUp → 'Name moved to New group, position 1 of 1.'; case 2 Notes ArrowUp → 'Notes moved to Contact, position 4 of 6.' (contact phone, email, mobile, notes, fax, website) and ArrowDown → 'Notes moved to Contact, position 6 of 6.'; case 3 Name ArrowUp ×2 → over New group 1 of 1, over Contact 3 of 3 → 'Name moved to Contact, position 3 of 3.'; case 4 Phone ArrowDown ×3 → over Contact 2 of 2, over New group 1 of 1, over Ungrouped 1 of 3 → 'Phone moved to Ungrouped, position 1 of 3.', commit '' [phone, name, industry]. Also eleven fields Phone ArrowDown ×8 and Owner ArrowUp ×8 on a 420px-tall viewport: every place in order, Contact's end slot included, the page scrolls with the chip, the drop commits the last place announced. Pointer control: Name onto the empty group, Phone onto Email, Name onto Email, Name released outside: live-region sequences and commits IDENTICAL before (d52d908 blob e6efb31f593c, swapped in and restored to HEAD blob c947f010ae64 with git diff HEAD empty) and after, at both widths. | REVERSE CHECKS (objectstack scripts/ablation-replace.mjs, WRAP mode nested per anchor, inside the lock, on committed head 8c1b262; tests import ./ObjectFormDesigner from src, no dist leg; every leg restored with blob c947f010ae64 == HEAD and git diff HEAD empty, proven by the tool and again by the wrapper's trap): leg 1, the base mechanism back (sortableKeyboardCoordinates as getter + closestCorners keyboard branch, 4 anchors each 1 → 0, blob → 87182889ee3e) → 9 failed / 13 passed: cases 1–4 red at 1280px and Name ArrowUp, case 3, case 4 red at 480px, each with Chromium's own pre-fix sentence (1280px Email 'over Ungrouped, position 2 of 3'; 480px Phone committed 'position 2 of 3' after 'over Ungrouped, position 1 of 3'), plus objectui#11871's three-step pin; green: the 480px Email and Notes moves Chromium reached before the fix too, the boundary cases, the pointer controls — the stub reproduces the pre-fix Chromium readings. Leg 2, the keyboard collision only back to closestCorners (2 anchors, blob → 972ba441e004) → 8 failed / 14 passed: case 4 red at both widths ('position 2 of 3' at 480px), cases 1–3 red at 1280px. Leg 3, the stock getter only (2 anchors, blob → 9059335fe7b8) → 15 failed / 7 passed: every keyboard move stays put, the one-column control included; pointer controls and boundary cases green. The order's 'restore the commit arithmetic' leg has no target: the arithmetic was not changed (H3); leg 2 is its counterpart. | DEVIATIONS: (a) dispatch conflict, reported in the PR and in open_questions: objectui#11871's pin 'carries a field up through the empty group into a group that has fields' pinned case 3's no-op first ArrowUp; its intermediate sentences now read 'Name is over New group, position 1 of 1.', 'Name is over Contact, position 3 of 3.', 'Name is over Contact, position 2 of 3.' (Chromium reads the same after the fix), final sentence and commit unchanged; its header's instrument line named sortableKeyboardCoordinates and now names the designer's own getter; every other objectui#11871 and objectui#11802 pin is unchanged. (b) H3's suggested route (fix the commit) not taken: the over is fixed instead, so no handler line changed. (c) the place is reset per keyboard pick-up through the KeyboardSensor's own onActivation option, on the same useSensor call as the getter, so onDragStart / onDragCancel are untouched. (d) a first version read the place from a ref during render and added three react-hooks/refs lint warnings; restructured before the first commit (state for the collision, ref for the getter) and the browser reading was re-taken on the committed blob. (e) case 2 runs on my own eleven-field fixture (the card's was not recorded): its pre-fix symptom was 'moved to the group's end', the card's 'does nothing'. (f) declared narrowing: repo-wide pnpm lint and the full test farm are CI's; the studio-design directory (136 files) ran instead. (g) 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 the pair too. (h) the 136-file directory run went through nohup + tail --pid under the lock (held 8m30s), read from its log.",
"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#11940 (relay dispatch POST to objectstack-ai/objectstack, run 37754162522 success, read-back 11861 bytes identical); (2) post-stamped.mjs --comment=11898 --repo=objectstack-ai/objectui: this os-dev-report. git push of claude/issue-11898-form-keyboard-grid four times (empty branch probe, fix, pins, merge head) — one push item per the order. Zero label, assignee or PR-body PATCH writes. Reads: gh api REST single card 11898 and its comments, objectui#11871's comments, PR 11940 read-back, one check-runs read; write-pace --status before each relay write.",
"open_questions": [
{
"question": "The dispatch rules both that case 3 is fixed (the first ArrowUp from a group's first field moves it, at 480px) and that objectui#11871's pins 'stay green as they are'. One objectui#11871 pin ('carries a field up through the empty group into a group that has fields') asserted the first ArrowUp from Name moved nothing — case 3 itself. Accept the updated intermediate sentences in that pin?",
"options": [
"A — keep the pin as updated in PR objectui#11940: intermediate sentences one place per step, final sentence and commit unchanged, Chromium agrees",
"B — restore the old pin, which requires leaving case 3 unfixed at one column and contradicts the triage direction"
],
"recommendation": "A, because the old sentence pinned the defect this card fixes, the pin's own outcome (final sentence and commit) is unchanged, and every other objectui#11871 / objectui#11802 pin stays as it was"
}
],
"out_of_scope_findings": [
"carrier: PR objectui#11940 (the seat landing it) · noted, not filed — the pending .changeset/11871-form-designer-keyboard-drop.md says 'The designer now uses `pointerWithin` for a pointer drag, unchanged, and `closestCorners` for a keyboard drag.'; after objectui#11940 lands in the same release, the keyboard branch is no longer closestCorners. objectui#11940's changeset names the replacement ('the keyboard branch of the drag's collision detection, `closestCorners` (objectui#11871)… The designer now steps through its own layout'), so the two entries read in order; the 11871 file is outside this claim and was not touched. If the seat wants the earlier entry corrected, it is a one-sentence edit to that pending changeset. Dedupe words: 11871 changeset closestCorners keyboard superseded · pending changeset mechanism sentence",
"carrier: 承接者:无 · noted, not filed — a field that is the only one in Ungrouped, carried into a group by a keyboard drag, cannot be stepped back: the designer hides the now-empty Ungrouped bucket mid-drag (the existing 'hide the ungrouped bucket only when it is empty AND groups exist' rule), so ArrowDown has no section to enter; Escape cancels; a pointer drag behaves the same. Read in Chromium (lone fixture, both widths). Recorded in the PR's Acceptance notes."
]
}
Review: ACCEPT — PR objectui#11940 (head a06a2a4) · the open question ruled A · queueing held for a red main
domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · reviewed against the PR's file list and diff hunks, git merge-tree, its check runs and the dev's report 6056539431, read 2026-10-08T09:10Z.
Shape: draft → main. The first line is Fixes #11898, and Clause-②: no starts a later line.
ObjectFormDesigner.tsx (+118/−19): a module-private keyboardStep replaces sortableKeyboardCoordinates as the KeyboardSensor's coordinateGetter. It steps through the designer's own layout in reading order (one place per arrow, across groups, an empty group one step like any other, whatever the column count). formCollision's keyboard branch answers the droppable that place reads off the layout.
onDragOver, onDragEnd, dropPlaceIn, formDndAnnouncements.ts and the whole pointer path are unchanged.
A new pin (15 tests: 6 per width, plus 3 controls), one objectui#11871 pin's intermediate sentences updated, and an @object-ui/app-shell patch changeset.
Clause-② re-read: no +export line, and no prop, type member or accepted input changes. no holds.
The four cases, in real Chromium at 1280px and 480px (on the committed blob):
Email ArrowDown reaches the empty New group, and Name ArrowUp reaches it too.
The full-row Notes moves one place up or down.
Name's first ArrowUp moves it.
Phone ArrowDown ×3 announces and commits "Ungrouped, position 1 of 3".
The pointer drags read identically before and after, at both widths.
H3, taken where it was measured: the announcement was right and the commit arithmetic was right for the over it was given. The over was wrong after onDragOver carried the card. Fixing the over (dnd-kit's own multi-container pattern) leaves every handler line unchanged. That is better than the order's suggested commit fix.
The open question (objectui#11871's pin that asserted the first ArrowUp moves nothing): ruled A. The old sentence pinned the defect this card fixes. The pin's own outcome (its final sentence and commit) is unchanged, Chromium agrees, and every other objectui#11871 and objectui#11802 pin is untouched.
Noted, accepted as is: the pending .changeset/11871-… names closestCorners as the keyboard branch. This PR's changeset names its replacement, and the two entries read in order in one release. The 11871 file is outside this claim.
Tests:
Local union on a06a2a4 (with mainf1781be merged in): the whole studio-design/ directory, 136 files and 854 tests, green, plus the seven named suites (60 tests) and the view-cache guard. The type-check and the derived check:* set are green.
Reverse checks:
leg 1 (the base mechanism back): 9 failed, reproducing the pre-fix Chromium sentences;
leg 2 (the keyboard collision only back): 8 failed;
leg 3 (the stock getter only): 15 failed.
The pointer controls stayed green in every leg, and each restore was proven by blob hash.
CI at a06a2a4:Bundle Analysis is red, and it is not this PR's failure. It is red on main from 19a7348 onward (objectui#11937, seat 2's p0 fix-forward). A standing-down note is on the PR. The rest is converging.
Queueing: held until Bundle Analysis is green on main again. Then CI is re-read on a fresh merge ref, and the seat queues it, with the assignee, in one relay write.
Noted, not filed: a lone Ungrouped field carried into a group cannot be stepped back, because the empty Ungrouped bucket hides mid-drag (the existing rule). Escape cancels, and a pointer drag behaves the same. It is in the PR's Acceptance notes.
Next for this seat on the same file: objectui#11924 (a read-only card's drag description) is released for dispatch when this PR lands.
Merged through the merge queue at 2026-10-08T10:53:17Z.
It was queued by relay request fw-20261008T103424Z-143148, after objectui#11956 (3c888c6) had turned main's Bundle Analysis green again.
CI on head a06a2a4: 40 of 44 check runs passed, and the 3 skipped are the expected ones. The one red is Bundle Analysis: the base-branch failure named in ACCEPT 6056593508. That check does not run in the merge group.
check-governed-merges.mjs, re-run on the final file list: not governed, +485/−23.
Content check:
60d11cd is one squash commit on parent 2fec2e0 (objectui#11952, queued just ahead of it). It changes 4 files, +485/−23, the same as the PR.
All 4 files are blob-identical to the reviewed head a06a2a4, and the whole diff's -U0 patch-id matches (63d133db…).
Card: closed completed by the PR's Fixes. pm:dispatched comes off right after this note, and is read back.
On record: in the form designer, on a multi-column canvas, the arrow keys now step one place at a time in reading order.
They reach an empty group.
A keyboard drop commits the place the live region announced.
The pointer path is unchanged.
One objectui#11871 pin's intermediate sentences moved with the fix (ruled A in the ACCEPT).
Released: objectui#11924 (a read-only card's drag description, same file) was held behind this PR on the seat post. It is free for dispatch. It stays pm:queue.
Activity
objectstack-fleet commentedon Oct 8, 2026
Path: write metadata — the form designer by keyboard and screen reader | 缺项 | P3
Triage: first grade,
bug·priority:p3·domain:ui·area:studio·pm:queue. Direction: keyboard moves reach every group on a multi-column canvas, and the drop commits where it was announcedTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T05:58Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
ObjectFormDesigner's keyboard coordinates and collision handling ⇒domain:ui; rationale: Studio's form designer in objectui.Clause-②: no. Patch changeset in objectui.objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round 9
Session:
session_01DrKzdPdyLLBW3qpZ4vtk7zAccount:
huangyiireneBranch:
claude/issue-11898-form-keyboard-gridWorktree:
objectui-issue-11898Domain:
domain:uiSeat:
domain:ui#1File surface (line numbers on
maind52d908, read 2026-10-08T07:22Z), underpackages/app-shell/src/views/studio-design/:ObjectFormDesigner.tsx, in four places only:useSensor(KeyboardSensor, { coordinateGetter: … }), about:547), replaced by a module-private getter wheresortableKeyboardCoordinatescannot reach an empty group or move a full-row field;formCollision(about:90), only for the keyboard branch;onDragOver/onDragEnd(about:584–:640), only where the announced place and the committed place differ;ObjectFormDesigner.keyboardGrid-11898.test.tsx; objectui#11871's and objectui#11802's pins stay green), and.changeset/11898-*.md.⛔ Not on it:
formDndAnnouncements.ts(objectui#11802's sentences). If the announced place itself is wrong, the dev reports it.SortableField's attributes (objectui#11872, landed), and the read-only card'saria-describedby(objectui#11924, queued behind this card).pointerWithin) and its commits. These are unchanged and pinned as controls.metadata-admin/i18n.ts,packages/i18n/**andpackages/components/src/ui/**.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/objectuiover this path: no path-derived mandate; default tier)Clause-②: no
Responsibility:
objectui app-shell Studio form designer: on a multi-column canvas the arrow keys never reach an empty group or move a full-row field, and one keyboard drop commits a place other than the one announced | the platform path is right: dnd-kit's KeyboardSensor takes a coordinateGetter, and the designer owns its layout | every keyboard and screen-reader author arranging a form wider than one columnThread-read:
6053422526Serial constraints cleared:
noneblocking.a476dee), and objectui#11872 landed (fc82a31) through PR objectui#11920, merged 2026-10-08T07:10:07Z. Triage's grade6053422526asked for this card to run after both.area:studioin flight: this seat's objectui#11799 (PR objectui#11928, in CI; other files), and seats 2 and 3's objectui#11858, studio(landing): the Duplicate form judges a package id by its own regex, not the spec's id rule — it refuses 163.com.crm and lets com.acme.my_app through #11855, studio(changes): the Changes panel shows no diff, marks a no-op draft as Update, and publish gives no confirmation #11807, studio(field type picker): 46 field types in one flat Select with no search, icons or descriptions #11793 and studio(interfaces): create flows for list views, dashboards, reports and pages in a writable package (ADR-0084), after #11790's nav item types #11823. No claim namesObjectFormDesigner.tsx, read 2026-10-08T07:22Z.ObjectFormDesigner.tsx. Read 2026-10-08T07:22Z.batch3 working devs, on the maintainer's word in this session's chat (「改为并行派发,batch 3」, 「继续派发」), paced by the shared hourly write budget.Why
Clause-②: no: keyboard geometry and drop arithmetic inside the designer. No export, prop,@object-ui/typesmember or accepted input changes.objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{ "issue": 11898, "status": "done", "branch": "claude/issue-11898-form-keyboard-grid", "pr": "https://github.com/objectstack-ai/objectui/pull/11940", "session": "session_01DrKzdPdyLLBW3qpZ4vtk7z — the session the PM claim comment 6054823047 names (subagent run; the parent session id)", "premise_still_valid": true, "summary": "The Studio form designer's keyboard drag now steps through the designer's own layout in reading order (ArrowDown/ArrowRight one place later, ArrowUp/ArrowLeft one place earlier, across groups, an empty group one step like any other, whatever the column count): a module-private keyboardStep replaces dnd-kit's sortableKeyboardCoordinates as the KeyboardSensor's coordinateGetter, and formCollision's keyboard branch (was closestCorners) answers the droppable that place reads off the layout, which is the dragged card itself once onDragOver has carried it into another group. That fixes all four cases at 1280px and 480px, read in real Chromium and pinned in a new ObjectFormDesigner.keyboardGrid-11898.test.tsx. onDragOver, onDragEnd, dropPlaceIn, formDndAnnouncements.ts and the whole pointer path are unchanged; case 4 is fixed at the over, not in the commit arithmetic (H3 below), and the pointer drags read identically before and after. One dispatch conflict is reported, not silently chosen: objectui#11871's pin 'carries a field up through the empty group…' pinned case 3's no-op first ArrowUp, so its intermediate sentences now read one place per step (its final sentence and commit are unchanged). Draft PR objectui#11940; zero label and assign writes.", "tests": "HEAD the gate union ran on: a06a2a4 (git rev-parse --short HEAD; merge commit of origin/main f1781be into the branch, no rebase). Commits: 2fb6b8b (fix), 8c1b262 (pins + changeset), a06a2a4 (merge). | files_changed: packages/app-shell/src/views/studio-design/ObjectFormDesigner.tsx (+118 −19: drops the closestCorners and sortableKeyboardCoordinates imports, adds type KeyboardCoordinateGetter; formCollision becomes a factory whose keyboard branch answers keyboardOverAt; adds KeyboardSlot, keyboardOverAt, stepSlot, STEP_BY_KEY, keyboardStep; the KeyboardSensor gets coordinateGetter keyboardStep and onActivation that resets the place; DndContext's collisionDetection prop passes the layout); ObjectFormDesigner.keyboardGrid-11898.test.tsx (new, 15 tests: 6 per width × 2 + 3 controls); ObjectFormDesigner.keyboardDrop-11871.test.tsx (+5 −4: the header's instrument line, and one pin's intermediate sentences); .changeset/11898-form-keyboard-grid.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, 29 of 47 projects in scope, tree clean after) ; pnpm exec vitest run over ObjectFormDesigner.dndAnnouncements-11802, .keyboardDrop-11871, .keyboardGrid-11898, .roleDescription-11872, ObjectFormDesigner.test, StudioDesignSurface.formFields, viewCacheInvalidation.guard → 0 → 'Test Files 7 passed (7) / Tests 60 passed (60)' ; pnpm exec vitest run packages/app-shell/src/views/studio-design/ → 0 → 'Test Files 136 passed (136) / Tests 854 passed (854)' ; pnpm --filter @object-ui/app-shell type-check → 0 (script name echoed; tsc -p tsconfig.test.json --listFilesOnly lists both pin files) ; 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 from the diff: check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:phantom-deps, check:unused-deps, check:unreferenced-sources, check:i18n-keys, check:shell-escape-residue → 0 each (OK lines) ; node scripts/check-changeset-presence.mjs → 0 → '3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/11898-form-keyboard-grid.md.' ; check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite → 0 each ; eslint --no-inline-config --format json on the 3 changed source/test files → 0 → 3 files, 0 errors, 1 warning (react-hooks/set-state-in-effect on the untouched draft re-sync effect, present on main). Lint narrowing evidence: population = root eslint.config.js as eslint --print-config resolves it; count = JSON result length 3; invariance = parserOptions empty (no project / projectService), so no type-aware rule can move an untouched file. Every build and test ran through os-verify-lock, slot issue-11898. CI on the PR head: read once after opening, 42 check runs, 37 in_progress, 1 queued, 3 skipped, 1 success — in_progress, not waited on. | H1 (holds in part; real Chromium on d52d908, a probe wrapping the stock getter recorded its direction-filtered candidates with closestCorners values): 1280px Email ArrowDown ranked f:industry 262, g:new_group 371, g:__ungrouped__ 451, f:name 630, so the card won (Ungrouped 2 of 3); Name ArrowUp ranked f:phone 230 first (Contact 1 of 3); 480px Name ArrowUp ranked its own section g:__ungrouped__ 69 first, the over stayed f:name, nothing moved (case 3). Correction for case 2: the full-row Notes' chip is as wide as its section, so Notes' OWN section won ArrowUp (g:contact 163, then f:email 410), the drag was over the section, and the same-group arithmetic moved Notes to the group's END ('Notes moved to Contact, position 6 of 6' from 5 of 6); a Notes that is already last reads as 'does nothing'. Case 3 also depends on geometry: on the eleven-field canvas at 480px the empty group (105) beat the own section (192). | H2 (holds, amended): the reading-order getter reaches every case at both widths; but a getter alone is not enough — reverse-check leg 2 shows that with closestCorners still judging the keyboard drop, the empty section loses on corner distance even with the chip placed on it (1280px Email ArrowDown over 'Contact, position 1 of 2'). | H3 (mechanism holds, fix location differs): at 480px Phone ArrowDown ×3, the third step was over f:name and announced 'Phone is over Ungrouped, position 1 of 3'; onDragOver carried Phone before Name; a probe at a no-op key right before the drop read the over still f:name; onDragEnd's same-group branch (old 0, new 1) committed 'position 2 of 3'. The announcement is right and the arithmetic is right for the over it is given; the over was wrong after the carry. With the keyboard over following the layout (the carried card itself, dnd-kit's own multi-container pattern) the existing arithmetic commits the announced place. onDragEnd and dropPlaceIn untouched; objectui#11802's pointer two-step pin untouched. | BROWSER (real Chromium /opt/pw-browsers/chromium, dev-only Vite page mounting ObjectFormDesigner, Playwright keys, MutationObserver on the live region; harness deleted, never committed; reading on HEAD blob c947f010ae64 of ObjectFormDesigner.tsx), identical at 1280px and 480px: case 1 Email ArrowDown → 'Email is over New group, position 1 of 1.' → 'Email moved to New group, position 1 of 1.', commit new_group [email]; case 1 Name ArrowUp → 'Name moved to New group, position 1 of 1.'; case 2 Notes ArrowUp → 'Notes moved to Contact, position 4 of 6.' (contact phone, email, mobile, notes, fax, website) and ArrowDown → 'Notes moved to Contact, position 6 of 6.'; case 3 Name ArrowUp ×2 → over New group 1 of 1, over Contact 3 of 3 → 'Name moved to Contact, position 3 of 3.'; case 4 Phone ArrowDown ×3 → over Contact 2 of 2, over New group 1 of 1, over Ungrouped 1 of 3 → 'Phone moved to Ungrouped, position 1 of 3.', commit '' [phone, name, industry]. Also eleven fields Phone ArrowDown ×8 and Owner ArrowUp ×8 on a 420px-tall viewport: every place in order, Contact's end slot included, the page scrolls with the chip, the drop commits the last place announced. Pointer control: Name onto the empty group, Phone onto Email, Name onto Email, Name released outside: live-region sequences and commits IDENTICAL before (d52d908 blob e6efb31f593c, swapped in and restored to HEAD blob c947f010ae64 with git diff HEAD empty) and after, at both widths. | REVERSE CHECKS (objectstack scripts/ablation-replace.mjs, WRAP mode nested per anchor, inside the lock, on committed head 8c1b262; tests import ./ObjectFormDesigner from src, no dist leg; every leg restored with blob c947f010ae64 == HEAD and git diff HEAD empty, proven by the tool and again by the wrapper's trap): leg 1, the base mechanism back (sortableKeyboardCoordinates as getter + closestCorners keyboard branch, 4 anchors each 1 → 0, blob → 87182889ee3e) → 9 failed / 13 passed: cases 1–4 red at 1280px and Name ArrowUp, case 3, case 4 red at 480px, each with Chromium's own pre-fix sentence (1280px Email 'over Ungrouped, position 2 of 3'; 480px Phone committed 'position 2 of 3' after 'over Ungrouped, position 1 of 3'), plus objectui#11871's three-step pin; green: the 480px Email and Notes moves Chromium reached before the fix too, the boundary cases, the pointer controls — the stub reproduces the pre-fix Chromium readings. Leg 2, the keyboard collision only back to closestCorners (2 anchors, blob → 972ba441e004) → 8 failed / 14 passed: case 4 red at both widths ('position 2 of 3' at 480px), cases 1–3 red at 1280px. Leg 3, the stock getter only (2 anchors, blob → 9059335fe7b8) → 15 failed / 7 passed: every keyboard move stays put, the one-column control included; pointer controls and boundary cases green. The order's 'restore the commit arithmetic' leg has no target: the arithmetic was not changed (H3); leg 2 is its counterpart. | DEVIATIONS: (a) dispatch conflict, reported in the PR and in open_questions: objectui#11871's pin 'carries a field up through the empty group into a group that has fields' pinned case 3's no-op first ArrowUp; its intermediate sentences now read 'Name is over New group, position 1 of 1.', 'Name is over Contact, position 3 of 3.', 'Name is over Contact, position 2 of 3.' (Chromium reads the same after the fix), final sentence and commit unchanged; its header's instrument line named sortableKeyboardCoordinates and now names the designer's own getter; every other objectui#11871 and objectui#11802 pin is unchanged. (b) H3's suggested route (fix the commit) not taken: the over is fixed instead, so no handler line changed. (c) the place is reset per keyboard pick-up through the KeyboardSensor's own onActivation option, on the same useSensor call as the getter, so onDragStart / onDragCancel are untouched. (d) a first version read the place from a ref during render and added three react-hooks/refs lint warnings; restructured before the first commit (state for the collision, ref for the getter) and the browser reading was re-taken on the committed blob. (e) case 2 runs on my own eleven-field fixture (the card's was not recorded): its pre-fix symptom was 'moved to the group's end', the card's 'does nothing'. (f) declared narrowing: repo-wide pnpm lint and the full test farm are CI's; the studio-design directory (136 files) ran instead. (g) 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 the pair too. (h) the 136-file directory run went through nohup + tail --pid under the lock (held 8m30s), read from its log.", "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#11940 (relay dispatch POST to objectstack-ai/objectstack, run 37754162522 success, read-back 11861 bytes identical); (2) post-stamped.mjs --comment=11898 --repo=objectstack-ai/objectui: this os-dev-report. git push of claude/issue-11898-form-keyboard-grid four times (empty branch probe, fix, pins, merge head) — one push item per the order. Zero label, assignee or PR-body PATCH writes. Reads: gh api REST single card 11898 and its comments, objectui#11871's comments, PR 11940 read-back, one check-runs read; write-pace --status before each relay write.", "open_questions": [ { "question": "The dispatch rules both that case 3 is fixed (the first ArrowUp from a group's first field moves it, at 480px) and that objectui#11871's pins 'stay green as they are'. One objectui#11871 pin ('carries a field up through the empty group into a group that has fields') asserted the first ArrowUp from Name moved nothing — case 3 itself. Accept the updated intermediate sentences in that pin?", "options": [ "A — keep the pin as updated in PR objectui#11940: intermediate sentences one place per step, final sentence and commit unchanged, Chromium agrees", "B — restore the old pin, which requires leaving case 3 unfixed at one column and contradicts the triage direction" ], "recommendation": "A, because the old sentence pinned the defect this card fixes, the pin's own outcome (final sentence and commit) is unchanged, and every other objectui#11871 / objectui#11802 pin stays as it was" } ], "out_of_scope_findings": [ "carrier: PR objectui#11940 (the seat landing it) · noted, not filed — the pending .changeset/11871-form-designer-keyboard-drop.md says 'The designer now uses `pointerWithin` for a pointer drag, unchanged, and `closestCorners` for a keyboard drag.'; after objectui#11940 lands in the same release, the keyboard branch is no longer closestCorners. objectui#11940's changeset names the replacement ('the keyboard branch of the drag's collision detection, `closestCorners` (objectui#11871)… The designer now steps through its own layout'), so the two entries read in order; the 11871 file is outside this claim and was not touched. If the seat wants the earlier entry corrected, it is a one-sentence edit to that pending changeset. Dedupe words: 11871 changeset closestCorners keyboard superseded · pending changeset mechanism sentence", "carrier: 承接者:无 · noted, not filed — a field that is the only one in Ungrouped, carried into a group by a keyboard drag, cannot be stepped back: the designer hides the now-empty Ungrouped bucket mid-drag (the existing 'hide the ungrouped bucket only when it is empty AND groups exist' rule), so ArrowDown has no section to enter; Escape cancels; a pointer drag behaves the same. Read in Chromium (lone fixture, both widths). Recorded in the PR's Acceptance notes." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Review: ACCEPT — PR objectui#11940 (head
a06a2a4) · the open question ruled A · queueing held for a redmaindomain:uiseat 1 ·session_01DrKzdPdyLLBW3qpZ4vtk7z· reviewed against the PR's file list and diff hunks,git merge-tree, its check runs and the dev's report6056539431, read 2026-10-08T09:10Z.main. The first line isFixes #11898, andClause-②: nostarts a later line.6054823047's surface.ObjectFormDesigner.tsx(+118/−19): a module-privatekeyboardStepreplacessortableKeyboardCoordinatesas theKeyboardSensor'scoordinateGetter. It steps through the designer's own layout in reading order (one place per arrow, across groups, an empty group one step like any other, whatever the column count).formCollision's keyboard branch answers the droppable that place reads off the layout.onDragOver,onDragEnd,dropPlaceIn,formDndAnnouncements.tsand the whole pointer path are unchanged.@object-ui/app-shellpatch changeset.+exportline, and no prop, type member or accepted input changes.noholds.overit was given. Theoverwas wrong afteronDragOvercarried the card. Fixing theover(dnd-kit's own multi-container pattern) leaves every handler line unchanged. That is better than the order's suggested commit fix..changeset/11871-…namesclosestCornersas the keyboard branch. This PR's changeset names its replacement, and the two entries read in order in one release. The 11871 file is outside this claim.Local union on
a06a2a4(withmainf1781bemerged in): the wholestudio-design/directory, 136 files and 854 tests, green, plus the seven named suites (60 tests) and the view-cache guard. The type-check and the derivedcheck:*set are green.Reverse checks:
The pointer controls stayed green in every leg, and each restore was proven by blob hash.
a06a2a4:Bundle Analysisis red, and it is not this PR's failure. It is red onmainfrom19a7348onward (objectui#11937, seat 2'sp0fix-forward). A standing-down note is on the PR. The rest is converging.Bundle Analysisis green onmainagain. Then CI is re-read on a fresh merge ref, and the seat queues it, with the assignee, in one relay write.Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Landed: PR objectui#11940 →
main60d11cddomain:uiseat 1 ·session_01DrKzdPdyLLBW3qpZ4vtk7z· verified 2026-10-08T10:57Z.fw-20261008T103424Z-143148, after objectui#11956 (3c888c6) had turnedmain'sBundle Analysisgreen again.a06a2a4: 40 of 44 check runs passed, and the 3 skipped are the expected ones. The one red isBundle Analysis: the base-branch failure named in ACCEPT6056593508. That check does not run in the merge group.check-governed-merges.mjs, re-run on the final file list: not governed, +485/−23.60d11cdis one squash commit on parent2fec2e0(objectui#11952, queued just ahead of it). It changes 4 files, +485/−23, the same as the PR.a06a2a4, and the whole diff's-U0patch-id matches (63d133db…).completedby the PR'sFixes.pm:dispatchedcomes off right after this note, and is read back.pm:queue.Generated by Claude Code
merge: origin/main 70e3d77 into claude/issue-11930-workspace-timezone…
fix(app-shell): form designer arrow keys reach every place on a multi…