-
Notifications
You must be signed in to change notification settings - Fork 1
[DREAM-704] Tree view selection based on path identity breaks use cases where similar paths are allowed #475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
098da7b
773e344
1f5d29e
e00ca96
1af01f9
798851b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| '@openproject/primer-view-components': minor | ||
| --- | ||
|
|
||
| Fix selection of nodes with the same path in Primer::Alpha::TreeView | ||
|
|
||
| When a `[role=treeitem]` element carries a `data-node-id` attribute, that id is now included as `nodeId` in the hidden form input payload (`{path, nodeId?, value?}`). Trees with duplicate-path nodes should set `data-node-id` to a stable unique identifier so the server can distinguish which node was selected. | ||
|
|
||
| **Breaking change in `TreeViewElement#checkOnlyAtPath`:** if the given path is not found the method is now a no-op. Previously it would uncheck all active nodes before failing to check the missing node, which could be used as an indirect "clear selection" mechanism. Use explicit `setNodeCheckedValue(node, 'false')` calls if that behaviour is needed. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import {controller, target} from '@github/catalyst' | ||
| import {SelectStrategy, SelectVariant, TreeViewSubTreeNodeElement} from './tree_view_sub_tree_node_element' | ||
|
Check failure on line 2 in app/components/primer/alpha/tree_view/tree_view.ts
|
||
| import {useRovingTabIndex} from './tree_view_roving_tab_index' | ||
| import type {TreeViewCheckedValue, TreeViewNodeInfo, TreeViewNodeType} from '../../shared_events' | ||
|
|
||
|
|
@@ -24,7 +24,7 @@ | |
| for (const addedNode of mutation.addedNodes) { | ||
| if (!(addedNode instanceof HTMLElement)) continue | ||
|
|
||
| // eslint-disable-next-line custom-elements/no-dom-traversal-in-connectedcallback | ||
| if (addedNode.querySelector('[aria-expanded=true]')) { | ||
| this.#autoExpandFrom(addedNode) | ||
| } | ||
|
|
@@ -179,8 +179,8 @@ | |
| // 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 newCheckedValue = this.getNodeCheckedValue(node) === 'true' ? 'false' : 'true' | ||
| const nodeInfo = this.infoFromNode(node, newCheckedValue) | ||
|
|
||
| const checkSuccess = this.dispatchEvent( | ||
| new CustomEvent('treeViewBeforeNodeChecked', { | ||
|
|
@@ -192,10 +192,10 @@ | |
|
|
||
| if (!checkSuccess) return | ||
|
|
||
| if (this.getNodeCheckedValue(node) === 'true') { | ||
| if (newCheckedValue === 'false') { | ||
| this.setNodeCheckedValue(node, 'false') | ||
| } else { | ||
| this.checkOnlyAtPath(path) | ||
| this.#checkNodeOnly(node) | ||
| } | ||
|
|
||
| this.dispatchEvent( | ||
|
|
@@ -271,7 +271,11 @@ | |
| } else if (this.selectVariant(node) === 'single') { | ||
| event.preventDefault() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (pre-existing) it looks like single-select sets |
||
|
|
||
| this.checkOnlyAtPath(this.getNodePath(node)) | ||
| if (this.getNodeCheckedValue(node) === 'true') { | ||
| this.setNodeCheckedValue(node, 'false') | ||
| } else { | ||
| this.#checkNodeOnly(node) | ||
| } | ||
| } else if (node instanceof HTMLAnchorElement) { | ||
| // simulate click on space | ||
| node.click() | ||
|
|
@@ -313,7 +317,7 @@ | |
| } | ||
|
|
||
| get activeNodes() { | ||
| return document.querySelectorAll('[aria-checked="true"]') | ||
| return this.querySelectorAll('[aria-checked="true"]') | ||
| } | ||
|
|
||
| expandAtPath(path: string[]) { | ||
|
|
@@ -352,11 +356,19 @@ | |
| } | ||
|
|
||
| checkOnlyAtPath(path: string[]) { | ||
| const node = this.nodeAtPath(path) | ||
| if (!node) return | ||
|
HDinger marked this conversation as resolved.
|
||
|
|
||
| this.#checkNodeOnly(node) | ||
| } | ||
|
|
||
| #checkNodeOnly(node: Element) { | ||
| for (const el of this.activeNodes) { | ||
| this.uncheckAtPath(this.getNodePath(el)) | ||
| if (el === node) continue | ||
| this.setNodeCheckedValue(el, 'false') | ||
|
HDinger marked this conversation as resolved.
|
||
| } | ||
|
|
||
| this.checkAtPath(path) | ||
| this.setNodeCheckedValue(node, 'true') | ||
|
HDinger marked this conversation as resolved.
|
||
| } | ||
|
|
||
| toggleCheckedAtPath(path: string[]) { | ||
|
|
@@ -473,16 +485,19 @@ | |
| updateHiddenFormInputs() { | ||
| const newInputs = [] | ||
|
|
||
| // eslint-disable-next-line custom-elements/no-dom-traversal-in-connectedcallback | ||
| for (const node of this.querySelectorAll('[role=treeitem][aria-checked=true]')) { | ||
| const newInput = this.formInputPrototype.cloneNode() as HTMLInputElement | ||
| newInput.removeAttribute('data-target') | ||
| newInput.removeAttribute('form') | ||
|
|
||
| const payload: {path: string[]; value?: string} = { | ||
| const payload: {path: string[]; nodeId?: string; value?: string} = { | ||
| path: this.getNodePath(node), | ||
| } | ||
|
|
||
| const nodeId = node.getAttribute('data-node-id') | ||
| if (nodeId) payload.nodeId = nodeId | ||
|
|
||
|
HDinger marked this conversation as resolved.
|
||
| const inputValue = this.getFormInputValueForNode(node) | ||
| if (inputValue) payload.value = inputValue | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <div style="max-width: 400px"> | ||
| <%= render(Primer::Alpha::TreeView.new) do |tree_view| %> | ||
| <% tree_view.with_sub_tree(label: "src", expanded: true, select_variant: :single) do |sub_tree| %> | ||
| <% sub_tree.with_leaf(label: "button.rb", select_variant: :single) %> | ||
| <% sub_tree.with_leaf(label: "icon_button.rb", current: true, select_variant: :single) %> | ||
| <% end %> | ||
|
|
||
| <% tree_view.with_leaf(label: "action_menu.rb", select_variant: :single) %> | ||
| <% tree_view.with_leaf(label: "action_menu.rb", select_variant: :single) %> | ||
| <% tree_view.with_leaf(label: "action_menu.rb", select_variant: :single) %> | ||
| <% end %> | ||
| </div> |
Uh oh!
There was an error while loading. Please reload this page.