Title: [291570] trunk
Revision
291570
Author
tyle...@apple.com
Date
2022-03-21 11:48:25 -0700 (Mon, 21 Mar 2022)

Log Message

AX: Include display: contents elements in the AX tree
https://bugs.webkit.org/show_bug.cgi?id=237834

Reviewed by Chris Fleizach.

Source/WebCore:

Because display: contents intentionally prevents a render object from being
generated for the element it's applied to, we don't add it to the AX tree as
part of our normal render tree walk, making these elements inaccessible.

This patch includes these elements as part of the DOM walk that
addHiddenChildren (now renamed to addNodeOnlyChildren) already does.

Also, because display: contents moves the affected element's children up a
level in the render tree, this patch also special cases:

  1. AccessibilityRenderObject::parentObject and similar methods to
  return their display: contents parent instead of their render tree
  parent
  2. AccessibilityObject::insertChild to only insert display: contents
  children to their display: contents parent, rather than their render
  tree parent

Test: accessibility/display-contents-element-roles.html, accessibility/aria-hidden-display-contents-element.html

* accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::getOrCreate):
Allow creation of AX objects from display: contents `Node`s.
Also, don't create an object for a renderer that is in the process of
being destroyed (prevents display-contents-element-roles.html from crashing in ITM)
* accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::displayContentsParent const):
(WebCore::AccessibilityObject::insertChild):
If an object has a display: contents parent, and that parent isn't
`this`, return early.
* accessibility/AccessibilityObject.h:
* accessibility/AccessibilityRenderObject.cpp:
(WebCore::AccessibilityRenderObject::parentObjectIfExists const):
(WebCore::AccessibilityRenderObject::parentObject const):
(WebCore::AccessibilityRenderObject::parentObjectUnignored const):
If an object has a display: contents parent, return that instead of
its render tree parent.
(WebCore::AccessibilityRenderObject::addNodeOnlyChildren):
Renamed from addHiddenChildren.
(WebCore::AccessibilityRenderObject::addChildren):
Don't clear m_subtreeDirty until after all children have been added.
Necessary to make aria-hidden-display-contents-element.html pass, as
we were clearing this too early, causing display: contents subtrees to
not be updated after aria-hidden changes.
(WebCore::AccessibilityRenderObject::addHiddenChildren):
Renamed to addNodeOnlyChildren.
* accessibility/AccessibilityRenderObject.h:

LayoutTests:

* accessibility/aria-hidden-display-contents-element-expected.txt: Added.
* accessibility/aria-hidden-display-contents-element.html: Added.
* accessibility/display-contents-element-roles-expected.txt: Added.
* accessibility/display-contents-element-roles.html: Added.
* platform/glib/accessibility/aria-hidden-display-contents-element-expected.txt: Added.
* platform/glib/accessibility/display-contents-element-roles-expected.txt: Added.
* platform/ios/TestExpectations: Enable new tests.
* platform/ios/accessibility/aria-hidden-display-contents-element-expected.txt: Added.
* platform/ios/accessibility/display-contents-element-roles-expected.txt: Added.
* platform/win/TestExpectations: Skip display-contents-element-roles.html.
* platform/win/accessibility/aria-hidden-display-contents-element-expected.txt: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (291569 => 291570)


--- trunk/LayoutTests/ChangeLog	2022-03-21 18:01:29 UTC (rev 291569)
+++ trunk/LayoutTests/ChangeLog	2022-03-21 18:48:25 UTC (rev 291570)
@@ -1,3 +1,22 @@
+2022-03-21  Tyler Wilcock  <tyle...@apple.com>
+
+        AX: Include display: contents elements in the AX tree
+        https://bugs.webkit.org/show_bug.cgi?id=237834
+
+        Reviewed by Chris Fleizach.
+
+        * accessibility/aria-hidden-display-contents-element-expected.txt: Added.
+        * accessibility/aria-hidden-display-contents-element.html: Added.
+        * accessibility/display-contents-element-roles-expected.txt: Added.
+        * accessibility/display-contents-element-roles.html: Added.
+        * platform/glib/accessibility/aria-hidden-display-contents-element-expected.txt: Added.
+        * platform/glib/accessibility/display-contents-element-roles-expected.txt: Added.
+        * platform/ios/TestExpectations: Enable new tests.
+        * platform/ios/accessibility/aria-hidden-display-contents-element-expected.txt: Added.
+        * platform/ios/accessibility/display-contents-element-roles-expected.txt: Added.
+        * platform/win/TestExpectations: Skip display-contents-element-roles.html.
+        * platform/win/accessibility/aria-hidden-display-contents-element-expected.txt: Added.
+
 2022-03-21  Ziran Sun  <z...@igalia.com>
 
         [selection] HTMLTextFormControlElement::subtreeHasChanged() shouldn't be called in setRangeText

Added: trunk/LayoutTests/accessibility/aria-hidden-display-contents-element-expected.txt (0 => 291570)


--- trunk/LayoutTests/accessibility/aria-hidden-display-contents-element-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/accessibility/aria-hidden-display-contents-element-expected.txt	2022-03-21 18:48:25 UTC (rev 291570)
@@ -0,0 +1,23 @@
+This test ensures that an aria-hidden display:contents element behaves as expected.
+
+Testing element #main
+AXRole: AXGroup
+computedRoleString: main
+AXSubrole: AXLandmarkMain
+
+Testing element #list
+AXRole: AXList
+computedRoleString: list
+AXSubrole: AXContentList
+
+Testing element #paragraph
+AXRole: AXGroup
+computedRoleString: paragraph
+
+Hiding #main with aria-hidden='true'.
+PASS: All elements are now hidden.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+

