Skip to content
Permalink
Browse files
Implement ARIA id-ref reflection for ElementInternals
https://bugs.webkit.org/show_bug.cgi?id=245328

Reviewed by Manuel Rego Casasnovas.

This patch adds the support for id-ref ARIA attributes to ElementInternals.

* LayoutTests/accessibility/custom-elements/controls-expected.txt: Added.
* LayoutTests/accessibility/custom-elements/controls.html: Added.
* LayoutTests/accessibility/custom-elements/describedby-expected.txt: Added.
* LayoutTests/accessibility/custom-elements/describedby.html: Added.
* LayoutTests/accessibility/custom-elements/flowto-expected.txt: Added.
* LayoutTests/accessibility/custom-elements/flowto.html: Added.
* LayoutTests/accessibility/custom-elements/menuitem-expected.txt:
* LayoutTests/accessibility/custom-elements/menuitem.html:
* LayoutTests/imported/w3c/web-platform-tests/custom-elements/form-associated/ElementInternals-accessibility-expected.txt:
* LayoutTests/platform/gtk/accessibility/custom-elements/describedby-expected.txt: Added.
* LayoutTests/platform/mac-wk1/TestExpectations:

* Source/WebCore/Sources.txt:
* Source/WebCore/WebCore.xcodeproj/project.pbxproj:

* Source/WebCore/accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::isModalElement const):
(WebCore::nodeHasRole):
(WebCore::AXObjectCache::updateRelationsIfNeeded): Add the support for defining relations via ElementInternals.

* Source/WebCore/accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::getAttribute const):
(WebCore::AccessibilityObject::elementsFromAttribute const): Ditto.

* Source/WebCore/bindings/js/JSElementInternalsCustom.cpp: Added.
(WebCore::getElementsArrayAttribute): Added.
(WebCore::JSElementInternals::ariaControlsElements const): Added.
(WebCore::JSElementInternals::ariaDescribedByElements const): Added.
(WebCore::JSElementInternals::ariaDetailsElements const): Added.
(WebCore::JSElementInternals::ariaErrorMessageElements const):
(WebCore::JSElementInternals::ariaFlowToElements const): Added.
(WebCore::JSElementInternals::ariaLabelledByElements const): Added.
(WebCore::JSElementInternals::ariaOwnsElements const): Added.

* Source/WebCore/dom/CustomElementDefaultARIA.cpp:
(WebCore::isElementVisible): Added.
(WebCore::CustomElementDefaultARIA::valueForAttribute const): Added the support for converting
WeakPtr<Element> and Vector<WeakPtr<Element>> to ID strings.
(WebCore::CustomElementDefaultARIA::elementForAttribute const): Added.
(WebCore::CustomElementDefaultARIA::setElementForAttribute): Added.
(WebCore::CustomElementDefaultARIA::elementsForAttribute const): Added.
(WebCore::CustomElementDefaultARIA::setElementsForAttribute): Added.

* Source/WebCore/dom/CustomElementDefaultARIA.h:

* Source/WebCore/dom/ElementInternals.cpp:
(WebCore::computeValueForAttribute): Extracted out of setAttributeWithoutSynchronization.
(WebCore::ElementInternals::setAttributeWithoutSynchronization):
(WebCore::ElementInternals::attributeWithoutSynchronization const):
(WebCore::ElementInternals::getElementAttribute const): Added.
(WebCore::ElementInternals::setElementAttribute): Added.
(WebCore::ElementInternals::getElementsArrayAttribute const): Added.
(WebCore::ElementInternals::setElementsArrayAttribute): Added.

* Source/WebCore/dom/ElementInternals.h:
* Source/WebCore/dom/ElementInternals.idl:

Canonical link: https://commits.webkit.org/254709@main
  • Loading branch information
rniwa committed Sep 21, 2022
1 parent ace2a41 commit 6989a5b880ac9b18befc8e0c921bac4f778a2189
Show file tree
Hide file tree
Showing 21 changed files with 567 additions and 37 deletions.
@@ -0,0 +1,16 @@
This tests that aria fallback roles work correctly.

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


