Class: Primer::Alpha::TreeView

Inherits:
Component
  • Object
show all
Defined in:
app/components/primer/alpha/tree_view.rb,
app/components/primer/alpha/tree_view/icon.rb,
app/components/primer/alpha/tree_view/node.rb,
app/components/primer/alpha/tree_view/visual.rb,
app/components/primer/alpha/tree_view/sub_tree.rb,
app/components/primer/alpha/tree_view/icon_pair.rb,
app/components/primer/alpha/tree_view/leaf_node.rb,
app/components/primer/alpha/tree_view/sub_tree_node.rb,
app/components/primer/alpha/tree_view/leading_action.rb,
app/components/primer/alpha/tree_view/spinner_loader.rb,
app/components/primer/alpha/tree_view/skeleton_loader.rb,
app/components/primer/alpha/tree_view/trailing_action.rb,
app/components/primer/alpha/tree_view/sub_tree_container.rb,
app/components/primer/alpha/tree_view/loading_failure_message.rb

Overview

TreeView is a hierarchical list of items that may have a parent-child relationship where children can be toggled into view by expanding or collapsing their parent item.

Terminology

Consider the following tree structure:

src ├ button.rb └ action_list ├ item.rb └ header.rb

  1. Node. A node is an item in the tree. Nodes can either be "leaf" nodes (i.e. have no children), or "sub-tree" nodes, which do have children. In the example above, button.rb, item.rb, and header.rb are all leaf nodes, while action_list is a sub-tree node.
  2. Path. A node's path is like its ID. It's an array of strings containing the current node's label and all the labels of its ancestors, in order. In the example above, header.rb's path is ["src", "action_list", "header.rb"].

Static nodes

The TreeView component allows items to be provided statically or loaded dynamically from the server. Providing items statically is done using the leaf and sub_tree slots:

<%= render(Primer::Alpha::TreeView.new) do |tree| %>
  <% tree.with_sub_tree(label: "Directory") do |sub_tree| %>
    <% sub_tree.with_leaf(label: "File 1")
  <% end %>
  <% tree.with_leaf(label: "File 2") %>
<% end %>

Dynamic nodes

Tree nodes can also be fetched dynamically from the server and will require creating a Rails controller action to respond with the list of nodes. Unlike other Primer components, TreeView allows the programmer to specify loading behavior on a per-sub-tree basis, i.e. each sub-tree must specify how its nodes are loaded. To load nodes dynamically for a given sub-tree, configure it with either a loading spinner or a loading skeleton, and provide the URL to fetch nodes from:

<%= render(Primer::Alpha::TreeView.new) do |tree| %>
  <% tree.with_sub_tree(label: "Directory") do |sub_tree| %>
    <% sub_tree.with_loading_spinner(src: primer_view_components.tree_view_items_path) %>
  <% end %>
<% end %>

Define a controller action to serve the list of nodes. The TreeView component automatically includes the sub-tree's path as a GET parameter, encoded as a JSON array.

class TreeViewItemsController < ApplicationController
  def show
    @path = JSON.parse(params[:path])
    @results = get_tree_items(starting_at: path)
  end
end

Responses must be HTML fragments, eg. have a content type of text/html+fragment. This content type isn't available by default in Rails, so you may have to register it eg. in an initializer:

Mime::Type.register("text/fragment+html", :html_fragment)

Render a Primer::Alpha::TreeView::SubTree in the action's template, tree_view_items/show.html_fragment.erb:

<%= render(Primer::Alpha::TreeView::SubTree.new(path: @path, node_variant: :div)) do |tree| %>
  <% tree.with_leaf(...) %>
  <% tree.with_sub_tree(...) do |sub_tree| %>
    ...
  <% end %>
<% end %>

Multi-select mode

Passing select_variant: :multiple to both sub-tree and leaf nodes will add a check box to the left of the node's label. These check boxes behave according to the value of a second argument, select_strategy:.

The default select strategy, :descendants, will cause all child nodes to be checked when the node is checked. This includes both sub-tree and leaf nodes. When the node is unchecked, all child nodes will also be unchecked. Unchecking a child node of a checked parent will cause the parent to enter a mixed or indeterminate state, which is represented by a horizontal line icon instead of a check mark. This icon indicates that some children are checked, but not all.

A secondary select strategy, :self, is provided to allow disabling the automatic checking of child nodes. When select_strategy: :self is specified, checking sub-tree nodes does not check child nodes, and sub-tree nodes cannot enter a mixed or indeterminate state.

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

TreeViews support three different node variants, :anchor, :button, and :div (the default), which controls which HTML tag is used to construct the nodes. The :anchor and :button variants correspond to <a> and <button> tags respectively, which are browser-native elements. Anchors and buttons can be activated (i.e. "clicked") using the mouse or keyboard via the enter or space keys. The node variant must be the same for all nodes in the tree, and is therefore specified at the root level, eg. TreeView.new(node_variant: :anchor).

