Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
AX: incorrect accessible name from multiple label elements
https://bugs.webkit.org/show_bug.cgi?id=152663
rdar://problem/24033482

Reviewed by Chris Fleizach.

Per https://www.w3.org/TR/html-aam-1.0/#input-type-text-input-type-password-input-type-number-input-type-search-input-type-tel-input-type-email-input-type-url-and-textarea-element-accessible-name-computation:

> Otherwise use the associated label element(s) accessible name(s) - if more than one label is associated; concatenate by DOM order, delimited by spaces.

Prior to this patch, we only used accessible text from the first associated label element.

* LayoutTests/accessibility-isolated-tree/TestExpectations:
* LayoutTests/accessibility/multiple-label-input-expected.txt: Added.
* LayoutTests/accessibility/multiple-label-input.html: Added.
* LayoutTests/accessibility/title-ui-element-correctness-expected.txt:
* LayoutTests/accessibility/title-ui-element-correctness.html:
* LayoutTests/platform/ios/accessibility/multiple-label-input-expected.txt: Added.
* Source/WebCore/accessibility/AccessibilityNodeObject.cpp:
(WebCore::labelForNode):
(WebCore::AccessibilityNodeObject::checkboxOrRadioRect const):
(WebCore::AccessibilityNodeObject::correspondingLabelForControlElement const):
(WebCore::AccessibilityNodeObject::textForLabelElements const):
(WebCore::AccessibilityNodeObject::titleElementText const):
(WebCore::AccessibilityNodeObject::titleUIElement const):
(WebCore::AccessibilityNodeObject::exposesTitleUIElement const):
(WebCore::AccessibilityNodeObject::title const):
(WebCore::AccessibilityNodeObject::labelForElement const): Deleted.
(WebCore::AccessibilityNodeObject::textForLabelElement const): Deleted.
* Source/WebCore/accessibility/AccessibilityNodeObject.h:
* Source/WebCore/accessibility/cocoa/AccessibilityObjectCocoa.mm:
(WebCore::AccessibilityObject::descriptionAttributeValue const):
(WebCore::AccessibilityObject::helpTextAttributeValue const):
* Source/WebCore/dom/TreeScope.cpp:
(WebCore::TreeScope::labelElementForId): Deleted.
* Source/WebCore/dom/TreeScope.h:
* Source/WebCore/dom/TreeScopeOrderedMap.cpp:
(WebCore:: const):
(WebCore::TreeScopeOrderedMap::getElementByLabelForAttribute const): Deleted.
* Source/WebCore/dom/TreeScopeOrderedMap.h:

Canonical link: https://commits.webkit.org/266151@main
  • Loading branch information
twilco committed Jul 19, 2023
1 parent 7bbea20 commit 3c4dcc0
Show file tree
Hide file tree
Showing 15 changed files with 313 additions and 144 deletions.
4 changes: 3 additions & 1 deletion LayoutTests/accessibility-isolated-tree/TestExpectations
Expand Up @@ -9,6 +9,9 @@ accessibility/slider-with-lost-renderer.html [ Timeout ]
accessibility/mac/combobox-activedescendant-notifications.html [ Timeout ]
accessibility/mac/relationships-in-frames.html [ Timeout ]

# Times out because `innerText` changes on linked <label>s don't update cached AXTitle value.
accessibility/multiple-label-input.html [ Timeout ]

# Text failures
accessibility/aria-current.html [ Failure ]
accessibility/aria-labelledby-overrides-aria-label.html [ Failure ]
Expand Down Expand Up @@ -51,7 +54,6 @@ accessibility/scroll-to-make-visible-nested.html [ Failure ]
accessibility/scroll-to-make-visible-with-subfocus.html [ Failure ]
accessibility/slider-with-lost-renderer.html [ Failure ]
accessibility/textarea-insertion-point-line-number.html [ Failure ]
accessibility/title-ui-element-correctness.html [ Failure ]
accessibility/text-alternative-calculation-hidden-nodes.html [ Failure ]
accessibility/math-multiscript-attributes.html [ Failure ]

Expand Down
19 changes: 19 additions & 0 deletions LayoutTests/accessibility/multiple-label-input-expected.txt
@@ -0,0 +1,19 @@
This test ensures that inputs with multiple labels have the correct accessibility text exposed.

