diff --git a/lib/commons/text/form-control-value.js b/lib/commons/text/form-control-value.js index e892781aa5..b4ef057cc0 100644 --- a/lib/commons/text/form-control-value.js +++ b/lib/commons/text/form-control-value.js @@ -13,7 +13,7 @@ import isHiddenForEveryone from '../dom/is-hidden-for-everyone'; import { nodeLookup, querySelectorAll } from '../../core/utils'; import log from '../../core/log'; -const controlValueRoles = [ +export const controlValueRoles = [ 'textbox', 'progressbar', 'scrollbar', diff --git a/lib/commons/text/subtree-text.js b/lib/commons/text/subtree-text.js index a3d669f342..09fbe6a581 100644 --- a/lib/commons/text/subtree-text.js +++ b/lib/commons/text/subtree-text.js @@ -1,8 +1,10 @@ import accessibleTextVirtual from './accessible-text-virtual'; import namedFromContents from '../aria/named-from-contents'; import getOwnedVirtual from '../aria/get-owned-virtual'; +import getRole from '../aria/get-role'; import getElementsByContentType from '../standards/get-elements-by-content-type'; import getElementSpec from '../standards/get-element-spec'; +import { controlValueRoles } from './form-control-value'; /** * Get the accessible text for an element that can get its name from content @@ -16,20 +18,23 @@ function subtreeText(virtualNode, context = {}) { const { alreadyProcessed } = accessibleTextVirtual; context.startNode = context.startNode || virtualNode; const { strict, inControlContext, inLabelledByContext } = context; + const role = getRole(virtualNode); const { contentTypes } = getElementSpec(virtualNode, { noMatchAccessibleName: true }); if ( alreadyProcessed(virtualNode, context) || virtualNode.props.nodeType !== 1 || - contentTypes?.includes('embedded') // canvas, video, etc + contentTypes?.includes('embedded') || // canvas, video, etc + controlValueRoles.includes(role) ) { return ''; } if ( - !namedFromContents(virtualNode, { strict }) && - !context.subtreeDescendant + !context.subtreeDescendant && + !context.inLabelledByContext && + !namedFromContents(virtualNode, { strict }) ) { return ''; } @@ -40,6 +45,7 @@ function subtreeText(virtualNode, context = {}) { * chosen to ignore this, but only for direct content, not for labels / aria-labelledby. * That way in `a[href] > article > #text` the text is used for the accessible name, * See: https://github.com/dequelabs/axe-core/issues/1461 + * See: https://github.com/w3c/accname/issues/120 */ if (!strict) { const subtreeDescendant = !inControlContext && !inLabelledByContext; diff --git a/lib/commons/text/unsupported.js b/lib/commons/text/unsupported.js index 04d63f1836..197b41600f 100644 --- a/lib/commons/text/unsupported.js +++ b/lib/commons/text/unsupported.js @@ -1,5 +1,7 @@ -const unsupported = { - accessibleNameFromFieldValue: ['combobox', 'listbox', 'progressbar'] +export default { + // Element's who's value is not consistently picked up in the accessible name + // Supported in Chrome 114, Firefox 115, but not Safari 16.5: + // + //
+ accessibleNameFromFieldValue: ['progressbar'] }; - -export default unsupported; diff --git a/test/commons/text/accessible-text.js b/test/commons/text/accessible-text.js index 3fd2ca785e..fd0552f0cd 100644 --- a/test/commons/text/accessible-text.js +++ b/test/commons/text/accessible-text.js @@ -1,6 +1,6 @@ describe('text.accessibleTextVirtual', () => { const fixture = document.getElementById('fixture'); - const shadowSupport = axe.testUtils.shadowSupport; + const { html, shadowSupport } = axe.testUtils; afterEach(() => { fixture.innerHTML = ''; @@ -9,26 +9,32 @@ describe('text.accessibleTextVirtual', () => { it('is called through accessibleText with a DOM node', () => { const accessibleText = axe.commons.text.accessibleText; - fixture.innerHTML = ''; + fixture.innerHTML = html` `; axe.testUtils.flatTreeSetup(fixture); const target = fixture.querySelector('input'); assert.equal(accessibleText(target), ''); }); it('should match the first example from the ARIA spec', () => { - fixture.innerHTML = - ''; + fixture.innerHTML = html` + + `; axe.testUtils.flatTreeSetup(fixture); const rule2a = axe.utils.querySelectorAll(axe._tree, '#rule2a')[0]; @@ -39,29 +45,35 @@ describe('text.accessibleTextVirtual', () => { }); it('should match the second example from the ARIA spec', () => { - fixture.innerHTML = - '
' + - ' Meeting alarms' + - ' ' + - '
' + - '
' + - ' ' + - ' ' + - ' ' + - '
'; + fixture.innerHTML = html` +
+ Meeting alarms + + +
+ +
+ + + +
+ `; axe.testUtils.flatTreeSetup(fixture); const rule2a = axe.utils.querySelectorAll(axe._tree, '#beep')[0]; const rule2b = axe.utils.querySelectorAll(axe._tree, '#flash')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(rule2a), 'Beep'); - // Chrome 72: "Flash the screen 3 times" - // Firefox 62: "Flash the screen 3 times" - // Safari 12.0: "Flash the screen 3 times" assert.equal( axe.commons.text.accessibleTextVirtual(rule2b), 'Flash the screen 3 times' @@ -69,12 +81,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should use aria-labelledby if present', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; @@ -85,12 +107,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should use recusive aria-labelledby properly', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; @@ -101,12 +133,15 @@ describe('text.accessibleTextVirtual', () => { }); it('should include hidden text referred to with aria-labelledby', () => { - fixture.innerHTML = - '' + - '' + - ''; + fixture.innerHTML = html` + + + ' + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; @@ -117,8 +152,9 @@ describe('text.accessibleTextVirtual', () => { }); it('should allow setting the initial includeHidden value', () => { - fixture.innerHTML = - ''; + fixture.innerHTML = html` + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#lbl1')[0]; @@ -138,12 +174,16 @@ describe('text.accessibleTextVirtual', () => { }); it('should use aria-label if present with no labelledby', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; @@ -151,13 +191,16 @@ describe('text.accessibleTextVirtual', () => { }); it('should use alt on imgs with no ARIA', () => { - fixture.innerHTML = - '
This is of everything
' + - 'Alt text goes here' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is of + everything +
+ Alt text goes here +
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; @@ -168,13 +211,16 @@ describe('text.accessibleTextVirtual', () => { }); it('should use alt on image inputs with no ARIA', () => { - fixture.innerHTML = - '
This is of everything
' + - '' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is of + everything +
+ +
This is a label
+ + ' + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; @@ -185,13 +231,16 @@ describe('text.accessibleTextVirtual', () => { }); it('should use not use alt on text inputs with no ARIA', () => { - fixture.innerHTML = - '
This is of everything
' + - '' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is of + everything +
+ +
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; @@ -199,12 +248,15 @@ describe('text.accessibleTextVirtual', () => { }); it('should use HTML label if no ARIA information', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is of + everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; @@ -212,12 +264,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should handle last ditch title attribute', () => { - fixture.innerHTML = - '
This is of
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; @@ -228,12 +290,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should handle totally empty elements', () => { - fixture.innerHTML = - '
This is of
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; @@ -244,20 +316,28 @@ describe('text.accessibleTextVirtual', () => { }); it('should handle author name-from roles properly', () => { - fixture.innerHTML = - '
This is ' + - ' ' + - ' of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; - // Chrome 86: This is This is a label of - // Firefox 82: This is ARIA Label everything - // Safari 14.0: This is This is a label of everything + // Chrome 114: "This is the value of " + // Firefox 115: "This is ARIA Label the value everything" + // Safari 16.5: This is the value This is a label of everything assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is This is a label of everything' @@ -265,9 +345,11 @@ describe('text.accessibleTextVirtual', () => { }); it('should only show each node once when label is before input', () => { - fixture.innerHTML = - '
' + - '
'; + fixture.innerHTML = html` +
+ +
+ `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal( @@ -277,10 +359,12 @@ describe('text.accessibleTextVirtual', () => { }); it('should only show each node once when label follows input', () => { - fixture.innerHTML = - '
' + - '
' + - ''; + fixture.innerHTML = html` +
+ +
+ + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal( @@ -290,12 +374,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should handle nested inputs in normal context', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; @@ -306,18 +400,28 @@ describe('text.accessibleTextVirtual', () => { }); it('should use handle nested inputs properly in labelledby context', () => { - // Chrome 72: This is This is a label of everything - // Firefox 62: This is ARIA Label the value of everything - // Safari 12.0: THis is This is a label of everything - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; + // Chrome 114: This is the value of everything + // Firefox 115: This is ARIA Label the value of everything + // Safari 16.5: THis is This is a label of everything assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is ARIA Label of everything' @@ -325,12 +429,15 @@ describe('text.accessibleTextVirtual', () => { }); it('should use ignore hidden inputs', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is of + everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; @@ -341,18 +448,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should use handle inputs with no type as if they were text inputs', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; - // Chrome 70: "This is This is a label of everything" - // Firefox 62: "This is the value of everything" - // Safari 12.0: "This is This is a label of everything" + // Chrome 114: "This is the value of everything" + // Firefox 115: "This is the value of everything" + // Safari 16.5: "This is This is a label of everything" assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is the value of everything' @@ -360,39 +471,49 @@ describe('text.accessibleTextVirtual', () => { }); it('should use handle nested selects properly in labelledby context', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; - // Chrome 70: "This is This is a label of everything" - // Firefox 62: "This is of everything" - // Safari 12.0: "This is first third label of" + // Chrome 114: "This is first third of everything" + // Firefox 115: "This is of everything" + // Safari 16.5: "This is first third of everything" assert.equal( axe.commons.text.accessibleTextVirtual(target), - 'This is of everything' + 'This is first third of everything' ); }); it('should use handle nested textareas properly in labelledby context', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; - // Chrome 70: "This is This is a label of everything" - // Firefox 62: "This is ARIA Label the value of everything" - // Safari 12.0: "This is This is a label of everything" + // Chrome 114: "This is the value of everything" + // Firefox 115: "This is the value of everything" + // Safari 16.5: "This is This is a label of everything" assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is the value of everything' @@ -400,13 +521,21 @@ describe('text.accessibleTextVirtual', () => { }); it('should use handle ARIA labels properly in labelledby context', () => { - fixture.innerHTML = - '
This span' + - ' is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html`
+ This + span + is + + of everything +
+
This is a label
+ + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; @@ -417,73 +546,82 @@ describe('text.accessibleTextVirtual', () => { }); it('should come up empty if input is labeled only by select options', () => { - fixture.innerHTML = - '' + - ''; + fixture.innerHTML = html` + + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - // Chrome 70: "" - // Firefox 62: "Chosen" - // Safari 12.0: "Chosen" - assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); + // Chrome 114: "Chosen" + // Firefox 115: "Chosen" + // Safari 16.5: "Chosen" + assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Chosen'); }); it("should be empty if input is labeled by labeled select (ref'd string labels have spotty support)", () => { - fixture.innerHTML = - '' + - '' + - ''; + fixture.innerHTML = html` + + + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); + // Chrome 114: "Chosen" + // Firefox 115: "Chosen" + // Safari 16.5: "Chosen" + assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Chosen'); }); it('should be empty for an empty label wrapping a select', () => { - fixture.innerHTML = - ''; + fixture.innerHTML = html` + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); }); it('should not return select options if input is aria-labelled by a select', () => { - fixture.innerHTML = - '' + - ''; + fixture.innerHTML = html` + + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - // Chrome 70: "" - // Firefox 62: "" - // Safari 12.0: "Chosen" - assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); + // Chrome 114: "Chosen" + // Firefox 115: "Chosen" + // Safari 16.5: "Chosen" + assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Chosen'); }); it('shoud properly fall back to title', () => { - fixture.innerHTML = ''; + fixture.innerHTML = html` + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; @@ -491,7 +629,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should give text even for role=presentation on anchors', () => { - fixture.innerHTML = 'Hello'; + fixture.innerHTML = html` Hello `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; @@ -499,7 +637,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should give text even for role=presentation on buttons', () => { - fixture.innerHTML = ''; + fixture.innerHTML = html` `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; @@ -507,21 +645,21 @@ describe('text.accessibleTextVirtual', () => { }); it('should give text even for role=presentation on summary', () => { - fixture.innerHTML = 'Hello'; + fixture.innerHTML = html` Hello `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'summary')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); it('shoud properly fall back to title', () => { - fixture.innerHTML = ''; + fixture.innerHTML = html` `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); it('should give text even for role=none on anchors', () => { - fixture.innerHTML = 'Hello'; + fixture.innerHTML = html` Hello `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; @@ -529,7 +667,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should give text even for role=none on buttons', () => { - fixture.innerHTML = ''; + fixture.innerHTML = html` `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; @@ -537,7 +675,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should give text even for role=none on summary', () => { - fixture.innerHTML = 'Hello'; + fixture.innerHTML = html` Hello `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'summary')[0]; @@ -545,7 +683,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should not add extra spaces around phrasing elements', () => { - fixture.innerHTML = 'HelloWorld'; + fixture.innerHTML = html` HelloWorld `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; @@ -553,7 +691,12 @@ describe('text.accessibleTextVirtual', () => { }); it('should add spaces around non-phrasing elements', () => { - fixture.innerHTML = 'Hello
World
'; + fixture.innerHTML = html` + Hello +
World
+ `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; @@ -570,7 +713,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should use