Skip to content

Fix: allow anchors with role none, presentation or slider - #1870

Open
mjoslyn wants to merge 1 commit into
equalizedigital:developfrom
mjoslyn:fix/1748-role-none-slider
Open

Fix: allow anchors with role none, presentation or slider#1870
mjoslyn wants to merge 1 commit into
equalizedigital:developfrom
mjoslyn:fix/1748-role-none-slider

Conversation

@mjoslyn

@mjoslyn mjoslyn commented Aug 6, 2026

Copy link
Copy Markdown

Fixes #1748.

link_improper was flagging two anchors that are not exposed as links:

  • <a role="none"> / <a role="presentation"> — removed from the accessibility tree entirely.
  • <a role="slider"> — exposed as a slider. Not ideal markup, as the issue notes, but common in older media players and technically correct.

slider joins the existing button / tab allowlist, which stays unconditional: none of those roles is exposed as a link, so the anchor is not being used improperly however it happens to be focused.

The presentational roles get a narrower exemption, following the presentational roles conflict resolution. The role is ignored — and the element therefore still exposed as a link — when the anchor is either:

  • Focusable, so <a href="#" role="none"> continues to fail.
  • Carrying a global ARIA state or property. Approximated as any aria-* attribute, which errs toward reporting. aria-hidden is excluded, since it removes the element from the tree outright and makes the role moot — matching how linked-image-alt-present already handles it.

Compatibility

Both changes are relaxations. Nothing that passed on develop fails after this change, so it should not surface new issues on existing scans — only remove false positives.

Testing

14 cases added to tests/jest/rules/linkImproper.test.js, including both markup samples from the issue verbatim, the focusability and global-ARIA boundaries in both directions, and a roving-tabindex role="tab" case pinning the allowlist as unconditional. Full file passes at 46/46; phpcs and wp-scripts lint-js are clean on the changed files.

Checklist

  • PR is linked to the main issue in the repo
  • Tests are added that cover changes

Summary by CodeRabbit

  • Bug Fixes

    • Improved link accessibility checks for decorative, non-focusable anchors using role="none" or role="presentation".
    • Added support for valid slider and tab widget roles without weakening checks for conflicting ARIA attributes or invalid links.
    • Reduced incorrect warnings for media-player and other presentational markup.
  • Documentation

    • Updated remediation guidance to explain when presentational roles are appropriate for decorative anchors.

An <a> with role="none" or role="presentation" is removed from the
accessibility tree, and an <a> with role="slider" is exposed as a
slider. Neither is a link, so neither should be reported as improper
link usage. role="slider" joins the existing button and tab allowlist,
which stays unconditional: none of those roles is exposed as a link, so
the anchor is not being used improperly however it is focused.

The presentational roles get a narrower exemption, per the presentational
roles conflict resolution: the role is ignored, and the element still
exposed, when the anchor is focusable or carries a global ARIA state or
property. So <a href="#" role="none"> continues to fail.

The global ARIA half is approximated as any aria-* attribute, which errs
toward reporting. aria-hidden is excluded because it removes the element
from the tree outright, making the role moot, matching how
linked-image-alt-present handles it.

Update how_to_fix and the rule help text to name the presentational
roles the scanner now accepts, following ac471ce.

Fixes equalizedigital#1748
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The link validation check now accepts slider widget roles and eligible non-focusable anchors with none or presentation roles. Rule guidance and Jest tests document and verify these cases, including conflicting focusability and ARIA attributes.

Changes

Link role validation

Layer / File(s) Summary
Role-based link validation
src/pageScanner/checks/link-has-valid-href-or-role.js
The check allows slider roles. It accepts non-focusable none and presentation anchors when they have no conflicting global ARIA attributes other than aria-hidden.
Guidance and validation coverage
includes/classes/Rules/Rule/LinkImproperRule.php, src/pageScanner/rules/link-improper.js, tests/jest/rules/linkImproper.test.js
Rule guidance documents presentational anchors. Tests cover valid exemptions, focusability conflicts, ARIA attributes, slider anchors, and inactive tabs.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: pattonwebz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix for anchors using none, presentation, or slider roles.
Linked Issues check ✅ Passed The changes address issue #1748 by allowing valid none, presentation, and slider role cases while preserving conflict checks.
Out of Scope Changes check ✅ Passed All changes support issue #1748 through rule logic, documentation updates, and focused tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mjoslyn
mjoslyn marked this pull request as ready for review August 6, 2026 12:17
@mjoslyn