PASS labelForControl(accessibilityController.accessibleElementById("custom-tab-1").ariaControlsElementAtIndex(0)) is "AXValue: Panel 1"
PASS labelForControl(accessibilityController.accessibleElementById("custom-tab-1").ariaControlsElementAtIndex(1)) is "AXValue: Panel 2"
PASS labelForControl(accessibilityController.accessibleElementById("custom-tab-2").ariaControlsElementAtIndex(0)) is "AXValue: Panel 3"
PASS accessibilityController.accessibleElementById("custom-tab-2").ariaControlsElementAtIndex(1) is null
PASS successfullyParsed is true

TEST COMPLETE
Panel 1
Panel 2
Panel 3
Panel 4
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<body>
<script src="../../resources/js-test.js"></script>
<script src="../../resources/accessibility-helper.js"></script>
<custom-tab id="custom-tab-1"></custom-tab>
<div class="control">Panel 1</div>
<div class="control">Panel 2</div>
<custom-tab id="custom-tab-2" aria-controls="panel3"></custom-tab>
<div id="panel3" class="control">Panel 3</div>
<div id="panel4" class="control">Panel 4</div>
<script>

customElements.define('custom-tab', class CustomElement extends HTMLElement {
constructor()
{
super();
const internals = this.attachInternals();
internals.role = 'tab';
internals.ariaControlsElements = Array.from(document.querySelectorAll('.control'));
}
});

description("This tests that aria fallback roles work correctly.");
if (!window.accessibilityController)
debug('This test requires accessibilityController');
else {
function labelForControl(control) {
if (accessibilityController.platformName == "mac")
return control.childAtIndex(0).stringValue;
return control.stringValue;
}
shouldBeEqualToString('labelForControl(accessibilityController.accessibleElementById("custom-tab-1").ariaControlsElementAtIndex(0))', 'AXValue: Panel 1');
shouldBeEqualToString('labelForControl(accessibilityController.accessibleElementById("custom-tab-1").ariaControlsElementAtIndex(1))', 'AXValue: Panel 2');
shouldBeEqualToString('labelForControl(accessibilityController.accessibleElementById("custom-tab-2").ariaControlsElementAtIndex(0))', 'AXValue: Panel 3');
shouldBe('accessibilityController.accessibleElementById("custom-tab-2").ariaControlsElementAtIndex(1)', 'null');
}

</script>
</body>
</html>
@@ -0,0 +1,28 @@
This tests that aria fallback roles work correctly.

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


PASS accessibilityController.accessibleElementById("custom-1").role is "AXRole: AXCheckBox"
PASS accessibilityController.accessibleElementById("custom-1").helpText is "AXHelp: some description other description"
PASS accessibilityController.accessibleElementById("custom-1").detailsElements().length is 2
PASS accessibilityController.accessibleElementById("custom-1").detailsElements()[0].domIdentifier is "details1"
PASS accessibilityController.accessibleElementById("custom-1").detailsElements()[1].domIdentifier is "details2"
PASS accessibilityController.accessibleElementById("custom-1").errorMessageElements().length is 2
PASS accessibilityController.accessibleElementById("custom-1").errorMessageElements()[0].domIdentifier is "error1"
PASS accessibilityController.accessibleElementById("custom-1").errorMessageElements()[1].domIdentifier is "error2"
PASS accessibilityController.accessibleElementById("custom-2").role is "AXRole: AXCheckBox"
PASS accessibilityController.accessibleElementById("custom-2").helpText is "AXHelp: some description"
PASS accessibilityController.accessibleElementById("custom-2").detailsElements().length is 1
PASS accessibilityController.accessibleElementById("custom-2").detailsElements()[0].domIdentifier is "details2"
PASS accessibilityController.accessibleElementById("custom-2").errorMessageElements().length is 1
PASS accessibilityController.accessibleElementById("custom-2").errorMessageElements()[0].domIdentifier is "error2"
PASS successfullyParsed is true

