Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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/full-states-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/view-components': minor
---

Support single selection variant for TreeView
7 changes: 7 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions .idea/primer_view_components.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

277 changes: 277 additions & 0 deletions .idea/workspace.xml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- tree:
- treeitem "src" [checked] [level=1] [selected]
- treeitem "action_menu.rb" [level=1]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- tree:
- treeitem "src" [level=1]
- treeitem "action_menu.rb" [level=1]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- tree:
- treeitem "src" [checked] [level=1] [selected]
- treeitem "action_menu.rb" [level=1]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- tree:
- treeitem "src" [level=1]
- treeitem "action_menu.rb" [level=1]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions app/components/primer/alpha/tree_view.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@
}
}

&[aria-checked='false'] {
/* singleselect checkmark */
& .TreeViewItem-singleSelectCheckmark {
visibility: hidden;
}
}

&[aria-checked='mixed'] {
& .FormControl-checkbox {
background: var(--control-checked-bgColor-rest);
Expand Down
16 changes: 15 additions & 1 deletion app/components/primer/alpha/tree_view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ module Alpha
#
# Nodes can be checked via the keyboard by pressing the space key.
#
# ## Single-select mode
#
# By passing `select_variant: :single` to both sub-tree and leaf nodes:
# - Nodes become selectable and can be toggled via keyboard (space key).
# - A selected node displays a checkmark at the end of the line.
# Note: This checkmark conflicts with the `trailing_visual_icon` slot,
# so both cannot be used simultaneously.
#
# ## Node tags
#
# `TreeView`s support three different node variants, `:anchor`, `:button`, and `:div` (the default), which controls
Expand All @@ -259,13 +267,19 @@ module Alpha
# |:---------------|:-------------|:------------|:--------------------------|
# |Enter/space |none |div |Expands/collapses |
# |Enter/space |none |anchor/button|Activates anchor/button |
# |Enter/space |single |div |Selects |
# |Enter/space |single |anchor/button|N/A (not allowed) |
# |Enter/space |multiple |div |Checks or unchecks |
# |Enter/space |multiple |anchor/button|N/A (not allowed) |
# |Left/right arrow|none |div |Expands/collapses |
# |Left/right arrow|none |anchor/button|Expands/collapses |
# |Left/right arrow|single |div |Expands/collapses |
# |Left/right arrow|single |anchor/button|N/A (not allowed) |
# |Left/right arrow|multiple |div |Expands/collapses |
# |Left/right arrow|multiple |anchor/button|N/A (not allowed) |
# |Click |none |div |Expands/collapses |
# |Click |single |div |Selects |
# |Click |single |anchor/button|N/A (not allowed) |
# |Click |multiple |div |Checks or unchecks |
# |Click |multiple |anchor/button|N/A (not allowed) |
#
Expand Down Expand Up @@ -351,7 +365,7 @@ module Alpha
# )
# ```
#
# Because checking or unchecking a sub-tree results in the checking or unchecking of all its children recursively,
# Because checking or unchecking a sub-tree may result in the checking or unchecking of all its children recursively,
# both the `treeViewNodeChecked` and `treeViewBeforeNodeChecked` events provide an array of `TreeViewNodeInfo`
# objects, which contain entries for every modified node in the tree.
class TreeView < Primer::Component
Expand Down
6 changes: 5 additions & 1 deletion app/components/primer/alpha/tree_view/node.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
<%= leading_visual %>
<% end %>
<span class="TreeViewItemContentText"><%= text_content %></span>
<% if trailing_visual? %>
<% if @select_variant == :single %>
<span class="TreeViewItemVisual" aria-hidden="true">
<%= render(Primer::Beta::Octicon.new(icon: :check, classes: "TreeViewItem-singleSelectCheckmark")) %>
</span>
<% elsif trailing_visual? %>
<%= trailing_visual %>
<% end %>
<% end %>
Expand Down
8 changes: 7 additions & 1 deletion app/components/primer/alpha/tree_view/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Node < Primer::Component

DEFAULT_SELECT_VARIANT = :none
SELECT_VARIANT_OPTIONS = [
:single,
:multiple,
DEFAULT_SELECT_VARIANT
].freeze
Expand Down Expand Up @@ -128,7 +129,8 @@ def initialize(
@content_arguments, {
data: {
value: value,
path: @path.to_json
path: @path.to_json,
select_variant: @select_variant
}
}
)
Expand Down Expand Up @@ -169,6 +171,10 @@ def merge_system_arguments!(**other_arguments)
private

def before_render
if trailing_visual? && select_variant == :single
raise ArgumentError, "Trailing visuals can't be used in combination with single select mode as the icon is reserved."
end

if leading_action?
@content_arguments[:data] = merge_data(
@content_arguments,
Expand Down
2 changes: 1 addition & 1 deletion app/components/primer/alpha/tree_view/sub_tree_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def initialize(
node_variant: node_variant
)

return if @node.select_variant == :none
return unless @node.select_variant == :multiple

@node.merge_system_arguments!(
data: {
Expand Down
70 changes: 67 additions & 3 deletions app/components/primer/alpha/tree_view/tree_view.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {controller, target} from '@github/catalyst'
import {TreeViewSubTreeNodeElement} from './tree_view_sub_tree_node_element'
import {SelectVariant, TreeViewSubTreeNodeElement} from './tree_view_sub_tree_node_element'
import {useRovingTabIndex} from './tree_view_roving_tab_index'
import type {TreeViewNodeType, TreeViewCheckedValue, TreeViewNodeInfo} from '../../shared_events'
import type {TreeViewCheckedValue, TreeViewNodeInfo, TreeViewNodeType} from '../../shared_events'

@controller
export class TreeViewElement extends HTMLElement {
Expand Down Expand Up @@ -114,6 +114,8 @@ export class TreeViewElement extends HTMLElement {
#handleNodeEvent(node: Element, event: Event) {
if (this.#eventIsCheckboxToggle(event, node)) {
this.#handleCheckboxToggle(event, node)
} else if (this.#eventIsSingleSelection(event, node)) {
this.handleSingleSelection(event, node)
} else if (this.#eventIsActivation(event)) {
this.#handleNodeActivated(event, node)
} else if (event.type === 'focusin') {
Expand Down Expand Up @@ -145,6 +147,48 @@ export class TreeViewElement extends HTMLElement {
}
}

#eventIsSingleSelection(event: Event, node: Element) {
return event.type === 'click' && this.selectVariant(node) === 'single'
}

handleSingleSelection(event: Event, node: Element) {
if (this.getNodeDisabledValue(node)) {
event.preventDefault()
return
}

// do not emit activation events for buttons and anchors, since it is assumed any activation
// behavior for these element types is user- or browser-defined
if (!(node instanceof HTMLDivElement)) return

const path = this.getNodePath(node)
const nodeInfo = this.infoFromNode(node, 'true')

const checkSuccess = this.dispatchEvent(
new CustomEvent('treeViewBeforeNodeChecked', {
bubbles: true,
cancelable: true,
detail: [nodeInfo],
}),
)

if (!checkSuccess) return

const currentlyChecked = !this.getNodeCheckedValue(node)

// disallow unchecking checked item in single-select mode
if (!currentlyChecked) {
this.checkOnlyAtPath(path)
}

this.dispatchEvent(
new CustomEvent('treeViewNodeChecked', {
bubbles: true,
detail: [nodeInfo],
}),
)
}

#handleNodeActivated(event: Event, node: Element) {
if (this.getNodeDisabledValue(node)) {
event.preventDefault()
Expand Down Expand Up @@ -199,14 +243,18 @@ export class TreeViewElement extends HTMLElement {
break
}

if (this.nodeHasCheckBox(node)) {
if (this.selectVariant(node) === 'multiple') {
event.preventDefault()

if (this.getNodeCheckedValue(node) === 'true') {
this.setNodeCheckedValue(node, 'false')
} else {
this.setNodeCheckedValue(node, 'true')
}
} else if (this.selectVariant(node) === 'single') {
event.preventDefault()

this.checkOnlyAtPath(this.getNodePath(node))
} else if (node instanceof HTMLAnchorElement) {
// simulate click on space
node.click()
Expand Down Expand Up @@ -247,6 +295,10 @@ export class TreeViewElement extends HTMLElement {
return this.querySelector('[aria-current=true]')
}

get activeNodes() {
return document.querySelectorAll('[aria-checked="true"]')
}

expandAtPath(path: string[]) {
const node = this.subTreeAtPath(path)
if (!node) return
Expand Down Expand Up @@ -282,6 +334,14 @@ export class TreeViewElement extends HTMLElement {
this.setNodeCheckedValue(node, 'false')
}

checkOnlyAtPath(path: string[]) {
for (const el of this.activeNodes) {
this.uncheckAtPath(this.getNodePath(el))
}

this.checkAtPath(path)
}

toggleCheckedAtPath(path: string[]) {
const node = this.nodeAtPath(path)
if (!node) return
Expand Down Expand Up @@ -382,6 +442,10 @@ export class TreeViewElement extends HTMLElement {
previousCheckedValue: checkedValue,
}
}

selectVariant(node: Element): SelectVariant {
return (node.getAttribute('data-select-variant') || 'none') as SelectVariant
}
}

if (!window.customElements.get('tree-view')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ type LoadingState = 'loading' | 'error' | 'success'

export type SelectStrategy = 'self' | 'descendants' | 'mixed_descendants'

export type SelectVariant = 'none' | 'single' | 'multiple'

@controller
export class TreeViewSubTreeNodeElement extends HTMLElement {
@target node: HTMLElement
Expand Down Expand Up @@ -133,6 +135,10 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {
return (this.node.getAttribute('data-select-strategy') || 'descendants') as SelectStrategy
}

get selectVariant(): SelectVariant {
return (this.node.getAttribute('data-select-variant') || 'none') as SelectVariant
}

disconnectedCallback() {
this.#abortController.abort()
}
Expand Down Expand Up @@ -311,6 +317,9 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {

if (this.#checkboxElement) {
this.toggleChecked()
} else if (this.selectVariant === 'single') {
// Follow the standard implementation of TreeView and select that item
this.treeView.handleSingleSelection(event, node)
} else if (!this.treeView?.nodeHasNativeAction(node)) {
// toggle only if this node isn't eg. an anchor or button
this.toggle()
Expand Down Expand Up @@ -342,6 +351,9 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {
event.preventDefault()

this.toggleChecked()
} else if (this.selectVariant === 'single') {
// Follow the standard implementation of TreeView and select that item
this.treeView.handleSingleSelection(event, node)
} else {
if (node instanceof HTMLAnchorElement) {
// simulate click on space for anchors (buttons already handle this natively)
Expand Down
Loading