Repository navigation
fix(components,plugin-detail,app-shell,i18n): a record action disabled by its predicate says why it is unavailable (objectui#11811) - #11841
Conversation
… why it is unavailable A declared action greyed out by its `disabled` predicate now carries the generic reason "Not available for this record" as a tooltip (on hover and on keyboard focus, through a focusable wrapper span, the Radix idiom for a disabled trigger) and as a persistent accessible description (`aria-describedby` onto an `sr-only` copy). A button disabled only while its own action runs stays as it was. Surfaces: `record:quick_actions` (plugin-detail, the record section bar) and `DeclaredActionsBar` (app-shell). One new key, `actions.notAvailableForRecord`, in all ten language packs. Part of objectui#11811. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…r, focus and description; add the changeset Both surfaces: the predicate-disabled action shows the reason on hover and on keyboard focus and is described by it; a predicate that does not hold and a button greyed out only while it runs show none; the text comes from the en and zh packs. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…e served CEL envelope
The bare predicate string takes the legacy evaluator, where `has()` faults and
a fail-soft `disabled` greys the action out on both rows; the pins now use the
`{ dialect: 'cel', source }` shape the spec normalizes the authored string to,
so the disabled case and its control each reach a real verdict.
Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…is unavailable The record header's inline action button gets the same reason as the other two surfaces: a tooltip on hover and keyboard focus through a focusable wrapper span, plus a persistent sr-only description. In the overflow menu the reason is a visible second line and the item's description, because the menu skips a disabled item and traps Tab, so a tooltip there is out of keyboard reach. Only an authored action's declared `disabled` earns it; the host's Edit and Delete (a host-computed boolean) and the inline-edit lock are unchanged. The changeset now names @object-ui/components and the three surfaces. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: Inputs read: card #11811 (body and all five comments: triage 6043323597, claim 6048244335, dev report 6049038332, claim amendment 6049093782, dev addendum 6049354721), PR #11841 (body, 17-file list, net diff ① Derived judgmentsAccept-set changes (what an end user on a record page gets):
Public-surface changes (what a published package's accept set gains or claims):
Nothing in the diff is judged wrong. ② Semver level
③ Boundary flagsFrom report 6049038332:
From report 6049354721: Flags the reports did not raise, judged here: Nothing left unanswered; nothing further to escalate for this head beyond ③.6. Implemented-by: VERDICT: PASS Rendered 2026-10-08T00:28Z on the inputs named above. Generated by Claude Code |
Fixes #11811
Clause-②: yes
A record action greyed out by its declared
disabledpredicate now says why. It shows the generic reason "Not available for this record" as a tooltip, on hover and on keyboard focus, and the same text is its accessible description (aria-describedby). Before, the button carried no tooltip, notitleand no description.Where the card's defect actually lives
The claim's original Responsibility line named
DeclaredActionsBaras the code that draws the showcase Task page's Archive. It does not:DeclaredActionsBarrenders onlysys_approval_requestactions (record page approvals bar, approvals inbox). Measured on objectstackmain(the card's879bd38cis its ancestor), the Task page (showcase_task_detail,kind: 'full') draws Archive in two other renderers:page:headernode whoseactionsare ids, drawn byPageHeaderRenderer'srenderButton(@object-ui/components);record:quick_actionsatlocation: 'record_section', drawn byQuickActionButton(@object-ui/plugin-detail).The seat recorded the correction in claim amendment 6049093782 and added the header file to the surface.
DeclaredActionsBarhas the same defect class, so it gets the same fix.What changed
page:header(packages/components/src/renderers/layout/containers.tsx): the inline button gets a tooltip trigger plus ansr-onlydescription. In the ⋯ overflow menu the reason is a visible second line under the label and is the item'saria-describedby; the item's name stays the label (aria-labelledby). A tooltip there would be out of keyboard reach: the menu's roving focus skips a disabled item (focusable: !disabled) and the menu traps Tab. Only an authored action's declareddisabledgets the reason. The host-injected Edit and Delete (RecordContext.headerSystemActions, a host-computed boolean) and the inline-edit lock are unchanged. Authored and host actions are told apart by key, from the dedupe that lets an authored action win.record:quick_actions(QuickActionButton) andDeclaredActionsBar: the same tooltip trigger and description on the inline button.actions.notAvailableForRecord, with real translations in all ten packs.Why a wrapper span. A natively
disabledbutton fires no pointer or focus events, and the Button primitive addsdisabled:pointer-events-none, so a tooltip or a nativetitleon the button itself never opens. The span is the tooltip trigger, the pattern Radix documents for a disabled trigger. It takes the hover, andtabIndex={0}lets a keyboard user focus it. Both the button and the span carryaria-describedbypointing at a persistentsr-onlycopy, so a screen reader gets the reason with the tooltip closed. The repo had no disabled-tooltip pattern. Itstitle-on-disabled-Button sites cannot show at all; a Chromium probe found that the hit target at apointer-events: nonedisabled button's centre is its parent.Not changed: a button greyed out only while its own action runs (spinner) shows no reason. No export, prop or type member is added. The author-written reason beside the predicate is a spec key for objectstack, not part of this PR.
Tests (head
e165c34)page-header-disabledReason-11811.test.tsx(7),record-quick-actions.disabledReason-11811.test.tsx(7),DeclaredActionsBar.disabledReason-11811.test.tsx(7).role=tooltip; Tab reaches the trigger, which opens it.disabled: truegive no reason.has()faults and a fail-softdisabledgreys out both rows, so it would pin the wrong reason.vitest run packages/components/plus the 36 app-shell, console and example files that namepage:header,PageHeaderRendererorheaderSystemActions, ate165c34:Test Files 401 passed | 1 skipped (402).vitest run packages/plugin-detail/ packages/i18n/plus the 27 app-shell and console files that nameDeclaredActionsBar,record-quick-actionsor the key, ate2a498f:Test Files 356 passed | 1 skipped (357).e165c34changes no file in those packages.@object-ui/componentsexit 0 ate165c34;@object-ui/i18n,@object-ui/plugin-detailand@object-ui/app-shellexit 0 ate2a498f, on dists rebuilt from this branch.scripts/ablation-replace.mjs: the anchorif (!disabledReason) return button;is replaced by an unconditionalreturn button;.Tests 4 failed | 3 passed (7). Red: description, hover, focus, zh. Green: overflow and both controls.DeclaredActionsBar:Tests 10 failed | 4 passed (14). Red: description, hover, focus, zh, en on each. Green: the controls.git diff HEADis empty.e165c34, all exit 0:check:i18n-keys,check:i18n-drift(1 key added, no en value changed),check:i18n-dead-keys(report; the new key is not a candidate),check:i18n-designer-parity;check-changeset-presence(4 released packages, 1 changeset),check-changeset-no-major,check-changeset-fixed,check:changeset-claims,check:pending-changeset-literals;check:control-bytes,check:test-path-roots,check:new-line-citations(0 new); eslint on touched files (0 errors).Changeset
.changeset/11811-disabled-action-reason.md:@object-ui/i18nand@object-ui/componentsareminor,@object-ui/app-shelland@object-ui/plugin-detailarepatch. The fixed group publishes all of them atminor, nevermajor. The body names the three surfaces and the one key.Acceptance notes
action:button/action:icon/action:group/action:menu) have the same gap. They are their own family card, objectui#11839, and this PR leaves them alone.disabledpredicate that faults (fail-soft) also greys the action out, and now also shows the generic reason. That is consistent, since the action is unavailable either way, but the wording slightly overstates the case for a broken predicate.InlineEditSaveBar's Save button setstitleon a disabled Button, which cannot show for the pointer-events reason above. The approval-lock band already shows that reason on screen. Noted only.main.The session behind this change is
https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8.Generated by Claude Code