Initial AX text:
AXTitle: Enter your color
AXDescription:
AXHelp:
document.getElementById('label2').remove();
AXTitle: Enter color
AXDescription:
AXHelp:
document.getElementById('label3').innerText = 'choice';
AXTitle: Enter choice
AXDescription:
AXHelp:

PASS successfullyParsed is true

TEST COMPLETE
Enter choice
46 changes: 46 additions & 0 deletions LayoutTests/accessibility/multiple-label-input.html
@@ -0,0 +1,46 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<script src="../resources/accessibility-helper.js"></script>
<script src="../resources/js-test.js"></script>
</head>
<body>

<input id="input" type="text">
<label for="input">Enter</label>
<label id="label2" for="input">your</label>
<label id="label3" for="input">color</label>

<script>
var output = "This test ensures that inputs with multiple labels have the correct accessibility text exposed.\n\n";

if (window.accessibilityController) {
window.jsTestIsAsync = true;

var input = accessibilityController.accessibleElementById("input");
output += `Initial AX text: \n${platformTextAlternatives(input)}\n`;

output += evalAndReturn("document.getElementById('label2').remove();");
var newText;
setTimeout(async function() {
await waitFor(() => {
newText = platformTextAlternatives(input);
return !newText.includes("your");
});
output += `${newText}\n`;

output += evalAndReturn("document.getElementById('label3').innerText = 'choice';");
await waitFor(() => {
newText = platformTextAlternatives(input);
return !newText.includes("color");
});
output += `${newText}\n`;

debug(output);
finishJSTest();
}, 0);
}
</script>
</body>
</html>

35 changes: 19 additions & 16 deletions LayoutTests/accessibility/title-ui-element-correctness-expected.txt
@@ -1,22 +1,25 @@
This tests that titleUIElement works correctly even when things change dynamically.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
PASS: axElement('control1').titleUIElement().isEqual(axElement('label1')) === true
PASS: axElement('control2').titleUIElement().isEqual(axElement('label2')) === true
PASS: hasTitleUIElement(axElement('control3')) === false
document.getElementById('label3').setAttribute('for', 'control3');
PASS: axElement('control3').titleUIElement().isEqual(axElement('label3')) === true
label4Element = createLabelWithIdAndForAttribute('label4', 'control4');
PASS: hasTitleUIElement(axElement('control4')) === false
document.getElementById('container').appendChild(label4Element);
PASS: hasTitleUIElement(axElement('control4')) === true
PASS: axElement('control4').titleUIElement().isEqual(axElement('label4')) === true
label4Element.parentElement.removeChild(label4Element);
PASS: hasTitleUIElement(axElement('control4')) === false
PASS: hasTitleUIElement(axElement('control5')) === false
reparentNodeIntoContainer(document.getElementById('control5'), document.getElementById('label5'));
PASS: axElement('control5').titleUIElement() != null === true
PASS: axElement('control5').titleUIElement().isEqual(axElement('label5')) === true
PASS: hasTitleUIElement(axElement('control6')) === false
document.getElementById('label6c').remove()
PASS: axElement('control6').titleUIElement().isEqual(axElement('label6b')) === true


PASS axElement('control1').titleUIElement().isEqual(axElement('label1')) is true
PASS axElement('control2').titleUIElement().isEqual(axElement('label2')) is true
PASS hasTitleUIElement(axElement('control3')) is false
PASS document.getElementById('label3').setAttribute('for', 'control3'); axElement('control3').titleUIElement().isEqual(axElement('label3')) is true
PASS var label4Element = createLabelWithIdAndForAttribute('label4', 'control4'); hasTitleUIElement(axElement('control4')) is false
PASS document.getElementById('container').appendChild(label4Element); hasTitleUIElement(axElement('control4')) is true
PASS axElement('control4').titleUIElement().isEqual(axElement('label4')) is true
PASS label4Element.parentElement.removeChild(label4Element); hasTitleUIElement(axElement('control4')) is false
PASS hasTitleUIElement(axElement('control5')) is false
PASS reparentNodeIntoContainer(document.getElementById('control5'), document.getElementById('label5')); axElement('control5').titleUIElement() != null is true
PASS axElement('control5').titleUIElement().isEqual(axElement('label5')) is true
PASS axElement('control6').titleUIElement().isEqual(axElement('label6b')) is true
PASS newLabel6Element = createLabelWithIdAndForAttribute('label6a', 'control6'); document.body.insertBefore(newLabel6Element, document.body.firstChild); axElement('control6').titleUIElement().isEqual(axElement('label6a')) is true
PASS document.body.removeChild(newLabel6Element); axElement('control6').titleUIElement().isEqual(axElement('label6b')) is true
PASS successfullyParsed is true