Added: trunk/LayoutTests/accessibility/aria-hidden-display-contents-element.html (0 => 291570)


--- trunk/LayoutTests/accessibility/aria-hidden-display-contents-element.html	                        (rev 0)
+++ trunk/LayoutTests/accessibility/aria-hidden-display-contents-element.html	2022-03-21 18:48:25 UTC (rev 291570)
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<script src=""
+<script src=""
+</head>
+<body id="body">
+
+<main id="main" style="display: contents">
+    <ol id="list">
+        <li>List item one</li>
+        <li>List item two</li>
+    </ol>
+    <p id="paragraph">Content inside main > p tag.<p>
+</main>
+
+<script>
+    var testOutput = "This test ensures that an aria-hidden display:contents element behaves as expected.";
+
+    function debugElement(id) {
+        testOutput += `\n\nTesting element #${id}\n`;
+        const axElement = accessibilityController.accessibleElementById(id);
+        if (!axElement) {
+            debug(`\nFAIL: Couldn't get AX element for #${id}.`);
+            return;
+        }
+
+        testOutput += axElement.role;
+        const computedRoleString = axElement.computedRoleString;
+        if (computedRoleString)
+            testOutput += `\ncomputedRoleString: ${computedRoleString}`;
+
+        let subrole = axElement.subrole;
+        if (subrole.replace("AXSubrole: ", ""))
+            testOutput += `\n${subrole}`;
+    }
+
+    if (window.accessibilityController) {
+        window.jsTestIsAsync = true;
+
+        debugElement("main");
+        debugElement("list");
+        debugElement("paragraph");
+
+        testOutput += "\n\nHiding #main with aria-hidden='true'.";
+        document.getElementById("main").ariaHidden = "true";
+        setTimeout(async function() {
+            await waitFor(() => {
+                // Wait for the main to be hidden.
+                return !accessibilityController.accessibleElementById("main");
+            });
+            const list = accessibilityController.accessibleElementById("list");
+            const paragraph = accessibilityController.accessibleElementById("paragraph");
+            if (!list && !paragraph)
+                testOutput += "\nPASS: All elements are now hidden.";
+            else {
+                if (list)
+                    testOutput += "\nFAIL: <ol> was not hidden by aria-hidden.";
+                if (paragraph)
+                    testOutput += "\nFAIL: <p> was not hidden by aria-hidden.";
+            }
+
+            document.getElementById("main").style.visibility = "hidden";
+            debug(testOutput);
+            finishJSTest();
+        }, 0);
+    }
+</script>
+</body>
+</html>
+

Added: trunk/LayoutTests/accessibility/display-contents-element-roles-expected.txt (0 => 291570)


