Skip to content

Conversation

@francinelucca
Copy link
Member

@francinelucca francinelucca commented Jan 28, 2026

Authors: Please fill out this form carefully and completely.

Reviewers: By approving this Pull Request you are approving the code change, as well as its deployment and mitigation plans.
Please read this description carefully. If you feel there is anything unclear or missing, please ask for updates.

Relates to https://github.com/github/accessibility-audits/issues/14650

What are you trying to accomplish?

Resolve accessibility audit issue where child role is missing

Accessibility and Focus Management Improvements:

  • Add role="treeitem" to include-fragment elements to improve accessibility.
  • Ensures that when a subtree node is an include-fragment with role="treeitem", it is assigned the correct tabindex and role attributes when expanded, and these attributes are removed when collapsed. This allows the fragment to participate in keyboard navigation
  • Updates focus handling so that if the include-fragment subtree element itself is focused, its tabindex is managed correctly when moving focus between nodes.
  • Adjusts logic to correctly track when the loader or the include-fragment itself has focus during fragment replacement, ensuring focus state is preserved.

Testing:

  • Adds a system test to verify that keyboard focus moves back to the parent node and that it becomes tabbable when an expanded include-fragment node is collapsed.

Screenshots

Integration

N/A

List the issues that this change affects.

Closes https://github.com/github/accessibility-audits/issues/14650

Risk Assessment

  • Low risk the change is small, highly observable, and easily rolled back.
  • Medium risk changes that are isolated, reduced in scope or could impact few users. The change will not impact library availability.
  • High risk changes are those that could impact customers and SLOs, low or no test coverage, low observability, or slow to rollback.

What approach did you choose and why?

Consulted a11y on the best fix and decided to add role="treeitem" to the shadow dom node to solve the a11y scan issue, though this may not translate to a real world scenario in this case.

Anything you want to highlight for special attention from reviewers?

N/A

Accessibility

  • Fixes axe scan violation - This change fixes an existing axe scan violation.

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Lookbook)
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge

Take a look at the What we look for in reviews section of the contributing guidelines for more information on how we review PRs.

@francinelucca francinelucca requested a review from a team as a code owner January 28, 2026 03:45
@francinelucca francinelucca requested review from Copilot and llastflowers and removed request for Copilot January 28, 2026 03:45
@changeset-bot
Copy link

changeset-bot bot commented Jan 28, 2026

🦋 Changeset detected

Latest commit: 4947038

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/view-components Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@francinelucca francinelucca added the skip changeset Pull requests that don't change the library output label Jan 28, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR attempts to fix an accessibility audit issue by adding role="treeitem" to the tree-view-include-fragment custom element in the spinner loader component. However, this creates an invalid ARIA tree structure.

Changes:

  • Added role="treeitem" attribute to the tree-view-include-fragment element in spinner_loader.html.erb
Comments suppressed due to low confidence (1)

app/components/primer/alpha/tree_view/spinner_loader.html.erb:5

  • The skeleton_loader.html.erb file does not have role="treeitem" on its tree-view-include-fragment element (line 5), creating an inconsistency with the spinner_loader change. This inconsistency suggests that adding the role to spinner_loader may not be the correct approach. Both loaders should be treated the same way since they serve the same purpose and have the same DOM structure.
<%= render(Primer::BaseComponent.new(tag: :"tree-view-include-fragment", src: @src, loading: :lazy, data: { target: "tree-view-sub-tree-node.subTree tree-view-sub-tree-node.includeFragment", path: @container.path.to_json }, hidden: @container.expanded?, accept: "text/fragment+html", role: "treeitem")) do %>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

<% end %>

<%= render(Primer::BaseComponent.new(tag: :"tree-view-include-fragment", src: @src, loading: :lazy, data: { target: "tree-view-sub-tree-node.subTree tree-view-sub-tree-node.includeFragment", path: @container.path.to_json }, hidden: @container.expanded?, accept: "text/fragment+html")) do %>
<%= render(Primer::BaseComponent.new(tag: :"tree-view-include-fragment", src: @src, loading: :lazy, data: { target: "tree-view-sub-tree-node.subTree tree-view-sub-tree-node.includeFragment", path: @container.path.to_json }, hidden: @container.expanded?, accept: "text/fragment+html", role: "treeitem")) do %>
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding role="treeitem" to the tree-view-include-fragment element creates an invalid ARIA tree structure. According to the WAI-ARIA specification, a treeitem element cannot contain a group element, but this tree-view-include-fragment wraps a ul[role="group"] (SubTreeContainer).

