@@ -13,6 +13,8 @@ import { v4 as uuidv4 } from 'uuid'
1313import NeBadge from ' ./NeBadge.vue'
1414import NeLink from ' ./NeLink.vue'
1515import type { ButtonSize } from ' ./NeButton.vue'
16+ import NeTextInput from ' ./NeTextInput.vue'
17+ import { focusElement } from ' @/main'
1618
1719export type FilterKind = ' radio' | ' checkbox'
1820
@@ -39,6 +41,12 @@ export interface Props {
3941 openMenuAriaLabel: string
4042 showClearFilter? : boolean
4143 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
4250 alignToRight? : boolean
4351 size? : ButtonSize
4452 disabled? : boolean
@@ -48,6 +56,9 @@ export interface Props {
4856const props = withDefaults (defineProps <Props >(), {
4957 showClearFilter: true ,
5058 showSelectionCount: true ,
59+ showOptionsFilter: false ,
60+ optionsFilterPlaceholder: ' ' ,
61+ maxOptionsShown: 25 ,
5162 alignToRight: false ,
5263 size: ' md' ,
5364 disabled: false ,
@@ -61,13 +72,43 @@ const top = ref(0)
6172const left = ref (0 )
6273const right = ref (0 )
6374const buttonRef = ref ()
75+ const optionsFilter = ref (' ' )
76+ const optionsFilterRef = ref ()
6477
6578const componentId = computed (() => (props .id ? props .id : uuidv4 ()))
6679
6780const isSelectionCountShown = computed (() => {
6881 return props .showSelectionCount && props .kind == ' checkbox' && checkboxModel .value .length > 0
6982})
7083
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+
71112watch (
72113 () => props .alignToRight ,
73114 () => {
@@ -119,6 +160,12 @@ function calculatePosition() {
119160 buttonRef .value ?.$el .getBoundingClientRect ().right -
120161 window .scrollX
121162}
163+
164+ function maybeFocusOptionsFilter() {
165+ if (isShowingOptionsFilter .value ) {
166+ focusElement (optionsFilterRef )
167+ }
168+ }
122169 </script >
123170
124171<template >
@@ -155,20 +202,39 @@ function calculatePosition() {
155202 leave-active-class =" transition ease-in duration-75"
156203 leave-from-class =" transform opacity-100 scale-100"
157204 leave-to-class =" transform opacity-0 scale-95"
205+ @after-enter =" maybeFocusOptionsFilter"
158206 >
159207 <MenuItems
160208 :style =" [
161209 { top: top + 'px' },
162210 alignToRight ? { right: right + 'px' } : { left: left + 'px' }
163211 ]"
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"
165213 >
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" >
167228 <NeLink @click.stop =" checkboxModel = []" >
168229 {{ clearFilterLabel }}
169230 </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+ >
172238 <!-- divider -->
173239 <hr
174240 v-if =" option.id.includes('divider')"
@@ -235,6 +301,16 @@ function calculatePosition() {
235301 </div >
236302 </div >
237303 </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 >
238314 </MenuItems >
239315 </transition >
240316 </Teleport >
0 commit comments