--- trunk/LayoutTests/accessibility/display-contents-element-roles-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/accessibility/display-contents-element-roles-expected.txt	2022-03-21 18:48:25 UTC (rev 291570)
@@ -0,0 +1,166 @@
+This test ensures elements with CSS display: contents have the correct role.
+
+<a href="" id="link" class="testcase"></a>
+    AXRole: AXLink
+    computedRoleString: link
+
+<article class="testcase" id="article"></article>
+    AXRole: AXGroup
+    computedRoleString: article
+    AXSubrole: AXDocumentArticle
+
+<aside class="testcase" id="aside"></aside>
+    AXRole: AXGroup
+    computedRoleString: complementary
+    AXSubrole: AXLandmarkComplementary
+
+<blockquote class="testcase" id="blockquote"></blockquote>
+    AXRole: AXGroup
+    computedRoleString: blockquote
+
+<button class="testcase" id="button"></button>
+    AXRole: AXButton
+    computedRoleString: button
+
+<code class="testcase" id="code"></code>
+    AXRole: AXGroup
+    AXSubrole: AXCodeStyleGroup
+
+<del class="testcase" id="del"></del>
+    AXRole: AXGroup
+    computedRoleString: deletion
+    AXSubrole: AXDeleteStyleGroup
+
+<details class="testcase" id="details"></details>
+    AXRole: AXGroup
+    AXSubrole: AXDetails
+
+<summary class="testcase" id="summary"></summary>
+    AXRole: AXButton
+    AXSubrole: AXSummary
+
+<dfn class="testcase" id="dfn"></dfn>
+    AXRole: AXGroup
+    computedRoleString: definition
+    AXSubrole: AXDefinition
+
+<div class="testcase" id="div"></div>
+    AXRole: AXGroup
+
+<dl class="testcase" id="dl"></dl>
+    AXRole: AXList
+
+<dt class="testcase" id="dt"></dt>
+    AXRole: AXGroup
+    AXSubrole: AXTerm
+
+<dd class="testcase" id="dd"></dd>
+    AXRole: AXGroup
+    AXSubrole: AXDescription
+
+<legend class="testcase" id="legend"></legend>
+    AXRole: AXGroup
+
+<figure class="testcase" id="figure"></figure>
+    AXRole: AXGroup
+    computedRoleString: figure
+    AXSubrole: AXEmptyGroup
+
+<form class="testcase" id="form"></form>
+    AXRole: AXGroup
+    computedRoleString: form
+    AXSubrole: AXEmptyGroup
+
+<h2 class="testcase" id="h2"></h2>
+    AXRole: AXHeading
+    computedRoleString: heading
+
+<hr class="testcase" id="hr">
+    AXRole: AXSplitter
+    computedRoleString: separator
+    AXSubrole: AXContentSeparator
+
+<ins class="testcase" id="ins"></ins>
+    AXRole: AXGroup
+    computedRoleString: insertion
+    AXSubrole: AXInsertStyleGroup
+
+<label class="testcase" id="label"></label>
+    AXRole: AXGroup
+
+<main class="testcase" id="main"></main>
+    AXRole: AXGroup
+    computedRoleString: main
+    AXSubrole: AXLandmarkMain
+
+<mark class="testcase" id="mark"></mark>
+    AXRole: AXGroup
+
+<menu class="testcase" type="toolbar" id="menu-toolbar"></menu>
+    AXRole: AXToolbar
+    computedRoleString: toolbar
+
+<nav class="testcase" id="nav"></nav>
+    AXRole: AXGroup
+    computedRoleString: navigation
+    AXSubrole: AXLandmarkNavigation
+
+<ol class="testcase" id="ol"></ol>
+    AXRole: AXList
+    computedRoleString: list
+
+<li class="testcase" id="ol-li-element"></li>
+    AXRole: AXGroup
+    computedRoleString: listitem
+
+<output class="testcase" id="output"></output>
+    AXRole: AXGroup
+    computedRoleString: status
+    AXSubrole: AXApplicationStatus
+
+<p class="testcase" id="p"></p>
+    AXRole: AXGroup
+    computedRoleString: paragraph
+
+<pre class="testcase" id="pre"></pre>
+    AXRole: AXGroup
+    AXSubrole: AXPreformattedStyleGroup
+
+<section class="testcase" id="section-with-name" aria-label="Section name"></section>
+    AXRole: AXGroup
+    computedRoleString: region
+    AXSubrole: AXLandmarkRegion
+
+<section class="testcase" id="section-without-name"></section>
+    AXRole: AXGroup
+
+<sub class="testcase" id="sub"></sub>
+    AXRole: AXGroup
+    computedRoleString: subscript
+    AXSubrole: AXSubscriptStyleGroup
+
+<sup class="testcase" id="sup"></sup>
+    AXRole: AXGroup
+    computedRoleString: superscript
+    AXSubrole: AXSuperscriptStyleGroup
+
+<time class="testcase" id="time"></time>
+    AXRole: AXGroup
+    computedRoleString: time
+    AXSubrole: AXEmptyGroup
+
+<ul class="testcase" id="ul"></ul>
+    AXRole: AXList
+    computedRoleString: list
+
+<li class="testcase" id="ul-li-element"></li>
+    AXRole: AXGroup
+    computedRoleString: listitem
+
+
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+
+

Added: trunk/LayoutTests/accessibility/display-contents-element-roles.html (0 => 291570)


--- trunk/LayoutTests/accessibility/display-contents-element-roles.html	                        (rev 0)
+++ trunk/LayoutTests/accessibility/display-contents-element-roles.html	2022-03-21 18:48:25 UTC (rev 291570)
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<script src=""
+<script src=""
+<style>
+.testcase { display: contents };
+</style>
+</head>
+<body>
+
+<div id="content">
+    <a href="" id="link" class="testcase">apple.com</a>
+
+    <article class="testcase" id="article">Article content</article>
+
+    <aside class="testcase" id="aside">Aside content</aside>
+
+    <blockquote class="testcase" id="blockquote">To be or not to be, that is the question</blockquote>
+
+    <button class="testcase" id="button">Click me</button>
+
+    <code class="testcase" id="code">Hello world</code>
+
+    <del class="testcase" id="del">Hello world</del>
+
+    <details class="testcase" id="details">
+        <summary class="testcase" id="summary">Summary text</summary>
+    </details>
+
+    <dfn class="testcase" id="dfn">Some definition</dfn>
+
+    <div class="testcase" id="div">Some text</div>
+
+    <dl class="testcase" id="dl">
+        <dt class="testcase" id="dt">dt element</dt>
+        <dd class="testcase" id="dd">dd element</dd>
+    </dl>
+
+    <fieldset>
+        <legend class="testcase" id="legend">Choose your favorite monster</legend>
+
+        <input type="radio" id="radio-button" name="monster">
+        <label for=""
+
+        <input type="radio" id="sasquatch" name="monster">
+        <label for=""
+    </fieldset>
+
+    <figure class="testcase" id="figure"></figure>
+
+    <form class="testcase" id="form"></form>
+
+    <h2 class="testcase" id="h2">Hello world</h2>
+
+    <hr class="testcase" id="hr"></hr>
+
+    <ins class="testcase" id="ins">Hello world</ins>
+
+    <label class="testcase" id="label">Label</label>
+
+    <main class="testcase" id="main">Main content</main>
+
+    <mark class="testcase" id="mark">Marked text</mark>
+
+    <menu class="testcase" type="toolbar" id="menu-toolbar"></menu>
+
+    <nav class="testcase" id="nav">Nav</nav>
+
+    <ol class="testcase" id="ol">
+        <li class="testcase" id="ol-li-element">Hello world</li>
+    </ol>
+
+    <output class="testcase" id="output">Output</output>
+
+    <p class="testcase" id="p">Paragraph</p>
+
+    <pre class="testcase" id="pre">Pre-text</pre>
+
+    <section class="testcase" id="section-with-name" aria-label="Section name">Section</section>
+
+    <section class="testcase" id="section-without-name">Section</section>
+
+    <sub class="testcase" id="sub">Hello world</sub>
+
+    <sup class="testcase" id="sup">Hello world</sup>
+
+    <time class="testcase" id="time"></time>
+
+    <ul class="testcase" id="ul">
+        <li class="testcase" id="ul-li-element">Hello world</li>
+    </ul>
+</div>
+
+<script>
+    // Buffer test output and dump it at the end to make the test run faster vs. individual `debug` calls.
+    var testOutput = "This test ensures elements with CSS display: contents have the correct role.\n\n";
+
+    var axElement;
+    function verifyRoles() {
+        const testcases = document.getElementsByClassName("testcase");
+        for (const domElement of testcases) {
+            const outerHTML = escapeHTML(domElement.cloneNode().outerHTML);
+            axElement = accessibilityController.accessibleElementById(domElement.id);
+            if (!axElement) {
+                testOutput += `FAIL. Couldn't get AX element for #${domElement.id}: ${outerHTML}\n`;
+                return;
+            }
+            testOutput += `${outerHTML}\n`;
+            testOutput += `    ${axElement.role}`;
+
+            let computedRoleString = axElement.computedRoleString;
+            if (computedRoleString)
+                testOutput += `\n    computedRoleString: ${computedRoleString}`;
+
+            let subrole = axElement.subrole;
+            if (subrole.replace("AXSubrole: ", ""))
+                testOutput += `\n    ${subrole}`;
+            testOutput += `\n\n`;
+        }
+    }
+
+    if (window.accessibilityController) {
+        verifyRoles();
+        document.getElementById("content").style.visibility = "hidden";
+        debug(testOutput);
+    }
+</script>
+</body>

