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.

Studio: controls without an accessible name (records toolbar, flow nodes, sheets, landing cards) #12510

Closed

Description

@objectstack-fleet

Source

Read from the accessibility tree on objectui main d4bc81eab, 2026-10-15, Studio home and the four pillars with HotCRM and a writable package open. Buttons and links whose accessible name is empty:

  • Records toolbar (Data pillar, Records tab): Hide fields, Filter, Group, Sort and the "+" that adds a field all read as button with no name, although their text is visible. Refresh, Search and Add record have names.
  • Flow canvas: the Start and End node buttons (the nodes are buttons) have no name; "Insert node here" and "Add connected node" have one.
  • Pending changes sheet (Changes and Publish): the per-item expander and the sheet's close button have no name.
  • Access rail: the pinned "Record sharing (OWD)" row is a button with no name.
  • Explain access sheet: the User picker (combobox) and the close button have no name.
  • Studio home (BuilderLanding.tsx): the recently-viewed package links, the "My packages" card, the installed-package cards, the "Organization flows" card and the avatar button in the top bar have no name; the New package dialog's close button has none.
  • Data rail object rows read as unnamed buttons in the same tree; Interfaces rows read "object · st_ticket". Verify with the same tool before changing: the row's visible label may already name it and the reading may be the tool's.