Trees with node variants other than :div cannot have check boxes, i.e. cannot be put into multi-select mode.

Trees with node variants other than :div do not emit the treeViewNodeActivated or treeViewBeforeNodeActivated events, since it is assumed any behavior associated with these variants is user- or browser-defined.

Interaction behavior matrix

Interaction Select variant Tag Result
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)

JavaScript API

TreeViews render a <tree-view> custom element that exposes behavior to the client.

Name Notes
getNodePath(node: Element): string[] Returns the path to the given node.
`getNodeType(node: Element): TreeViewNodeType null` Returns either "leaf" or "sub-tree".
markCurrentAtPath(path: string[]) Marks the node as the "current" node, which appears visually distinct from other nodes.
`get currentNode(): HTMLLIElement null` Returns the current node.
expandAtPath(path: string[]) Expands the sub-tree at path.
collapseAtPath(path: string[]) Collapses the sub-tree at path.
toggleAtPath(path: string[]) If the sub-tree at path is collapsed, this function expands it, and vice-versa.
checkAtPath(path: string[]) If the node at path has a checkbox, this function checks it.
uncheckAtPath(path: string[]) If the node at path has a checkbox, this function unchecks it.
toggleCheckedAtPath(path: string[]) If the sub-tree at path is checked, this function unchecks it, and vice-versa.
checkedValueAtPath(path: string[]): TreeViewCheckedValue Returns "true" (all child nodes are checked), "false" (no child nodes are checked), or "mixed" (some child nodes are checked, some are not).
`nodeAtPath(path: string, selector?: string): Element null` Returns the node for the given path, either a leaf node or sub-tree node.
`subTreeAtPath(path: string): TreeViewSubTreeNodeElement null` Returns the sub-tree at the given path, if it exists.
`leafAtPath(path: string): HTMLLIElement null` Returns the leaf node at the given path, if it exists.
getNodeCheckedValue(node: Element): TreeViewCheckedValue The same as checkedValueAtPath, but accepts a node instead of a path.

Events

The events enumerated below include node information by way of the TreeViewNodeInfo object, which has the following signature:

type TreeViewNodeType = 'leaf' | 'sub-tree'
type TreeViewCheckedValue = 'true' | 'false' | 'mixed'

type TreeViewNodeInfo = {
  node: Element
  type: TreeViewNodeType
  path: string[]
  checkedValue: TreeViewCheckedValue
  previousCheckedValue: TreeViewCheckedValue
}
Name Type Bubbles Cancelable
treeViewNodeActivated CustomEvent<TreeViewNodeInfo> Yes No
treeViewBeforeNodeActivated CustomEvent<TreeViewNodeInfo> Yes Yes
treeViewNodeExpanded CustomEvent<TreeViewNodeInfo>> Yes No
treeViewNodeCollapsed CustomEvent<TreeViewNodeInfo>> Yes No
treeViewNodeChecked CustomEvent<TreeViewNodeInfo[]> Yes Yes
treeViewBeforeNodeChecked CustomEvent<TreeViewNodeInfo[]> Yes No

Item activation

The <tree-view> element fires an treeViewNodeActivated event whenever a node is activated (eg. clicked) via the mouse or keyboard.

The treeViewBeforeNodeActivated event fires before a node is activated. Canceling this event will prevent the node from being activated.

document.querySelector("select-panel").addEventListener(
  "treeViewBeforeNodeActivated",
  (event: CustomEvent<TreeViewNodeInfo>) => {
    event.preventDefault() // Cancel the event to prevent activation (eg. expanding/collapsing)
  }
)

Item checking/unchecking

The tree-view element fires a treeViewNodeChecked event whenever a node is checked or unchecked.

The treeViewBeforeNodeChecked event fires before a node is checked or unchecked. Canceling this event will prevent the check/uncheck operation.

document.querySelector("select-panel").addEventListener(
  "treeViewBeforeNodeChecked",
  (event: CustomEvent<TreeViewNodeInfo[]>) => {
    event.preventDefault() // Cancel the event to prevent activation (eg. expanding/collapsing)
  }
)

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.

Direct Known Subclasses

FileTreeView

Defined Under Namespace

Classes: Icon, IconPair, LeadingAction, LeafNode, LoadingFailureMessage, Node, SkeletonLoader, SpinnerLoader, SubTree, SubTreeContainer, SubTreeNode, TrailingAction, Visual

Constant Summary collapse

DEFAULT_NODE_VARIANT =
:div
NODE_VARIANT_OPTIONS =
[DEFAULT_NODE_VARIANT, :anchor, :button].freeze

Constants inherited from Component

Component::INVALID_ARIA_LABEL_TAGS

Constants included from Status::Dsl