Added: trunk/LayoutTests/platform/glib/accessibility/aria-hidden-display-contents-element-expected.txt (0 => 291570)


--- trunk/LayoutTests/platform/glib/accessibility/aria-hidden-display-contents-element-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/platform/glib/accessibility/aria-hidden-display-contents-element-expected.txt	2022-03-21 18:48:25 UTC (rev 291570)
@@ -0,0 +1,21 @@
+This test ensures that an aria-hidden display:contents element behaves as expected.
+
+Testing element #main
+AXRole: AXLandmarkMain
+computedRoleString: main
+
+Testing element #list
+AXRole: AXList
+computedRoleString: list
+
+Testing element #paragraph
+AXRole: AXParagraph
+computedRoleString: paragraph
+
+Hiding #main with aria-hidden='true'.
+PASS: All elements are now hidden.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+

Added: trunk/LayoutTests/platform/glib/accessibility/display-contents-element-roles-expected.txt (0 => 291570)


--- trunk/LayoutTests/platform/glib/accessibility/display-contents-element-roles-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/platform/glib/accessibility/display-contents-element-roles-expected.txt	2022-03-21 18:48:25 UTC (rev 291570)
@@ -0,0 +1,145 @@
+This test ensures elements with CSS display: contents have the correct role.
+
+<a href="" id="link" class="testcase"></a>
+    AXRole: AXLink
+    computedRoleString: link
+
+<article class="testcase" id="article"></article>
+    AXRole: AXArticle
+    computedRoleString: article
+
+<aside class="testcase" id="aside"></aside>
+    AXRole: AXLandmarkComplementary
+    computedRoleString: complementary
+
+<blockquote class="testcase" id="blockquote"></blockquote>
+    AXRole: AXBlockquote
+    computedRoleString: blockquote
+
+<button class="testcase" id="button"></button>
+    AXRole: AXButton
+    computedRoleString: button
+
+<code class="testcase" id="code"></code>
+    AXRole: AXSection
+
+<del class="testcase" id="del"></del>
+    AXRole: AXDeletion
+    computedRoleString: deletion
+
+<details class="testcase" id="details"></details>
+    AXRole: AXUnknown
+
+<summary class="testcase" id="summary"></summary>
+    AXRole: AXUnknown
+
+<dfn class="testcase" id="dfn"></dfn>
+    AXRole: AXDefinition
+    computedRoleString: definition
+
+<div class="testcase" id="div"></div>
+    AXRole: AXSection
+
+<dl class="testcase" id="dl"></dl>
+    AXRole: AXDescriptionList
+
+<dt class="testcase" id="dt"></dt>
+    AXRole: AXDescriptionTerm
+
+<dd class="testcase" id="dd"></dd>
+    AXRole: AXDescriptionValue
+
+<legend class="testcase" id="legend"></legend>
+    AXRole: AXLabel
+
+<figure class="testcase" id="figure"></figure>
+    AXRole: AXGroup
+    computedRoleString: figure
+
+<form class="testcase" id="form"></form>
+    AXRole: AXForm
+    computedRoleString: form
+
+<h2 class="testcase" id="h2"></h2>
+    AXRole: AXHeading
+    computedRoleString: heading
+
+<hr class="testcase" id="hr">
+    AXRole: AXSeparator
+    computedRoleString: separator
+
+<ins class="testcase" id="ins"></ins>
+    AXRole: AXInsertion
+    computedRoleString: insertion
+
+<label class="testcase" id="label"></label>
+    AXRole: AXLabel
+
+<main class="testcase" id="main"></main>
+    AXRole: AXLandmarkMain
+    computedRoleString: main
+
+<mark class="testcase" id="mark"></mark>
+
+
+<menu class="testcase" type="toolbar" id="menu-toolbar"></menu>
+    AXRole: AXToolbar
+    computedRoleString: toolbar
+
+<nav class="testcase" id="nav"></nav>
+    AXRole: AXLandmarkNavigation
+    computedRoleString: navigation
+
+<ol class="testcase" id="ol"></ol>
+    AXRole: AXList
+    computedRoleString: list
+
+<li class="testcase" id="ol-li-element"></li>
+    AXRole: AXListItem
+    computedRoleString: listitem
+
+<output class="testcase" id="output"></output>
+    AXRole: AXStatusBar
+    computedRoleString: status
+
+<p class="testcase" id="p"></p>
+    AXRole: AXParagraph
+    computedRoleString: paragraph
+
+<pre class="testcase" id="pre"></pre>
+    AXRole: AXSection
+
+<section class="testcase" id="section-with-name" aria-label="Section name"></section>
+    AXRole: AXLandmarkRegion
+    computedRoleString: region
+
+<section class="testcase" id="section-without-name"></section>
+    AXRole: AXSection
+
+<sub class="testcase" id="sub"></sub>
+    AXRole: AXSubscript
+    computedRoleString: subscript
+
+<sup class="testcase" id="sup"></sup>
+    AXRole: AXSuperscript
+    computedRoleString: superscript
+
+<time class="testcase" id="time"></time>
+    AXRole: AXStatic
+    computedRoleString: time
+
+<ul class="testcase" id="ul"></ul>
+    AXRole: AXList
+    computedRoleString: list
+
+<li class="testcase" id="ul-li-element"></li>
+    AXRole: AXListItem
+    computedRoleString: listitem
+
+
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+
+

