Skip to content

Commit

Permalink
Apply patch. rdar://123741292
Browse files Browse the repository at this point in the history
    AX: AccessibilityText is not updated when static text descendant changes https://bugs.webkit.org/show_bug.cgi?id=270356 rdar://123741292

    Reviewed by Chris Fleizach.

    Add a new TextUnderElementChanged notification to represent this
    scenario so we can make precise updates to the isolated tree.

    * LayoutTests/accessibility/dynamic-text-expected.txt: Added.
    * LayoutTests/accessibility/dynamic-text.html: Added.
    * LayoutTests/platform/ios/accessibility/dynamic-text-expected.txt: Added.
    * LayoutTests/platform/ios/TestExpectations: Enable new test.

    * Source/WebCore/accessibility/AXCoreObject.h:
    * Source/WebCore/accessibility/AXLogger.cpp:
    (WebCore::operator<<):
    * Source/WebCore/accessibility/AXObjectCache.cpp:
    (WebCore::AXObjectCache::handleTextChanged):
    (WebCore::AXObjectCache::updateIsolatedTree):
    * Source/WebCore/accessibility/AXObjectCache.h:
    * Source/WebCore/accessibility/AccessibilityNodeObject.cpp:
    (WebCore::AccessibilityNodeObject::visibleText const):
    * Source/WebCore/accessibility/AccessibilityObject.cpp:
    (WebCore::AccessibilityObject::dependsOnTextUnderElement const):
    * Source/WebCore/accessibility/AccessibilityObject.h:
    * Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp:
    (WebCore::AXIsolatedTree::updateNodeProperties):
    (WebCore::AXIsolatedTree::updateDependentProperties):
    * Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h:
    (WebCore::AXIsolatedTree::updateNodeProperty):

    Canonical link: https://commits.webkit.org/275693@main

Identifier: 272448.800@safari-7618-branch
  • Loading branch information
Dan Robson committed Mar 26, 2024
1 parent 1f9ed5f commit 8687d2f
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 51 deletions.
36 changes: 36 additions & 0 deletions LayoutTests/accessibility/dynamic-text-expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This test ensures we update the accessibility tree when static text changes.

PASS: webarea.childAtIndex(0).role.toLowerCase().includes('text') === true
AXValue: Initial static text

#button text alternatives:
AXTitle: Initial button text
AXDescription:
AXHelp:

PASS: accessibilityController.accessibleElementById('label').stringValue.includes('Initial label text') === true
#checkbox text alternatives:
AXTitle: Initial label text
AXDescription:
AXHelp:

PASS: accessibilityController.accessibleElementById('textarea').stringValue.includes('Initial textarea text') === true

#button text alternatives:
AXTitle: Changed button text
AXDescription:
AXHelp:

PASS: webarea.childAtIndex(0).stringValue.includes('Changed static text') === true
PASS: accessibilityController.accessibleElementById('label').stringValue.includes('Changed label text') === true
#checkbox text alternatives:
AXTitle: Changed label text
AXDescription:
AXHelp:

PASS: accessibilityController.accessibleElementById('textarea').stringValue.includes('Changed textarea text') === true

PASS successfullyParsed is true

TEST COMPLETE
Changed static textChanged button text Changed label text
77 changes: 77 additions & 0 deletions LayoutTests/accessibility/dynamic-text.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!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>
Initial static text
<button id="button">Initial button text</button>
<label id="label" for="checkbox">Initial label text</label>
<input id="checkbox" type="checkbox" name="checkbox" />
<textarea id="textarea">Initial textarea text</textarea>

<script>
var output = "This test ensures we update the accessibility tree when static text changes.\n\n";

function textAlternatives(id) {
let result = '';
result += `#${id} text alternatives:\n`
result += `${platformTextAlternatives(accessibilityController.accessibleElementById(id))}\n\n`;
return result;
}

