Skip to content

Commit 24ef939

Browse files
committed
AX: Controls with aria-labelledby should use native label geometry when both they and their ARIA label has no visible bounding box
https://bugs.webkit.org/show_bug.cgi?id=308022 rdar://problem/170518900 Reviewed by Joshua Hoffman. When a visually-hidden control has both aria-labelledby pointing to a visually-hidden element and a native <label for> element with visible geometry, we were unable to compute a bounding box for the control because we rejected the native label relationship entirely when ARIA labelling existed. This was done because ARIA labels beat out native labels in the accname calculation. This patch introduces NativeLabelFor/NativeLabeledBy relations that are always created for label-for="id" associations, independent of the presence of ARIA labelling. The existing LabelFor/LabeledBy relations continue to respect ARIA precedence for accessible name calculation. In relativeFrame(), we now fall back to native label geometry when ARIA labels don't provide a usable frame. * LayoutTests/accessibility/native-label-geometry-with-aria-labelledby-expected.txt: Added. * LayoutTests/accessibility/native-label-geometry-with-aria-labelledby.html: Added. * Source/WebCore/accessibility/AXCoreObject.h: (WebCore::AXCoreObject::nativeLabeledByObjects const): * Source/WebCore/accessibility/AXLogger.cpp: (WebCore::operator<<): * Source/WebCore/accessibility/AXObjectCache.cpp: (WebCore::AXObjectCache::symmetricRelation): (WebCore::AXObjectCache::addLabelForRelation): * Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp: (WebCore::AXIsolatedObject::relativeFrame const): Canonical link: https://commits.webkit.org/307727@main
1 parent 6ad274b commit 24ef939

File tree

7 files changed

+112
-12
lines changed

7 files changed

+112
-12
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
This test ensures an input that has both aria-labelledby pointing to a visually-hidden element, and a native label with visible geometry, gets geometry from the native label.
2+
3+
PASS: radioInput.width >= 195 === true
4+
PASS: radioInput.height >= 45 === true
5+
PASS: Math.abs(radioInput.pageX - 8) <= 2 === true
6+
PASS: Math.abs(radioInput.pageY - 8) <= 2 === true
7+
8+
PASS successfullyParsed is true
9+
10+
TEST COMPLETE
11+
12+
Sky Blue
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<script src="../resources/accessibility-helper.js"></script>
5+
<script src="../resources/js-test.js"></script>
6+
<style>
7+
.visually-hidden {
8+
position: absolute;
9+
width: 1px;
10+
height: 1px;
11+
padding: 0;
12+
margin: -1px;
13+
overflow: hidden;
14+
clip: rect(0, 0, 0, 0);
15+
white-space: nowrap;
16+
border: 0;
17+
}
18+
.visible-label {
19+
display: block;
20+
width: 200px;
21+
height: 50px;
22+
background: lightblue;
23+
}
24+
</style>
25+
</head>
26+
<body>
27+
28+
<input class="visually-hidden" type="radio" id="radio-input" aria-labelledby="hidden-label-text" name="test" value="test">
29+
<label for="radio-input" class="visible-label">
30+
<span id="hidden-label-text" class="visually-hidden">Sky Blue</span>
31+
</label>
32+
33+
<script>
34+
var output = "This test ensures an input that has both aria-labelledby pointing to a visually-hidden element, and a native label with visible geometry, gets geometry from the native label.\n\n";
35+
36+
if (window.accessibilityController) {
37+
window.jsTestIsAsync = true;
38+
39+
var pageX, pageY;
40+
var radioInput = accessibilityController.accessibleElementById("radio-input");
41+
setTimeout(async function() {
42+
// The width should be approximately 200 (the label width), but leave a little buffer since the exact value doesn't
43+
// matter much.
44+
output += await expectAsync("radioInput.width >= 195", "true");
45+
// The height should be approximately 50 (the label height).
46+
output += await expectAsync("radioInput.height >= 45", "true");
47+
output += await expectAsync("Math.abs(radioInput.pageX - 8) <= 2", "true");
48+
output += await expectAsync("Math.abs(radioInput.pageY - 8) <= 2", "true");
49+
50+
debug(output);
51+
finishJSTest();
52+
}, 0);
53+
54+
}
55+
</script>
56+
</body>
57+
</html>
58+

LayoutTests/platform/glib/TestExpectations

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,9 @@ accessibility/select-whitespace-collapsed-text.html [ Failure ]
907907
# Need to implement AccessibilityUIElement::insertText.
908908
accessibility/insert-text-into-password-field.html [ Skip ]
909909

910+
# Need to implement AccessibilityUIElement::{pageX, pageY}.
911+
accessibility/native-label-geometry-with-aria-labelledby.html [ Skip ]
912+
910913
# These test a Cocoa-only API.
911914
accessibility/mixed-contenteditable-visible-character-range-hang.html [ Skip ]
912915
accessibility/mixed-contenteditable-double-br-visible-character-range-hang.html [ Skip ]

