@@ -13,6 +13,8 @@ import { v4 as uuidv4 } from 'uuid'
13
13
import NeBadge from ' ./NeBadge.vue'
14
14
import NeLink from ' ./NeLink.vue'
15
15
import type { ButtonSize } from ' ./NeButton.vue'
16
+ import NeTextInput from ' ./NeTextInput.vue'
17
+ import { focusElement } from ' @/main'
16
18
17
19
export type FilterKind = ' radio' | ' checkbox'
18
20
@@ -39,6 +41,12 @@ export interface Props {
39
41
openMenuAriaLabel: string
40
42
showClearFilter? : boolean
41
43
showSelectionCount? : boolean
44
+ noOptionsLabel: string
45
+ showOptionsFilter? : boolean
46
+ optionsFilterPlaceholder? : string
47
+ // limit the number of options displayed for performance
48
+ maxOptionsShown? : number
49
+ moreOptionsHiddenLabel: string
42
50
alignToRight? : boolean
43
51
size? : ButtonSize
44
52
disabled? : boolean
@@ -48,6 +56,9 @@ export interface Props {
48
56
const props = withDefaults (defineProps <Props >(), {
49
57
showClearFilter: true ,
50
58
showSelectionCount: true ,
59
+ showOptionsFilter: false ,
60
+ optionsFilterPlaceholder: ' ' ,
61
+ maxOptionsShown: 25 ,
51
62
alignToRight: false ,
52
63
size: ' md' ,
53
64
disabled: false ,
@@ -61,13 +72,43 @@ const top = ref(0)
61
72
const left = ref (0 )
62
73
const right = ref (0 )
63
74
const buttonRef = ref ()
75
+ const optionsFilter = ref (' ' )
76
+ const optionsFilterRef = ref ()
64
77
65
78
const componentId = computed (() => (props .id ? props .id : uuidv4 ()))
66
79
67
80
const isSelectionCountShown = computed (() => {
68
81
return props .showSelectionCount && props .kind == ' checkbox' && checkboxModel .value .length > 0
69
82
})
70
83
84
+ const optionsToDisplay = computed (() => {
85
+ return filteredOptions .value .slice (0 , props .maxOptionsShown )
86
+ })
87
+
88
+ const moreOptionsHidden = computed (() => {
89
+ return filteredOptions .value .length > props .maxOptionsShown
90
+ })
91
+
92
+ const isShowingOptionsFilter = computed (() => {
93
+ return props .showOptionsFilter || props .options .length > props .maxOptionsShown
94
+ })
95
+
96
+ const filteredOptions = computed (() => {
97
+ if (! isShowingOptionsFilter .value ) {
98
+ // return all options
99
+ return props .options
100
+ }
101
+
102
+ // show only options that match the options filter
103
+
104
+ const regex = / [^ a-zA-Z0-9 -] / g
105
+ const queryText = optionsFilter .value .replace (regex , ' ' )
106
+
107
+ return props .options .filter ((option ) => {
108
+ return new RegExp (queryText , ' i' ).test (option .label ?.replace (regex , ' ' ))
109
+ })
110
+ })
111
+
71
112
watch (
72
113
() => props .alignToRight ,
73
114
() => {
@@ -119,6 +160,12 @@ function calculatePosition() {
119
160
buttonRef .value ?.$el .getBoundingClientRect ().right -
120
161
window .scrollX
121
162
}
163
+
164
+ function maybeFocusOptionsFilter() {
165
+ if (isShowingOptionsFilter .value ) {
166
+ focusElement (optionsFilterRef )
167
+ }
168
+ }
122
169
</script >
123
170
124
171
<template >
@@ -155,20 +202,39 @@ function calculatePosition() {
155
202
leave-active-class =" transition ease-in duration-75"
156
203
leave-from-class =" transform opacity-100 scale-100"
157
204
leave-to-class =" transform opacity-0 scale-95"
205
+ @after-enter =" maybeFocusOptionsFilter"
158
206
>
159
207
<MenuItems
160
208
:style =" [
161
209
{ top: top + 'px' },
162
210
alignToRight ? { right: right + 'px' } : { left: left + 'px' }
163
211
]"
164
- class =" absolute z-50 mt-2.5 max-h-[16.5rem ] min-w-[10rem] overflow-y-auto rounded-md bg-white px-4 py-2 text-sm shadow-lg ring-1 ring-gray-900/5 focus:outline-none dark:bg-gray-950 dark:ring-gray-500/50"
212
+ class =" absolute z-50 mt-2.5 max-h-[17.2rem ] min-w-[10rem] overflow-y-auto rounded-md bg-white px-4 py-2 text-sm shadow-lg ring-1 ring-gray-900/5 focus:outline-none dark:bg-gray-950 dark:ring-gray-500/50"
165
213
>
166
- <MenuItem v-if =" showClearFilter && kind == 'checkbox'" as =" div" class =" py-2" >
214
+ <div v-if =" isShowingOptionsFilter" class =" py-2" >
215
+ <label class =" sr-only" :for =" `${componentId}-options-filter`" >
216
+ {{ optionsFilterPlaceholder }}
217
+ </label >
218
+ <NeTextInput
219
+ :id =" `${componentId}-options-filter`"
220
+ ref =" optionsFilterRef"
221
+ v-model =" optionsFilter"
222
+ :placeholder =" optionsFilterPlaceholder"
223
+ is-search
224
+ @keydown.stop
225
+ />
226
+ </div >
227
+ <div v-if =" showClearFilter && kind == 'checkbox'" class =" py-2" >
167
228
<NeLink @click.stop =" checkboxModel = []" >
168
229
{{ clearFilterLabel }}
169
230
</NeLink >
170
- </MenuItem >
171
- <MenuItem v-for =" option in options" :key =" option.id" as =" div" :disabled =" option.disabled" >
231
+ </div >
232
+ <MenuItem
233
+ v-for =" option in optionsToDisplay"
234
+ :key =" option.id"
235
+ as =" div"
236
+ :disabled =" option.disabled"
237
+ >
172
238
<!-- divider -->
173
239
<hr
174
240
v-if =" option.id.includes('divider')"
@@ -235,6 +301,16 @@ function calculatePosition() {
235
301
</div >
236
302
</div >
237
303
</MenuItem >
304
+ <!-- showing a limited number of options for performance, but more options are available -->
305
+ <div v-if =" moreOptionsHidden" class =" cursor-default py-2 opacity-50" >
306
+ {{ moreOptionsHiddenLabel }}
307
+ </div >
308
+ <!-- no option matching filter -->
309
+ <div v-if =" !filteredOptions.length" >
310
+ <div class =" py-2 opacity-50" >
311
+ {{ noOptionsLabel }}
312
+ </div >
313
+ </div >
238
314
</MenuItems >
239
315
</transition >
240
316
</Teleport >
0 commit comments