Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/11839-action-disabled-reason.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@object-ui/components': patch
---

An action drawn by `action:button`, `action:icon`, `action:group` or `action:menu` and greyed out by its declared `disabled` predicate now says why (objectui#11839). It shows the reason "Not available for this record", the same generic sentence and the same language-pack key (`actions.notAvailableForRecord`) objectui#11811 gave the record header, the section bar and `DeclaredActionsBar`. The same text is the control's accessible description (`aria-describedby`), so a screen reader announces it too. Before, the control carried no tooltip, no `title` and no description.

Where it shows:

- **A button** (`action:button`, `action:icon`, and an `action:group` member in inline mode, including the members `action:bar` draws). Hovering it or focusing it from the keyboard opens a tooltip with the reason. A natively disabled button receives no pointer or focus events, so the tooltip's trigger is a focusable wrapper around it. On `action:icon`, which has no visible label, the tooltip shows the label above the reason, and the icon's accessible name stays its label.
- **A menu item** (an `action:menu` item, including `action:bar`'s overflow menu, and an `action:group` member in dropdown mode). The reason is a second line under the label. A tooltip there could not open: a disabled menu item takes no pointer events, and the menu's keyboard navigation skips it.

What stays unchanged: a control greyed out while its own action runs, a control the host disables (the `disabled` it forwards, such as a disabled group's members), and an action disabled only through the legacy `enabled` key show no reason.

**Clause-②: no.** No export, prop, type member or language-pack key is added, removed or changed.
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ const moreTrigger = () => screen.queryByRole('button', { name: MORE });
/**
* Open the overflow menu and return its item labels, in order. Radix opens on
* `pointerdown` (a plain click does nothing) and mounts the content in a portal.
*
* A label is the item's NAME: an item greyed out by its declared `disabled`
* shows the reason as a second line and names itself by its label element
* (`aria-labelledby`, objectui#11839), so its text is the label plus the reason.
*/
async function openMenuLabels(): Promise<string[]> {
const trigger = moreTrigger();
Expand All @@ -147,7 +151,11 @@ async function openMenuLabels(): Promise<string[]> {
const menu = await screen.findByRole('menu');
return within(menu)
.queryAllByRole('menuitem')
.map((item) => (item.textContent ?? '').trim());
.map((item) => {
const labelledBy = item.getAttribute('aria-labelledby');
const label = labelledBy ? document.getElementById(labelledBy) : item;
return (label?.textContent ?? '').trim();
});
}

/**
Expand Down Expand Up @@ -358,7 +366,10 @@ describe('a relocated member is gated exactly as an inline one (objectui#10345)'
it('a disabled menu-placed member is a disabled menu item', async () => {
renderBar({ actions: [CONTROL, { ...MEMBER, component: 'action:menu', disabled: true }] });
expect(await openMenuLabels()).toEqual(['Member']);
expect(screen.getByRole('menuitem', { name: 'Member' })).toHaveAttribute('data-disabled');
const item = screen.getByRole('menuitem', { name: 'Member' });
expect(item).toHaveAttribute('data-disabled');
// Its declared `disabled` says why, as the item's description (objectui#11839).
expect(item).toHaveAccessibleDescription('Not available for this record');
});

it.each<[string, Placement]>([
Expand Down
Loading
Loading