Modified: trunk/LayoutTests/platform/ios/TestExpectations (291569 => 291570)


--- trunk/LayoutTests/platform/ios/TestExpectations	2022-03-21 18:01:29 UTC (rev 291569)
+++ trunk/LayoutTests/platform/ios/TestExpectations	2022-03-21 18:48:25 UTC (rev 291570)
@@ -2088,6 +2088,8 @@
 # Enable "phone number linkifying" test for iOS
 fast/dom/linkify-phone-numbers.html [ Pass ]
 
+accessibility/aria-hidden-display-contents-element.html [ Pass ]
+accessibility/display-contents-element-roles.html [ Pass ]
 accessibility/video-element-url-attribute.html [ Pass ]
 accessibility/visible-character-range.html [ Pass ]
 accessibility/ancestor-computation.html [ Pass ]

Added: trunk/LayoutTests/platform/ios/accessibility/aria-hidden-display-contents-element-expected.txt (0 => 291570)


--- trunk/LayoutTests/platform/ios/accessibility/aria-hidden-display-contents-element-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/platform/ios/accessibility/aria-hidden-display-contents-element-expected.txt	2022-03-21 18:48:25 UTC (rev 291570)
@@ -0,0 +1,18 @@
+This test ensures that an aria-hidden display:contents element behaves as expected.
+
+Testing element #main
+LandmarkMain
+
+Testing element #list
+List
+
+Testing element #paragraph
+Paragraph
+
+Hiding #main with aria-hidden='true'.
+PASS: All elements are now hidden.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+

Added: trunk/LayoutTests/platform/ios/accessibility/display-contents-element-roles-expected.txt (0 => 291570)


--- trunk/LayoutTests/platform/ios/accessibility/display-contents-element-roles-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/platform/ios/accessibility/display-contents-element-roles-expected.txt	2022-03-21 18:48:25 UTC (rev 291570)
@@ -0,0 +1,120 @@
+This test ensures elements with CSS display: contents have the correct role.
+
+<a href="" id="link" class="testcase"></a>
+    WebCoreLink
+
+<article class="testcase" id="article"></article>
+    DocumentArticle
+
+<aside class="testcase" id="aside"></aside>
+    LandmarkComplementary
+
+<blockquote class="testcase" id="blockquote"></blockquote>
+    Blockquote
+
+<button class="testcase" id="button"></button>
+    Button
+
+<code class="testcase" id="code"></code>
+    TextGroup
+
+<del class="testcase" id="del"></del>
+    Deletion
+
+<details class="testcase" id="details"></details>
+    Details
+
+<summary class="testcase" id="summary"></summary>
+    Summary
+
+<dfn class="testcase" id="dfn"></dfn>
+    Definition
+
+<div class="testcase" id="div"></div>
+    Div
+
+<dl class="testcase" id="dl"></dl>
+    DescriptionList
+
+<dt class="testcase" id="dt"></dt>
+    DescriptionListTerm
+
+<dd class="testcase" id="dd"></dd>
+    DescriptionListDetail
+
+<legend class="testcase" id="legend"></legend>
+    Legend
+
+<figure class="testcase" id="figure"></figure>
+    Figure
+
+<form class="testcase" id="form"></form>
+    Form
+
+<h2 class="testcase" id="h2"></h2>
+    Heading
+
+<hr class="testcase" id="hr">
+    HorizontalRule
+
+<ins class="testcase" id="ins"></ins>
+    Insertion
+
+<label class="testcase" id="label"></label>
+    Label
+
+<main class="testcase" id="main"></main>
+    LandmarkMain
+
+<mark class="testcase" id="mark"></mark>
+    Mark
+
+<menu class="testcase" type="toolbar" id="menu-toolbar"></menu>
+    Toolbar
+
+<nav class="testcase" id="nav"></nav>
+    LandmarkNavigation
+
+<ol class="testcase" id="ol"></ol>
+    List
+
+<li class="testcase" id="ol-li-element"></li>
+    ListItem
+
+<output class="testcase" id="output"></output>
+    ApplicationStatus
+
+<p class="testcase" id="p"></p>
+    Paragraph
+
+<pre class="testcase" id="pre"></pre>
+    TextGroup
+
+<section class="testcase" id="section-with-name" aria-label="Section name"></section>
+    LandmarkRegion
+
+<section class="testcase" id="section-without-name"></section>
+    TextGroup
+
+<sub class="testcase" id="sub"></sub>
+    Subscript
+
+<sup class="testcase" id="sup"></sup>
+    Superscript
+
+<time class="testcase" id="time"></time>
+    Time
+
+<ul class="testcase" id="ul"></ul>
+    List
+
+<li class="testcase" id="ul-li-element"></li>
+    ListItem
+
+
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+
+

