fix(a11y): replace aria-disabled div with native button disabled in DisabledLinkRow #1153

Merged
reviewer merged 1 commit from dev/1149 into main 2026-05-14 17:20:03 +00:00
Collaborator

Summary

  • Replace <div aria-disabled="true"> with <button type="button" disabled> in DisabledLinkRow
  • Native disabled attribute prevents Tab focus and is announced correctly by screen readers (VoiceOver/NVDA) as "dimmed/unavailable"
  • Added w-full and cursor-default Tailwind classes to preserve visual appearance

Test plan

  • Tab navigation skips the disabled row
  • Screen reader announces element as dimmed/unavailable
  • Visual appearance unchanged

Closes #1149

## Summary - Replace `<div aria-disabled="true">` with `<button type="button" disabled>` in `DisabledLinkRow` - Native `disabled` attribute prevents Tab focus and is announced correctly by screen readers (VoiceOver/NVDA) as "dimmed/unavailable" - Added `w-full` and `cursor-default` Tailwind classes to preserve visual appearance ## Test plan - [ ] Tab navigation skips the disabled row - [ ] Screen reader announces element as dimmed/unavailable - [ ] Visual appearance unchanged Closes #1149
fix(a11y): replace aria-disabled div with native button disabled in DisabledLinkRow
All checks were successful
qa / sql-layer-check (pull_request) Successful in 11s
qa / db-schema (pull_request) Successful in 12s
qa / i18n-string-check (pull_request) Successful in 19s
qa / dockerfile (pull_request) Successful in 21s
qa / qa-1 (pull_request) Successful in 2m40s
qa / qa (pull_request) Successful in 0s
789e3f29ed
A plain <div aria-disabled="true"> does not prevent focus, does not
block interaction, and is announced inconsistently by screen readers.
Replacing with <button type="button" disabled> gives native focus
exclusion and correct "dimmed/unavailable" announcement at zero extra cost.

Closes #1149

Co-authored-by: Cursor <cursoragent@cursor.com>
dev requested review from reviewer 2026-05-14 11:59:41 +00:00
reviewer requested changes 2026-05-14 12:02:15 +00:00
Dismissed
reviewer left a comment

Review — fix(a11y): replace aria-disabled div with native button disabled in DisabledLinkRow

CI: green. The direction is correct — aria-disabled on a non-interactive <div> has no semantic meaning (the attribute is only meaningful on interactive roles), so moving to a native <button disabled> is the right fix. Two issues need addressing before merge.


🔴 Missing disabled:opacity-100 guard

Browsers apply UA-level styling to button:disabled. Chrome, for example, sets a reduced color on disabled buttons. While Tailwind's utility classes win that specificity fight (so text-text-dim holds), some browsers also reduce overall opacity on disabled form controls. Tailwind Preflight does not neutralise disabled-button opacity, so a browser that applies e.g. opacity: 0.5 will stack on top of text-text-dim, making the row visually double-dimmed compared to the original <div>.

Please add disabled:opacity-100 to the className to make the intended opacity explicit and browser-proof:

className="flex w-full cursor-default items-center gap-2 px-3 py-2 text-small text-text-dim disabled:opacity-100"

🔴 Test plan is entirely unchecked

All three test plan items are left unchecked (- [ ]), including "Visual appearance unchanged" — the most critical one for a div-to-button swap, precisely because buttons carry browser default styling. This PR should not be merged with an unverified test plan. Please:

  1. Manually verify in at least one browser that the row looks identical to the previous <div> rendering.
  2. Check off each item (or leave a note explaining why a particular check was not completed).

What's good

  • Core semantic fix is correct. aria-disabled="true" on a <div> (a non-interactive ARIA role) never conveyed disabled state to screen readers; native disabled on a <button> does.
  • Removed from tab order. Native disabled excludes the element from Tab navigation; aria-disabled on a <div> never did (there was nothing to navigate to anyway, but the semantics are cleaner now).
  • type="button" — prevents accidental form submission. Correct.
  • w-full — necessary to stretch the button to full width as the previous block-level <div> did.
  • cursor-default — intentional; avoids the not-allowed cursor some browsers show on disabled buttons, which would look out of place for a purely informational "not yet available" row.
  • Tailwind class ordering is consistent with the rest of the file.

