feat(aria-allowed/prohibited-attr, aria-required-parent/children): partially support element internals role - #5080
Conversation
…pport element internals implicit role
| fixture.setAttribute('id', 'fixture'); | ||
| document.body.insertBefore(fixture, document.body.firstChild); | ||
| } | ||
| testUtils.fixture = fixture; |
There was a problem hiding this comment.
Adding fixture to the testutils objects makes getting it a bit easier instead of having to do const fixture = document.querySelector('#fixture') in every test that needs it.
| @@ -1,170 +1,115 @@ | |||
| describe('aria-required-parent', function () { | |||
| 'use strict'; | |||
| describe('aria-required-parent', () => { | |||
There was a problem hiding this comment.
Most of this file is cleaning it up to match the new es6 style. Bottom of the file has the newest tests.
| } else if (role || hasGlobalAriaOrFocusable) { | ||
| const attr = globalAriaAttr || 'tabindex'; | ||
| const attr = | ||
| globalAriaAttr || (vNode.hasAttr('tabindex') ? 'tabindex' : undefined); |
There was a problem hiding this comment.
This code assumed that if an element had a role it also had a global aria attr available, which wasn't true. This lead to a custom element with an internal role to always have [tabindex] added to the selector even though that attr didn't exist.
| describe('ElementInternals', () => { | ||
| it('should detect incorrectly used attributes', () => { | ||
| const vNode = queryFixture( | ||
| '<testutils-element with-role="link" id="target" tabindex="1" aria-selected="true"></testutils-element>' |
There was a problem hiding this comment.
See the changes to the test/testutils.js file for how this test custom element works.
chutchins25
left a comment
There was a problem hiding this comment.
Nice work! A few questions for you:
-
Are we ok bundling large refactors like this in with the feature work? Would it be worth splitting the ES6 cleanup into its own PR next time to keep the feature diff focused? CLAUDE.md §3 leans that way, but curious if there's a reason the bundle made sense here.
-
On naming — since
internalslands onVirtualNodeas public-ish surface (consumers can reach it through any VirtualNode instance), renaming it later would be a breaking change. WaselementInternalsconsidered as a more explicit name, or is the short form intentional?
Co-authored-by: Chris Hutchins <[email protected]>
Yep. Any time we edit files that use the old syntax we convert the file first to the new syntax and then add the changes. It allows us to slowly convert the files over time without having to take the time to do it on it's own.
I can rename it |
chutchins25
left a comment
There was a problem hiding this comment.
Took another pass. Naming and bundling questions from the earlier review are resolved on my end — thanks for the rename. Two small fixture tag-mismatch nits noted inline; not blockers.
Small easy change. Applied it.
chutchins25
left a comment
There was a problem hiding this comment.
Line Numbers got messed up on previous review for suggested changes.
Another quick fix. Applying.
WilcoFiers
left a comment
There was a problem hiding this comment.
Looks solid. Couple nitpicky things. Leaving it to you if you want to pick those up.
| export default function getElementInternals(node) { | ||
| // internals can only be attached to custom-elements | ||
| // trying to do otherwise results in an error "Cannot attach ElementInternals to a customized built-in or non-custom element" | ||
| if (isHtmlElement(node)) { |
There was a problem hiding this comment.
What about SVG or MathML?
There was a problem hiding this comment.
Both apparently don't define the function in any browser (Chrome, Safari, Firefox). Trying to attach internals reports it's not a function. I'm not sure how we would short circuit svg or math child elements though since we don't maintain a list of those anywhere.
Co-authored-by: Wilco Fiers <[email protected]>
e8a0b99 to
2a1dfe1
Compare
Testing this was interesting as it doesn't get added to the `axe` object. I needed to test the logic of the code but the way we have to build this for the extension script means we only get the returned object when the file is injected, so I split out the logic into it's own file that is built separately and tested. This uses the `testutils` changes from #5080, so will fail until that is merged into develop Closes: #5041
## [4.12.0](v4.11.4...v4.12.0) (2026-06-01) ### Features - add gather-internals.js external script ([#5099](#5099)) ([c61d58b](c61d58b)), closes [#5080](#5080) - **aria-allowed/prohibited-attr, aria-required-parent/children:** partially support element internals role ([#5080](#5080)) ([417b48a](417b48a)), closes [#5039](#5039) [#4259](#4259) - **axe.externalAPIs:** add public api for setting elementInternal data ([#5105](#5105)) ([63bab8f](63bab8f)) - **core:** expose normalizeRunOptions ([#4998](#4998)) ([b8e6a59](b8e6a59)) - expose axe.resetLocale() to restore the default locale ([#5108](#5108)) ([c2b5292](c2b5292)), closes [#5107](#5107) - **getRules:** include rule enabled state in returned objects ([#5118](#5118)) ([75bf772](75bf772)), closes [#5116](#5116) - **list,listitem:** support element internals role ([#5119](#5119)) ([7d9d696](7d9d696)) - **new-rule:** check that aria-tab have an accessible name ([#5001](#5001)) ([0d4e4e7](0d4e4e7)), closes [#4842](#4842) - **rules:** deprecate landmark-complementary-is-top-level rules ([#4992](#4992)) ([9e09139](9e09139)), closes [#4950](#4950) - **utils:** add `getElementInternals` function ([#5077](#5077)) ([1c15f82](1c15f82)) ### Bug Fixes - **aria-allowed-attr:** restrict br and wbr elements to aria-hidden only ([#4974](#4974)) ([c6245e7](c6245e7)) - **aria-conditional-attr:** add support for radio ([#5100](#5100)) ([8223c98](8223c98)) - **aria-valid-attr-value:** handle multiple aria-errormessage IDs ([#4973](#4973)) ([0489e30](0489e30)) - **aria:** prevent getOwnedVirtual from returning duplicate nodes ([#4987](#4987)) ([48ca955](48ca955)), closes [#4840](#4840) - **commons/text:** exclude natively hidden elements from aria-labelledby accessible name ([#5076](#5076)) ([ea7202c](ea7202c)), closes [#4704](#4704) - **DqElement:** avoid calling constructors with cloneNode ([#5013](#5013)) ([0281fa1](0281fa1)) - **existing-rule:** aria-busy now shows an error message for a use with unallowed children ([#5017](#5017)) ([2067b87](2067b87)) - **helpUrl:** ensure axe.configure always updates the help URLs ([#5114](#5114)) ([c4f60ff](c4f60ff)) - **label-content-name-mismatch:** match visible text with aria-label and exclude invisible text ([#5096](#5096)) ([3a012a1](3a012a1)) - **locale:** ensure all subtags are correctly set ([#5112](#5112)) ([13005ed](13005ed)) - **scrollable-region-focusable:** clarify the issue is in safari ([#4995](#4995)) ([4ec5211](4ec5211)), closes [WebKit#190870](https://github.com/dequelabs/WebKit/issues/190870) [WebKit#277290](https://github.com/dequelabs/WebKit/issues/277290) - **scrollable-region-focusable:** do not fail scroll areas when all content is visible without scrolling ([#4993](#4993)) ([838707a](838707a)) - **target-size:** determine offset using clientRects if target is display:inline ([#5012](#5012)) ([a4b8091](a4b8091)) - **target-size:** ignore position: fixed elements that are offscreen when page is scrolled ([#5066](#5066)) ([1229a6e](1229a6e)), closes [#5065](#5065) - **target-size:** ignore widgets that are inline with other inline elements ([#5000](#5000)) ([a8dd81b](a8dd81b)) - **utils/getAncestry:** escape node name ([#5079](#5079)) ([d1fabaa](d1fabaa)), closes [#5078](#5078) - **utils:** Add null check to parseCrossOriginStylesheet, closes [#5074](#5074) ([#5075](#5075)) ([f12ef32](f12ef32)) - **utils:** update isShadowRoot to use spec-compliant custom element regex ([#5059](#5059)) ([edc6ce2](edc6ce2)), closes [#5030](#5030) This PR was opened by a robot 🤖 🎉
Note that this can only support rules that don't select for
[role]on the element itself, which is why we can't supportaria-required-attryet, or any rules that look for specific roles (e.g.aria-command-name). Additionally we can only test the supported rules partially as those rules look for an element with[role]. For example, foraria-required-parentwe can test that the parent of an element with[role]has a required internal role, but we cannot test a child with an internal role has a required parent.Fully supporting rules can only be supported once we've figured out how to update our selectors to look at more than just CSS selectors (planned but not fully spec'd out yet).
Closes: #5039
Closes: #4259