Source/WebCore/accessibility/AXCoreObject.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,8 @@ enum class AXRelation : uint8_t {
381381
HeaderFor,
382382
LabeledBy,
383383
LabelFor,
384+
NativeLabeledBy,
385+
NativeLabelFor,
384386
OwnedBy,
385387
OwnerFor,
386388
};
@@ -762,6 +764,11 @@ class AXCoreObject : public RefCountedAndCanMakeWeakPtr<AXCoreObject> {
762764
AccessibilityChildrenVector flowFromObjects() const { return relatedObjects(AXRelation::FlowsFrom); }
763765
AccessibilityChildrenVector labeledByObjects() const { return relatedObjects(AXRelation::LabeledBy); }
764766
AccessibilityChildrenVector labelForObjects() const { return relatedObjects(AXRelation::LabelFor); }
767+
// This function exists because in the accname calculation, aria-labelledby takes precedence over "native"
768+
// labels (like <label for="z"><input id="z">), and thus we do not create a LabelFor relationship for the native
769+
// label. However, sometimes outside of accname, we do also want to know the native label relationship,
770+
// which is what this function is for.
771+
AccessibilityChildrenVector nativeLabeledByObjects() const { return relatedObjects(AXRelation::NativeLabeledBy); }
765772
AccessibilityChildrenVector ownedObjects() const { return relatedObjects(AXRelation::OwnerFor); }
766773
AccessibilityChildrenVector owners() const { return relatedObjects(AXRelation::OwnedBy); }
767774
virtual AccessibilityChildrenVector relatedObjects(AXRelation) const = 0;

Source/WebCore/accessibility/AXLogger.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,12 @@ TextStream& operator<<(TextStream& stream, AXRelation relation)
573573
case AXRelation::LabelFor:
574574
stream << "LabelFor";
575575
break;
576+
case AXRelation::NativeLabeledBy:
577+
stream << "NativeLabeledBy";
578+
break;
579+
case AXRelation::NativeLabelFor:
580+
stream << "NativeLabelFor";
581+
break;
576582
case AXRelation::OwnedBy:
577583
stream << "OwnedBy";
578584
break;

Source/WebCore/accessibility/AXObjectCache.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5675,6 +5675,10 @@ AXRelation AXObjectCache::symmetricRelation(AXRelation relation)
56755675
return AXRelation::LabelFor;
56765676
case AXRelation::LabelFor:
56775677
return AXRelation::LabeledBy;
5678+
case AXRelation::NativeLabeledBy:
5679+
return AXRelation::NativeLabelFor;
5680+
case AXRelation::NativeLabelFor:
5681+
return AXRelation::NativeLabeledBy;
56785682
case AXRelation::OwnedBy:
56795683
return AXRelation::OwnerFor;
56805684
case AXRelation::OwnerFor:
@@ -6009,8 +6013,13 @@ void AXObjectCache::addLabelForRelation(Element& origin)
60096013

60106014
// LabelFor relations are established for <label for=...>.
60116015
if (RefPtr label = dynamicDowncast<HTMLLabelElement>(origin)) {
6012-
if (RefPtr control = Accessibility::controlForLabelElement(*label))
6013-
addedRelation = addRelation(origin, *control, AXRelation::LabelFor);
6016+
if (RefPtr control = Accessibility::controlForLabelElement(*label)) {
6017+
// Always add NativeLabelFor for geometry purposes.
6018+
addedRelation = addRelation(origin, *control, AXRelation::NativeLabelFor);
6019+
// Only add LabelFor (for accname) if no ARIA labelling exists.
6020+
if (!hasAnyARIALabelling(*control))
6021+
addRelation(origin, *control, AXRelation::LabelFor);
6022+
}
60146023
}
60156024

60166025
if (addedRelation)

Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,16 +1227,21 @@ FloatRect AXIsolatedObject::relativeFrame() const
12271227
std::optional<IntRect> rectFromLabels;
12281228
if (isControl()) {
12291229
// For controls, we can try to use the frame of any associated labels.
1230-
auto labels = labeledByObjects();
1231-
for (const auto& label : labels) {
1232-
std::optional frame = downcast<AXIsolatedObject>(label)->cachedRelativeFrame();
1233-
if (!frame)
1234-
continue;
1235-
if (!rectFromLabels)
1236-
rectFromLabels = *frame;
1237-
else if (rectFromLabels->intersects(*frame))
1238-
rectFromLabels->unite(*frame);
1239-
}
1230+
// Prefer ARIA labels first, fall back to native labels if none provide geometry.
1231+
auto uniteLabelsIntoRect = [&rectFromLabels](const AccessibilityChildrenVector& labels) {
1232+
for (const auto& label : labels) {
1233+
std::optional frame = downcast<AXIsolatedObject>(label)->cachedRelativeFrame();
1234+
if (!frame)
1235+
continue;
1236+
if (!rectFromLabels)
1237+
rectFromLabels = *frame;
1238+
else if (rectFromLabels->intersects(*frame))
1239+
rectFromLabels->unite(*frame);
1240+
}
1241+
};
1242+
uniteLabelsIntoRect(labeledByObjects());
1243+
if (!rectFromLabels)
1244+
uniteLabelsIntoRect(nativeLabeledByObjects());
12401245
}
12411246

12421247
if (rectFromLabels && !rectFromLabels->isEmpty())

0 commit comments

Comments
 (0)