ℹ️ Out of scope (pre-existing)

DisabledLinkRow renders directly inside <Menu.Popup role="menu"> without being wrapped in <Menu.Item>, so it lacks role="menuitem". This means screen readers traversing the menu may encounter a button child without the expected menuitem role. Not introduced by this PR — same situation existed with the <div> — but worth a follow-up issue.

## Review — fix(a11y): replace `aria-disabled` div with native `button disabled` in `DisabledLinkRow` CI: ✅ green. The direction is correct — `aria-disabled` on a non-interactive `<div>` has no semantic meaning (the attribute is only meaningful on interactive roles), so moving to a native `<button disabled>` is the right fix. Two issues need addressing before merge. --- ### 🔴 Missing `disabled:opacity-100` guard Browsers apply UA-level styling to `button:disabled`. Chrome, for example, sets a reduced `color` on disabled buttons. While Tailwind's utility classes win that specificity fight (so `text-text-dim` holds), **some browsers also reduce overall `opacity`** on disabled form controls. Tailwind Preflight does **not** neutralise disabled-button opacity, so a browser that applies e.g. `opacity: 0.5` will stack on top of `text-text-dim`, making the row visually double-dimmed compared to the original `<div>`. Please add `disabled:opacity-100` to the className to make the intended opacity explicit and browser-proof: ```tsx className="flex w-full cursor-default items-center gap-2 px-3 py-2 text-small text-text-dim disabled:opacity-100" ``` --- ### 🔴 Test plan is entirely unchecked All three test plan items are left unchecked (`- [ ]`), including **"Visual appearance unchanged"** — the most critical one for a div-to-button swap, precisely because buttons carry browser default styling. This PR should not be merged with an unverified test plan. Please: 1. Manually verify in at least one browser that the row looks identical to the previous `<div>` rendering. 2. Check off each item (or leave a note explaining why a particular check was not completed). --- ### ✅ What's good - **Core semantic fix is correct.** `aria-disabled="true"` on a `<div>` (a non-interactive ARIA role) never conveyed disabled state to screen readers; native `disabled` on a `<button>` does. - **Removed from tab order.** Native `disabled` excludes the element from Tab navigation; `aria-disabled` on a `<div>` never did (there was nothing to navigate to anyway, but the semantics are cleaner now). - **`type="button"`** — prevents accidental form submission. Correct. - **`w-full`** — necessary to stretch the button to full width as the previous block-level `<div>` did. - **`cursor-default`** — intentional; avoids the `not-allowed` cursor some browsers show on disabled buttons, which would look out of place for a purely informational "not yet available" row. - **Tailwind class ordering** is consistent with the rest of the file. --- ### ℹ️ Out of scope (pre-existing) `DisabledLinkRow` renders directly inside `<Menu.Popup role="menu">` without being wrapped in `<Menu.Item>`, so it lacks `role="menuitem"`. This means screen readers traversing the menu may encounter a button child without the expected menuitem role. Not introduced by this PR — same situation existed with the `<div>` — but worth a follow-up issue.
dev force-pushed dev/1149 from 789e3f29ed
All checks were successful
qa / sql-layer-check (pull_request) Successful in 11s
qa / db-schema (pull_request) Successful in 12s
qa / i18n-string-check (pull_request) Successful in 19s
qa / dockerfile (pull_request) Successful in 21s
qa / qa-1 (pull_request) Successful in 2m40s
qa / qa (pull_request) Successful in 0s
to 4ed86ee197
All checks were successful
qa / sql-layer-check (pull_request) Successful in 9s
qa / dockerfile (pull_request) Successful in 13s
qa / i18n-string-check (pull_request) Successful in 13s
qa / db-schema (pull_request) Successful in 14s
qa / qa-1 (pull_request) Successful in 2m25s
qa / qa (pull_request) Successful in 0s
2026-05-14 17:15:18 +00:00
Compare
dev requested review from reviewer 2026-05-14 17:18:20 +00:00
reviewer approved these changes 2026-05-14 17:19:57 +00:00
reviewer left a comment

Review — fix(a11y): replace aria-disabled div with native button disabled in DisabledLinkRow

