From 4f25150a0ab4d55e3f48a8c6212c57f0ca979123 Mon Sep 17 00:00:00 2001 From: Chris Hutchins Date: Tue, 16 Jun 2026 09:35:19 -0400 Subject: [PATCH 1/2] feat(checks/shared): support ARIA element internals properties Use hasAriaValue in presentational-role so a global ARIA attribute supplied via the reflected AOM property or element internals counts toward role-conflict reporting, not only the HTML attribute. The elementInternals flag gates only the internals source. Closes #5145 --- .../shared/presentational-role-evaluate.js | 6 ++++-- test/checks/shared/presentational-role.js | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/checks/shared/presentational-role-evaluate.js b/lib/checks/shared/presentational-role-evaluate.js index 750320dbab..e8c59f4fc7 100644 --- a/lib/checks/shared/presentational-role-evaluate.js +++ b/lib/checks/shared/presentational-role-evaluate.js @@ -1,4 +1,4 @@ -import { getExplicitRole, getRole } from '../../commons/aria'; +import { getExplicitRole, getRole, hasAriaValue } from '../../commons/aria'; import { getGlobalAriaAttrs } from '../../commons/standards'; import { isFocusable } from '../../commons/dom'; @@ -34,8 +34,10 @@ export default function presentationalRoleEvaluate(node, options, virtualNode) { // user intended to make this presentational so inform them of // problems caused by role conflict resolution + // use hasAriaValue so a global ARIA attribute supplied via the reflected AOM + // property or element internals counts, not only the HTML attribute const hasGlobalAria = getGlobalAriaAttrs().some(attr => - virtualNode.hasAttr(attr) + hasAriaValue(virtualNode, attr) ); const focusable = isFocusable(virtualNode); let messageKey; diff --git a/test/checks/shared/presentational-role.js b/test/checks/shared/presentational-role.js index 74a338b3cc..8da8de5a33 100644 --- a/test/checks/shared/presentational-role.js +++ b/test/checks/shared/presentational-role.js @@ -62,6 +62,27 @@ describe('presentational-role', () => { assert.deepEqual(checkContext._data.messageKey, 'both'); }); + it('detects a global aria attribute set via the reflected AOM property', () => { + const vNode = queryFixture( + '' + ); + // set via the property (no aria-live attribute present) + vNode.actualNode.ariaLive = 'assertive'; + + assert.isFalse(checkEvaluate.call(checkContext, null, null, vNode)); + assert.deepEqual(checkContext._data.messageKey, 'both'); + }); + + it('detects a global aria attribute set via element internals', () => { + const vNode = queryFixture( + 'x' + ); + vNode.actualNode._internals.ariaLive = 'assertive'; + + assert.isFalse(checkEvaluate.call(checkContext, null, null, vNode)); + assert.deepEqual(checkContext._data.messageKey, 'both'); + }); + it('should return false for iframe element with role=none and title', () => { const vNode = queryFixture( '' From 5f26bdac805d66e2afbbe0d7265bf74efd399372 Mon Sep 17 00:00:00 2001 From: Chris Hutchins Date: Tue, 30 Jun 2026 08:51:50 -0400 Subject: [PATCH 2/2] test(checks/shared): confirm element internals do not trigger role conflict resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the presentational-role conversion to hasAttr — per #5162 no browser treats element internals as participating in role conflict resolution. Add a test confirming an internals-set global ARIA attribute does not override a presentational role. --- .../shared/presentational-role-evaluate.js | 6 ++--- test/checks/shared/presentational-role.js | 22 +++++-------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/lib/checks/shared/presentational-role-evaluate.js b/lib/checks/shared/presentational-role-evaluate.js index e8c59f4fc7..750320dbab 100644 --- a/lib/checks/shared/presentational-role-evaluate.js +++ b/lib/checks/shared/presentational-role-evaluate.js @@ -1,4 +1,4 @@ -import { getExplicitRole, getRole, hasAriaValue } from '../../commons/aria'; +import { getExplicitRole, getRole } from '../../commons/aria'; import { getGlobalAriaAttrs } from '../../commons/standards'; import { isFocusable } from '../../commons/dom'; @@ -34,10 +34,8 @@ export default function presentationalRoleEvaluate(node, options, virtualNode) { // user intended to make this presentational so inform them of // problems caused by role conflict resolution - // use hasAriaValue so a global ARIA attribute supplied via the reflected AOM - // property or element internals counts, not only the HTML attribute const hasGlobalAria = getGlobalAriaAttrs().some(attr => - hasAriaValue(virtualNode, attr) + virtualNode.hasAttr(attr) ); const focusable = isFocusable(virtualNode); let messageKey; diff --git a/test/checks/shared/presentational-role.js b/test/checks/shared/presentational-role.js index 8da8de5a33..3d8c3cb674 100644 --- a/test/checks/shared/presentational-role.js +++ b/test/checks/shared/presentational-role.js @@ -62,25 +62,15 @@ describe('presentational-role', () => { assert.deepEqual(checkContext._data.messageKey, 'both'); }); - it('detects a global aria attribute set via the reflected AOM property', () => { + it('does not trigger conflict resolution for a global aria attribute set via element internals', () => { + // internals do not participate in role conflict resolution; no browser + // supports it yet (see issue #5162), so the presentational role sticks const vNode = queryFixture( - '' - ); - // set via the property (no aria-live attribute present) - vNode.actualNode.ariaLive = 'assertive'; - - assert.isFalse(checkEvaluate.call(checkContext, null, null, vNode)); - assert.deepEqual(checkContext._data.messageKey, 'both'); - }); - - it('detects a global aria attribute set via element internals', () => { - const vNode = queryFixture( - 'x' + 'x' ); - vNode.actualNode._internals.ariaLive = 'assertive'; - assert.isFalse(checkEvaluate.call(checkContext, null, null, vNode)); - assert.deepEqual(checkContext._data.messageKey, 'both'); + assert.isTrue(checkEvaluate.call(checkContext, null, null, vNode)); + assert.deepEqual(checkContext._data.role, 'none'); }); it('should return false for iframe element with role=none and title', () => {