Skip to content

Commit 5cfb632

Browse files
authored
Merge pull request #487 from opf/bug/dream-743-first-element-is-not-triggered-when-pressing-enter
[DREAM-743] First element is not triggered when pressing Enter
2 parents 519cb9e + faeb2b0 commit 5cfb632

4 files changed

Lines changed: 82 additions & 2 deletions

File tree

.changeset/wet-carrots-appear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openproject/primer-view-components': patch
3+
---
4+
5+
Allow navigation with the FilterableTreeView from the input to the tree (and back)

app/components/primer/open_project/filterable_tree_view.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class FilterableTreeViewElement extends HTMLElement {
4949
this.addEventListener('treeViewNodeChecked', this, {signal})
5050
this.addEventListener('itemActivated', this, {signal})
5151
this.addEventListener('input', this, {signal})
52+
this.addEventListener('keydown', this, {signal})
5253

5354
if (this.#isAsyncMode) {
5455
void this.#fetchAndReplaceTree()
@@ -77,11 +78,17 @@ export class FilterableTreeViewElement extends HTMLElement {
7778
if (event.target === this.filterModeControl) {
7879
this.#handleFilterModeEvent(event)
7980
} else if (event.target === this.filterInput) {
80-
this.#handleFilterInputEvent(event)
81+
if (event.type === 'keydown') {
82+
this.#handleFilterInputKeyDown(event as KeyboardEvent)
83+
} else {
84+
this.#handleFilterInputEvent(event)
85+
}
8186
} else if (event.target === this.includeSubItemsCheckBox) {
8287
this.#handleIncludeSubItemsCheckBoxEvent(event)
8388
} else if (event.target instanceof TreeViewElement || event.target instanceof TreeViewSubTreeNodeElement) {
8489
this.#handleTreeViewEvent(event)
90+
} else if (event.type === 'keydown' && this.treeViewList?.contains(event.target as Node)) {
91+
this.#handleTreeKeyDown(event as KeyboardEvent)
8592
}
8693
}
8794

@@ -231,6 +238,25 @@ export class FilterableTreeViewElement extends HTMLElement {
231238
}
232239
}
233240

241+
#handleFilterInputKeyDown(event: KeyboardEvent) {
242+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
243+
244+
const treeitems = [...(this.treeViewList?.querySelectorAll<HTMLElement>('[role=treeitem]') ?? [])]
245+
const visibleItems = treeitems.filter(item => !item.closest('[hidden]'))
246+
247+
if (visibleItems.length === 0) return
248+
249+
event.preventDefault()
250+
const focusTarget = event.key === 'ArrowDown' ? visibleItems[0] : visibleItems[visibleItems.length - 1]
251+
focusTarget.focus()
252+
}
253+
254+
#handleTreeKeyDown(event: KeyboardEvent) {
255+
if (event.key !== 'Escape') return
256+
event.preventDefault()
257+
this.filterInput.focus()
258+
}
259+
234260
#handleIncludeSubItemsCheckBoxEvent(event: Event) {
235261
if (!this.treeView) return
236262
if (event.type !== 'input') return

previews/primer/open_project/filterable_tree_view_preview/default.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<%= render(Primer::OpenProject::FilterableTreeView.new) do |tree| %>
1+
<%= render(Primer::OpenProject::FilterableTreeView.new(filter_input_arguments: {autocomplete: "off"})) do |tree| %>
22
<% tree.with_sub_tree(label: "Students", expanded: expanded) do |hogwarts| %>
33
<% hogwarts.with_sub_tree(label: "Ravenclaw", expanded: expanded) do |ravenclaw| %>
44
<% ravenclaw.with_leaf(label: "Luna Lovegood") %>

test/system/open_project/filterable_tree_view_test.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55

66
module OpenProject
77
class IntegrationFilterableTreeViewTest < System::TestCase
8+
include Primer::DriverTestHelpers
9+
include Primer::KeyboardTestHelpers
810
include Primer::TreeViewHelpers
911

12+
def active_element
13+
page.evaluate_script("document.activeElement")
14+
end
15+
1016
def test_filtering_matches_sub_trees
1117
visit_preview(:default)
1218

@@ -370,6 +376,49 @@ def test_form_submits_checked_nodes_when_filtering_for_selected_only
370376
assert_equal character["path"], ["Students", "Slytherin", "Draco Malfoy"]
371377
end
372378

379+
# ─── Keyboard navigation ──────────────────────────────────────────────────
380+
381+
def test_arrow_down_from_filter_input_focuses_first_treeitem
382+
visit_preview(:default)
383+
384+
find_field("Filter").click
385+
keyboard.type(:down)
386+
387+
assert_equal "treeitem", active_element["role"]
388+
assert_equal "Students", label_of(active_element)
389+
end
390+
391+
def test_arrow_up_from_filter_input_focuses_last_treeitem
392+
visit_preview(:default)
393+
394+
find_field("Filter").click
395+
keyboard.type(:up)
396+
397+
assert_equal "treeitem", active_element["role"]
398+
assert_equal "Rubeus Hagrid", label_of(active_element)
399+
end
400+
401+
def test_arrow_down_after_filtering_focuses_first_visible_treeitem
402+
visit_preview(:default)
403+
404+
fill_in "Filter", with: "Luna"
405+
refute_path("Albus Dumbledore") # wait for filter to be applied
406+
find_field("Filter").click
407+
keyboard.type(:down)
408+
409+
assert_equal "treeitem", active_element["role"]
410+
assert_equal "Students", label_of(active_element)
411+
end
412+
413+
def test_escape_from_treeitem_returns_focus_to_filter_input
414+
visit_preview(:default)
415+
416+
node_at_path("Students").evaluate_script("this.focus()")
417+
keyboard.type(:escape)
418+
419+
assert_equal "input", active_element.tag_name
420+
end
421+
373422
# ─── Async: initial load ─────────────────────────────────────────────────
374423

375424
HOGWARTS = "Hogwarts School of Witchcraft and Wizardry"

0 commit comments

Comments
 (0)