Status::Dsl::STATUSES

Constants included from ViewHelper

ViewHelper::HELPERS

Constants included from TestSelectorHelper

TestSelectorHelper::TEST_SELECTOR_TAG

Constants included from FetchOrFallbackHelper

FetchOrFallbackHelper::InvalidValueError

Constants included from Primer::AttributesHelper

Primer::AttributesHelper::PLURAL_ARIA_ATTRIBUTES, Primer::AttributesHelper::PLURAL_DATA_ATTRIBUTES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Component

deprecated?, generate_id

Methods included from JoinStyleArgumentsHelper

#join_style_arguments

Methods included from TestSelectorHelper

#add_test_selector

Methods included from FetchOrFallbackHelper

#fetch_or_fallback, #fetch_or_fallback_boolean, #silence_deprecations?

Methods included from ClassNameHelper

#class_names

Methods included from Primer::AttributesHelper

#aria, #data, #extract_data, #merge_aria, #merge_data, #merge_prefixed_attribute_hashes

Methods included from ExperimentalSlotHelpers

included

Methods included from ExperimentalRenderHelpers

included

Constructor Details

#initialize(node_variant: DEFAULT_NODE_VARIANT, form_arguments: {}, **system_arguments) ⇒ TreeView

Returns a new instance of TreeView.

Parameters:

  • node_variant (Symbol) (defaults to: DEFAULT_NODE_VARIANT)

    The variant to use for this node. <%= one_of(Primer::Alpha::TreeView::NODE_VARIANT_OPTIONS) %>

  • form_arguments (Hash) (defaults to: {})

    These arguments allow the selections made within a TreeView to be submitted to the server as part of a Rails form. Pass the builder: and name: options to this hash. builder: should be an instance of ActionView::Helpers::FormBuilder, which are created by the standard Rails #form_with and #form_for helpers. The name: option is the desired name of the field that will be included in the params sent to the server on form submission.

  • system_arguments (Hash)

    <%= link_to_system_arguments_docs %>.



426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'app/components/primer/alpha/tree_view.rb', line 426

def initialize(node_variant: DEFAULT_NODE_VARIANT, form_arguments: {}, **system_arguments)
  @system_arguments = deny_tag_argument(**system_arguments)
  @form_arguments = form_arguments

  @node_variant = fetch_or_fallback(NODE_VARIANT_OPTIONS, node_variant, DEFAULT_NODE_VARIANT)

  @system_arguments[:tag] = :ul
  @system_arguments[:role] = :tree
  @system_arguments[:classes] = class_names(
    @system_arguments.delete(:classes),
    "TreeViewRootUlStyles"
  )
end

Instance Attribute Details

#node_variantObject (readonly)

Returns the value of attribute node_variant.



421
422
423
# File 'app/components/primer/alpha/tree_view.rb', line 421

def node_variant
  @node_variant
end

Instance Method Details

#acts_as_form_input?Boolean

Returns:

  • (Boolean)


440
441
442
# File 'app/components/primer/alpha/tree_view.rb', line 440

def acts_as_form_input?
  @form_arguments[:builder] && @form_arguments[:name]
end

#with_leaf(**system_arguments, &block) ⇒ Object

Adds an leaf node to the tree. Leaf nodes are nodes that do not have children.

Parameters:

  • component_klass (Class)

    The class to use instead of the default <%= link_to_component(Primer::Alpha::TreeView::LeafNode) %>

  • system_arguments (Hash)

    These arguments are forwarded to <%= link_to_component(Primer::Alpha::TreeView::LeafNode) %>, or whatever class is passed as the component_klass argument.



# File 'app/components/primer/alpha/tree_view.rb', line 377

#with_sub_tree(**system_arguments, &block) ⇒ Object

Adds a sub-tree node to the tree. Sub-trees are nodes that have children, which can be both leaf nodes and other sub-trees.

Parameters:

  • component_klass (Class)

    The class to use instead of the default <%= link_to_component(Primer::Alpha::TreeView::SubTreeNode) %>

  • system_arguments (Hash)

    These arguments are forwarded to <%= link_to_component(Primer::Alpha::TreeView::SubTreeNode) %>, or whatever class is passed as the component_klass argument.



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'app/components/primer/alpha/tree_view.rb', line 393

renders_many :nodes, types: {
  leaf: {
    renders: lambda { |component_klass: LeafNode, label:, **system_arguments|
      component_klass.new(
        **system_arguments,
        node_variant: node_variant,
        path: [label],
        label: label
      )
    },

    as: :leaf
  },

  sub_tree: {
    renders: lambda { |component_klass: SubTreeNode, label:, **system_arguments|
      component_klass.new(
        **system_arguments,
        node_variant: node_variant,
        path: [label],
        label: label
      )
    },

    as: :sub_tree
  }
}