2121 * @property {string? } style
2222 * @property {string? } testId
2323 * @property {number? } portalClass
24+ * @property {boolean? } filterable
2425 * @property {('normal' | 'inline')? } triggerStyle
2526 */
2627import van from '../van.min.js' ;
2728import { getRandomId , getValue , loadStylesheet , isState , isEqual } from '../utils.js' ;
2829import { Portal } from './portal.js' ;
2930import { Icon } from './icon.js' ;
3031
31- const { div, i, label, span } = van . tags ;
32+ const { div, i, input , label, span } = van . tags ;
3233
3334const Select = ( /** @type {Properties } */ props ) => {
3435 loadStylesheet ( 'select' , stylesheet ) ;
3536
3637 const domId = van . derive ( ( ) => props . id ?. val ?? getRandomId ( ) ) ;
3738 const opened = van . state ( false ) ;
39+ const optionsFilter = van . state ( '' ) ;
3840 const options = van . derive ( ( ) => {
3941 const options = getValue ( props . options ) ?? [ ] ;
4042 const allowNull = getValue ( props . allowNull ) ;
@@ -48,6 +50,27 @@ const Select = (/** @type {Properties} */ props) => {
4850
4951 return options ;
5052 } ) ;
53+ const filteredOptions = van . derive ( ( ) => {
54+ const allOptions = getValue ( options ) ;
55+ const isFilterable = getValue ( props . filterable ) ;
56+ const filterTerm = getValue ( optionsFilter ) ;
57+ if ( isFilterable && filterTerm . length ) {
58+ const filteredOptions_ = [ ] ;
59+ for ( let i = 0 ; i < allOptions . length ; i ++ ) {
60+ const option = allOptions [ i ] ;
61+ if ( option . label === filterTerm ) {
62+ return allOptions ;
63+ }
64+
65+ if ( option . label . toLowerCase ( ) . includes ( filterTerm . toLowerCase ( ) ) ) {
66+ filteredOptions_ . push ( option ) ;
67+ }
68+ }
69+ return filteredOptions_ ;
70+ }
71+ return allOptions ;
72+ } ) ;
73+
5174 const value = isState ( props . value ) ? props . value : van . state ( props . value ?? null ) ;
5275 const initialSelection = options . val ?. find ( ( op ) => op . value === value . val ) ;
5376 const valueLabel = van . state ( initialSelection ?. label ?? '' ) ;
@@ -58,6 +81,16 @@ const Select = (/** @type {Properties} */ props) => {
5881 value . val = option . value ;
5982 } ;
6083
84+ const filterOptions = ( /** @type InputEvent */ event ) => {
85+ optionsFilter . val = event . target . value ;
86+ } ;
87+
88+ const showPortal = ( /** @type Event */ event ) => {
89+ event . stopPropagation ( ) ;
90+ event . stopImmediatePropagation ( ) ;
91+ opened . val = getValue ( props . disabled ) ? false : true ;
92+ } ;
93+
6194 van . derive ( ( ) => {
6295 const currentOptions = getValue ( options ) ;
6396 const previousValue = value . oldVal ;
@@ -82,8 +115,8 @@ const Select = (/** @type {Properties} */ props) => {
82115 id : domId ,
83116 class : ( ) => `flex-column fx-gap-1 text-caption tg-select--label ${ getValue ( props . disabled ) ? 'disabled' : '' } ` ,
84117 style : ( ) => `width: ${ props . width ? getValue ( props . width ) + 'px' : 'auto' } ; ${ getValue ( props . style ) } ` ,
85- onclick : van . derive ( ( ) => ! getValue ( props . disabled ) ? ( ) => opened . val = ! opened . val : null ) ,
86118 'data-testid' : getValue ( props . testId ) ?? '' ,
119+ onclick : showPortal ,
87120 } ,
88121 span (
89122 { class : 'flex-row fx-gap-1' , 'data-testid' : 'select-label' } ,
@@ -111,17 +144,27 @@ const Select = (/** @type {Properties} */ props) => {
111144 style : ( ) => getValue ( props . height ) ? `height: ${ getValue ( props . height ) } px;` : '' ,
112145 'data-testid' : 'select-input' ,
113146 } ,
114- ( ) => div (
115- { class : 'tg-select--field--content' , 'data-testid' : 'select-input-display' } ,
116- valueIcon . val
117- ? Icon ( { classes : 'mr-2' } , valueIcon . val )
118- : undefined ,
119- valueLabel . val ,
120- ) ,
147+ ( ) => {
148+ return div (
149+ { class : 'tg-select--field--content' , 'data-testid' : 'select-input-display' } ,
150+ valueIcon . val
151+ ? Icon ( { classes : 'mr-2' } , valueIcon . val )
152+ : undefined ,
153+ getValue ( props . filterable )
154+ ? input ( {
155+ id : `tg-select--field--${ getRandomId ( ) } ` ,
156+ value : valueLabel . val ,
157+ onkeyup : filterOptions ,
158+ } )
159+ : valueLabel . val ,
160+ ) ;
161+ } ,
121162 div (
122163 { class : 'tg-select--field--icon' , 'data-testid' : 'select-input-trigger' } ,
123164 i (
124- { class : 'material-symbols-rounded' } ,
165+ {
166+ class : 'material-symbols-rounded' ,
167+ } ,
125168 'expand_more' ,
126169 ) ,
127170 ) ,
@@ -134,7 +177,7 @@ const Select = (/** @type {Properties} */ props) => {
134177 class : ( ) => `tg-select--options-wrapper mt-1 ${ getValue ( props . portalClass ) ?? '' } ` ,
135178 'data-testid' : 'select-options' ,
136179 } ,
137- getValue ( options ) . map ( option =>
180+ getValue ( filteredOptions ) . map ( option =>
138181 div (
139182 {
140183 class : ( ) => `tg-select--option ${ getValue ( value ) === option . value ? 'selected' : '' } ` ,
@@ -196,6 +239,16 @@ stylesheet.replace(`
196239 font-weight: 500;
197240}
198241
242+ .tg-select--field--content > input {
243+ border: unset !important;
244+ background: transparent !important;
245+ outline: none !important;
246+ width: 100%;
247+ font-weight: 500;
248+ font-family: 'Roboto', 'Helvetica Neue', sans-serif;
249+ color: var(--primary-text-color);
250+ }
251+
199252.tg-select--field--icon {
200253 display: flex;
201254 align-items: center;
0 commit comments