The actual DOM structure becomes:

li[role="none"]
  └── tree-view-include-fragment[role="treeitem"]  ← Invalid
      └── ul[role="group"]
          └── li[role="none"]
              └── div[role="treeitem"]

The tree-view-include-fragment is a technical implementation detail for lazy loading and should remain role-less (allowing it to be transparent in the accessibility tree). The actual treeitems are the div[role="treeitem"] elements inside the ul[role="group"] that this fragment contains.

Note that skeleton_loader.html.erb does not have this role attribute, creating an inconsistency. The correct fix depends on what the actual accessibility issue is. If the concern is that the loading indicator needs to be announced, the role should be on the inner Node element (which already has role="treeitem" from Node.rb line 108), not on the outer include-fragment wrapper.

Suggested change
<%= render(Primer::BaseComponent.new(tag: :"tree-view-include-fragment", src: @src, loading: :lazy, data: { target: "tree-view-sub-tree-node.subTree tree-view-sub-tree-node.includeFragment", path: @container.path.to_json }, hidden: @container.expanded?, accept: "text/fragment+html", role: "treeitem")) do %>
<%= render(Primer::BaseComponent.new(tag: :"tree-view-include-fragment", src: @src, loading: :lazy, data: { target: "tree-view-sub-tree-node.subTree tree-view-sub-tree-node.includeFragment", path: @container.path.to_json }, hidden: @container.expanded?, accept: "text/fragment+html")) do %>

Copilot uses AI. Check for mistakes.
@francinelucca francinelucca requested a review from Copilot February 5, 2026 02:25
@francinelucca francinelucca removed the skip changeset Pull requests that don't change the library output label Feb 5, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (3)

app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.ts:432

  • Potential null reference error: this code checks attributes on this.subTree without first verifying that this.subTree exists. Line 425 shows that this.subTree can be null/undefined. This check should be inside the if (this.subTree) block on line 425 to prevent accessing attributes on a null/undefined object.

  #update() {
    if (this.expanded) {
      if (this.subTree) this.subTree.hidden = false
      if (this.subTree.getAttribute('data-target')?.includes('tree-view-sub-tree-node.includeFragment')) {
        this.subTree.setAttribute('role', 'treeitem')
        // Ensure the include-fragment can participate in roving tab index

app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.ts:450

  • Potential null reference error: this code checks attributes on this.subTree without first verifying that this.subTree exists. Line 445 shows that this.subTree can be null/undefined. This check should be inside the if (this.subTree) block on line 445 to prevent accessing attributes on a null/undefined object.
        this.collapsedToggleIcon.setAttribute('hidden', 'hidden')
      }
    } else {
      if (this.subTree) this.subTree.hidden = true
      if (this.subTree.getAttribute('data-target')?.includes('tree-view-sub-tree-node.includeFragment')) {

app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.ts:432

  • The approach of checking if this.subTree is an include-fragment by string matching on the data-target attribute is fragile and could break if the attribute format changes. Consider using a more robust check like this.subTree instanceof TreeViewIncludeFragmentElement or this.subTree === this.includeFragment to verify if the subTree element is an include-fragment.

  #update() {
    if (this.expanded) {
      if (this.subTree) this.subTree.hidden = false
      if (this.subTree.getAttribute('data-target')?.includes('tree-view-sub-tree-node.includeFragment')) {
        this.subTree.setAttribute('role', 'treeitem')
        // Ensure the include-fragment can participate in roving tab index

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI commented Feb 5, 2026

@francinelucca I've opened a new pull request, #3917, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Feb 5, 2026

@francinelucca I've opened a new pull request, #3918, to work on those changes. Once the pull request is ready, I'll request review from you.

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: francinelucca <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: francinelucca <[email protected]>
Add treeitem role to shadow DOM node in treeview.
@francinelucca francinelucca added this pull request to the merge queue Feb 6, 2026
Merged via the queue into main with commit 4ddf815 Feb 6, 2026
30 of 31 checks passed
@francinelucca francinelucca deleted the chore/add-treeview-role branch February 6, 2026 04:01
@primer primer bot mentioned this pull request Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants