Skip to content
Permalink
Browse files
AX: Dynamic aria-disabled changes don't update AXPropertyName::IsEnab…
…led for descendants

https://bugs.webkit.org/show_bug.cgi?id=248108
rdar://problem/102534000

Reviewed by Chris Fleizach.

With this patch, when an object's disabled state changes, we now update
AXPropertyName::IsEnabled and AXPropertyName::CanSetFocusAttribute for
its descendants, too. This is required because aria-disabled also
disables all descendants:

https://w3c.github.io/aria/#aria-disabled

> The state of being disabled applies to the element with aria-disabled and
all focusable descendant elements of the element on which the aria-disabled
attribute is applied.

* LayoutTests/accessibility/dynamic-attribute-changes-should-update-isenabled-expected.txt:
* LayoutTests/accessibility/dynamic-attribute-changes-should-update-isenabled.html:
* LayoutTests/resources/accessibility-helper.js:
* Source/WebCore/accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::updateIsolatedTree):
* Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp:
(WebCore::AXIsolatedTree::updatePropertiesForSelfAndDescendants):
(WebCore::AXIsolatedTree::updateNodeProperties):
(WebCore::AXIsolatedTree::updateNodeProperty):
* Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h:
(WebCore::AXIsolatedTree::updateNodeProperty):

Canonical link: https://commits.webkit.org/257159@main
  • Loading branch information
twilco committed Nov 30, 2022
1 parent 68a960c commit e113b29b327f1b381e5d683901c9221efef7b65a
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 155 deletions.
@@ -1,28 +1,34 @@
This test ensures that dynamically changing elements disabled and aria-disabled attributes properly updates their isEnabled property.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


Verifying initial element enabled state.
PASS axButton.isEnabled is true
PASS axOption.isEnabled is true
domButton.ariaDisabled = true
PASS axButton.isEnabled === false
domButton.ariaDisabled = false
PASS axButton.isEnabled === true
domButton.disabled = true
PASS axButton.isEnabled === false
domButton.disabled = false
PASS axButton.isEnabled === true
domOption.ariaDisabled = true
PASS axOption.isEnabled === false
domOption.ariaDisabled = false
PASS axOption.isEnabled === true
domOption.disabled = true
PASS axOption.isEnabled === false
domOption.disabled = false
PASS axOption.isEnabled === true
PASS: axButton.isEnabled === true
PASS: axOption.isEnabled === true
PASS: axRadio1.isEnabled === true
PASS: axRadio2.isEnabled === true
document.getElementById('button').ariaDisabled = true
PASS: axButton.isEnabled === false
document.getElementById('button').ariaDisabled = false
PASS: axButton.isEnabled === true
document.getElementById('button').disabled = true
PASS: axButton.isEnabled === false
document.getElementById('button').disabled = false
PASS: axButton.isEnabled === true
document.getElementById('option1').ariaDisabled = true
PASS: axOption.isEnabled === false
document.getElementById('option1').ariaDisabled = false
PASS: axOption.isEnabled === true
document.getElementById('option1').disabled = true
PASS: axOption.isEnabled === false
document.getElementById('option1').disabled = false
PASS: axOption.isEnabled === true
document.getElementById('fieldset').ariaDisabled = true
PASS: axRadio1.isEnabled === false
PASS: axRadio2.isEnabled === false
document.getElementById('fieldset').ariaDisabled = false
PASS: axRadio1.isEnabled === true
PASS: axRadio2.isEnabled === true

PASS successfullyParsed is true

TEST COMPLETE
Click me
Click me Foo label Bar label
@@ -1,8 +1,8 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<script src="../resources/js-test.js"></script>
<script src="../resources/accessibility-helper.js"></script>
<script src="../resources/js-test.js"></script>
</head>
<body>

@@ -14,45 +14,65 @@
<option>Option 3</option>
</select>

<fieldset id="fieldset">
<input type="radio" id="fieldset-radio1" name="foo">
<label for="fieldset-radio1">Foo label</label>

<input type="radio" id="fieldset-radio2" name="bar">
<label for="fieldset-radio2">Bar label</label>
</fieldset>

<script>
description("This test ensures that dynamically changing elements disabled and aria-disabled attributes properly updates their isEnabled property.");
var output = "This test ensures that dynamically changing elements disabled and aria-disabled attributes properly updates their isEnabled property.\n\n";

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

var axOption = accessibilityController.accessibleElementById("option1");
var domOption = document.getElementById("option1");
var axButton = accessibilityController.accessibleElementById("button");
var domButton = document.getElementById("button");
debug("Verifying initial element enabled state.");
shouldBe("axButton.isEnabled", "true");
shouldBe("axOption.isEnabled", "true");
var axRadio1 = accessibilityController.accessibleElementById("fieldset-radio1");
var axRadio2 = accessibilityController.accessibleElementById("fieldset-radio2");

output += "Verifying initial element enabled state.\n";
output += expect("axButton.isEnabled", "true");
output += expect("axOption.isEnabled", "true");
output += expect("axRadio1.isEnabled", "true");
output += expect("axRadio2.isEnabled", "true");

