From 9befd31649883f7a4f0609127b9d0db35575b51a Mon Sep 17 00:00:00 2001 From: Gary Ritchie Date: Fri, 23 Oct 2026 11:15:32 -0600 Subject: [PATCH 1/2] Hide CellTree expand images and group nested tree items Nested treeitems had no group parent (Axe aria-required-parent), and the open/closed/loading images had no alt text (Axe image-alt). Mark the image wrapper aria-hidden and give each non-root node's child container role=group. Addresses rstudio/rstudio#19244 --- .../gwt/user/cellview/client/CellTree.java | 4 +- .../cellview/client/CellTreeNodeView.java | 5 ++ .../user/cellview/client/CellTreeTest.java | 49 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/user/src/com/google/gwt/user/cellview/client/CellTree.java b/user/src/com/google/gwt/user/cellview/client/CellTree.java index 94b61de117c..6a5b40e70e8 100644 --- a/user/src/com/google/gwt/user/cellview/client/CellTree.java +++ b/user/src/com/google/gwt/user/cellview/client/CellTree.java @@ -455,7 +455,9 @@ interface BasicStyle extends Style { } interface Template extends SafeHtmlTemplates { - @Template("
{2}
") + // The open/closed/loading image is decorative: the item's aria-expanded conveys it. + @Template("
" + + "{2}
") SafeHtml imageWrapper(String classes, SafeStyles cssLayout, SafeHtml image); } diff --git a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java index 878d80a9162..fa09a7e7ab7 100644 --- a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java +++ b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java @@ -1170,6 +1170,11 @@ Element ensureAnimationFrame() { Element ensureChildContainer() { if (childContainer == null) { childContainer = Document.get().createDivElement(); + // Nested items need a group between them and their parent item; the root's + // children are owned by the tree element itself. + if (!isRootNode()) { + Roles.getGroupRole().set(childContainer); + } ensureContentContainer().insertFirst(childContainer); } return childContainer; diff --git a/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java index 229a7e68eae..51ae84e25f7 100644 --- a/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java +++ b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java @@ -17,6 +17,8 @@ import com.google.gwt.cell.client.AbstractCell; import com.google.gwt.cell.client.TextCell; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.user.client.ui.RootPanel; @@ -267,6 +269,53 @@ public void testAriaAttributes() { assertEquals("5", cViewChildLast.getElement().getAttribute("aria-setsize")); } + public void testChildContainerRoles() { + CellTree cellTree = (CellTree) tree; + TreeNode node = cellTree.getRootTreeNode(); + while (!node.isChildLeaf(0)) { + node = node.setChildOpen(0, true); + } + + // Top-level items are owned by the tree itself. + CellTreeNodeView view = cellTree.rootNode.getChildNode(0); + assertEquals(cellTree.getElement(), getClosestRoleAncestor(view.getElement())); + + // Nested items, expandable or leaf, sit in a group owned by their parent item. + while (view.getChildCount() > 0) { + view = view.getChildNode(0); + Element group = getClosestRoleAncestor(view.getElement()); + assertEquals("group", group.getAttribute("role")); + assertEquals(view.getParentNode().getElement(), getClosestRoleAncestor(group)); + } + assertTrue(view.isLeaf()); + } + + public void testExpandImagesHiddenFromAssistiveTech() { + CellTree cellTree = (CellTree) tree; + TreeNode rootNode = cellTree.getRootTreeNode(); + // Reading the child count flushes the pending render of the opened node's children. + assertTrue(rootNode.setChildOpen(0, true).getChildCount() > 0); + CellTreeNodeView aView = cellTree.rootNode.getChildNode(0); + assertEquals("true", aView.getImageElement().getAttribute("aria-hidden")); + CellTreeNodeView aViewChild = aView.getChildNode(0); + assertEquals("true", aViewChild.getImageElement().getAttribute("aria-hidden")); + + rootNode.setChildOpen(0, false); + assertEquals("true", aView.getImageElement().getAttribute("aria-hidden")); + + Element loading = Document.get().createDivElement(); + loading.setInnerSafeHtml(cellTree.getLoadingImageHtml()); + assertEquals("true", loading.getFirstChildElement().getAttribute("aria-hidden")); + } + + private static Element getClosestRoleAncestor(Element elem) { + Element parent = elem.getParentElement(); + while (parent != null && parent.getAttribute("role").isEmpty()) { + parent = parent.getParentElement(); + } + return parent; + } + public void testExplicitKeyboardSelection() { CellTree cellTree = (CellTree) tree; TreeNode root = cellTree.getRootTreeNode(); From 2b7f7ccdf67da5f36d9608963f84f8ae815ebd8c Mon Sep 17 00:00:00 2001 From: Gary Ritchie Date: Fri, 23 Oct 2026 11:44:31 -0600 Subject: [PATCH 2/2] Narrow the CellTree image comment and pin the test's node shape aria-expanded conveys open/closed but not loading, so the comment no longer claims it covers the loading image. The image test now asserts the child it inspects is expandable, since a leaf renders a placeholder without the image. Addresses rstudio/rstudio#19244 --- user/src/com/google/gwt/user/cellview/client/CellTree.java | 2 +- user/test/com/google/gwt/user/cellview/client/CellTreeTest.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/user/src/com/google/gwt/user/cellview/client/CellTree.java b/user/src/com/google/gwt/user/cellview/client/CellTree.java index 6a5b40e70e8..ea7a2e187ae 100644 --- a/user/src/com/google/gwt/user/cellview/client/CellTree.java +++ b/user/src/com/google/gwt/user/cellview/client/CellTree.java @@ -455,7 +455,7 @@ interface BasicStyle extends Style { } interface Template extends SafeHtmlTemplates { - // The open/closed/loading image is decorative: the item's aria-expanded conveys it. + // The open/closed/loading image is decorative; aria-expanded conveys the open/closed state. @Template("
" + "{2}
") SafeHtml imageWrapper(String classes, SafeStyles cssLayout, SafeHtml image); diff --git a/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java index 51ae84e25f7..11d67a6bb32 100644 --- a/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java +++ b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java @@ -298,6 +298,8 @@ public void testExpandImagesHiddenFromAssistiveTech() { CellTreeNodeView aView = cellTree.rootNode.getChildNode(0); assertEquals("true", aView.getImageElement().getAttribute("aria-hidden")); CellTreeNodeView aViewChild = aView.getChildNode(0); + // A leaf renders a placeholder div instead of the closed image. + assertFalse(aViewChild.isLeaf()); assertEquals("true", aViewChild.getImageElement().getAttribute("aria-hidden")); rootNode.setChildOpen(0, false);