mjoslyn commented Aug 6, 2026

Copy link
Copy Markdown
Author

@pattonwebz - this is ready for your review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@includes/classes/Rules/Rule/LinkImproperRule.php`:
- Around line 44-49: Update the help text in LinkImproperRule to state that the
presentational-anchor exception requires the anchor to be non-focusable and have
no applicable global ARIA state or property. Also update the corresponding
guidance in src/pageScanner/rules/link-improper.js to include that global-ARIA
condition and both role="none" and role="presentation".

In `@src/pageScanner/checks/link-has-valid-href-or-role.js`:
- Around line 21-22: The role exemption in link-has-valid-href-or-role.js must
honor ARIA token order: resolve the first recognized non-abstract role, then
allow the exemption only when that role is button, tab, or slider rather than
matching any later token. Add the requested regression case in
tests/jest/rules/linkImproper.test.js at lines 218-220 for role="none slider"
with href="#", which should produce the improper-link result.
- Around line 40-44: The global-ARIA detection in link-has-valid-href-or-role.js
must exclude aria-hidden only when its value is "true", not for every
aria-hidden attribute; update the attribute check accordingly. In
tests/jest/rules/linkImproper.test.js lines 245-247, add coverage showing an
anchor with aria-hidden="false" fails validation.

In `@src/pageScanner/rules/link-improper.js`:
- Line 16: Wrap the user-facing help string in the link-improper rule with
wp.i18n.__, using the existing accessibility-checker text domain and preserving
the current message content.

In `@tests/jest/rules/linkImproper.test.js`:
- Around line 245-247: Add a separate fixture in the link rule test cases near
“Passes with role="none" and aria-hidden="true"” for an anchor with role="none"
and aria-hidden="false", and set shouldPass to false to verify explicit false is
rejected while the true hidden case remains passing.
- Around line 218-220: Extend the role-resolution tests near the existing
“multiple roles including none and no href” case with a failing case for an
anchor using href="#" and role="none slider". Ensure the expected result
indicates an improper link, verifying that the scanner selects the first
recognized role, none, and does not let the later slider token suppress the
result.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e7bba759-cacc-4d82-a87a-62321f44a0be

📥 Commits

Reviewing files that changed from the base of the PR and between d9e8df3 and edb6645.

📒 Files selected for processing (4)
  • includes/classes/Rules/Rule/LinkImproperRule.php
  • src/pageScanner/checks/link-has-valid-href-or-role.js
  • src/pageScanner/rules/link-improper.js
  • tests/jest/rules/linkImproper.test.js

Comment on lines +44 to +49
// translators: %1$s is <code>&lt;button&gt;</code>, %2$s is <code>role="button"</code>, %3$s is <code>role="none"</code>, %4$s is <code>role="presentation"</code>.
esc_html__( 'If the element is used to trigger an action, replace the anchor tag with a %1$s. If you cannot replace it, ensure that %2$s is added to the link, along with JavaScript that adds support for triggering it with the space bar key, and that appropriate ARIA attributes are used for toggle states or other functionality. If the anchor is purely decorative and cannot be focused, %3$s or %4$s marks it as presentational so it is not reported.', 'accessibility-checker' ),
'<code>&lt;button&gt;</code>',
'<code>role="button"</code>'
'<code>role="button"</code>',
'<code>role="none"</code>',
'<code>role="presentation"</code>'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align help text with the evaluator predicate.

Both guidance strings omit part of the presentational-anchor exception. The anchor must be non-focusable and have no applicable global ARIA state or property. The JavaScript help text also omits role="presentation".

  • includes/classes/Rules/Rule/LinkImproperRule.php#L44-L49: state the global-ARIA condition.
  • src/pageScanner/rules/link-improper.js#L16-L16: state the global-ARIA condition and include both presentational roles.
📍 Affects 2 files
  • includes/classes/Rules/Rule/LinkImproperRule.php#L44-L49 (this comment)
  • src/pageScanner/rules/link-improper.js#L16-L16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@includes/classes/Rules/Rule/LinkImproperRule.php` around lines 44 - 49,