Modified: trunk/LayoutTests/platform/win/TestExpectations (291569 => 291570)


--- trunk/LayoutTests/platform/win/TestExpectations	2022-03-21 18:01:29 UTC (rev 291569)
+++ trunk/LayoutTests/platform/win/TestExpectations	2022-03-21 18:48:25 UTC (rev 291570)
@@ -1540,6 +1540,8 @@
 ##################        Accessibility Issues              ####################
 ################################################################################
 
+accessibility/display-contents-element-roles.html [ Skip ]
+
 # Missing 'press' support
 accessibility/axpress-on-aria-button.html [ Skip ]
 accessibility/svg-element-press.html [ Skip ]

Added: trunk/LayoutTests/platform/win/accessibility/aria-hidden-display-contents-element-expected.txt (0 => 291570)


--- trunk/LayoutTests/platform/win/accessibility/aria-hidden-display-contents-element-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/platform/win/accessibility/aria-hidden-display-contents-element-expected.txt	2022-03-21 18:48:25 UTC (rev 291570)
@@ -0,0 +1,18 @@
+This test ensures that an aria-hidden display:contents element behaves as expected.
+
+Testing element #main
+AXRole: AXGroup
+
+Testing element #list
+AXRole: AXList
+
+Testing element #paragraph
+AXRole: AXGroup
+
+Hiding #main with aria-hidden='true'.
+PASS: All elements are now hidden.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+

Modified: trunk/Source/WebCore/ChangeLog (291569 => 291570)


--- trunk/Source/WebCore/ChangeLog	2022-03-21 18:01:29 UTC (rev 291569)
+++ trunk/Source/WebCore/ChangeLog	2022-03-21 18:48:25 UTC (rev 291570)
@@ -1,3 +1,57 @@
+2022-03-21  Tyler Wilcock  <tyle...@apple.com>
+
+        AX: Include display: contents elements in the AX tree
+        https://bugs.webkit.org/show_bug.cgi?id=237834
+
+        Reviewed by Chris Fleizach.
+
+        Because display: contents intentionally prevents a render object from being
+        generated for the element it's applied to, we don't add it to the AX tree as
+        part of our normal render tree walk, making these elements inaccessible.
+
+        This patch includes these elements as part of the DOM walk that
+        addHiddenChildren (now renamed to addNodeOnlyChildren) already does.
+
+        Also, because display: contents moves the affected element's children up a
+        level in the render tree, this patch also special cases:
+
+          1. AccessibilityRenderObject::parentObject and similar methods to
+          return their display: contents parent instead of their render tree
+          parent
+          2. AccessibilityObject::insertChild to only insert display: contents
+          children to their display: contents parent, rather than their render
+          tree parent
+
+        Test: accessibility/display-contents-element-roles.html, accessibility/aria-hidden-display-contents-element.html
+
+        * accessibility/AXObjectCache.cpp:
+        (WebCore::AXObjectCache::getOrCreate):
+        Allow creation of AX objects from display: contents `Node`s.
+        Also, don't create an object for a renderer that is in the process of
+        being destroyed (prevents display-contents-element-roles.html from crashing in ITM)
+        * accessibility/AccessibilityObject.cpp:
+        (WebCore::AccessibilityObject::displayContentsParent const):
+        (WebCore::AccessibilityObject::insertChild):
+        If an object has a display: contents parent, and that parent isn't
+        `this`, return early.
+        * accessibility/AccessibilityObject.h:
+        * accessibility/AccessibilityRenderObject.cpp:
+        (WebCore::AccessibilityRenderObject::parentObjectIfExists const):
+        (WebCore::AccessibilityRenderObject::parentObject const):
+        (WebCore::AccessibilityRenderObject::parentObjectUnignored const):
+        If an object has a display: contents parent, return that instead of
+        its render tree parent.
+        (WebCore::AccessibilityRenderObject::addNodeOnlyChildren):
+        Renamed from addHiddenChildren.
+        (WebCore::AccessibilityRenderObject::addChildren):
+        Don't clear m_subtreeDirty until after all children have been added.
+        Necessary to make aria-hidden-display-contents-element.html pass, as
+        we were clearing this too early, causing display: contents subtrees to
+        not be updated after aria-hidden changes.
+        (WebCore::AccessibilityRenderObject::addHiddenChildren):
+        Renamed to addNodeOnlyChildren.
+        * accessibility/AccessibilityRenderObject.h:
+
 2022-03-21  Oriol Brufau  <obru...@igalia.com>
 
         [css-cascade] Don't defer applying text decoration properties

