fix(landmark-unique): exclude section/form with non-landmark roles from landmark match - #5085
Conversation
…om landmark match Previously, section and form elements with an explicit non-landmark role (e.g. role=tabpanel) were still matched by landmark-unique as long as they had an accessible name. Now only elements with a landmark role are considered. Closes issue dequelabs#4722
WilcoFiers
left a comment
There was a problem hiding this comment.
Thank you for raising this. There's already a solution built into axe-core for doing the thing isLandmarkVirtual is trying to do. We should use that instead.
| if (nodeName === 'section' || nodeName === 'form') { | ||
| const accessibleText = accessibleTextVirtual(vNode); | ||
| return !!accessibleText; | ||
| return isLandmarkRole && !!accessibleTextVirtual(vNode); |
There was a problem hiding this comment.
I think the real bug here is that section / form elements are always treated as landmarks if they have an accessible name, even if they have a role that doesn't map to your name. We should add a test for that:
<form role="tabpanel" aria-label="foo" id="pass-form-tabpanel1"></form>
<form role="tabpanel" aria-label="foo" id="pass-form-tabpanel1"></form>There was a problem hiding this comment.
Axe-core already has code for checking which element is a landmark. The real bug here is that this matches function wasn't updated to use getRoleType. You can remove isLandmarkVirtual from this file.
| getRoleType(virtualNode) === 'landmark' && | |
| isVisibleToScreenReaders(virtualNode) |
This fix also addresses the other issue you raised.
| <div id="violation-role-search-2" role="search"></div> | ||
| <form id="violation-role-search" role="search"></form> | ||
| <form id="violation-role-search-2" role="search"></form> | ||
|
|
There was a problem hiding this comment.
Ideally, I wanted to add new test cases for <section>/<form> with a landmark role but no aria-label. However, all landmark roles were already used in the no-label section of this file, so there was no available role to use for new elements. Instead, I replaced the existing <div role="search"> with <form role="search">.
|
I've made the requested changes. Could you take another look? |
WilcoFiers
left a comment
There was a problem hiding this comment.
Fantastic. One more suggestion.
| <form | ||
| id="pass-form-search-aria-label-1" | ||
| role="search" | ||
| aria-label="form-search-label-1" | ||
| ></form> | ||
| <form | ||
| id="pass-form-search-aria-label-2" | ||
| role="search" | ||
| aria-label="form-search-label-2" | ||
| ></form> |
There was a problem hiding this comment.
This is the false positive of the original issue. Good to have that in the E2E suite.
| <form | |
| id="pass-form-search-aria-label-1" | |
| role="search" | |
| aria-label="form-search-label-1" | |
| ></form> | |
| <form | |
| id="pass-form-search-aria-label-2" | |
| role="search" | |
| aria-label="form-search-label-2" | |
| ></form> | |
| <section | |
| id="pass-section-tabpanel-aria-label-repeat-1" | |
| role="tabpanel" | |
| aria-label="My tab panel" | |
| ></section> | |
| <section | |
| id="pass-section-tabpanel-aria-label-repeat-2" | |
| role="tabpanel" | |
| aria-label="My tab panel" | |
| ></section> |
| ["#pass-form-search-aria-label-1"], | ||
| ["#pass-form-search-aria-label-2"], |
There was a problem hiding this comment.
| ["#pass-form-search-aria-label-1"], | |
| ["#pass-form-search-aria-label-2"], | |
| ["#pass-section-tabpanel-aria-label-repeat-1"], | |
| ["#pass-section-tabpanel-aria-label-repeat-2"], |
There was a problem hiding this comment.
Thanks for the suggestion. I tried applying the suggested JSON change by adding these selectors, but the integration test fails with "Element not found".
If I add the suggested elements only to landmark-unique-pass.html, without listing them in landmark-unique-pass.json, the test passes. Would you prefer that approach?
If the rule-matches test already verifies that these elements are not matched, I think we may not need additional integration coverage for this case.
Also, I originally added the form + role="search" + aria-label cases because I did not see existing integration coverage for form/section + accessible name + explicit landmark role passing. I also added similar coverage in test/rule-matches/landmark-unique-matches.js. If that coverage is not useful here, I can remove those cases.
|
Approved for security |
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. ## [4.13.0](v4.12.1...v4.13.0) (2026-08-05) ### Features - **aria-actions:** add aria-actions to allowed ARIA attributes ([#5200](#5200)) ([029655d](029655d)), closes [#4584](#4584) [#5199](#5199), references [#5215](#5215) [#5215](#5215) - **aria-allowed-attr:** flag deprecated ARIA attributes as needs-review ([#5246](#5246)) ([518f3cc](518f3cc)), closes [#3341](#3341) - **aria-prohibited-attr:** allow many elements to be named and disallow label and body from being named ([#5259](#5259)) ([d8b1ea5](d8b1ea5)) - **aria-roles:** add sectionheader and sectionfooter roles ([#5238](#5238)) ([c36c109](c36c109)), closes [#4734](#4734), references [#4734](#4734) - **aria/get-aria-value:** new function to get aria values of a node ([#5109](#5109)) ([a7d8f3e](a7d8f3e)), references [#5042](#5042) - **aria/has-attr-value:** new function to check if node has aria value ([#5136](#5136)) ([61f2624](61f2624)), references [#5109](#5109) - **aria:** support role=image as equivalent to role=img ([#5248](#5248)) ([5aa8aaf](5aa8aaf)), closes [#4656](#4656), references [#5272](#5272) - **checks/aria:** support ARIA element internals properties ([#5172](#5172)) ([9b7f754](9b7f754)) - **checks/label:** support ARIA element internals properties ([#5170](#5170)) ([21c5f8b](21c5f8b)) - **checks/navigation:** support ARIA element internals properties ([#5167](#5167)) ([2c3a98f](2c3a98f)) - **commons/aria:** support ARIA element internals properties ([#5171](#5171)) ([31f09e7](31f09e7)) - **commons/dom:** support ARIA element internals properties ([#5163](#5163)) ([f0a12cf](f0a12cf)) - **commons/forms:** support ARIA element internals properties ([#5165](#5165)) ([27a4686](27a4686)) - **commons/matches/fromPrimative:** deprecate in favor of correct spelling ([#5270](#5270)) ([31cfb2e](31cfb2e)) - **commons/text:** support ARIA element internals properties ([#5169](#5169)) ([e841a33](e841a33)) - **commons/text:** support form-associated labels via element internals ([#5182](#5182)) ([57cfe0a](57cfe0a)), closes [#5045](#5045), references [#5170](#5170) [#5039](#5039) [#5151](#5151) [#5039](#5039) - **dom/getResolvedRefs:** new function to get the resolved virtual nodes of idrefs ([#5151](#5151)) ([489cdea](489cdea)), references [#5109](#5109) - **element-internals:** enable ElementInternals by default ([#5284](#5284)) ([2740d42](2740d42)), closes [#5277](#5277) - **i18n:** Add Swedish locale ([#5190](#5190)) ([dcd13f2](dcd13f2)), references [#5189](#5189) - **matches:** add inSectioningContent, hasChild, and isSummaryForDetails matches ([#5262](#5262)) ([c47cdcd](c47cdcd)) - **rules:** support ARIA element internals properties ([#5168](#5168)) ([065baf7](065baf7)) - **standards/ariaAttrs:** add caseInsensitive property for attributes ([#5224](#5224)) ([bcd791c](bcd791c)) ### Bug Fixes - **aria-allowed-role:** allow roles on a non-details summary ([#5242](#5242)) ([3bd9875](3bd9875)), closes [#3911](#3911), references [#3443](#3443) [#3911](#3911) - **aria-allowed-role:** restrict figure roles with child figcaption ([#5240](#5240)) ([178a635](178a635)), closes [#3443](#3443) - **aria-prohibited-attr:** visible aria-labelledby requires review only ([#5285](#5285)) ([fd6fa9f](fd6fa9f)) - **axe.d.ts:** make enabled property of RuleMetadata optional ([#5129](#5129)) ([90fce18](90fce18)) - **color-contrast:** fix various stacking context bugs ([#5214](#5214)) ([d5e5b04](d5e5b04)), references [#8](#8) [#5213](#5213) - **gather-internals:** handle non-HTMLElement nodes ([#5161](#5161)) ([06e84c3](06e84c3)) - **get-selector:** escape control characters in attribute selectors ([#5273](#5273)) ([4b60ac5](4b60ac5)), closes [#5204](#5204) [#5204](#5204) - **image-alt:** allow whitespace alt on presentational images ([#5218](#5218)) ([c5dd0ef](c5dd0ef)), closes [#5216](#5216) - **landmark-unique:** exclude section/form with non-landmark roles from landmark match ([#5085](#5085)) ([c5fd013](c5fd013)), closes [#4722](#4722) [#5064](#5064) - name the image role in role-img-alt and svg-img-alt metadata ([#5279](#5279)) ([995a269](995a269)), closes [#5272](#5272), references [#5248](#5248) [#5248](#5248) - **standards:** update aria-errormessage and aria-details to be idrefs ([#5157](#5157)) ([fb94f8a](fb94f8a)) This PR was opened by a robot 🤖 🎉
Closes #4722, #5064
Updated
landmark-unique-matchesto change the matching logic forsection/formelements as follows:before
aria-labelaria-labelroleafter
aria-labelaria-labelroleUnrelated to this issue, but I thinksection/formwith an explicit landmark role and no accessible name might also need to match, opened a separate issue for it: #5064