TEST COMPLETE
Expand Down
83 changes: 46 additions & 37 deletions LayoutTests/accessibility/title-ui-element-correctness.html
@@ -1,10 +1,12 @@
<!DOCTYPE HTML>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<body>
<head>
<script src="../resources/accessibility-helper.js"></script>
<script src="../resources/js-test.js"></script>
</head>
<body>

<div id="container">

<div>
<label id="label1" for="control1">Label 1</label>
<input id="control1" type="text">
Expand Down Expand Up @@ -40,11 +42,12 @@
</div>
</div>

<div id="console"></div>
<script>
description("This tests that titleUIElement works correctly even when things change dynamically.");
var output = "This tests that titleUIElement works correctly even when things change dynamically.\n\n";

if (window.accessibilityController) {
window.jsTestIsAsync = true;

if (window.testRunner && window.accessibilityController) {
function hasTitleUIElement(axElement) {
var label1 = accessibilityController.accessibleElementById("label1");
var titleUIElement = axElement.titleUIElement();
Expand All @@ -70,40 +73,46 @@
return accessibilityController.accessibleElementById(id);
}

shouldBe("axElement('control1').titleUIElement().isEqual(axElement('label1'))", "true");

shouldBe("axElement('control2').titleUIElement().isEqual(axElement('label2'))", "true");
output += expect("axElement('control1').titleUIElement().isEqual(axElement('label1'))", "true");
output += expect("axElement('control2').titleUIElement().isEqual(axElement('label2'))", "true");

var label4Element;
// Test changing the "for" attribute dynamically.
shouldBe("hasTitleUIElement(axElement('control3'))", "false");
shouldBe("document.getElementById('label3').setAttribute('for', 'control3'); axElement('control3').titleUIElement().isEqual(axElement('label3'))", "true");

// Test unattached label element that's subsequently attached.
var label4Element = document.createElement("label");
label4Element.id = "label4";
label4Element.setAttribute("for", "control4");
label4Element.innerText = "Label 4";
shouldBe("var label4Element = createLabelWithIdAndForAttribute('label4', 'control4'); hasTitleUIElement(axElement('control4'))", "false");
shouldBe("document.getElementById('container').appendChild(label4Element); hasTitleUIElement(axElement('control4'))", "true");
shouldBe("axElement('control4').titleUIElement().isEqual(axElement('label4'))", "true");

// Test what happens when the label is detached.
shouldBe("label4Element.parentElement.removeChild(label4Element); hasTitleUIElement(axElement('control4'))", "false");

// Test label that gets a control reparented into it.
shouldBe("hasTitleUIElement(axElement('control5'))", "false");

shouldBe("reparentNodeIntoContainer(document.getElementById('control5'), document.getElementById('label5')); axElement('control5').titleUIElement() != null", "true");
shouldBe("axElement('control5').titleUIElement().isEqual(axElement('label5'))", "true");

// Make sure the first label is returned, even as labels are added and removed.
shouldBe("axElement('control6').titleUIElement().isEqual(axElement('label6b'))", "true");
shouldBe("newLabel6Element = createLabelWithIdAndForAttribute('label6a', 'control6'); document.body.insertBefore(newLabel6Element, document.body.firstChild); axElement('control6').titleUIElement().isEqual(axElement('label6a'))", "true");
shouldBe("document.body.removeChild(newLabel6Element); axElement('control6').titleUIElement().isEqual(axElement('label6b'))", "true");

document.getElementById('container').style.display = 'none';
output += expect("hasTitleUIElement(axElement('control3'))", "false");
output += evalAndReturn("document.getElementById('label3').setAttribute('for', 'control3');");
setTimeout(async function() {
output += await expectAsync("axElement('control3').titleUIElement().isEqual(axElement('label3'))", "true");

// Test unattached label element that's subsequently attached.
output += evalAndReturn("label4Element = createLabelWithIdAndForAttribute('label4', 'control4');");
output += await expectAsync("hasTitleUIElement(axElement('control4'))", "false");
output += evalAndReturn("document.getElementById('container').appendChild(label4Element);");
output += await expectAsync("hasTitleUIElement(axElement('control4'))", "true");
output += await expectAsync("axElement('control4').titleUIElement().isEqual(axElement('label4'))", "true");

// Test what happens when the label is detached.
output += evalAndReturn("label4Element.parentElement.removeChild(label4Element);");
output += await expectAsync("hasTitleUIElement(axElement('control4'))", "false");

// Test label that gets a control reparented into it.
output += expect("hasTitleUIElement(axElement('control5'))", "false");
output += evalAndReturn("reparentNodeIntoContainer(document.getElementById('control5'), document.getElementById('label5'));");
output += await expectAsync("axElement('control5').titleUIElement() != null", "true");
output += await expectAsync("axElement('control5').titleUIElement().isEqual(axElement('label5'))", "true");

// On Cocoa platforms, when there is no singular title UI element for an element (e.g. multiple <label for="some-id">), none should be returned.
const platform = accessibilityController.platformName;
const isCocoa = platform === "ios" || platform === "mac";
output += expect("hasTitleUIElement(axElement('control6'))", isCocoa ? "false" : "true");
output += evalAndReturn("document.getElementById('label6c').remove()");
output += await expectAsync("axElement('control6').titleUIElement().isEqual(axElement('label6b'))", "true");
document.getElementById('container').style.display = 'none';

debug(output);
finishJSTest();
}, 0);
}

</script>
</body>
</html>

@@ -0,0 +1,16 @@
This test ensures that inputs with multiple labels have the correct accessibility text exposed.

Initial AX text:
AXTitle: Enter your color
AXDescription:
document.getElementById('label2').remove();
AXTitle: Enter color
AXDescription:
document.getElementById('label3').innerText = 'choice';
AXTitle: Enter choice
AXDescription:

PASS successfullyParsed is true

TEST COMPLETE
Enter choice
@@ -0,0 +1,26 @@
This tests that titleUIElement works correctly even when things change dynamically.

PASS: axElement('control1').titleUIElement().isEqual(axElement('label1')) === true
PASS: axElement('control2').titleUIElement().isEqual(axElement('label2')) === true
PASS: hasTitleUIElement(axElement('control3')) === false
document.getElementById('label3').setAttribute('for', 'control3');
PASS: axElement('control3').titleUIElement().isEqual(axElement('label3')) === true
label4Element = createLabelWithIdAndForAttribute('label4', 'control4');
PASS: hasTitleUIElement(axElement('control4')) === false
document.getElementById('container').appendChild(label4Element);
PASS: hasTitleUIElement(axElement('control4')) === true
PASS: axElement('control4').titleUIElement().isEqual(axElement('label4')) === true
label4Element.parentElement.removeChild(label4Element);
PASS: hasTitleUIElement(axElement('control4')) === false
PASS: hasTitleUIElement(axElement('control5')) === false
reparentNodeIntoContainer(document.getElementById('control5'), document.getElementById('label5'));
PASS: axElement('control5').titleUIElement() != null === true
PASS: axElement('control5').titleUIElement().isEqual(axElement('label5')) === true
PASS: hasTitleUIElement(axElement('control6')) === true
document.getElementById('label6c').remove()
PASS: axElement('control6').titleUIElement().isEqual(axElement('label6b')) === true

PASS successfullyParsed is true

TEST COMPLETE

@@ -0,0 +1,13 @@
This test ensures that inputs with multiple labels have the correct accessibility text exposed.

Initial AX text:
AXLabel: Enter your color
document.getElementById('label2').remove();
AXLabel: Enter color
document.getElementById('label3').innerText = 'choice';
AXLabel: Enter choice

PASS successfullyParsed is true

TEST COMPLETE
Enter choice

0 comments on commit 3c4dcc0

Please sign in to comment.