Modified: trunk/Source/WebCore/accessibility/AXObjectCache.cpp (291569 => 291570)


--- trunk/Source/WebCore/accessibility/AXObjectCache.cpp	2022-03-21 18:01:29 UTC (rev 291569)
+++ trunk/Source/WebCore/accessibility/AXObjectCache.cpp	2022-03-21 18:48:25 UTC (rev 291570)
@@ -720,14 +720,10 @@
         return object.get();
     }
 
-    // It's only allowed to create an AccessibilityObject from a Node if it's in a canvas subtree.
-    // Or if it's a hidden element, but we still want to expose it because of other ARIA attributes.
     bool inCanvasSubtree = lineageOfType<HTMLCanvasElement>(*node->parentElement()).first();
-    bool isHidden = isNodeAriaVisible(node);
-
     bool insideMeterElement = is<HTMLMeterElement>(*node->parentElement());
-    
-    if (!inCanvasSubtree && !isHidden && !insideMeterElement)
+    bool hasDisplayContents = is<Element>(*node) && downcast<Element>(*node).hasDisplayContents();
+    if (!inCanvasSubtree && !insideMeterElement && !hasDisplayContents && !isNodeAriaVisible(node))
         return nullptr;
 
     Ref protectedNode { *node };
@@ -760,6 +756,9 @@
     if (AccessibilityObject* obj = get(renderer))
         return obj;
 
+    // Don't create an object for this renderer if it's being destroyed.
+    if (renderer->beingDestroyed())
+        return nullptr;
     RefPtr<AccessibilityObject> newObj = createFromRenderer(renderer);
 
     // Will crash later if we have two objects for the same renderer.

Modified: trunk/Source/WebCore/accessibility/AccessibilityObject.cpp (291569 => 291570)


--- trunk/Source/WebCore/accessibility/AccessibilityObject.cpp	2022-03-21 18:01:29 UTC (rev 291569)
+++ trunk/Source/WebCore/accessibility/AccessibilityObject.cpp	2022-03-21 18:48:25 UTC (rev 291570)
@@ -448,6 +448,16 @@
     });
 }
 
+AccessibilityObject* AccessibilityObject::displayContentsParent() const
+{
+    auto* parentNode = node() ? node()->parentNode() : nullptr;
+    if (!is<Element>(parentNode) || !downcast<Element>(parentNode)->hasDisplayContents())
+        return nullptr;
+
+    auto* cache = axObjectCache();
+    return cache ? cache->getOrCreate(parentNode) : nullptr;
+}
+
 AccessibilityObject* AccessibilityObject::previousSiblingUnignored(int limit) const
 {
     AccessibilityObject* previous;
@@ -575,7 +585,7 @@
 {
     if (!newChild)
         return;
-    
+
     ASSERT(is<AccessibilityObject>(newChild));
     if (!is<AccessibilityObject>(newChild))
         return;
@@ -600,7 +610,12 @@
             }
         }
     }
-    
+
+    auto* displayContentsParent = child->displayContentsParent();
+    // To avoid double-inserting a child of a `display: contents` element, only insert if `this` is the rightful parent.
+    if (displayContentsParent && displayContentsParent != this)
+        return;
+
     auto thisAncestorFlags = computeAncestorFlags();
     child->initializeAncestorFlags(thisAncestorFlags);
     setIsIgnoredFromParentDataForChild(child);
@@ -625,7 +640,7 @@
     } else {
         // Table component child-parent relationships often don't line up properly, hence the need for methods
         // like parentTable() and parentRow(). Exclude them from this ASSERT.
-        ASSERT((!isTableComponent(*child) && !isTableComponent(*this)) ? child->parentObject() == this : true);
+        ASSERT(isTableComponent(*child) || isTableComponent(*this) || child->parentObject() == this);
         m_children.insert(index, child);
     }
     

Modified: trunk/Source/WebCore/accessibility/AccessibilityObject.h (291569 => 291570)


--- trunk/Source/WebCore/accessibility/AccessibilityObject.h	2022-03-21 18:01:29 UTC (rev 291569)
+++ trunk/Source/WebCore/accessibility/AccessibilityObject.h	2022-03-21 18:48:25 UTC (rev 291570)
@@ -373,6 +373,7 @@
     AccessibilityObject* nextSiblingUnignored(int limit) const override;
     AccessibilityObject* previousSiblingUnignored(int limit) const override;
     AccessibilityObject* parentObject() const override { return nullptr; }
+    AccessibilityObject* displayContentsParent() const;
     AXCoreObject* parentObjectUnignored() const override;
     AccessibilityObject* parentObjectIfExists() const override { return nullptr; }
     static AccessibilityObject* firstAccessibleObjectFromNode(const Node*);

Modified: trunk/Source/WebCore/accessibility/AccessibilityRenderObject.cpp (291569 => 291570)


