Skip to content

feat(aria-allowed/prohibited-attr, aria-required-parent/children): partially support element internals role - #5080

Merged
straker merged 17 commits into
developfrom
elm-internals-implicit-role
May 12, 2026
Merged

feat(aria-allowed/prohibited-attr, aria-required-parent/children): partially support element internals role#5080
straker merged 17 commits into
developfrom
elm-internals-implicit-role

Conversation

@straker

@straker straker commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Note that this can only support rules that don't select for [role] on the element itself, which is why we can't support aria-required-attr yet, 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, for aria-required-parent we 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

@straker
straker requested a review from a team as a code owner April 22, 2026 22:41
Comment thread test/testutils.js
fixture.setAttribute('id', 'fixture');
document.body.insertBefore(fixture, document.body.firstChild);
}
testUtils.fixture = fixture;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread test/commons/aria/implicit-role.js
@@ -1,170 +1,115 @@
describe('aria-required-parent', function () {
'use strict';
describe('aria-required-parent', () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@straker straker changed the title feat(aria-allowed/prohivited-attr, aria-required-parent/children): partially support element internals role feat(aria-allowed/prohibited-attr, aria-required-parent/children): partially support element internals role Apr 22, 2026
Comment thread test/checks/aria/aria-allowed-attr.js Outdated
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>'

@straker straker Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See the changes to the test/testutils.js file for how this test custom element works.

@chutchins25 chutchins25 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.

Nice work! A few questions for you:

  1. 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.

  2. On naming — since internals lands on VirtualNode as public-ish surface (consumers can reach it through any VirtualNode instance), renaming it later would be a breaking change. Was elementInternals considered as a more explicit name, or is the short form intentional?

Comment thread lib/core/utils/get-element-internals.js Outdated
Comment thread test/checks/aria/required-parent.js
Comment thread test/core/utils/get-element-internals.js Outdated
@straker

straker commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author
  1. 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.

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.

  1. On naming — since internals lands on VirtualNode as public-ish surface (consumers can reach it through any VirtualNode instance), renaming it later would be a breaking change. Was elementInternals considered as a more explicit name, or is the short form intentional?

I can rename it

@chutchins25
chutchins25 self-requested a review April 28, 2026 15:51

@chutchins25 chutchins25 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.

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.

Comment thread test/checks/aria/aria-prohibited-attr.js Outdated
Small easy change. Applied it.

@chutchins25 chutchins25 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.

Line Numbers got messed up on previous review for suggested changes.

Comment thread test/checks/aria/aria-prohibited-attr.js Outdated
chutchins25
chutchins25 previously approved these changes Apr 29, 2026

@chutchins25 chutchins25 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.

Looks good to me.

WilcoFiers
WilcoFiers previously approved these changes May 6, 2026

@WilcoFiers WilcoFiers 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.

Looks solid. Couple nitpicky things. Leaving it to you if you want to pick those up.

Comment thread lib/core/utils/get-element-internals.js Outdated
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)) {

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.

What about SVG or MathML?

@straker straker May 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread lib/core/base/virtual-node/virtual-node.js
Comment thread lib/commons/aria/implicit-role.js Outdated
Comment thread test/commons/aria/implicit-role.js
Comment thread lib/core/base/virtual-node/virtual-node.js
Comment thread test/checks/aria/aria-allowed-attr.js Outdated
@straker
straker dismissed stale reviews from WilcoFiers and chutchins25 via 2bac0e2 May 6, 2026 15:05
@straker
straker force-pushed the elm-internals-implicit-role branch from e8a0b99 to 2a1dfe1 Compare May 7, 2026 15:39
@straker
straker merged commit 417b48a into develop May 12, 2026
23 checks passed
@straker
straker deleted the elm-internals-implicit-role branch May 12, 2026 16:16
straker added a commit that referenced this pull request May 19, 2026
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
WilcoFiers added a commit that referenced this pull request Jun 1, 2026
##
[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 🤖 🎉
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants