avo 4.0.23 → 4.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
 
3
3
  export default class extends Controller {
4
+ static targets = ['searchInput', 'empty']
5
+
4
6
  // Used by both onToggle (auto-focus) and handleKeydown (arrow navigation).
5
7
  get focusableItems() {
6
8
  return [...this.element.querySelectorAll('a, button')].filter(
@@ -25,9 +27,19 @@ export default class extends Controller {
25
27
  onToggle(event) {
26
28
  if (event.newState !== 'open') return
27
29
 
30
+ // Each open starts unfiltered — a leftover query from the last visit would
31
+ // silently hide items.
32
+ if (this.hasSearchInputTarget) this.resetFilter()
33
+
28
34
  // requestAnimationFrame ensures the popover is fully rendered before we focus.
29
35
  // Focusing before the browser paints causes the scroll to jump in some cases.
30
36
  requestAnimationFrame(() => {
37
+ // A searchable menu hands focus to its filter input so the user can just type.
38
+ if (this.hasSearchInputTarget) {
39
+ this.searchInputTarget.focus()
40
+ return
41
+ }
42
+
31
43
  const items = this.focusableItems
32
44
  if (items.length === 0) return
33
45
 
@@ -38,24 +50,46 @@ export default class extends Controller {
38
50
  })
39
51
  }
40
52
 
53
+ // Narrows the rendered items to those whose text matches the query. Items are
54
+ // queried at call time (not cached) so lazily loaded content — e.g. a
55
+ // turbo-frame inside the list — is filterable as soon as it arrives. An item
56
+ // carrying data-filter-text matches on that instead of its full text, so
57
+ // decorations (timestamps, tags) don't count as matches.
58
+ filter() {
59
+ const query = this.searchInputTarget.value.trim().toLowerCase()
60
+ let anyVisible = false
61
+
62
+ this.element.querySelectorAll('.dropdown-menu__list a, .dropdown-menu__list button').forEach((item) => {
63
+ const text = item.dataset.filterText ?? item.textContent
64
+ const match = query === '' || text.replace(/\s+/g, ' ').trim().toLowerCase().includes(query)
65
+ item.hidden = !match
66
+ if (match) anyVisible = true
67
+ })
68
+
69
+ if (this.hasEmptyTarget) this.emptyTarget.hidden = anyVisible || query === ''
70
+ }
71
+
72
+ resetFilter() {
73
+ this.searchInputTarget.value = ''
74
+ this.filter()
75
+ }
76
+
41
77
  handleKeydown(event) {
42
78
  // The keydown listener is always attached, so guard against firing when closed.
43
79
  if (!this.element.matches(':popover-open')) return
44
80
 
45
- const items = this.focusableItems
46
- if (items.length === 0) return
47
-
48
- const idx = items.indexOf(document.activeElement)
49
-
50
81
  switch (event.key) {
51
82
  case 'ArrowDown':
83
+ case 'ArrowUp': {
84
+ const items = this.focusableItems
85
+ if (items.length === 0) return
86
+
52
87
  event.preventDefault()
53
- items[idx < items.length - 1 ? idx + 1 : 0].focus()
54
- break
55
- case 'ArrowUp':
56
- event.preventDefault()
57
- items[idx > 0 ? idx - 1 : items.length - 1].focus()
88
+ const idx = items.indexOf(document.activeElement)
89
+ if (event.key === 'ArrowDown') items[idx < items.length - 1 ? idx + 1 : 0].focus()
90
+ else items[idx > 0 ? idx - 1 : items.length - 1].focus()
58
91
  break
92
+ }
59
93
  case 'Escape':
60
94
  // Close on Escape ourselves. Native popover light-dismiss is unreliable
61
95
  // here because other document-level keydown handlers (e.g. the index-row
@@ -63,7 +97,13 @@ export default class extends Controller {
63
97
  // also reacting; hidePopover restores focus to the trigger.
64
98
  event.preventDefault()
65
99
  event.stopPropagation()
66
- this.element.hidePopover()
100
+ // With a non-empty search query the first Escape only clears it; the next closes.
101
+ if (this.hasSearchInputTarget && this.searchInputTarget.value !== '') {
102
+ this.resetFilter()
103
+ this.searchInputTarget.focus()
104
+ } else {
105
+ this.element.hidePopover()
106
+ }
67
107
  break
68
108
  default:
69
109
  }
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "4.0.23" unless const_defined?(:VERSION)
2
+ VERSION = "4.0.24" unless const_defined?(:VERSION)
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avo
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.23
4
+ version: 4.0.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Marin
@@ -224,6 +224,8 @@ files:
224
224
  - app/assets/builds/avo/application.css
225
225
  - app/assets/builds/avo/application.js
226
226
  - app/assets/builds/avo/application.js.map
227
+ - app/assets/builds/avo/avo.custom.js
228
+ - app/assets/builds/avo/avo.custom.js.map
227
229
  - app/assets/builds/avo/dependencies.css
228
230
  - app/assets/builds/avo/late-registration.js
229
231
  - app/assets/builds/avo/late-registration.js.map