diff --git a/.changeset/full-states-beg.md b/.changeset/full-states-beg.md new file mode 100644 index 0000000000..0347022f54 --- /dev/null +++ b/.changeset/full-states-beg.md @@ -0,0 +1,5 @@ +--- +'@primer/view-components': minor +--- + +Support single selection variant for TreeView diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000000..7c4836fbe3 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..3916080457 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/primer_view_components.iml b/.idea/primer_view_components.iml new file mode 100644 index 0000000000..f8017aa776 --- /dev/null +++ b/.idea/primer_view_components.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..35eb1ddfbb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000000..4000edcbbb --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "associatedIndex": 6 +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1748258548189 + + + + + + + + + + \ No newline at end of file diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/aria-snapshot--after-interaction.yml b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/aria-snapshot--after-interaction.yml new file mode 100644 index 0000000000..665d7e6a46 --- /dev/null +++ b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/aria-snapshot--after-interaction.yml @@ -0,0 +1,3 @@ +- tree: + - treeitem "src" [checked] [level=1] [selected] + - treeitem "action_menu.rb" [level=1] \ No newline at end of file diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/aria-snapshot.yml b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/aria-snapshot.yml new file mode 100644 index 0000000000..4a15b2ed7d --- /dev/null +++ b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/aria-snapshot.yml @@ -0,0 +1,3 @@ +- tree: + - treeitem "src" [level=1] + - treeitem "action_menu.rb" [level=1] \ No newline at end of file diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark.png new file mode 100644 index 0000000000..5e16e90c4d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark_colorblind.png new file mode 100644 index 0000000000..5e16e90c4d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark_dimmed.png new file mode 100644 index 0000000000..71074aca2d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark_high_contrast.png new file mode 100644 index 0000000000..1f60736f87 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/default.png new file mode 100644 index 0000000000..5078c5fc5c Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/focused.png new file mode 100644 index 0000000000..f19e9e2d1c Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/light.png new file mode 100644 index 0000000000..dd5c10e341 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/light_colorblind.png new file mode 100644 index 0000000000..dd5c10e341 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/light_high_contrast.png new file mode 100644 index 0000000000..fa672aa1f1 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/multi_select/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/aria-snapshot--after-interaction.yml b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/aria-snapshot--after-interaction.yml new file mode 100644 index 0000000000..665d7e6a46 --- /dev/null +++ b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/aria-snapshot--after-interaction.yml @@ -0,0 +1,3 @@ +- tree: + - treeitem "src" [checked] [level=1] [selected] + - treeitem "action_menu.rb" [level=1] \ No newline at end of file diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/aria-snapshot.yml b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/aria-snapshot.yml new file mode 100644 index 0000000000..4a15b2ed7d --- /dev/null +++ b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/aria-snapshot.yml @@ -0,0 +1,3 @@ +- tree: + - treeitem "src" [level=1] + - treeitem "action_menu.rb" [level=1] \ No newline at end of file diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark.png new file mode 100644 index 0000000000..d0433c1588 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark_colorblind.png new file mode 100644 index 0000000000..d0433c1588 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark_dimmed.png new file mode 100644 index 0000000000..75729dd4de Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark_high_contrast.png new file mode 100644 index 0000000000..9521a0e645 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/default.png new file mode 100644 index 0000000000..6036d10df6 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/focused.png new file mode 100644 index 0000000000..5ec645eafe Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/light.png new file mode 100644 index 0000000000..1548a0e42f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/light_colorblind.png new file mode 100644 index 0000000000..1548a0e42f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/light_high_contrast.png new file mode 100644 index 0000000000..0fd64ceef6 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/tree_view/single_select/light_high_contrast.png differ diff --git a/app/components/primer/alpha/tree_view.pcss b/app/components/primer/alpha/tree_view.pcss index 01685aaab3..a410fc4932 100644 --- a/app/components/primer/alpha/tree_view.pcss +++ b/app/components/primer/alpha/tree_view.pcss @@ -197,6 +197,13 @@ } } + &[aria-checked='false'] { + /* singleselect checkmark */ + & .TreeViewItem-singleSelectCheckmark { + visibility: hidden; + } + } + &[aria-checked='mixed'] { & .FormControl-checkbox { background: var(--control-checked-bgColor-rest); diff --git a/app/components/primer/alpha/tree_view.rb b/app/components/primer/alpha/tree_view.rb index 903c5aa398..e93655b12d 100644 --- a/app/components/primer/alpha/tree_view.rb +++ b/app/components/primer/alpha/tree_view.rb @@ -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 @@ -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) | # @@ -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 diff --git a/app/components/primer/alpha/tree_view/node.html.erb b/app/components/primer/alpha/tree_view/node.html.erb index 2c4627172e..0ccd51ce7d 100644 --- a/app/components/primer/alpha/tree_view/node.html.erb +++ b/app/components/primer/alpha/tree_view/node.html.erb @@ -23,7 +23,11 @@ <%= leading_visual %> <% end %> <%= text_content %> - <% if trailing_visual? %> + <% if @select_variant == :single %> + + <% elsif trailing_visual? %> <%= trailing_visual %> <% end %> <% end %> diff --git a/app/components/primer/alpha/tree_view/node.rb b/app/components/primer/alpha/tree_view/node.rb index a155c65b85..2df5e1c899 100644 --- a/app/components/primer/alpha/tree_view/node.rb +++ b/app/components/primer/alpha/tree_view/node.rb @@ -56,6 +56,7 @@ class Node < Primer::Component DEFAULT_SELECT_VARIANT = :none SELECT_VARIANT_OPTIONS = [ + :single, :multiple, DEFAULT_SELECT_VARIANT ].freeze @@ -128,7 +129,8 @@ def initialize( @content_arguments, { data: { value: value, - path: @path.to_json + path: @path.to_json, + select_variant: @select_variant } } ) @@ -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, diff --git a/app/components/primer/alpha/tree_view/sub_tree_node.rb b/app/components/primer/alpha/tree_view/sub_tree_node.rb index b42c54eaa1..3dd622a7da 100644 --- a/app/components/primer/alpha/tree_view/sub_tree_node.rb +++ b/app/components/primer/alpha/tree_view/sub_tree_node.rb @@ -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: { diff --git a/app/components/primer/alpha/tree_view/tree_view.ts b/app/components/primer/alpha/tree_view/tree_view.ts index a316e0d8d4..88977adf5a 100644 --- a/app/components/primer/alpha/tree_view/tree_view.ts +++ b/app/components/primer/alpha/tree_view/tree_view.ts @@ -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 { @@ -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') { @@ -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() @@ -199,7 +243,7 @@ export class TreeViewElement extends HTMLElement { break } - if (this.nodeHasCheckBox(node)) { + if (this.selectVariant(node) === 'multiple') { event.preventDefault() if (this.getNodeCheckedValue(node) === 'true') { @@ -207,6 +251,10 @@ export class TreeViewElement extends HTMLElement { } 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() @@ -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 @@ -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 @@ -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')) { diff --git a/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.ts b/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.ts index 5d2b45f41f..4316dcdcdc 100644 --- a/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.ts +++ b/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.ts @@ -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 @@ -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() } @@ -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() @@ -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) diff --git a/previews/primer/alpha/tree_view_preview.rb b/previews/primer/alpha/tree_view_preview.rb index 54fbbae100..b4bca3f24d 100644 --- a/previews/primer/alpha/tree_view_preview.rb +++ b/previews/primer/alpha/tree_view_preview.rb @@ -9,7 +9,7 @@ class TreeViewPreview < ViewComponent::Preview # @snapshot interactive # @param expanded [Boolean] toggle # @param disabled [Boolean] toggle - # @param select_variant [Symbol] select [multiple, none] + # @param select_variant [Symbol] select [multiple, single, none] # @param select_strategy [Symbol] select [self, descendants, mixed_descendants] def default( expanded: false, @@ -28,7 +28,7 @@ def default( # @label Playground # # @param expanded [Boolean] toggle - # @param select_variant [Symbol] select [multiple, none] + # @param select_variant [Symbol] select [multiple, single, none] # @param select_strategy [Symbol] select [self, descendants, mixed_descendants] def playground( expanded: false, @@ -43,6 +43,38 @@ def playground( }) end + # @label Single select + # + # @snapshot interactive + # @param expanded [Boolean] toggle + # @param disabled [Boolean] toggle + def single_select( + expanded: false, + disabled: false + ) + render_with_template(locals: { + expanded: coerce_bool(expanded), + disabled: coerce_bool(disabled), + select_variant: :single, + }) + end + + # @label Multi select + # + # @snapshot interactive + # @param expanded [Boolean] toggle + # @param disabled [Boolean] toggle + def multi_select( + expanded: false, + disabled: false + ) + render_with_template(locals: { + expanded: coerce_bool(expanded), + disabled: coerce_bool(disabled), + select_variant: :multiple, + }) + end + # @label Empty # # @snapshot interactive @@ -94,7 +126,7 @@ def async_alpha(action_menu_expanded: false) # @param leading_visual_icon [Symbol] octicon # @param leading_action_icon [Symbol] octicon # @param trailing_visual_icon [Symbol] octicon - # @param select_variant [Symbol] select [multiple, none] + # @param select_variant [Symbol] select [multiple, single, none] # @param disabled [Boolean] toggle def leaf_node_playground( label: "Leaf node", @@ -153,9 +185,11 @@ def auto_expansion # @label Form input # + # @param select_variant [Symbol] select [multiple, single] # @param expanded [Boolean] toggle - def form_input(expanded: true) + def form_input(select_variant: :multiple, expanded: true) render_with_template(locals: { + select_variant: select_variant.to_sym, expanded: coerce_bool(expanded) }) end diff --git a/previews/primer/alpha/tree_view_preview/default.html.erb b/previews/primer/alpha/tree_view_preview/default.html.erb index fc1bb3ff69..5dd618fc01 100644 --- a/previews/primer/alpha/tree_view_preview/default.html.erb +++ b/previews/primer/alpha/tree_view_preview/default.html.erb @@ -6,7 +6,7 @@ <% icons.with_collapsed_icon(icon: :"file-directory-fill", color: :accent) %> <% end %> - <% sub_tree.with_trailing_visual_icon(icon: :"diff-modified") %> + <% sub_tree.with_trailing_visual_icon(icon: :"diff-modified") unless select_variant == :single %> <% sub_tree.with_leaf(label: "button.rb", disabled: disabled, select_variant: select_variant) do |item| %> <% item.with_leading_visual_icon(icon: :file) %> diff --git a/previews/primer/alpha/tree_view_preview/form_input.html.erb b/previews/primer/alpha/tree_view_preview/form_input.html.erb index 9d3ca62ff7..805269649e 100644 --- a/previews/primer/alpha/tree_view_preview/form_input.html.erb +++ b/previews/primer/alpha/tree_view_preview/form_input.html.erb @@ -1,12 +1,12 @@ <%= form_with(url: primer_view_components.generic_form_submission_path(format: :json)) do |f| %> <%= render(Primer::Alpha::Stack.new) do %> <%= render(Primer::Alpha::TreeView.new(form_arguments: { builder: f, name: "folder_structure" })) do |tree| %> - <% tree.with_sub_tree(label: "src", expanded: expanded, select_variant: :multiple, value: 0) do |sub_tree| %> - <% sub_tree.with_leaf(label: "button.rb", select_variant: :multiple, value: 1) %> - <% sub_tree.with_leaf(label: "icon_button.rb", current: true, select_variant: :multiple, value: 2) %> + <% tree.with_sub_tree(label: "src", expanded: expanded, select_variant: select_variant, value: 0) do |sub_tree| %> + <% sub_tree.with_leaf(label: "button.rb", select_variant: select_variant, value: 1) %> + <% sub_tree.with_leaf(label: "icon_button.rb", current: true, select_variant: select_variant, value: 2) %> <% end %> - <% tree.with_leaf(label: "action_menu.rb", select_variant: :multiple, value: 3) %> + <% tree.with_leaf(label: "action_menu.rb", select_variant: select_variant, value: 3) %> <% end %> <%= render(Primer::Alpha::SubmitButton.new(name: :submit, label: "Submit")) %> diff --git a/previews/primer/alpha/tree_view_preview/multi_select.html.erb b/previews/primer/alpha/tree_view_preview/multi_select.html.erb new file mode 100644 index 0000000000..74bc3f57ad --- /dev/null +++ b/previews/primer/alpha/tree_view_preview/multi_select.html.erb @@ -0,0 +1,10 @@ +
+ <%= render(Primer::Alpha::TreeView.new) do |tree_view| %> + <% tree_view.with_sub_tree(label: "src", expanded: expanded, disabled: disabled, select_variant: select_variant) do |sub_tree| %> + <% sub_tree.with_leaf(label: "button.rb", disabled: disabled, select_variant: select_variant) %> + <% sub_tree.with_leaf(label: "icon_button.rb", current: true, disabled: disabled, select_variant: select_variant) %> + <% end %> + + <% tree_view.with_leaf(label: "action_menu.rb", disabled: disabled, select_variant: select_variant) %> + <% end %> +
diff --git a/previews/primer/alpha/tree_view_preview/single_select.html.erb b/previews/primer/alpha/tree_view_preview/single_select.html.erb new file mode 100644 index 0000000000..74bc3f57ad --- /dev/null +++ b/previews/primer/alpha/tree_view_preview/single_select.html.erb @@ -0,0 +1,10 @@ +
+ <%= render(Primer::Alpha::TreeView.new) do |tree_view| %> + <% tree_view.with_sub_tree(label: "src", expanded: expanded, disabled: disabled, select_variant: select_variant) do |sub_tree| %> + <% sub_tree.with_leaf(label: "button.rb", disabled: disabled, select_variant: select_variant) %> + <% sub_tree.with_leaf(label: "icon_button.rb", current: true, disabled: disabled, select_variant: select_variant) %> + <% end %> + + <% tree_view.with_leaf(label: "action_menu.rb", disabled: disabled, select_variant: select_variant) %> + <% end %> +
diff --git a/test/components/primer/alpha/tree_view_test.rb b/test/components/primer/alpha/tree_view_test.rb index 2cb9ea3da4..423db56e76 100644 --- a/test/components/primer/alpha/tree_view_test.rb +++ b/test/components/primer/alpha/tree_view_test.rb @@ -193,6 +193,19 @@ def test_falls_back_to_div_variant_when_invalid_variant_given assert_selector "div[role=treeitem]", text: "Foobar" end + + def test_disallows_trailing_visuals_when_single_select_variant_is_used + error = assert_raises(ArgumentError) do + render_inline(Primer::Alpha::TreeView.new) do |tree| + tree.with_sub_tree(label: "src", select_variant: :single) do |sub_tree| + sub_tree.with_trailing_visual_icon(icon: :"diff-modified") + sub_tree.with_leaf(label: "button.rb") + end + end + end + + assert_equal error.message, "Trailing visuals can't be used in combination with single select mode as the icon is reserved." + end end end end diff --git a/test/system/alpha/tree_view_test.rb b/test/system/alpha/tree_view_test.rb index e1002c36e6..373eae24fc 100644 --- a/test/system/alpha/tree_view_test.rb +++ b/test/system/alpha/tree_view_test.rb @@ -607,6 +607,51 @@ def test_fires_activation_event assert_equal details["previousCheckedValue"], "false" end + def test_fires_event_before_checking_single_variant + visit_preview(:default, select_variant: :single) + + details = capture_event("treeViewBeforeNodeChecked") do + activate_at_path("src") + end + + assert_equal details.size, 1 + + assert details[0]["node"] + assert_equal details[0]["type"], "sub-tree" + assert_equal details[0]["path"], ["src"] + assert_equal details[0]["checkedValue"], "true" + assert_equal details[0]["previousCheckedValue"], "false" + end + + def test_canceling_check_event_prevents_checking_for_single_variant + visit_preview(:default, select_variant: :single) + + refute_path_checked "src" + + capture_event("treeViewBeforeNodeChecked", cancel: true) do + activate_at_path("src") + end + + # src should still not be checked + refute_path_checked "src" + end + + def test_fires_check_event_after_single_variant + visit_preview(:default, select_variant: :single) + + details = capture_event("treeViewNodeChecked") do + activate_at_path("src") + end + + assert_equal details.size, 1 + + assert details[0]["node"] + assert_equal details[0]["type"], "sub-tree" + assert_equal details[0]["path"], ["src"] + assert_equal details[0]["checkedValue"], "true" + assert_equal details[0]["previousCheckedValue"], "false" + end + def test_fires_event_before_checking visit_preview(:default, select_variant: :multiple) @@ -786,7 +831,6 @@ def test_self_select_strategy_checking_sub_tree_does_not_check_children assert_path_checked "primer", "alpha", "action_bar" end - def test_form_submission visit_preview(:form_input, expanded: true, route_format: :json) @@ -799,5 +843,18 @@ def test_form_submission assert_equal "{\"path\":[\"action_menu.rb\"],\"value\":\"3\"}", response.dig("form_params", "folder_structure", 0) end + + def test_form_submission_with_single_select_variant + visit_preview(:form_input, expanded: true, select_variant: :single, route_format: :json) + + activate_at_path("action_menu.rb") + + find("button[type=submit]").click + + # for some reason the JSON response is wrapped in HTML, I have no idea why + response = JSON.parse(find("pre").text) + + assert_equal "{\"path\":[\"action_menu.rb\"],\"value\":\"3\"}", response.dig("form_params", "folder_structure", 0) + end end end