TEST COMPLETE
some description
other description
some details
other details
some error
other error
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<body id="body">
<script src="../../resources/js-test.js"></script>
<script src="../../resources/accessibility-helper.js"></script>
<custom-element id="custom-1"></custom-element>
<custom-element id="custom-2" aria-describedby="some" aria-details="details2" aria-errormessage="error2"></custom-element>
<div id="some" class="note">some description</div>
<div class="note">other description</div>
<div id="details1" class="details">some details</div>
<div id="details2" class="details">other details</div>
<div id="error1" class="error">some error</div>
<div id="error2" class="error">other error</div>
<script>

customElements.define('custom-element', class CustomElement extends HTMLElement {
constructor()
{
super();
const internals = this.attachInternals();
internals.role = 'checkbox';
internals.ariaDescribedByElements = Array.from(document.querySelectorAll('.note'));
internals.ariaDetailsElements = Array.from(document.querySelectorAll('.details'));
internals.ariaErrorMessageElements = Array.from(document.querySelectorAll('.error'));
}
});

description("This tests that aria fallback roles work correctly.");
if (!window.accessibilityController)
debug('This test requires accessibilityController');
else {
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").role', 'AXRole: AXCheckBox');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").helpText', 'AXHelp: some description other description');
shouldBe('accessibilityController.accessibleElementById("custom-1").detailsElements().length', '2');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").detailsElements()[0].domIdentifier', 'details1');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").detailsElements()[1].domIdentifier', 'details2');
shouldBe('accessibilityController.accessibleElementById("custom-1").errorMessageElements().length', '2');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").errorMessageElements()[0].domIdentifier', 'error1');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").errorMessageElements()[1].domIdentifier', 'error2');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-2").role', 'AXRole: AXCheckBox');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-2").helpText', 'AXHelp: some description');
shouldBe('accessibilityController.accessibleElementById("custom-2").detailsElements().length', '1');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-2").detailsElements()[0].domIdentifier', 'details2');
shouldBe('accessibilityController.accessibleElementById("custom-2").errorMessageElements().length', '1');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-2").errorMessageElements()[0].domIdentifier', 'error2');
}

</script>
</body>
</html>
@@ -0,0 +1,24 @@
This tests that aria fallback roles work correctly.

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


PASS accessibilityController.accessibleElementById("custom-1").role is "AXRole: AXCheckBox"
PASS platformValueForW3CName(accessibilityController.accessibleElementById("custom-1")) is "label 1 label 2"
PASS accessibilityController.accessibleElementById("custom-1").ariaFlowToElementAtIndex(0).role is "AXRole: AXButton"
PASS accessibilityController.accessibleElementById("custom-1").ariaFlowToElementAtIndex(0).title is "AXTitle: FlowTo1"
PASS accessibilityController.accessibleElementById("custom-1").ariaFlowToElementAtIndex(1).role is "AXRole: AXButton"
PASS accessibilityController.accessibleElementById("custom-1").ariaFlowToElementAtIndex(1).title is "AXTitle: FlowTo2"
PASS accessibilityController.accessibleElementById("custom-1").ariaFlowToElementAtIndex(2) is null
PASS accessibilityController.accessibleElementById("custom-2").role is "AXRole: AXCheckBox"
PASS platformValueForW3CName(accessibilityController.accessibleElementById("custom-2")) is "label 2"
PASS accessibilityController.accessibleElementById("custom-2").ariaFlowToElementAtIndex(0).role is "AXRole: AXButton"
PASS accessibilityController.accessibleElementById("custom-2").ariaFlowToElementAtIndex(0).title is "AXTitle: FlowTo2"
PASS accessibilityController.accessibleElementById("custom-2").ariaFlowToElementAtIndex(1) is null
PASS successfullyParsed is true

TEST COMPLETE
FlowTo1
FlowTo2
label 1
label 2
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<body id="body">
<script src="../../resources/js-test.js"></script>
<script src="../../resources/accessibility-helper.js"></script>
<custom-element id="custom-1"></custom-element>
<custom-element id="custom-2" aria-flowto="flowto2" aria-labelledby="label2"></custom-element>
<div role="button" class="flowto">FlowTo1</div>
<div id="flowto2" role="button" class="flowto">FlowTo2</div>
<div class="label">label 1</div>
<div id="label2" class="label">label 2</div>
<script>