Update the help text in LinkImproperRule to state that the presentational-anchor
exception requires the anchor to be non-focusable and have no applicable global
ARIA state or property. Also update the corresponding guidance in
src/pageScanner/rules/link-improper.js to include that global-ARIA condition and
both role="none" and role="presentation".

Comment on lines +21 to +22
// Allow roles of button, tab or slider
if ( roles.some( ( r ) => [ 'button', 'tab', 'slider' ].includes( r ) ) ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Honor ordered ARIA role tokens.

The evaluator accepts any allowlisted role token. ARIA applies the first recognized non-abstract role token. This permits a later slider, tab, or button token to suppress a required improper-link result.

  • src/pageScanner/checks/link-has-valid-href-or-role.js#L21-L22: resolve the first recognized ARIA role before applying a role exemption.
  • tests/jest/rules/linkImproper.test.js#L218-L220: add a failing role="none slider" case with href="#".
📍 Affects 2 files
  • src/pageScanner/checks/link-has-valid-href-or-role.js#L21-L22 (this comment)
  • tests/jest/rules/linkImproper.test.js#L218-L220
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pageScanner/checks/link-has-valid-href-or-role.js` around lines 21 - 22,
The role exemption in link-has-valid-href-or-role.js must honor ARIA token
order: resolve the first recognized non-abstract role, then allow the exemption
only when that role is button, tab, or slider rather than matching any later
token. Add the requested regression case in
tests/jest/rules/linkImproper.test.js at lines 218-220 for role="none slider"
with href="#", which should produce the improper-link result.

Comment on lines +40 to +44
// Approximated as any aria-* attribute, which errs toward reporting. aria-hidden is
// excluded because it removes the element from the tree outright, making the role moot.
const hasGlobalAria = Array.from( node.attributes ).some(
( attr ) => attr.name.startsWith( 'aria-' ) && attr.name !== 'aria-hidden'
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not treat every aria-hidden value as hidden.

The presentational-role exemption excludes aria-hidden without checking its value. This incorrectly exempts anchors with aria-hidden="false".

  • src/pageScanner/checks/link-has-valid-href-or-role.js#L40-L44: exempt only aria-hidden="true" from global-ARIA detection.
  • tests/jest/rules/linkImproper.test.js#L245-L247: add an aria-hidden="false" case that fails validation.
📍 Affects 2 files
  • src/pageScanner/checks/link-has-valid-href-or-role.js#L40-L44 (this comment)
  • tests/jest/rules/linkImproper.test.js#L245-L247
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pageScanner/checks/link-has-valid-href-or-role.js` around lines 40 - 44,
The global-ARIA detection in link-has-valid-href-or-role.js must exclude
aria-hidden only when its value is "true", not for every aria-hidden attribute;
update the attribute check accordingly. In tests/jest/rules/linkImproper.test.js
lines 245-247, add coverage showing an anchor with aria-hidden="false" fails
validation.

Comment thread src/pageScanner/rules/link-improper.js
Comment on lines +218 to +220
name: 'Passes with multiple roles including none and no href',
html: '<a role="foo none bar"><span>Menu</span></a>',
shouldPass: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test ordered-role resolution.

Keep this fallback-role test. Add a failing case for <a href="#" role="none slider">. The scanner must use none because it is the first recognized role. The later slider token must not suppress the improper-link result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/jest/rules/linkImproper.test.js` around lines 218 - 220, Extend the
role-resolution tests near the existing “multiple roles including none and no
href” case with a failing case for an anchor using href="#" and role="none
slider". Ensure the expected result indicates an improper link, verifying that
the scanner selects the first recognized role, none, and does not let the later
slider token suppress the result.

Comment on lines +245 to +247
name: 'Passes with role="none" and aria-hidden="true"',
html: '<a role="none" aria-hidden="true"><span>Menu</span></a>',
shouldPass: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test aria-hidden="false" separately.

Add <a role="none" aria-hidden="false"> with shouldPass: false. This protects the distinction between a true hidden state and an explicit false value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/jest/rules/linkImproper.test.js` around lines 245 - 247, Add a separate
fixture in the link rule test cases near “Passes with role="none" and
aria-hidden="true"” for an anchor with role="none" and aria-hidden="false", and
set shouldPass to false to verify explicit false is rejected while the true
hidden case remains passing.

@pattonwebz
pattonwebz self-requested a review August 6, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rules: links with role none, role slider still flagging improper use of link

1 participant