setTimeout(async function() {
evalAndLog("domButton.ariaDisabled = true");
await expectAsyncExpression("axButton.isEnabled", "false");
output += evalAndReturn("document.getElementById('button').ariaDisabled = true");
output += await expectAsync("axButton.isEnabled", "false");

output += evalAndReturn("document.getElementById('button').ariaDisabled = false");
output += await expectAsync("axButton.isEnabled", "true");

output += evalAndReturn("document.getElementById('button').disabled = true");
output += await expectAsync("axButton.isEnabled", "false");

evalAndLog("domButton.ariaDisabled = false");
await expectAsyncExpression("axButton.isEnabled", "true");
output += evalAndReturn("document.getElementById('button').disabled = false");
output += await expectAsync("axButton.isEnabled", "true");

evalAndLog("domButton.disabled = true");
await expectAsyncExpression("axButton.isEnabled", "false");
output += evalAndReturn("document.getElementById('option1').ariaDisabled = true");
output += await expectAsync("axOption.isEnabled", "false");

evalAndLog("domButton.disabled = false");
await expectAsyncExpression("axButton.isEnabled", "true");
output += evalAndReturn("document.getElementById('option1').ariaDisabled = false");
output += await expectAsync("axOption.isEnabled", "true");

evalAndLog("domOption.ariaDisabled = true");
await expectAsyncExpression("axOption.isEnabled", "false");
output += evalAndReturn("document.getElementById('option1').disabled = true");
output += await expectAsync("axOption.isEnabled", "false");

evalAndLog("domOption.ariaDisabled = false");
await expectAsyncExpression("axOption.isEnabled", "true");
output += evalAndReturn("document.getElementById('option1').disabled = false");
output += await expectAsync("axOption.isEnabled", "true");

evalAndLog("domOption.disabled = true");
await expectAsyncExpression("axOption.isEnabled", "false");
output += evalAndReturn("document.getElementById('fieldset').ariaDisabled = true");
output += await expectAsync("axRadio1.isEnabled", "false");
output += await expectAsync("axRadio2.isEnabled", "false");

evalAndLog("domOption.disabled = false");
await expectAsyncExpression("axOption.isEnabled", "true");
output += evalAndReturn("document.getElementById('fieldset').ariaDisabled = false");
output += await expectAsync("axRadio1.isEnabled", "true");
output += await expectAsync("axRadio2.isEnabled", "true");

debug(output);
finishJSTest();
}, 0);
}
@@ -155,6 +155,17 @@ function expect(expression, expectedValue) {
return `FAIL: ${expression} !== ${expectedValue}, was ${eval(expression)}\n`;
}

async function expectAsync(expression, expectedValue) {
if (typeof expression !== "string")
debug("WARN: The expression arg in expectAsync should be a string.");

const evalExpression = `${expression} === ${expectedValue}`;
await waitFor(() => {
return eval(evalExpression);
});
return `PASS: ${evalExpression}\n`;
}

async function expectAsyncExpression(expression, expectedValue) {
if (typeof expression !== "string")
debug("WARN: The expression arg in waitForExpression() should be a string.");
@@ -166,3 +177,10 @@ async function expectAsyncExpression(expression, expectedValue) {
debug(`PASS ${evalExpression}`);
}

function evalAndReturn(expression) {
if (typeof expression !== "string")
debug("FAIL: evalAndReturn() expects a string argument");

eval(expression);
return `${expression}\n`;
}
@@ -3699,26 +3699,22 @@ void AXObjectCache::updateIsolatedTree(const Vector<std::pair<RefPtr<Accessibili
tree->updateNodeProperty(*notification.first, AXPropertyName::AXColumnIndex);
break;
case AXDisabledStateChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::CanSetFocusAttribute);
tree->updateNodeProperty(*notification.first, AXPropertyName::IsEnabled);
tree->updatePropertiesForSelfAndDescendants(*notification.first, { AXPropertyName::CanSetFocusAttribute, AXPropertyName::IsEnabled });
break;
case AXExpandedChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::IsExpanded);
break;
case AXMaximumValueChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::MaxValueForRange);
tree->updateNodeProperty(*notification.first, AXPropertyName::ValueForRange);
tree->updateNodeProperties(*notification.first, { AXPropertyName::MaxValueForRange, AXPropertyName::ValueForRange });
break;
case AXMinimumValueChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::MinValueForRange);
tree->updateNodeProperty(*notification.first, AXPropertyName::ValueForRange);
tree->updateNodeProperties(*notification.first, { AXPropertyName::MinValueForRange, AXPropertyName::ValueForRange });
break;
case AXOrientationChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::Orientation);
break;
case AXPositionInSetChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::PosInSet);
tree->updateNodeProperty(*notification.first, AXPropertyName::SupportsPosInSet);
tree->updateNodeProperties(*notification.first, { AXPropertyName::PosInSet, AXPropertyName::SupportsPosInSet });
break;
case AXSortDirectionChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::SortDirection);
@@ -3727,8 +3723,7 @@ void AXObjectCache::updateIsolatedTree(const Vector<std::pair<RefPtr<Accessibili
tree->updateNodeProperty(*notification.first, AXPropertyName::IdentifierAttribute);
break;
case AXReadOnlyStatusChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::CanSetValueAttribute);
tree->updateNodeProperty(*notification.first, AXPropertyName::ReadOnlyValue);
tree->updateNodeProperties(*notification.first, { AXPropertyName::CanSetValueAttribute, AXPropertyName::ReadOnlyValue });
break;
case AXRequiredStatusChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::IsRequired);
@@ -3744,8 +3739,7 @@ void AXObjectCache::updateIsolatedTree(const Vector<std::pair<RefPtr<Accessibili
tree->updateNodeProperty(*notification.first, AXPropertyName::IsSelected);
break;
case AXSetSizeChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::SetSize);
tree->updateNodeProperty(*notification.first, AXPropertyName::SupportsSetSize);
tree->updateNodeProperties(*notification.first, { AXPropertyName::SetSize, AXPropertyName::SupportsSetSize });
break;
case AXTableHeadersChanged:
tree->updateNodeProperty(*notification.first, AXPropertyName::ColumnHeaders);

0 comments on commit e113b29

Please sign in to comment.