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

fix(a11y): name icon-only buttons and give clickable div/span controls a keyboard path (objectui#13461) - #13494

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-byazyd
Oct 25, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-byazyd

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #13461

Clause-②: no
Metadata-protocol: none

What changed

Icon-only buttons in the console get an accessible name, and the console's mouse-only clickable div / span controls become real buttons. A new deletions-only census keeps clickable div / span sites from coming back.

Premise re-measured (the card's counts were a script reading)

The card's 25 / 24 came from a line-window script. Re-measured on the TypeScript AST at base c85408f:

  • Icon-only buttons. Several of the card's examples already carry aria-label a few lines below the opening tag (the AI chat page, the invitations page, the chat dock), so they were never unnamed. The instrument that already re-derives this family is iconOnlyButtonCensus-12143.test.ts (it exists; the card says nobody filed it). This PR names every entry of its BACKLOG inside the claim's file surface (app-shell, components, fields, plugin-chatbot: 16 sites in 12 files) and deletes those rows. The top bar's avatar button, which that census cannot see (an Avatar is not an icon), is named too.
  • Clickable div / span. The new census reads every div / span with an onClick and no role, tabIndex or key handler under packages/*/src and apps/*/src. Most sites are not controls: propagation guards around real buttons, scrims behind overlays, empty design-canvas backgrounds, and drag handles. Each is declared in NOT_CONTROLS with its reason. ⛔ None got role="button".

Repaired

Where Before After
Organization picker card, "Manage" a clickable span inside the card's button (unreachable by Tab, read as part of the card's name) its own button beside the card button; organization-manage-SLUG test id
Flow canvas branch label clickable span; the wide hit path is pointer-only role="button", tabIndex, aria-pressed, Enter / Space when the edge is selectable
tree-view node row clickable div wrapped round the toggle button; toggle unnamed the toggle is named (common.expand / common.collapse) with aria-expanded; the node is a button only when the host passes onNodeClick
file-upload dropzone clickable div role="button", tabIndex={0}, Enter / Space opens the picker
App preview read-only nav row an onSelect click handler that could never fire (the list renders only outside design mode, where the handler is undefined) handler removed

Icon-only buttons named: AI live canvas close (common.close), action result dialog secret toggle (actions.resultDialog.showValue / hideValue), permission set editor remove policy / remove tab (perm.rls.remove / perm.tabs.remove, designer table en + zh), metadata history refresh (engine.edit.refresh), sort builder remove (sortBuilder.removeSort), file-upload and FileField remove (fields.file.remove), MasterDetailField open / remove (fields.masterDetail.*), PasswordField toggle (fields.password.*), QRCodeField copy / toggle (fields.qrCode.*), legacy Chatbot send (chatbot.sendMessage), chat scroll-to-latest (chatbot.scrollToBottom, before the props spread so a caller's name wins), top bar avatar (user.accountMenu). New pack keys are in all ten packs.

The census — packages/components/src/__tests__/clickableNonControlCensus-13461.test.ts

Same shape as the objectui#12143 and objectui#12145 censuses: an ordinary-suite test, ⛔ not a new CI gate. Sites are keyed by file, tag and handler (never by line); NOT_CONTROLS and BACKLOG are deletions-only and must equal what the scan finds. It carries width guards, a matcher fixture, and a stated blind-spot list (spread attributes, other tags, whether a reason is still true).

BACKLOG freezes 16 clickable controls that still have no keyboard path. 15 are outside the claim's file surface (plugin-ai, plugin-designer, plugin-gantt, plugin-grid, plugin-view). One is in app-shell: the studio data-form preview selects a field by a click delegated from its frame, and a keyboard path for that is a design question. The 12143 census keeps its 14 remaining plugin rows (plugin-ai, plugin-detail, plugin-list, plugin-view).

Verification (head after the merge of main, ecd30ca)

  • Ablation, done once and not kept: restored OrganizationsPage.tsx from origin/main and the new census went red with OrganizationsPage.tsx · span · stopPropagation,navigate — declared 0, found 1. Restored with git checkout HEAD --, and git diff HEAD was empty.
  • New and extended tests: keyboardControls-13461.test.tsx (components: tree toggle name and state, node button only with onNodeClick, zh pack; dropzone Enter / Space, remove-button name), iconButtonNames-13461.test.tsx (fields: password, QR code, master-detail names, zh pack), plus an OrganizationsPage.test.tsx case (Manage is a sibling button and does not switch org) and an AppHeader.myOrganizations.test.tsx case (avatar button name).
  • Every test file that imports a changed module, plus every test that reads a changed file by path: 261 files in five runs, all passed before the merge. After the merge, the three censuses, the new tests and the tree-view catalog pin (tree-view-nodes-mirror-6939, 8 files, 54 tests) passed.
  • packages/i18n/ whole package: 87 files passed.
  • turbo run type-check for app-shell, components, fields, plugin-chatbot and i18n with their dependencies: green after a test-typing fix in fields.
  • Gates: check:i18n-keys, check:i18n-dead-keys, check:i18n-drift, check:i18n-designer-parity, check:new-line-citations, check-changeset-presence, check-changeset-no-major, check-changeset-claims, check-pending-changeset-literals and check-changeset-fixed all exit 0. ESLint on touched files: 0 errors with inline config. One react-hooks/static-components error appears under --no-inline-config in AppPreview.tsx, and origin/main's copy of that file shows the same error.
  • NOT MEASURED: check:eager-locale-catalogues (needs a built console bundle; CI builds first). The whole-repo pnpm test and pnpm lint are left to CI.

Acceptance notes

  • Literal user-facing strings next to the touched controls are untouched (the legacy Chatbot's empty-state line, MasterDetailField's record count, file-upload's launch copy, ResourceHistoryPage's "Back to item"). They are outside this card.

  • The optional per-route axe pass, a required axe check and repo-wide eslint-plugin-jsx-a11y are out of scope, per the claim.

  • Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer)

🤖 Generated with Claude Code

https://claude.ai/code/session_018qafSnDdNuQgZGfnFwDajn

…ard path (objectui#13461)

Names the icon-only buttons the objectui#12143 census froze in app-shell,
components, fields and plugin-chatbot, plus the top bar's avatar button, through
each package's translation hook. Turns the console's mouse-only clickable
div/span controls into real buttons, and adds a deletions-only census of
clickable div/span sites with no keyboard path.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qafSnDdNuQgZGfnFwDajn
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 85 chunks) 3034.2 KB 3496.0 KB
Main entry chunk (gzip) 80.3 KB 350 KB
Entry file index-DLRMjOpu.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (buildIdentity.js) 0.80KB 0.49KB
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 21.41KB 7.90KB
app-shell (runtime-config.js) 24.52KB 8.48KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 45.59KB 12.03KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.67KB 6.12KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.74KB 3.94KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 42.84KB 11.66KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 8.84KB 3.61KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 615.71KB 150.71KB
core (index.js) 10.51KB 4.16KB
create-plugin (index.js) 27.92KB 9.50KB
data-objectstack (index.js) 253.04KB 70.68KB
fields (index.js) 296.40KB 75.98KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.17KB 11.96KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (ServerReadScope.js) 4.03KB 1.82KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.99KB 0.43KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.91KB
plugin-calendar (index.js) 55.31KB 16.16KB
plugin-charts (index.js) 88.30KB 24.33KB
plugin-chatbot (core.js) 5.22KB 2.18KB
plugin-chatbot (index.js) 238.05KB 56.30KB
plugin-dashboard (index.js) 147.15KB 39.95KB
plugin-designer (index.js) 232.83KB 50.01KB
plugin-detail (index.js) 268.92KB 71.56KB
plugin-editor (index.js) 2.21KB 1.04KB
plugin-form (index.js) 185.46KB 48.48KB
plugin-gantt (index.js) 178.19KB 45.06KB
plugin-grid (index.js) 277.62KB 77.39KB
plugin-kanban (index.js) 64.27KB 19.81KB
plugin-list (index.js) 149.51KB 39.15KB
plugin-map (index.js) 27.54KB 9.10KB
plugin-markdown (index.js) 13.79KB 4.79KB
plugin-report (index.js) 46.42KB 13.06KB
plugin-timeline (index.js) 39.74KB 12.00KB
plugin-tree (index.js) 15.25KB 5.38KB
plugin-view (index.js) 93.37KB 24.00KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 129.13KB 42.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.71KB 2.11KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (product-blocks.js) 0.20KB 0.19KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · PR #13494 @ ecd30cab3c029314f87160dabab3433303fb784b · domain:ui#4 · 2026-10-25T03:02Z

Reviewer of record: session_01XWGTsmqrtrt1Zqh1ioWqsT (seat 4). Implemented-by: session_018qafSnDdNuQgZGfnFwDajn on claude/project-thread-byazyd. Card objectui#13461, claim 6392244102, report 6392592744.

  1. Shape: draft, base main, Fixes #13461. No other open card number sits next to a closing keyword. 35 files, all inside the claimed surface:
    • the named controls in app-shell, components, fields and plugin-chatbot;
    • the ten locale packs and metadata-admin/i18n.ts;
    • two new tests, the census edits and one changeset.
  2. Premise measured: the card's 25 / 24 were a line-window count, and the dev re-measured as the claim asked. The icon-only ratchet already existed (iconOnlyButtonCensus-12143), so 16 of its BACKLOG sites inside the surface are now named and their rows deleted. The seat read the repaired controls:
    • The organization card's "Manage" was a clickable span inside the card's button. It is now a sibling button.
    • The tree node is a button only when the host passes onNodeClick. On main that handler did nothing without schema.onNodeClick, so nothing is lost. The toggle gets aria-expanded and a name.
    • The file-upload dropzone takes Enter/Space, and its remove button is named.
    • The flow-branch label is a keyboard stop.
    • The dead onSelect on AppPreview's read-only row is gone: that list renders only outside design mode, where the handler never existed.
  3. Gates on this head: 33 check-runs success: Lint, Type Check, Test shards 1–16 and the aggregate Test, Build & E2E, Build Docs, Repo Gates, the changeset family, Line Citation Gate, Governed Surface Queue Guard, Bundle Analysis (budget PASS), Test (dist pins), label, Live E2E (informational). 3 are skipped (coverage jobs, dependabot). mergeable_state: clean.
  4. Accepted deviations: the new clickableNonControlCensus-13461 sits beside iconOnlyButtonCensus-12143 in packages/components/src/__tests__ (its true sibling), not beside ariaLabelLiteralCensus-12145 as the claim said. It declares 33 non-control sites, each with one of four reasons (guard, scrim, canvas background, drag handle), and freezes 16 controls in BACKLOG, deletions only. Of those 16, 15 are in plugins outside the surface. It runs in the ordinary suite, not as a new required context.
  5. Out of scope: the 16 BACKLOG controls stay frozen for later cards. No axe gate and no repo-wide jsx-a11y.
  6. Landing: not governed. Changeset read against the diff; it matches. PR fix(console, app-shell): localise API Console, Public Forms, Integrations, AI Approvals and the datasource page (objectui#13460) #13489 (in the merge queue) adds keys to the same ten packs. If it lands first, this PR merges main before it can merge, as its claim says. The maintainer's standing landing word was given in this seat's project thread at 2026-10-25T01:48Z, so the seat lands it.

Generated by Claude Code

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

Projects

None yet

2 participants