if (window.accessibilityController) {
window.jsTestIsAsync = true;
const isIOS = accessibilityController.platformName === "ios";

var webarea = accessibilityController.rootElement.childAtIndex(0);
if (accessibilityController.platformName === "mac") {
// Static text stringValue.
output += expect("webarea.childAtIndex(0).role.toLowerCase().includes('text')", "true");
output += `${webarea.childAtIndex(0).stringValue}\n\n`;
}

output += textAlternatives("button");

if (!isIOS)
output += expect("accessibilityController.accessibleElementById('label').stringValue.includes('Initial label text')", "true");

// Labelled control title.
output += textAlternatives("checkbox");

// Textarea stringValue.
output += expect("accessibilityController.accessibleElementById('textarea').stringValue.includes('Initial textarea text')", "true");
output += "\n";

document.getElementById("button").firstChild.nodeValue = "Changed button text";
setTimeout(async function() {
let newPlatformText;
await waitFor(() => {
newPlatformText = textAlternatives("button");
return newPlatformText.includes("Changed button text");
});
output += newPlatformText;


if (!isIOS) {
document.getElementsByTagName("body")[0].firstChild.nodeValue = "Changed static text";
output += await expectAsync("webarea.childAtIndex(0).stringValue.includes('Changed static text')", "true");
}

document.getElementById("label").firstChild.nodeValue = "Changed label text";
if (!isIOS)
output += await expectAsync("accessibilityController.accessibleElementById('label').stringValue.includes('Changed label text')", "true");
output += textAlternatives("checkbox");

document.getElementById("textarea").firstChild.nodeValue = "Changed textarea text";
output += await expectAsync("accessibilityController.accessibleElementById('textarea').stringValue.includes('Changed textarea text')", "true");

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

29 changes: 29 additions & 0 deletions LayoutTests/platform/glib/accessibility/dynamic-text-expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
This test ensures we update the accessibility tree when static text changes.

#button text alternatives:
AXTitle: Initial button text
AXDescription:

PASS: accessibilityController.accessibleElementById('label').stringValue.includes('Initial label text') === true
#checkbox text alternatives:
AXTitle: Initial label text
AXDescription:

PASS: accessibilityController.accessibleElementById('textarea').stringValue.includes('Initial textarea text') === true

#button text alternatives:
AXTitle: Changed button text
AXDescription:

PASS: webarea.childAtIndex(0).stringValue.includes('Changed static text') === true
PASS: accessibilityController.accessibleElementById('label').stringValue.includes('Changed label text') === true
#checkbox text alternatives:
AXTitle: Changed label text
AXDescription:

PASS: accessibilityController.accessibleElementById('textarea').stringValue.includes('Changed textarea text') === true

PASS successfullyParsed is true

TEST COMPLETE
Changed static textChanged button text Changed label text
1 change: 1 addition & 0 deletions LayoutTests/platform/ios/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -2213,6 +2213,7 @@ accessibility/display-contents/element-roles.html [ Pass ]
accessibility/display-contents/object-ordering.html [ Pass ]
accessibility/display-contents/table.html [ Pass ]
accessibility/display-flex-shadow-dom-accname.html [ Pass ]
accessibility/dynamic-text.html [ Pass ]
accessibility/dynamically-ignored-canvas.html [ Pass ]
accessibility/editable-webpage-focused-ui-element.html [ Pass ]
accessibility/element-haspopup.html [ Pass ]
Expand Down
22 changes: 22 additions & 0 deletions LayoutTests/platform/ios/accessibility/dynamic-text-expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
This test ensures we update the accessibility tree when static text changes.

#button text alternatives:
AXLabel: Initial button text

#checkbox text alternatives:
AXLabel: Initial label text

PASS: accessibilityController.accessibleElementById('textarea').stringValue.includes('Initial textarea text') === true

#button text alternatives:
AXLabel: Changed button text

#checkbox text alternatives:
AXLabel: Changed label text

PASS: accessibilityController.accessibleElementById('textarea').stringValue.includes('Changed textarea text') === true

PASS successfullyParsed is true

TEST COMPLETE
Initial static text Changed button text Changed label text
1 change: 1 addition & 0 deletions Source/WebCore/accessibility/AXCoreObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -1711,6 +1711,7 @@ WTF::TextStream& operator<<(WTF::TextStream&, AccessibilitySearchKey);
WTF::TextStream& operator<<(WTF::TextStream&, const AccessibilitySearchCriteria&);
WTF::TextStream& operator<<(WTF::TextStream&, AccessibilityObjectInclusion);
WTF::TextStream& operator<<(WTF::TextStream&, const AXCoreObject&);
WTF::TextStream& operator<<(WTF::TextStream&, AccessibilityText);
WTF::TextStream& operator<<(WTF::TextStream&, AccessibilityTextSource);
WTF::TextStream& operator<<(WTF::TextStream&, AXRelationType);

Expand Down
9 changes: 9 additions & 0 deletions Source/WebCore/accessibility/AXLogger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,12 @@ TextStream& operator<<(TextStream& stream, const AccessibilitySearchCriteria& cr
return stream;
}

TextStream& operator<<(TextStream& stream, AccessibilityText text)
{
stream << text.textSource << ": " << text.text;
return stream;
}

TextStream& operator<<(TextStream& stream, AccessibilityTextSource source)
{
switch (source) {
Expand Down Expand Up @@ -763,6 +769,9 @@ TextStream& operator<<(TextStream& stream, AXObjectCache::AXNotification notific
case AXObjectCache::AXNotification::AXTextCompositionChanged:
stream << "AXTextCompositionChanged";
break;
case AXObjectCache::AXNotification::AXTextUnderElementChanged:
stream << "AXTextUnderElementChanged";
break;
case AXObjectCache::AXNotification::AXTextSecurityChanged:
stream << "AXTextSecurityChanged";
break;
Expand Down
33 changes: 28 additions & 5 deletions Source/WebCore/accessibility/AXObjectCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1162,17 +1162,35 @@ void AXObjectCache::handleTextChanged(AccessibilityObject* object)

Ref<AccessibilityObject> protectedObject(*object);

bool isText = object->isStaticText();
// If this element supports ARIA live regions, or is part of a region with an ARIA editable role,
// then notify the AT of changes.
bool notifiedNonNativeTextControl = false;
for (auto* parent = object; parent; parent = parent->parentObject()) {
if (parent->supportsLiveRegion())
postLiveRegionChangeNotification(parent);
for (RefPtr ancestor = object; ancestor; ancestor = ancestor->parentObject()) {
if (ancestor->supportsLiveRegion())
postLiveRegionChangeNotification(ancestor.get());

if (!notifiedNonNativeTextControl && parent->isNonNativeTextControl()) {
postNotification(parent, parent->document(), AXValueChanged);
if (!notifiedNonNativeTextControl && ancestor->isNonNativeTextControl()) {
postNotification(ancestor.get(), ancestor->document(), AXValueChanged);
notifiedNonNativeTextControl = true;
}

if (isText) {
bool dependsOnTextUnderElement = ancestor->dependsOnTextUnderElement();
auto role = ancestor->roleValue();
dependsOnTextUnderElement |= role == AccessibilityRole::ListItem || role == AccessibilityRole::Label;

// If the starting object is a static text, its underlying text has changed.
if (dependsOnTextUnderElement) {
// Inform this ancestor its textUnderElement-dependent data is now out-of-date.
postNotification(ancestor.get(), nullptr, AXTextUnderElementChanged);
}

// Any objects this ancestor labeled now also need new AccessibilityText.
auto labeledObjects = ancestor->labelForObjects();
for (const auto& labeledObject : labeledObjects)
postNotification(checkedDowncast<AccessibilityObject>(labeledObject.get()), nullptr, AXTextChanged);
}
}

postNotification(object, object->document(), AXTextChanged);
Expand Down Expand Up @@ -4278,6 +4296,11 @@ void AXObjectCache::updateIsolatedTree(const Vector<std::pair<RefPtr<Accessibili
case AXTextCompositionChanged:
tree->queueNodeUpdate(*notification.first, { AXPropertyName::TextInputMarkedTextMarkerRange });
break;
case AXTextUnderElementChanged:
tree->queueNodeUpdate(*notification.first, { { AXPropertyName::AccessibilityText, AXPropertyName::Title } });
if (notification.first->isLabel())
tree->queueNodeUpdate(*notification.first, { AXPropertyName::StringValue });
break;
case AXURLChanged:
tree->queueNodeUpdate(*notification.first, { AXPropertyName::URL });
break;
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/accessibility/AXObjectCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ class AXObjectCache : public CanMakeWeakPtr<AXObjectCache>, public CanMakeChecke
AXSortDirectionChanged,
AXTextChanged,
AXTextCompositionChanged,
AXTextUnderElementChanged,
AXTextSecurityChanged,
AXElementBusyChanged,
AXDraggingStarted,
Expand Down
47 changes: 2 additions & 45 deletions Source/WebCore/accessibility/AccessibilityNodeObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1897,7 +1897,7 @@ void AccessibilityNodeObject::alternativeText(Vector<AccessibilityText>& textOrd

void AccessibilityNodeObject::visibleText(Vector<AccessibilityText>& textOrder) const
{
Node* node = this->node();
WeakPtr node = this->node();
if (!node)
return;

Expand All @@ -1909,50 +1909,7 @@ void AccessibilityNodeObject::visibleText(Vector<AccessibilityText>& textOrder)
// If this node isn't rendered, there's no inner text we can extract from a select element.
if (!isAccessibilityRenderObject() && node->hasTagName(selectTag))
return;

bool useTextUnderElement = false;

switch (roleValue()) {
case AccessibilityRole::PopUpButton:
// Native popup buttons should not use their button children's text as a title. That value is retrieved through stringValue().
if (node->hasTagName(selectTag))
break;
FALLTHROUGH;
case AccessibilityRole::Summary:
// The text node for a <summary> element should be included in its visible text, unless a title attribute is present.
if (!hasAttribute(titleAttr))
useTextUnderElement = true;
break;
case AccessibilityRole::Button:
case AccessibilityRole::ToggleButton:
case AccessibilityRole::Checkbox:
case AccessibilityRole::ListBoxOption:
// MacOS does not expect native <li> elements to expose label information, it only expects leaf node elements to do that.
#if !PLATFORM(COCOA)
case AccessibilityRole::ListItem:
#endif
case AccessibilityRole::MenuButton:
case AccessibilityRole::MenuItem:
case AccessibilityRole::MenuItemCheckbox:
case AccessibilityRole::MenuItemRadio:
case AccessibilityRole::RadioButton:
case AccessibilityRole::Switch:
case AccessibilityRole::Tab:
useTextUnderElement = true;
break;
default:
break;
}

// If it's focusable but it's not content editable or a known control type, then it will appear to
// the user as a single atomic object, so we should use its text as the default title.
if (isHeading() || isLink())
useTextUnderElement = true;

if (isOutput())
useTextUnderElement = true;

if (useTextUnderElement) {
if (dependsOnTextUnderElement()) {
AccessibilityTextUnderElementMode mode;

// Headings often include links as direct children. Those links need to be included in text under element.
Expand Down
41 changes: 41 additions & 0 deletions Source/WebCore/accessibility/AccessibilityObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1918,6 +1918,47 @@ bool AccessibilityObject::hasContentEditableAttributeSet() const
return contentEditableAttributeIsEnabled(element());
}

bool AccessibilityObject::dependsOnTextUnderElement() const
{
switch (roleValue()) {
case AccessibilityRole::PopUpButton:
// Native popup buttons should not use their descendant's text as a title. That value is retrieved through stringValue().
if (hasTagName(selectTag))
break;
FALLTHROUGH;
case AccessibilityRole::Summary:
// The text node for a <summary> element should be included in its visible text, unless a title attribute is present.
if (!hasAttribute(titleAttr))
return true;
break;
case AccessibilityRole::Button:
case AccessibilityRole::ToggleButton:
case AccessibilityRole::Checkbox:
case AccessibilityRole::ListBoxOption:
#if !PLATFORM(COCOA)
// MacOS does not expect native <li> elements to expose label information, it only expects leaf node elements to do that.
case AccessibilityRole::ListItem:
#endif
case AccessibilityRole::MenuButton:
case AccessibilityRole::MenuItem:
case AccessibilityRole::MenuItemCheckbox:
case AccessibilityRole::MenuItemRadio:
case AccessibilityRole::RadioButton:
case AccessibilityRole::Switch:
case AccessibilityRole::Tab:
return true;
default:
break;
}

// If it's focusable but it's not content editable or a known control type, then it will appear to
// the user as a single atomic object, so we should use its text as the default title.
if (isHeading() || isLink())
return true;

return isOutput();
}

bool AccessibilityObject::supportsReadOnly() const
{
AccessibilityRole role = roleValue();
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/accessibility/AccessibilityObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ class AccessibilityObject : public AXCoreObject, public CanMakeWeakPtr<Accessibi
// Methods for determining accessibility text.
bool isARIAStaticText() const { return ariaRoleAttribute() == AccessibilityRole::StaticText; }
String stringValue() const override { return { }; }
bool dependsOnTextUnderElement() const;
String textUnderElement(AccessibilityTextUnderElementMode = AccessibilityTextUnderElementMode()) const override { return { }; }
String text() const override { return { }; }
unsigned textLength() const final;
Expand Down
Loading

0 comments on commit 8687d2f

Please sign in to comment.