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.
Form section headers are not keyboard-operable: a collapsed field group in the record Create/Edit dialog cannot be opened without a mouse #13220
Filing gate ① a user-visible defect with a named landing point and a reproduction. Found by the os-dev working objectui#13130 for the domain:ui seat 3 (session_01GL347th7aWQUnY2k8diSLG), outside that card's scope. ⛔ Not a claim. Grade and lane: triage's.
Reader who acts: objectui triage routes it; the suspected landing is the domain:ui lane owning packages/components/src/renderers/form/form.tsx (the form renderer's SectionDivider; not the Shadcn no-touch zone ui/**).
Measured
reach: named real producer: the showcase showcase_semantic_zoo field group money with collapse: 'collapsed' (examples/app-showcase/src/data/objects/semantic-zoo.object.ts in objectstack).
Actual:SectionDivider draws div role=button aria-expanded with an onClick only: no tabIndex, no key handler. A probe on objectui 7084cff through ModalForm with the collapsed "Money" group: the user-event Tab cycle was the Code input, Cancel, Create, Close, never the section header; Enter on the header left Budget hidden; only a mouse click opened it.
Expected: a section header is a real disclosure control: reachable by Tab, toggled by Enter and Space, as the record detail page's section header already is.
Who reaches it: every keyboard and screen-reader user of a record form whose object declares a collapsed field group, in the Create/Edit dialog and the drawer form.
Dedupe
Semantic search_issues on objectstack-ai/objectui, closed included, "collapsible form section header not keyboard focusable SectionDivider tabindex collapsed field group": 12 hits, all closed (the nearest: objectui#12559, a click on a collapsed header remounting the dialog; objectui#12732, the print page's disclosure chrome). None covers keyboard operation of the header.
Dedupe words: SectionDivider keyboard; collapsible section role=button tabindex; form section header not focusable.
Claim: PM loop round 1 (priority:p2, bug)
Session: session_01GL347th7aWQUnY2k8diSLG
Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-13220-section-header-keyboard
Worktree: objectui-issue-13220
Domain: domain:ui
Seat: domain:ui#3
File surface: packages/components/src/renderers/form/form.tsx (read at objectui main52907d7), SectionDivider: a collapsible section header becomes a real disclosure control, reachable by Tab and toggled by Enter and Space, keeping aria-expanded, as the record detail page's section header already is. Tests beside the form renderer. One .changeset/13220-section-header-keyboard.md (@object-ui/componentspatch). ⛔ Nothing under packages/components/src/ui/** (Shadcn-synced). ⛔ No new export or prop. ⛔ Not the record detail page's header (already a disclosure button). Stop on breach; explain in the report.
Container & model: S, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui over this surface reads no path-derived mandate)
Clause-②: no
Responsibility: the risk is produced by objectui's own form renderer, whose SectionDivider is a div role=button with only an onClick, so a keyboard user can neither reach nor open a collapsed group. The platform path that covers it: none in the form; the record detail page's header is already a button. Who reaches it: every keyboard and screen-reader user of a record form whose object declares a collapsed field group (named producer: the showcase showcase_semantic_zoo group money, collapse: 'collapsed').
Thread-read: none
Serial constraints cleared: open objectui PRs touching packages/components/src/renderers/form/form.tsx: none (the eleven open PRs' file lists were read at this claim).
Why Clause-②: no: a renderer's own header becomes keyboard-operable. No export, prop, schema key or accept set of a published package moves.
os-dev-report
{
"issue": 13220,
"status": "done",
"branch": "claude/issue-13220-section-header-keyboard",
"pr": "#13244",
"session": "session_01GL347th7aWQUnY2k8diSLG (subagent run: the parent seat's id)",
"premise_still_valid": true,
"summary": "SectionDivider (packages/components/src/renderers/form/form.tsx) now makes a collapsible header a keyboard-operable disclosure control. It is a tab stop (tabIndex 0), and an onKeyDown toggles it on Enter and Space. preventDefault keeps Space from scrolling, and a keydown with repeat set does not toggle again. It carries the Shadcn Button focus-ring classes. role=button and aria-expanded are kept, and a non-collapsible header is unchanged. I did not take the suggested route, a native button element. Measured first, it would break readers that live only in plugin-form test files outside the claimed surface: the [role="button"][aria-expanded] selector in collapseResolution-9849, collapsedImpliesCollapsible-9780, headinglessCollapseDiagnostic-9849, modalSectionToggleSuspense-12559 and objectFormSectionMembers-8071; the .border-b span and .border-b p lists in three of those, including the blurb-only row that asserts zero spans; and the row-container click in ungroupedFieldSection-11777. Per 'stop on breach' I chose the route that leaves every reader unedited, and it meets every acceptance point on the card. The record detail page's header was read, not touched. There is no aria-controls because a form group has no container element and collapsed members are not mounted. Mechanism assumptions: (1) confirmed on unfixed main ab4e4d7: the Tab stops were code, note, Create, never the header. (2) Read: DetailSection uses a native button via Radix CollapsibleTrigger. (3) Disproved as stated: the accessible-name comment is about the file widget's control. The real readers are the plugin-form tests above, and all stay correct.",
"tests": "New: packages/components/src/renderers/form/tests/form-section-header-keyboard-13220.test.tsx (6 rows: Tab reaches the collapsed header; a plain heading is not a Tab stop and has no role, tabindex or aria-expanded; Enter opens the group, focus stays, the next Tab lands on its first field, Enter closes it, no submit; Space opens and closes it, no submit; Space default prevented and a repeat keydown does not toggle, one key per reading; control: Enter in a field submits). RED on unfixed main ab4e4d7 plus the new test: 'Tests 4 failed | 1 passed (5)', 'AssertionError: expected [ input …(7), …(2) ] to include div …(3)'. GREEN at cc91445: 'Test Files 1 passed (1)' 'Tests 6 passed (6)'. Ablations at cc91445 through objectstack scripts/ablation-replace.mjs in wrap mode. Each anchor hit x1 -> x0, the change was verified on disk, and the restore was proven with blob == HEAD 47a6185eeae4 and an empty git diff HEAD. Results: drop tabIndex -> 'Tests 4 failed | 2 passed (6)'; drop the repeat guard -> 'Tests 1 failed | 5 passed (6)' (expected 'false' to be 'true'); drop preventDefault -> 'Tests 1 failed | 5 passed (6)' (expected true to be false). No build or dist step was involved: the test imports the renderers by relative source path, and @object-ui/components is aliased to src in vitest.config.mts. Readers and consumers at cc91445: 'pnpm exec vitest run --maxWorkers=2 packages/components/src/renderers/form/ packages/plugin-form/ apps/console/src/components/FormPage.sectionGroup.test.tsx' -> 'Test Files 274 passed (274)' 'Tests 2912 passed | 18 skipped (2930)' 'VERDICT command-exit 0'.",
"gates": "Local, head cc91445, exit codes captured before any pipe. pnpm turbo run build --filter='@object-ui/components^...' --concurrency=2 -> VERDICT command-exit 0 (7 tasks successful). pnpm --filter @object-ui/components type-check -> exit 0; the script echoed 'tsc --noEmit && tsc -p tsconfig.test.json', and --listFilesOnly on tsconfig.test.json lists the new test file once. node scripts/check-changeset-presence.mjs -> exit 0 ('2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'). pnpm check:new-line-citations -> exit 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)'). pnpm check:control-bytes -> exit 0 ('OK (scanned 9858 tracked text file(s)'). check-governed-queue-guard --test over the 3 paths -> NOT GOVERNED. No i18n or doc-fence gates apply (no locale key, no README or content/docs edit). No --ran reconciliation: dispatch-gates.mjs is objectstack-only, so the objectui list was hand-derived from the dispatch. CI on PR head cc91445 as read at report time: 35 check-runs. Completed success: Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report, label. Skipped: Test (coverage), Test (coverage shard), dependabot. in_progress (28): Lint, Type Check, Repo Gates, Test shards 1-16, Test (dist pins), Build & E2E, Build Docs, Bundle Analysis, Changeset Declaration, Changeset Claim Re-read, Governed Surface Queue Guard, Line Citation Gate, Live E2E (informational). NOT MEASURED for CI conclusions: not converged at report time, and I did not wait for them, per the contract.",
"files_changed": "packages/components/src/renderers/form/form.tsx (+19 -1); packages/components/src/renderers/form/tests/form-section-header-keyboard-13220.test.tsx (+211, new); .changeset/13220-section-header-keyboard.md (+9, new; @object-ui/components patch). Total 3 files, +239 -1.",
"line_budget": "n/a: no skills/** or ratcheted ledger file in the diff",
"deviations": "(1) Route: I did not take the suggested native-button route (it was optional). The reason is the file-surface breach measured above; details are in summary and in the PR body. (2) The first readers run passed the harness's foreground limit and was moved to the background by the harness. I waited for it in the foreground with tail --pid and read its log; its verdict line is quoted in tests. (3) The first pr_create dry-run was refused for the actions-file shape (an object, not an array), with zero writes. The corrected file was sent once. (4) Commit trailers use objectui AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude) instead of the harness reminder's model-named line, because AGENTS.md takes precedence. No environment variable value was printed.",
"mcp_calls": "0",
"api_writes": "2 relay strokes, executed as objectstack-fleet[bot]. (a) pr_create: POST /repos/objectstack-ai/objectstack/dispatches, then the relay sent POST /repos/objectstack-ai/objectui/pulls (draft, #13244) and POST /repos//issues/13244/assignees (os-support-ai). Read-back: 8326 bytes sent, 8326 stored, identical, and the assignee is listed. (b) This os-dev-report comment: post-stamped --comment=13220 --repo=objectstack-ai/objectui, i.e. POST /repos//issues/13220/comments. git push (2 pushes plus the empty-branch probe) is not REST. No labels were written; 'package: components' and 'tests' on the PR come from the path labeler.",
"open_questions": [
{
"question": "Should the form section header later become a native button element, matching the record detail page's element and not only its behaviour?",
"options": [
"A: keep this PR's div with role=button, tabIndex and the key handler. It is complete for the card, no reader is edited, and the accessible name stays label plus blurb, as today.",
"B: a follow-up card converts it to a native button with type=button. That card must edit the plugin-form readers listed in summary, and it would make the accessible name the label alone, as on the detail page."
],
"recommendation": "A for this card. Consider B only if a screen-reader user's report pulls for the label-only name; nothing measured here does."
}
],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed: a collapsible header's accessible name includes its blurb, because a div with role=button takes its name from all content. This is pre-existing and unchanged by this PR; the native-button route (open question B) would make the name the label alone."
],
"browser_reading": "Not owed and not run. Where to see it: showcase object showcase_semantic_zoo, field group 'money' (collapse: 'collapsed'), in its Create dialog. Press Tab from the Code field. Header selector: form div.col-span-full[role="button"][aria-expanded] with text 'Money'."
}
ACCEPT → PR #13244 comment 6290658834 · domain:ui seat 3, session_01GL347th7aWQUnY2k8diSLG · 2026-10-20T12:18Z · head 76e1ccb (one rework round: a lint error in the PR's own test) · live reading in the record · landing through the merge queue. Derived: objectui#13246 (first-opening focus loss, blocked on this card).
State: the card closed completed by the PR's Fixes; pm:dispatched is removed in this pass. The live keyboard reading is in the ACCEPT, 6290658834. objectui#13246 (the first-opening focus loss), pm:blocked on this card, is unblocked by this landing and taken next by this seat.
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 1 (
priority:p2,bug)Session:
session_01GL347th7aWQUnY2k8diSLGAccount:
os-support-ai(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-13220-section-header-keyboardWorktree:
objectui-issue-13220Domain:
domain:uiSeat:
domain:ui#3File surface:
packages/components/src/renderers/form/form.tsx(read at objectuimain52907d7),SectionDivider: a collapsible section header becomes a real disclosure control, reachable by Tab and toggled by Enter and Space, keepingaria-expanded, as the record detail page's section header already is. Tests beside the form renderer. One.changeset/13220-section-header-keyboard.md(@object-ui/componentspatch). ⛔ Nothing underpackages/components/src/ui/**(Shadcn-synced). ⛔ No new export or prop. ⛔ Not the record detail page's header (already a disclosure button). Stop on breach; explain in the report.Container & model:
S,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuiover this surface reads no path-derived mandate)Clause-②: no
Responsibility: the risk is produced by objectui's own form renderer, whose
SectionDivideris adiv role=buttonwith only anonClick, so a keyboard user can neither reach nor open a collapsed group. The platform path that covers it: none in the form; the record detail page's header is already a button. Who reaches it: every keyboard and screen-reader user of a record form whose object declares a collapsed field group (named producer: the showcaseshowcase_semantic_zoogroupmoney,collapse: 'collapsed').Thread-read: none
Serial constraints cleared: open objectui PRs touching
packages/components/src/renderers/form/form.tsx: none (the eleven open PRs' file lists were read at this claim).Why
Clause-②: no: a renderer's own header becomes keyboard-operable. No export, prop, schema key or accept set of a published package moves.Claimed at: 2026-10-20T11:13Z
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 13220,
"status": "done",
"branch": "claude/issue-13220-section-header-keyboard",
"pr": "#13244",
"session": "session_01GL347th7aWQUnY2k8diSLG (subagent run: the parent seat's id)",
"premise_still_valid": true,
"summary": "SectionDivider (packages/components/src/renderers/form/form.tsx) now makes a collapsible header a keyboard-operable disclosure control. It is a tab stop (tabIndex 0), and an onKeyDown toggles it on Enter and Space. preventDefault keeps Space from scrolling, and a keydown with repeat set does not toggle again. It carries the Shadcn Button focus-ring classes. role=button and aria-expanded are kept, and a non-collapsible header is unchanged. I did not take the suggested route, a native button element. Measured first, it would break readers that live only in plugin-form test files outside the claimed surface: the [role="button"][aria-expanded] selector in collapseResolution-9849, collapsedImpliesCollapsible-9780, headinglessCollapseDiagnostic-9849, modalSectionToggleSuspense-12559 and objectFormSectionMembers-8071; the .border-b span and .border-b p lists in three of those, including the blurb-only row that asserts zero spans; and the row-container click in ungroupedFieldSection-11777. Per 'stop on breach' I chose the route that leaves every reader unedited, and it meets every acceptance point on the card. The record detail page's header was read, not touched. There is no aria-controls because a form group has no container element and collapsed members are not mounted. Mechanism assumptions: (1) confirmed on unfixed main ab4e4d7: the Tab stops were code, note, Create, never the header. (2) Read: DetailSection uses a native button via Radix CollapsibleTrigger. (3) Disproved as stated: the accessible-name comment is about the file widget's control. The real readers are the plugin-form tests above, and all stay correct.",
"tests": "New: packages/components/src/renderers/form/tests/form-section-header-keyboard-13220.test.tsx (6 rows: Tab reaches the collapsed header; a plain heading is not a Tab stop and has no role, tabindex or aria-expanded; Enter opens the group, focus stays, the next Tab lands on its first field, Enter closes it, no submit; Space opens and closes it, no submit; Space default prevented and a repeat keydown does not toggle, one key per reading; control: Enter in a field submits). RED on unfixed main ab4e4d7 plus the new test: 'Tests 4 failed | 1 passed (5)', 'AssertionError: expected [ input …(7), …(2) ] to include div …(3)'. GREEN at cc91445: 'Test Files 1 passed (1)' 'Tests 6 passed (6)'. Ablations at cc91445 through objectstack scripts/ablation-replace.mjs in wrap mode. Each anchor hit x1 -> x0, the change was verified on disk, and the restore was proven with blob == HEAD 47a6185eeae4 and an empty git diff HEAD. Results: drop tabIndex -> 'Tests 4 failed | 2 passed (6)'; drop the repeat guard -> 'Tests 1 failed | 5 passed (6)' (expected 'false' to be 'true'); drop preventDefault -> 'Tests 1 failed | 5 passed (6)' (expected true to be false). No build or dist step was involved: the test imports the renderers by relative source path, and @object-ui/components is aliased to src in vitest.config.mts. Readers and consumers at cc91445: 'pnpm exec vitest run --maxWorkers=2 packages/components/src/renderers/form/ packages/plugin-form/ apps/console/src/components/FormPage.sectionGroup.test.tsx' -> 'Test Files 274 passed (274)' 'Tests 2912 passed | 18 skipped (2930)' 'VERDICT command-exit 0'.",
"gates": "Local, head cc91445, exit codes captured before any pipe. pnpm turbo run build --filter='@object-ui/components^...' --concurrency=2 -> VERDICT command-exit 0 (7 tasks successful). pnpm --filter @object-ui/components type-check -> exit 0; the script echoed 'tsc --noEmit && tsc -p tsconfig.test.json', and --listFilesOnly on tsconfig.test.json lists the new test file once. node scripts/check-changeset-presence.mjs -> exit 0 ('2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'). pnpm check:new-line-citations -> exit 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)'). pnpm check:control-bytes -> exit 0 ('OK (scanned 9858 tracked text file(s)'). check-governed-queue-guard --test over the 3 paths -> NOT GOVERNED. No i18n or doc-fence gates apply (no locale key, no README or content/docs edit). No --ran reconciliation: dispatch-gates.mjs is objectstack-only, so the objectui list was hand-derived from the dispatch. CI on PR head cc91445 as read at report time: 35 check-runs. Completed success: Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report, label. Skipped: Test (coverage), Test (coverage shard), dependabot. in_progress (28): Lint, Type Check, Repo Gates, Test shards 1-16, Test (dist pins), Build & E2E, Build Docs, Bundle Analysis, Changeset Declaration, Changeset Claim Re-read, Governed Surface Queue Guard, Line Citation Gate, Live E2E (informational). NOT MEASURED for CI conclusions: not converged at report time, and I did not wait for them, per the contract.",
"files_changed": "packages/components/src/renderers/form/form.tsx (+19 -1); packages/components/src/renderers/form/tests/form-section-header-keyboard-13220.test.tsx (+211, new); .changeset/13220-section-header-keyboard.md (+9, new; @object-ui/components patch). Total 3 files, +239 -1.",
"line_budget": "n/a: no skills/** or ratcheted ledger file in the diff",
"deviations": "(1) Route: I did not take the suggested native-button route (it was optional). The reason is the file-surface breach measured above; details are in summary and in the PR body. (2) The first readers run passed the harness's foreground limit and was moved to the background by the harness. I waited for it in the foreground with tail --pid and read its log; its verdict line is quoted in tests. (3) The first pr_create dry-run was refused for the actions-file shape (an object, not an array), with zero writes. The corrected file was sent once. (4) Commit trailers use objectui AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude) instead of the harness reminder's model-named line, because AGENTS.md takes precedence. No environment variable value was printed.",
"mcp_calls": "0",
"api_writes": "2 relay strokes, executed as objectstack-fleet[bot]. (a) pr_create: POST /repos/objectstack-ai/objectstack/dispatches, then the relay sent POST /repos/objectstack-ai/objectui/pulls (draft, #13244) and POST /repos//issues/13244/assignees (os-support-ai). Read-back: 8326 bytes sent, 8326 stored, identical, and the assignee is listed. (b) This os-dev-report comment: post-stamped --comment=13220 --repo=objectstack-ai/objectui, i.e. POST /repos//issues/13220/comments. git push (2 pushes plus the empty-branch probe) is not REST. No labels were written; 'package: components' and 'tests' on the PR come from the path labeler.",
"open_questions": [
{
"question": "Should the form section header later become a native button element, matching the record detail page's element and not only its behaviour?",
"options": [
"A: keep this PR's div with role=button, tabIndex and the key handler. It is complete for the card, no reader is edited, and the accessible name stays label plus blurb, as today.",
"B: a follow-up card converts it to a native button with type=button. That card must edit the plugin-form readers listed in summary, and it would make the accessible name the label alone, as on the detail page."
],
"recommendation": "A for this card. Consider B only if a screen-reader user's report pulls for the label-only name; nothing measured here does."
}
],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed: a collapsible header's accessible name includes its blurb, because a div with role=button takes its name from all content. This is pre-existing and unchanged by this PR; the native-button route (open question B) would make the name the label alone."
],
"browser_reading": "Not owed and not run. Where to see it: showcase object showcase_semantic_zoo, field group 'money' (collapse: 'collapsed'), in its Create dialog. Press Tab from the Code field. Header selector: form div.col-span-full[role="button"][aria-expanded] with text 'Money'."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #13244 comment 6290658834 ·
domain:uiseat 3,session_01GL347th7aWQUnY2k8diSLG· 2026-10-20T12:18Z · head76e1ccb(one rework round: a lint error in the PR's own test) · live reading in the record · landing through the merge queue. Derived: objectui#13246 (first-opening focus loss, blocked on this card).Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed ·
domain:uiseat 3,session_01GL347th7aWQUnY2k8diSLG· 2026-10-20T12:33Z4b92c5aonmaingh-readonly-queue/main/pr-13244-*git ls-remote: 0 refs)origin/main4b92c5a,packages/components/src/renderers/form/form.tsxonKeyDown={collapsible ? handleKeyDown : undefined}(1,SectionDivider)State: the card closed
completedby the PR'sFixes;pm:dispatchedis removed in this pass. The live keyboard reading is in the ACCEPT,6290658834. objectui#13246 (the first-opening focus loss),pm:blockedon this card, is unblocked by this landing and taken next by this seat.Generated by Claude Code
fix(components): a collapsible form section header is keyboard-operab…