Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1933706
chore(treeview): add treeitem role to shadow dom node
francinelucca Jan 28, 2026
6690fe2
Merge branch 'main' into chore/add-treeview-role
francinelucca Jan 28, 2026
62394fb
Merge branch 'main' of github.com:primer/view_components into chore/a…
francinelucca Feb 3, 2026
75f87c7
Merge branch 'chore/add-treeview-role' of github.com:primer/view_comp…
francinelucca Feb 3, 2026
75781e9
only add treeitem role when treeview fragment NOT hidden
francinelucca Feb 4, 2026
877807d
Merge branch 'main' into chore/add-treeview-role
francinelucca Feb 4, 2026
2a8b768
Merge branch 'main' into chore/add-treeview-role
francinelucca Feb 4, 2026
919cedd
Merge branch 'main' of github.com:primer/view_components into chore/a…
francinelucca Feb 4, 2026
68a0677
Merge branch 'chore/add-treeview-role' of github.com:primer/view_comp…
francinelucca Feb 4, 2026
5417621
test
francinelucca Feb 4, 2026
e43283f
Merge branch 'main' into chore/add-treeview-role
francinelucca Feb 5, 2026
9ad047a
replace includeFragment node when hidden, add keyboard test
francinelucca Feb 5, 2026
801b8c9
Merge branch 'chore/add-treeview-role' of github.com:primer/view_comp…
francinelucca Feb 5, 2026
1e80bba
prettier
francinelucca Feb 5, 2026
67ff13b
[WIP] Fix treeitem role on shadow DOM node based on feedback (#3918)
Copilot Feb 5, 2026
3b55d7c
Extract duplicated include-fragment check into private method (#3917)
Copilot Feb 5, 2026
ba55fbe
Update treeview with treeitem role
francinelucca Feb 6, 2026
4947038
Merge branch 'main' into chore/add-treeview-role
francinelucca Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nasty-moles-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/view-components": patch
---

chore(treeview): add treeitem role to shadow dom node
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {
// sub-tree and no node in the entire tree can be focused
const previousNode = this.subTree.querySelector("[tabindex='0']")
previousNode?.setAttribute('tabindex', '-1')

// Also check if the subtree element itself is an include-fragment with role="treeitem" and has focus
if (this.#isIncludeFragment() && this.subTree.getAttribute('tabindex') === '0') {
this.subTree.setAttribute('tabindex', '-1')
}

this.node.setAttribute('tabindex', '0')

this.treeView.dispatchEvent(
Expand Down Expand Up @@ -263,6 +269,10 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {
// request succeeded but element has not yet been replaced
case 'include-fragment-replace':
this.#activeElementIsLoader = document.activeElement === this.loadingIndicator.closest('[role=treeitem]')
// Also check if the include-fragment itself has focus (when it has role="treeitem")
if (!this.#activeElementIsLoader && document.activeElement === this.subTree && this.#isIncludeFragment()) {
this.#activeElementIsLoader = true
}
this.loadingState = 'success'
break

Expand Down Expand Up @@ -410,6 +420,13 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {
#update() {
if (this.expanded) {
if (this.subTree) this.subTree.hidden = false
if (this.#isIncludeFragment()) {
this.subTree.setAttribute('role', 'treeitem')
// Ensure the include-fragment can participate in roving tab index
if (!this.subTree.hasAttribute('tabindex')) {
this.subTree.setAttribute('tabindex', '-1')
}
}
this.node.setAttribute('aria-expanded', 'true')
this.treeView?.expandAncestorsForNode(this)

Expand All @@ -423,6 +440,11 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {
}
} else {
if (this.subTree) this.subTree.hidden = true
if (this.#isIncludeFragment()) {
this.subTree.removeAttribute('role')
// Remove tabindex when role is removed
this.subTree.removeAttribute('tabindex')
}
this.node.setAttribute('aria-expanded', 'false')

if (this.iconPair) {
Expand Down Expand Up @@ -453,6 +475,10 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {
}
}

#isIncludeFragment(): boolean {
return this.subTree?.getAttribute('data-target')?.includes('tree-view-sub-tree-node.includeFragment') ?? false
}

get #checkboxElement(): HTMLElement | null {
return this.querySelector('.TreeViewItemCheckbox')
}
Expand Down
28 changes: 28 additions & 0 deletions test/system/alpha/tree_view_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -799,5 +799,33 @@ def test_form_submission

assert_equal "{\"path\":[\"action_menu.rb\"],\"value\":\"3\"}", response.dig("form_params", "folder_structure", 0)
end

def test_keyboard_focus_moves_to_parent_when_include_fragment_with_role_treeitem_is_collapsed
visit_preview(:loading_spinner)

# Tab to focus the tree view
keyboard.type(:tab)

# Enter to expand the spinner node (which includes an include-fragment with role="treeitem")
keyboard.type(:enter)

# Verify that the include-fragment has role="treeitem" when expanded
assert_selector 'tree-view-include-fragment[role=treeitem]'

# Wait for the loader to be replaced
assert_path("primer", "alpha")

# Navigate to a child node
keyboard.type(:down)
assert_path_selected("primer", "alpha")

# Now collapse the parent node using arrow keys while focus is on child
keyboard.type(:up) # go back to parent
keyboard.type(:left) # collapse

# Focus should move to the parent node and it should be tabbable
assert_path_selected("primer")
assert_path_tabbable("primer")
end
end
end
Loading