customElements.define('custom-element', class CustomElement extends HTMLElement {
constructor()
{
super();
const internals = this.attachInternals();
internals.role = 'checkbox';
internals.ariaFlowToElements = Array.from(document.querySelectorAll('.flowto'));
internals.ariaLabelledByElements = Array.from(document.querySelectorAll('.label'));
}
});

description("This tests that aria fallback roles work correctly.");
if (!window.accessibilityController)
debug('This test requires accessibilityController');
else {
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").role', 'AXRole: AXCheckBox');
shouldBeEqualToString('platformValueForW3CName(accessibilityController.accessibleElementById("custom-1"))', 'label 1 label 2');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").ariaFlowToElementAtIndex(0).role', 'AXRole: AXButton');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").ariaFlowToElementAtIndex(0).title', 'AXTitle: FlowTo1');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").ariaFlowToElementAtIndex(1).role', 'AXRole: AXButton');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-1").ariaFlowToElementAtIndex(1).title', 'AXTitle: FlowTo2');
shouldBe('accessibilityController.accessibleElementById("custom-1").ariaFlowToElementAtIndex(2)', 'null');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-2").role', 'AXRole: AXCheckBox');
shouldBeEqualToString('platformValueForW3CName(accessibilityController.accessibleElementById("custom-2"))', 'label 2');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-2").ariaFlowToElementAtIndex(0).role', 'AXRole: AXButton');
shouldBeEqualToString('accessibilityController.accessibleElementById("custom-2").ariaFlowToElementAtIndex(0).title', 'AXTitle: FlowTo2');
shouldBe('accessibilityController.accessibleElementById("custom-2").ariaFlowToElementAtIndex(1)', 'null');
}

</script>
</body>
</html>
@@ -3,6 +3,12 @@ This tests that aria fallback roles work correctly.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS accessibilityController.accessibleElementById("menu-1").selectedChildrenCount is 1
PASS platformValueForW3CName(accessibilityController.accessibleElementById("menu-1").selectedChildAtIndex(0)) is "item 2"
PASS accessibilityController.accessibleElementById("menu-1").selectedChildAtIndex(0).isSelected is true
PASS accessibilityController.accessibleElementById("menu-2").selectedChildrenCount is 1
PASS platformValueForW3CName(accessibilityController.accessibleElementById("menu-2").selectedChildAtIndex(0)) is "item 3"
PASS accessibilityController.accessibleElementById("menu-2").selectedChildAtIndex(0).isSelected is true
PASS accessibilityController.accessibleElementById("item-1").isEnabled is false
PASS accessibilityController.accessibleElementById("item-1").isExpanded is true
PASS accessibilityController.accessibleElementById("item-1").hasPopup is true
@@ -3,10 +3,27 @@
<body>
<script src="../../resources/js-test.js"></script>
<script src="../../resources/accessibility-helper.js"></script>
<custom-menuitem id="item-1"></custom-menuitem>
<custom-menuitem id="item-2" aria-disabled="false" aria-expanded="false" aria-haspopup="false"></custom-menuitem>
<custom-menu id="menu-1">
<custom-menuitem id="item-1" aria-label="item 1"></custom-menuitem>
<custom-menuitem id="item-2" aria-label="item 2" aria-disabled="false" aria-expanded="false" aria-haspopup="false"></custom-menuitem>
</custom-menu>
<custom-menu id="menu-2" aria-activedescendant="item-3">
<custom-menuitem id="item-3" aria-label="item 3"></custom-menuitem>
<custom-menuitem id="item-4" aria-label="item 4"></custom-menuitem>
</custom-menu>
<script>

customElements.define('custom-menu', class CustomElement extends HTMLElement {
constructor()
{
super();
const internals = this.attachInternals();
internals.role = 'menubar';
internals.ariaActiveDescendantElement = document.getElementById('item-2');
window.customMenuInternals = internals;
}
});