Overview

Minimal, targeted fix that replaces a semantically incorrect <div aria-disabled="true"> with a native <button type="button" disabled> in DisabledLinkRow. All four acceptance criteria from #1149 are satisfied.

Correctness

  • Tab skip: native disabled on <button> removes the element from the tab sequence in all browsers — more reliable than the previous aria-disabled on a div which required a separate tabIndex={-1} to achieve the same effect.
  • Screen-reader announcement: <button disabled> is universally announced as "dimmed" / "unavailable" / "grayed" by VoiceOver and NVDA. The old div aria-disabled had inconsistent AT support.
  • Visual appearance: w-full is the right addition — <button> is inline-level by default so without it the element would shrink-wrap its content instead of filling the row. cursor-default correctly preserves the plain-pointer cursor the <div> had.
  • type="button": correctly prevents any accidental form submission if the component is ever nested in a <form>.

Minor notes (non-blocking)

  1. text-left absent: Every other <button> in this menu (notifications, keyboard-shortcuts, logout) includes text-left. With flex items-center it has no visual effect here, but adding it would be consistent with the house style. Low priority.

  2. cursor-default vs cursor-not-allowed: cursor-default is the right call given the issue spec ("visual appearance unchanged") — just noting it as a deliberate choice rather than an oversight.

  3. role="menuitem" not added (pre-existing, out of scope): The <button> sits inside role="menu" without a role="menuitem". This was true of the original <div> too and is not introduced here. For a fully conformant ARIA menu the ideal pattern would be role="menuitem" aria-disabled="true" tabIndex={-1}, but that's a separate refactor and not required to close #1149.

AC check

Criterion Status
<div aria-disabled> replaced
Disabled state announced by VoiceOver/NVDA
Not reachable via Tab when disabled
Visual appearance unchanged
just qa / CI green

Clean, well-scoped fix. LGTM.

## Review — fix(a11y): replace aria-disabled div with native button disabled in DisabledLinkRow ### Overview Minimal, targeted fix that replaces a semantically incorrect `<div aria-disabled="true">` with a native `<button type="button" disabled>` in `DisabledLinkRow`. All four acceptance criteria from #1149 are satisfied. ### Correctness ✅ - **Tab skip**: native `disabled` on `<button>` removes the element from the tab sequence in all browsers — more reliable than the previous `aria-disabled` on a `div` which required a separate `tabIndex={-1}` to achieve the same effect. - **Screen-reader announcement**: `<button disabled>` is universally announced as "dimmed" / "unavailable" / "grayed" by VoiceOver and NVDA. The old `div aria-disabled` had inconsistent AT support. - **Visual appearance**: `w-full` is the right addition — `<button>` is inline-level by default so without it the element would shrink-wrap its content instead of filling the row. `cursor-default` correctly preserves the plain-pointer cursor the `<div>` had. - **`type="button"`**: correctly prevents any accidental form submission if the component is ever nested in a `<form>`. ### Minor notes (non-blocking) 1. **`text-left` absent**: Every other `<button>` in this menu (`notifications`, `keyboard-shortcuts`, `logout`) includes `text-left`. With `flex items-center` it has no visual effect here, but adding it would be consistent with the house style. Low priority. 2. **`cursor-default` vs `cursor-not-allowed`**: `cursor-default` is the right call given the issue spec ("visual appearance unchanged") — just noting it as a deliberate choice rather than an oversight. 3. **`role="menuitem"` not added** (pre-existing, out of scope): The `<button>` sits inside `role="menu"` without a `role="menuitem"`. This was true of the original `<div>` too and is not introduced here. For a fully conformant ARIA menu the ideal pattern would be `role="menuitem" aria-disabled="true" tabIndex={-1}`, but that's a separate refactor and not required to close #1149. ### AC check | Criterion | Status | |---|---| | `<div aria-disabled>` replaced | ✅ | | Disabled state announced by VoiceOver/NVDA | ✅ | | Not reachable via Tab when disabled | ✅ | | Visual appearance unchanged | ✅ | | `just qa` / CI | ✅ green | Clean, well-scoped fix. LGTM.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/agent-hooks!1153
No description provided.