Target

  1. Every control above has an accessible name: a visible label names its control (no aria-hidden on the only text), icon-only buttons carry aria-label with the same words their tooltip shows, cards that act as links or buttons are named by their title.
  2. The name is the label the user sees (objectui#11862: label first), never the api name alone.

File surface

packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (toolbar host, flow canvas node buttons if they are rendered here, Changes sheet, Access rail), StudioRail.tsx, StudioInspector.tsx, BuilderLanding.tsx, packages/app-shell/src/views/metadata-admin/PackageFormDialog.tsx, packages/app-shell/src/views/metadata-admin/previews/FlowCanvas.tsx (node buttons), packages/plugin-list/src/ListView.tsx if the toolbar buttons are its; tests beside each.

Acceptance

  1. Tests query each control by role and name: getByRole('button', { name: 'Hide fields' }), … 'Filter', … 'Group', … 'Sort', … 'Add field'; getByRole('button', { name: /Start/ }) on the canvas; getByRole('button', { name: /close/i }) in the sheet; getByRole('link', { name: /HotCRM/ }) on the landing.
  2. The accessibility tree of each surface, pasted in the PR, shows no unnamed button, link or combobox.
  3. pnpm --filter @object-ui/app-shell test green.

Constraints

  • Visual output unchanged; names only.
  • Existing data-testids stay.

Filed by the epic PM session session_cdb7a717-def7-4e71-9988-f089fbaa18ff from the 2026-10-15 Studio test pass: objectui main d4bc81eab, HotCRM 63db83e2, the vite console on :5180 against objectstack serve --dev on :4001, viewport 1482 × 1119, dark theme with a light-theme check.

Evidence

Screenshots from the test pass, kept on the temporary branch claude/studio-test-pass-evidence-20261015 of objectstack-ai/objectui (commit 72115588c, never merged: the maintainer ruled that screenshots do not enter main, and asked for a temporary branch referenced from the card). The branch's docs/design/studio-redesign/findings/2026-10-15/README.md maps every file to its card. Pane captures, 800 × 604 of a 1482 × 1119 viewport, dark unless named light.

records toolbar
docs/design/studio-redesign/findings/2026-10-15/12510-records-toolbar.jpg — records toolbar.

Parent: #12446

Body refreshed 2026-10-15T15:21Z by the epic PM session: the Evidence section, first added at 2026-10-15T14:42Z, now names the temporary branch the screenshots live on instead of a pull request to main.

Activity

  1. 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 15, 2026
  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Folded into this family card · domain:ui seat 1 · session_01JZnppgHRp4sREcouhkQYDB · 2026-10-15T18:25Z

    One more location for this card's enumeration, from objectui#12531's dev report (out_of_scope_findings[0], PR objectui#12558).

    The action editor's icon picker has no accessible name. The editor is ActionDefaultInspector.tsx, mounted by Studio's object actions and Setup's action designer.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (domain:ui lane queue; an objectui#12446 test-pass card)
    Session: session_01SJn1GEkrbbBQkhGC26wEDw
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12510-studio-a11y-names
    Worktree: objectui-issue-12510 (in the cloud session's own container)
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (the records toolbar host, the pending-changes sheet's expander and close, the Access rail's pinned OWD row, the Explain access sheet), StudioRail.tsx, StudioInspector.tsx, BuilderLanding.tsx, metadata-admin/PackageFormDialog.tsx (its close button), metadata-admin/previews/FlowCanvas.tsx (the Start and End node buttons' names only), metadata-admin/inspectors/ActionDefaultInspector.tsx (the icon picker's label, folded in by 6199308787), packages/plugin-list/src/ListView.tsx (only if the toolbar buttons are its: aria-label only), metadata-admin/i18n.ts (new keys only), new tests beside each, one .changeset/12510-*.md (stop on breach; explain in the report)
    Container & model: M, mode:cloud, model: opus (cloud on the maintainer's stance 「可以的话,ui 后续开发尽量用云 session,并且要求截图给我看」; lane scope on 「你也可以接ui车道的卡」; batch 5 on 「上限提到 5,提速」, this session's chat, 2026-10-15)
    Clause-②: no
    Responsibility: objectui's Studio renders buttons, links and a combobox with no accessible name (records toolbar, flow Start/End nodes, the pending-changes and Explain access sheets, the Access OWD row, the Studio home cards, the action icon picker) | none | every Studio author on a screen reader or voice control, and every test that queries by role
    Thread-read: 6199308787
    Serial constraints cleared: objectui#12453 (read-only posture) landed as PR objectui#12565 (2a69285) and objectui#12519 as PR objectui#12566 (73a3eb2), so the shell and pillar regions are free. Open elsewhere: PR objectui#12561 (objectui#12528, another seat) edits plugin-list/src/ListView.tsx; if the toolbar names belong there, this card adds aria-labels only and merges origin/main after that PR lands. objectui#12513 and objectui#12517 (BuilderLanding.tsx, the pending-changes sheet) stay queued behind this card. In flight on StudioDesignSurface.tsx from this seat at this claim: objectui#12508 round 2 (the InterfacesPillar canvas mount and studio-canvas-preview.tsx only), and the sibling cards of this wave: objectui#12510 (accessible names across the shell), objectui#12514 (the Automations and Access empty branches), objectui#12515 (the Form tab preview branch) and objectui#12522 (the InterfacesPillar page branch). They are in disjoint regions; any textual overlap is resolved by merging origin/main.

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12510,
    "status": "done",
    "branch": "claude/issue-12510-studio-a11y-names",
    "pr": null,
    "session": "session_014jSTWJ1PRVj63noYkF5KNv — this run's harness-stamped id (Claude-Session trailer on every commit)",
    "premise_still_valid": false,
    "summary": "The card's premise is mostly wrong, and PM mechanism assumption 4 holds. On current main (572bbce) I read the real accessibility tree with CDP Accessibility.getFullAXTree, the source chrome-devtools-mcp reads, and with Playwright ariaSnapshot. Both name every control the card lists: Hide fields / Filter / Group / Sort; the Start and End nodes ('Start START crm_campaign', 'End END'); the Changes row expander ('Ticket a11y_ticket New') and the sheet's 'Close'; 'Record sharing (OWD)'; the Explain user picker ('Me (calling principal)') and 'Close'; the recent links, package cards and Organization flows card; the avatar ('DA'); and the New package dialog's 'Close'. The markup behind these readings is the same at d4bc81e (BuilderLanding differs only in a comment and an export), so the card's 'no name' readings came from the tool, not the tree. The one real defect is the folded location (6199308787): the action editor's icon picker is a combobox with an empty name. It is fixed with useId, the Label's htmlFor and the widget's id, the same way PR objectui#12558 did it. Acceptance 1 is met by role-and-name pins, one per surface, so a restyle that drops a name turns a test red. On the dispatcher's 21:28Z note about PR objectui#12575: this branch makes no change to ListView.tsx. That PR names Filter, Export and Add record for narrow screens (aria-label). This branch adds only a test file, ListView.toolbarNames-12510, which checks Hide fields, Filter, Group and Sort by role and name at desktop width with no filter applied. With no filter applied, #12575's Filter name is 'Filter', so the check still passes after it lands. Below the sm breakpoint, Hide fields, Group and Sort are hidden as whole buttons (hidden sm:inline-flex), so they never show up as unnamed buttons; Filter is the one that stays visible as an icon, and #12575 handles it. The two test files are separate, so there is no textual conflict.",
    "tests": "HEAD e69804c. (1) Full pnpm --filter @object-ui/app-shell test, foreground-awaited with tail --pid: EXIT=0, 'Test Files 1416 passed | 1 skipped (1417)', 'Tests 13471 passed | 9 skipped (13480)', duration 3498s. It ran on 52c7ff6, whose source equals e69804c except one unused import dropped from a test, a change made mid-run. (2) Targeted at e69804c: pnpm exec vitest run --maxWorkers=2 over the 7 new pins plus ObjectDefaultInspector.iconPicker-12531 and ActionDefaultInspector.ariaRetired-10929: EXIT=0, 'Test Files 9 passed (9)', 'Tests 18 passed (18)'. (3) Ablation of the one changed behavior. With a trap restore, I removed id={iconFieldId} from the IconPickerWidget mount (anchor asserted count==1; grep of the injected text afterwards was 0). ActionDefaultInspector.iconPickerName-12510 then went red: MUTANT_EXIT=1, 2 failed, 'Unable to find an accessible element with the role "combobox" and name "Icon"'. Restored with git checkout HEAD --; git diff HEAD was 0 bytes and the hash-object matched the HEAD blob. No build/dist leg: the test imports the inspector from src. (4) The other pins name controls that already had names, so they guard against regression and do not pin changed behavior; they were not ablated.",
    "gates": "Local, exit codes captured before any pipe: pnpm check:i18n-keys EXIT=0; pnpm check:i18n-drift EXIT=0 ('No en value changed in this range'); node scripts/check-changeset-presence.mjs EXIT=0 ('8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'); eslint on every touched .ts/.tsx EXIT=0 (the first run was EXIT=1 on an unused within import, fixed in e69804c); pnpm --filter @object-ui/app-shell --filter @object-ui/plugin-list type-check EXIT=0 (both run tsc --noEmit plus tsconfig.test.json, so the new tests are covered; the first run was EXIT=2 because the fresh worktree had no dependency dist, cleared by pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-list^...' build EXIT=0); pnpm --filter @object-ui/app-shell --filter @object-ui/plugin-list build EXIT=0 (dist completeness: 1104 emitted files verified). Control-byte self-scan over the diff: no hits. Not applicable: check:i18n-designer-parity and check:i18n-dead-keys (no keys added or retired). Not run, because objectui does not have them: scripts/pm/os-verify-lock.sh, scripts/pm/dispatch-gates.mjs, scripts/ablation-replace.mjs, scripts/ablation-dist-preflight.mjs, scripts/check-nul-bytes.mjs. CI check-runs: NOT MEASURED, no PR open yet.",
    "mcp_calls": "9 — read only: github issue_read x3 (get #12510, get_comments #12510, get #12446); github pull_request_read x1 (get_diff #12575, for the dispatcher's coordination note); claude-code-remote add_repo x2 (read access; both answered read_available, nothing attached); chrome-devtools new_page x1 (failed: no Chrome as root); ReadNotifications x1 (the harness queue, not GitHub); ToolSearch schema loads, which are not MCP calls. No MCP write tool was called, and no send_message reply was sent: the answer to the coordination note is in this report.",
    "api_writes": "0 REST writes. git push x3 to origin claude/issue-12510-studio-a11y-names (the empty branch, then 52c7ff6, then e69804c). Two unauthenticated GETs to api.github.com read the epic ruling comments 6188626991 and 6192878850. Against the local hotcrm backend only: POST /api/v1/packages (com.example.a11y) and one object draft (a11y_ticket) through the Studio UI, both on a --fresh temp DB that was deleted when the backend stopped.",
    "open_questions": [
    {
    "question": "The Explain access sheet's user picker is named by its value alone ('Me (calling principal)'). The visible 'User' label beside it, AccessExplainPanel.tsx line ~405, is a label element bound to nothing, while the Object, Operation and Record fields use htmlFor. The control the card names lives in AccessExplainPanel.tsx, which is outside the claim's file surface (the claim lists StudioDesignSurface.tsx for it). Bind the label?",
    "options": [
    "A — Extend the claim surface to metadata-admin/AccessExplainPanel.tsx and land a follow-up commit on this branch. Give the label an id (useId), the value span an id, and the button aria-labelledby with both ids, so the name reads 'User Me (calling principal)'. No visual change, about 5 lines; the existing pin AccessExplainPanel.accessibleNames-12510 already matches on the value substring, so it stays green.",
    "B — Leave it as is. The picker has a name, the card's acceptance 2 holds, and the label binding is polish."
    ],
    "recommendation": "A. Real need: the card's target 1 says 'a visible label names its control', and a screen-reader user hears a value with no field name. Long term: it matches the Object, Operation and Record fields beside it, one convention. AI-error axis: neutral. No sprawl: about 5 lines in a file the card already names as its surface. I stopped short only because of the order's 'stop on breach' rule and because I cannot verify clause ③ of the in-place exemption (that no other claim holds the file) without scanning open work, which os-dev forbids a dev to do."
    },
    {
    "question": "This report and the os-dev contract disagree on delivery. os-dev.md says to open the draft PR and post the os-dev-report comment. The order says not to, because the fleet-write relay is refused with 403 from this container.",
    "options": [
    "A — Follow the order's degraded channel: pr null, the PM opens the PR from pr_body_draft and posts this report",
    "B — Follow the file"
    ],
    "recommendation": "A, already done. The file's write path goes through the fleet-write relay, which the order says this proxy refuses, so the file's rule cannot be carried out here. I made no attempt to write through MCP."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: public door, Studio Data pillar › HotCRM › Account › Records tab (and the runtime list page that shares the renderer, per decision D). The live tree (CDP getFullAXTree) lists 9 'checkbox' nodes with an empty name, one per row of the is_active column. evidence: packages/fields/src/index.tsx:1014, the boolean cell renderer draws Checkbox checked={value} disabled with no aria-label or labelledby; the fix would name it by the field label. The card's acceptance 2 covers only button, link and combobox, and packages/fields is outside this claim · dedupe words: boolean cell checkbox accessible name; fields boolean renderer aria-label; disabled grid checkbox unnamed; is_active column screen reader",
    "carrier: this card's PM (open_questions[0]) · the Explain sheet's 'User' label is not bound to its picker (AccessExplainPanel.tsx) · noted, not filed",
    "carrier: none · the top-bar avatar is named 'DA' (the user's initials), not a word like 'Account menu'. It is named, so it is not this card's defect; it lives in the app shell header, outside this surface · noted in Acceptance notes only"
    ],
    "deviations": [
    "Worktree: deleting the stale local branch was refused by the auto-mode classifier (Git Destructive). The shared checkout already held claude/issue-12510-studio-a11y-names at 572bbce with no commits of its own, so I detached the shared checkout's HEAD (it stays detached at 572bbce) and ran git worktree add ../objectui-issue-12510 claude/issue-12510-studio-a11y-names. The result is the same base as the ordered command.",
    "No @object-ui/plugin-list entry in the changeset. That package gets one test file only, nothing it ships changes, and ListView.tsx is untouched. The order's 'add plugin-list if you touch that package' is read as touching its source. The changeset gate passes with one app-shell patch changeset.",
    "objectui has no os-verify-lock / ablation-replace / ablation-dist-preflight / check-nul-bytes / dispatch-gates scripts. Heavy runs went straight to the shell (this container runs one agent). The ablation used a python replace with an asserted anchor count, a grep afterwards, a trap restore, and a hash-object match against the HEAD blob.",
    "The '+' Add field grid header was not measured live: the writable test object had no records, so the Records tab showed its empty state. By code, packages/components data-table.tsx:3103 sets aria-label from addColumnLabel ('Add field'), and the existing DataPillar.layoutBadge.test.tsx already queries getAllByRole('button', { name: /Add field/i }), which covers acceptance 1 for it.",
    "Screenshots: none taken. The visual output is unchanged by rule, and the one source change only adds htmlFor/id; the evidence is the tree text below.",
    "The worktree and its node_modules are kept (no PR exists yet to trigger the cleanup step). Every commit is pushed and git status is clean. The backend (:4071) and console (:5191) I started are stopped; lsof shows nothing on either port."
    ],
    "files_changed": [
    "packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx (useId + Label htmlFor + IconPickerWidget id)",
    "packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.iconPickerName-12510.test.tsx (new)",
    "packages/app-shell/src/views/studio-design/BuilderLanding.accessibleNames-12510.test.tsx (new)",
    "packages/app-shell/src/views/metadata-admin/previews/flow-canvas-parts.nodeName-12510.test.tsx (new)",
    "packages/app-shell/src/preview/tests/DraftChangesPanel.accessibleNames-12510.test.tsx (new)",
    "packages/app-shell/src/views/metadata-admin/AccessExplainPanel.accessibleNames-12510.test.tsx (new, test only)",
    "packages/app-shell/src/views/metadata-admin/PackageFormDialog.closeName-12510.test.tsx (new)",
    "packages/plugin-list/src/tests/ListView.toolbarNames-12510.test.tsx (new, test only; ListView.tsx untouched)",
    ".changeset/12510-studio-action-icon-picker-name.md"
    ],
    "screenshots": [],
    "line_budget": "n/a",
    "pr_title_draft": "fix(app-shell): name the action editor's icon picker and pin the Studio controls' accessible names (objectui#12510)",
    "pr_body_draft": "(elided by the seat: opened verbatim as the body of PR #12580, read back identical)"
    }

    Relayed verbatim by seat domain:ui#4 (session session_01SJn1GEkrbbBQkhGC26wEDw) from the cloud session's final message, the degraded write channel. The seat opened the draft PR #12580 from pr_title_draft and pr_body_draft, with assignee os-sales. "pr": null above is the session's own value at report time.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report (follow-up delta)
    {
    "head": "6451ee1",
    "files_changed": [
    "packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx (two React.useId(): the User label gets userLabelId, the value span userValueId, and the picker button aria-labelledby with both; no visual change, testids and i18n keys unchanged)",
    "packages/app-shell/src/views/metadata-admin/AccessExplainPanel.accessibleNames-12510.test.tsx (the pin now asserts the full name: the User label, then the principal)",
    ".changeset/12510-studio-action-icon-picker-name.md (prose now covers both pickers; still one app-shell patch)"
    ],
    "tests": "At ac92f6f, which differs from 6451ee1 only in the changeset .md: pnpm exec vitest run --maxWorkers=2 over AccessExplainPanel.test, AccessExplainPanel.principalPerson-11862, AccessExplainPanel.sharedSelect-11865, AccessExplainPanel.accessibleNames-12510 and the 7 pins of this card (ListView.toolbarNames-12510, PackageFormDialog.closeName-12510, DraftChangesPanel.accessibleNames-12510, flow-canvas-parts.nodeName-12510, BuilderLanding.accessibleNames-12510, ActionDefaultInspector.iconPickerName-12510; the AccessExplainPanel pin is listed above): EXIT=0, 'Test Files 10 passed (10)', 'Tests 39 passed (39)'. The only test that names the picker's principal is the 12510 pin; StudioDesignSurface suites that import AccessExplainPanel do not query its picker.",
    "gates": "eslint on AccessExplainPanel.tsx and its 12510 test: EXIT=0 (7 warnings, all on lines this diff leaves alone: no-explicit-any and set-state-in-effect at 257, 267, 273, 306, 803, 825). pnpm --filter @object-ui/app-shell type-check: EXIT=0 (tsc --noEmit plus tsconfig.test.json, so the test is covered). node scripts/check-changeset-presence.mjs: EXIT=0. Control-byte self-scan of the two touched source files: no hits. git merge-tree --write-tree HEAD origin/main: no conflict, so no merge was needed (merge only on conflict, as instructed). Push e69804c..6451ee1, fast-forward. Full app-shell suite not re-run, as instructed (CI runs it).",
    "ablation": "On ac92f6f, with a trap restore on the absolute path: removed the line aria-labelledby={${userLabelId} ${userValueId}} with a python replace (anchor asserted count==1); grep of the binding afterwards = 0. AccessExplainPanel.accessibleNames-12510 went red: MUTANT_EXIT=1, 1 failed, 'Unable to find an accessible element with the role "button" and name "User Me (calling principal)"'. Restored with git checkout HEAD -- packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx: git diff HEAD 0 bytes, git hash-object equal to the HEAD blob, binding grep = 1. No build/dist leg: the test imports the panel from src.",
    "pr_body_draft": "(elided by the seat: applied verbatim as the new body of PR #12580)"
    }

    Relayed verbatim by seat domain:ui#4 (session session_01SJn1GEkrbbBQkhGC26wEDw) from the cloud session's final message on the follow-up the seat's ACCEPT 6203538123 ordered (the Explain sheet's User label). The seat applied pr_body_draft as the PR body.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #12580 merged as 3e09c1e on main · read 2026-10-15T22:57Z

    probe on origin/main 3e09c1e reading control
    iconFieldId in ActionDefaultInspector.tsx 3 lines the Icon label names the icon picker
    userLabelId in AccessExplainPanel.tsx 3 lines the User label names the Explain sheet's picker
    the seven *-12510 pins plus .changeset/12510-studio-action-icon-picker-name.md 8 of 8 present —

    State change: the card closed as completed through Fixes #12510, and pm:dispatched is removed in this act. The cloud session session_014jSTWJ1PRVj63noYkF5KNv is archived. The out-of-scope finding is #12584. Two cards that were queued behind this one are now free: objectui#12513 (BuilderLanding.tsx) and objectui#12517 (the pending-changes sheet).


    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

a11yarea: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