customElements.define('custom-menuitem', class CustomElement extends HTMLElement {
constructor()
{
@@ -23,6 +40,12 @@
if (!window.accessibilityController)
debug('This test requires accessibilityController');
else {
shouldBe('accessibilityController.accessibleElementById("menu-1").selectedChildrenCount', '1');
shouldBeEqualToString('platformValueForW3CName(accessibilityController.accessibleElementById("menu-1").selectedChildAtIndex(0))', 'item 2');
shouldBeTrue('accessibilityController.accessibleElementById("menu-1").selectedChildAtIndex(0).isSelected');
shouldBe('accessibilityController.accessibleElementById("menu-2").selectedChildrenCount', '1');
shouldBeEqualToString('platformValueForW3CName(accessibilityController.accessibleElementById("menu-2").selectedChildAtIndex(0))', 'item 3');
shouldBeTrue('accessibilityController.accessibleElementById("menu-2").selectedChildAtIndex(0).isSelected');
shouldBe('accessibilityController.accessibleElementById("item-1").isEnabled', 'false');
shouldBe('accessibilityController.accessibleElementById("item-1").isExpanded', 'true');
shouldBe('accessibilityController.accessibleElementById("item-1").hasPopup', 'true');
@@ -1,34 +1,34 @@

PASS role is defined in ElementInternals
FAIL ariaActiveDescendantElement is defined in ElementInternals assert_inherits: property "ariaActiveDescendantElement" not found in prototype chain
PASS ariaActiveDescendantElement is defined in ElementInternals
PASS ariaAtomic is defined in ElementInternals
PASS ariaAutoComplete is defined in ElementInternals
PASS ariaBusy is defined in ElementInternals
PASS ariaChecked is defined in ElementInternals
PASS ariaColCount is defined in ElementInternals
PASS ariaColIndex is defined in ElementInternals
PASS ariaColSpan is defined in ElementInternals
FAIL ariaControlsElements is defined in ElementInternals assert_inherits: property "ariaControlsElements" not found in prototype chain
PASS ariaControlsElements is defined in ElementInternals
PASS ariaCurrent is defined in ElementInternals
FAIL ariaDescribedByElements is defined in ElementInternals assert_inherits: property "ariaDescribedByElements" not found in prototype chain
FAIL ariaDetailsElements is defined in ElementInternals assert_inherits: property "ariaDetailsElements" not found in prototype chain
PASS ariaDescribedByElements is defined in ElementInternals
PASS ariaDetailsElements is defined in ElementInternals
PASS ariaDisabled is defined in ElementInternals
FAIL ariaErrorMessageElement is defined in ElementInternals assert_inherits: property "ariaErrorMessageElement" not found in prototype chain
PASS ariaExpanded is defined in ElementInternals
FAIL ariaFlowToElements is defined in ElementInternals assert_inherits: property "ariaFlowToElements" not found in prototype chain
PASS ariaFlowToElements is defined in ElementInternals
PASS ariaHasPopup is defined in ElementInternals
PASS ariaHidden is defined in ElementInternals
PASS ariaInvalid is defined in ElementInternals
PASS ariaKeyShortcuts is defined in ElementInternals
PASS ariaLabel is defined in ElementInternals
FAIL ariaLabelledByElements is defined in ElementInternals assert_inherits: property "ariaLabelledByElements" not found in prototype chain
PASS ariaLabelledByElements is defined in ElementInternals
PASS ariaLevel is defined in ElementInternals
PASS ariaLive is defined in ElementInternals
PASS ariaModal is defined in ElementInternals
PASS ariaMultiLine is defined in ElementInternals
PASS ariaMultiSelectable is defined in ElementInternals
PASS ariaOrientation is defined in ElementInternals
FAIL ariaOwnsElements is defined in ElementInternals assert_inherits: property "ariaOwnsElements" not found in prototype chain
PASS ariaOwnsElements is defined in ElementInternals
PASS ariaPlaceholder is defined in ElementInternals
PASS ariaPosInSet is defined in ElementInternals
PASS ariaPressed is defined in ElementInternals

0 comments on commit 6989a5b

Please sign in to comment.