--- trunk/Source/WebCore/accessibility/AccessibilityRenderObject.cpp	2022-03-21 18:01:29 UTC (rev 291569)
+++ trunk/Source/WebCore/accessibility/AccessibilityRenderObject.cpp	2022-03-21 18:48:25 UTC (rev 291570)
@@ -491,11 +491,17 @@
     if (m_renderer && isWebArea())
         return cache->get(&m_renderer->view().frameView());
 
+    if (auto* displayContentsParent = this->displayContentsParent())
+        return displayContentsParent;
+
     return cache->get(renderParentObject());
 }
     
 AccessibilityObject* AccessibilityRenderObject::parentObject() const
 {
+    if (auto* displayContentsParent = this->displayContentsParent())
+        return displayContentsParent;
+
     if (!m_renderer)
         return nullptr;
     
@@ -508,15 +514,14 @@
         if (parent)
             return parent;
     }
-    
+
     AXObjectCache* cache = axObjectCache();
     if (!cache)
         return nullptr;
+
+    if (auto* parentObject = renderParentObject())
+        return cache->getOrCreate(parentObject);
     
-    RenderObject* parentObj = renderParentObject();
-    if (parentObj)
-        return cache->getOrCreate(parentObj);
-    
     // WebArea's parent should be the scroll view containing it.
     if (isWebArea())
         return cache->getOrCreate(&m_renderer->view().frameView());
@@ -537,7 +542,7 @@
     }
 #endif
 
-    return AccessibilityNodeObject::parentObjectUnignored();
+    return AccessibilityObject::parentObjectUnignored();
 }
 
 bool AccessibilityRenderObject::isAttachment() const
@@ -3198,29 +3203,36 @@
 }
 #endif
 
-// Hidden children are those that are not rendered or visible, but are specifically marked as aria-hidden=false,
-// meaning that they should be exposed to the AX hierarchy.
-void AccessibilityRenderObject::addHiddenChildren()
+// Some elements don't have an associated render object, meaning they won't be picked up by a walk of the render tree.
+// For example, nodes that are `aria-hidden="false"` and `hidden`, or elements with `display: contents`.
+// This function will find and add these elements to the AX tree.
+void AccessibilityRenderObject::addNodeOnlyChildren()
 {
     Node* node = this->node();
     if (!node)
         return;
-    
-    // First do a quick run through to determine if we have any hidden nodes (most often we will not).
-    // If we do have hidden nodes, we need to determine where to insert them so they match DOM order as close as possible.
-    bool shouldInsertHiddenNodes = false;
+
+    auto nodeHasDisplayContents = [] (Node& node) {
+        return is<Element>(node) && downcast<Element>(node).hasDisplayContents();
+    };
+    // First do a quick run through to determine if we have any interesting nodes (most often we will not).
+    // If we do have any interesting nodes, we need to determine where to insert them so they match DOM order as close as possible.
+    bool hasNodeOnlyChildren = false;
     for (Node* child = node->firstChild(); child; child = child->nextSibling()) {
-        if (!child->renderer() && isNodeAriaVisible(child)) {
-            shouldInsertHiddenNodes = true;
+        if (child->renderer())
+            continue;
+
+        if (nodeHasDisplayContents(*child) || isNodeAriaVisible(child)) {
+            hasNodeOnlyChildren = true;
             break;
         }
     }
     
-    if (!shouldInsertHiddenNodes)
+    if (!hasNodeOnlyChildren)
         return;
     
     // Iterate through all of the children, including those that may have already been added, and
-    // try to insert hidden nodes in the correct place in the DOM order.
+    // try to insert the nodes in the correct place in the DOM order.
     unsigned insertionIndex = 0;
     for (Node* child = node->firstChild(); child; child = child->nextSibling()) {
         if (child->renderer()) {
@@ -3239,7 +3251,7 @@
             continue;
         }
 
-        if (!isNodeAriaVisible(child))
+        if (!nodeHasDisplayContents(*child) && !isNodeAriaVisible(child))
             continue;
         
         unsigned previousSize = m_children.size();
@@ -3311,9 +3323,7 @@
     for (RefPtr<AccessibilityObject> object = firstChild(); object; object = object->nextSibling())
         addChildIfNeeded(*object);
 
-    m_subtreeDirty = false;
-    
-    addHiddenChildren();
+    addNodeOnlyChildren();
     addAttachmentChildren();
     addImageMapChildren();
     addTextFieldChildren();
@@ -3322,11 +3332,11 @@
 #if USE(ATSPI)
     addListItemMarker();
 #endif
-
 #if PLATFORM(COCOA)
     updateAttachmentViewParents();
 #endif
-    
+
+    m_subtreeDirty = false;
     updateRoleAfterChildrenCreation();
 }
 

Modified: trunk/Source/WebCore/accessibility/AccessibilityRenderObject.h (291569 => 291570)


--- trunk/Source/WebCore/accessibility/AccessibilityRenderObject.h	2022-03-21 18:01:29 UTC (rev 291569)
+++ trunk/Source/WebCore/accessibility/AccessibilityRenderObject.h	2022-03-21 18:48:25 UTC (rev 291570)
@@ -248,7 +248,7 @@
     void offsetBoundingBoxForRemoteSVGElement(LayoutRect&) const;
     bool supportsPath() const override;
 
-    void addHiddenChildren();
+    void addNodeOnlyChildren();
     void addTextFieldChildren();
     void addImageMapChildren();
     void addCanvasChildren();
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to