@@ -35,9 +35,18 @@ export class MultipleChoice extends Root {
3535 @property ( )
3636 accessor selections : Primitives . StringValue | string [ ] = [ ] ;
3737
38+ @property ( )
39+ accessor type : "checkbox" | "chips" = "checkbox" ;
40+
41+ @property ( { type : Boolean } )
42+ accessor filterable = false ;
43+
3844 @state ( )
3945 accessor isOpen = false ;
4046
47+ @state ( )
48+ accessor filterText = "" ;
49+
4150 static styles = [
4251 structuralStyles ,
4352 css `
@@ -95,25 +104,57 @@ export class MultipleChoice extends Root {
95104 transform: rotate(180deg);
96105 }
97106
98- /* Dropdown List */
99- .options-list {
107+ /* Dropdown Wrapper */
108+ .dropdown-wrapper {
100109 background: var(--md-sys-color-surface);
101110 border: 1px solid var(--md-sys-color-outline-variant);
102- border-radius: 8px; /* Consistent rounding */
103- box-shadow: none; /* Remove shadow for inline feel, or keep subtle */
104- overflow-y: auto;
111+ border-radius: 8px;
112+ box-shadow: var(--md-sys-elevation-level2);
105113 padding: 0;
106114 display: none;
107115 flex-direction: column;
108- margin-top: 4px; /* Small gap */
109- max-height: 0; /* Animate height? */
110- transition: max-height 0.2s ease-out;
116+ margin-top: 4px;
117+ max-height: 300px;
118+ transition: opacity 0.2s ease-out;
119+ overflow: hidden; /* contain children */
111120 }
112121
113- .options-list .open {
122+ .dropdown-wrapper .open {
114123 display: flex;
115- max-height: 300px; /* Limit height but allow scrolling */
116- border: 1px solid var(--md-sys-color-outline-variant); /* efficient border */
124+ border: 1px solid var(--md-sys-color-outline-variant);
125+ }
126+
127+ /* Scrollable Area for Options */
128+ .options-scroll-container {
129+ overflow-y: auto;
130+ flex: 1; /* take remaining height */
131+ display: flex;
132+ flex-direction: column;
133+ }
134+
135+ /* Filter Input */
136+ .filter-container {
137+ padding: 8px;
138+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
139+ background: var(--md-sys-color-surface);
140+ z-index: 1; /* ensure top of stack */
141+ flex-shrink: 0; /* don't shrink */
142+ }
143+
144+ .filter-input {
145+ width: 100%;
146+ padding: 8px 12px;
147+ border: 1px solid var(--md-sys-color-outline);
148+ border-radius: 4px;
149+ font-family: inherit;
150+ font-size: 0.9rem;
151+ background: var(--md-sys-color-surface-container-low);
152+ color: var(--md-sys-color-on-surface);
153+ }
154+
155+ .filter-input:focus {
156+ outline: none;
157+ border-color: var(--md-sys-color-primary);
117158 }
118159
119160 /* Option Item (Checkbox style) */
@@ -164,6 +205,54 @@ export class MultipleChoice extends Root {
164205 transform: scale(1);
165206 }
166207
208+ /* Chips Layout */
209+ .chips-container {
210+ display: flex;
211+ flex-wrap: wrap;
212+ gap: 8px;
213+ padding: 4px 0;
214+ }
215+
216+ .chip {
217+ display: inline-flex;
218+ align-items: center;
219+ gap: 8px;
220+ padding: 6px 16px;
221+ border: 1px solid var(--md-sys-color-outline);
222+ border-radius: 16px;
223+ cursor: pointer;
224+ user-select: none;
225+ background: var(--md-sys-color-surface);
226+ color: var(--md-sys-color-on-surface);
227+ transition: all 0.2s ease;
228+ font-size: 0.9rem;
229+ }
230+
231+ .chip:hover {
232+ background: var(--md-sys-color-surface-container-high);
233+ }
234+
235+ .chip.selected {
236+ background: var(--md-sys-color-secondary-container);
237+ color: var(--md-sys-color-on-secondary-container);
238+ border-color: var(--md-sys-color-secondary-container);
239+ }
240+
241+ .chip.selected:hover {
242+ background: var(--md-sys-color-secondary-container-high, #e8def8);
243+ }
244+
245+ .chip-icon {
246+ display: none;
247+ width: 18px;
248+ height: 18px;
249+ }
250+
251+ .chip.selected .chip-icon {
252+ display: block;
253+ fill: currentColor;
254+ }
255+
167256 @keyframes fadeIn {
168257 from { opacity: 0; transform: translateY(-8px); }
169258 to { opacity: 1; transform: translateY(0); }
@@ -217,8 +306,78 @@ export class MultipleChoice extends Root {
217306 this . requestUpdate ( ) ;
218307 }
219308
309+ #renderFilter( ) {
310+ return html `
311+ < div class ="filter-container ">
312+ < input
313+ type ="text "
314+ class ="filter-input "
315+ placeholder ="Filter options... "
316+ .value =${ this . filterText }
317+ @input =${ ( e : Event ) => {
318+ const target = e . target as HTMLInputElement ;
319+ this . filterText = target . value ;
320+ } }
321+ @click=${ ( e : Event ) => e . stopPropagation ( ) }
322+ />
323+ </ div >
324+ ` ;
325+ }
326+
220327 render ( ) {
221328 const currentSelections = this . getCurrentSelections ( ) ;
329+
330+ // Filter options
331+ const filteredOptions = this . options . filter ( option => {
332+ if ( ! this . filterText ) return true ;
333+ const label = extractStringValue (
334+ option . label ,
335+ this . component ,
336+ this . processor ,
337+ this . surfaceId
338+ ) ;
339+ return label . toLowerCase ( ) . includes ( this . filterText . toLowerCase ( ) ) ;
340+ } ) ;
341+
342+ // Chips Layout
343+ if ( this . type === "chips" ) {
344+ return html `
345+ < div class ="container ">
346+ ${ this . description ? html `< div class ="header-text " style ="margin-bottom: 8px; "> ${ this . description } </ div > ` : nothing }
347+ ${ this . filterable ? this . #renderFilter( ) : nothing }
348+ < div class ="chips-container ">
349+ ${ filteredOptions . map ( ( option ) => {
350+ const label = extractStringValue (
351+ option . label ,
352+ this . component ,
353+ this . processor ,
354+ this . surfaceId
355+ ) ;
356+ const isSelected = currentSelections . includes ( option . value ) ;
357+ return html `
358+ < div
359+ class ="chip ${ isSelected ? "selected" : "" } "
360+ @click =${ ( e : Event ) => {
361+ e . stopPropagation ( ) ;
362+ this . toggleSelection ( option . value ) ;
363+ } }
364+ >
365+ ${ isSelected ? html `
366+ < svg class ="chip-icon " xmlns ="http://www.w3.org/2000/svg " viewBox ="0 -960 960 960 ">
367+ < path d ="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z "/>
368+ </ svg >
369+ ` : nothing }
370+ < span > ${ label } </ span >
371+ </ div >
372+ ` ;
373+ } ) }
374+ </ div >
375+ ${ filteredOptions . length === 0 ? html `< div style ="padding: 8px; font-style: italic; color: var(--md-sys-color-outline); "> No options found</ div > ` : nothing }
376+ </ div >
377+ ` ;
378+ }
379+
380+ // Default Checkbox Dropdown Layout
222381 const count = currentSelections . length ;
223382 const headerText = count > 0 ? `${ count } Selected` : ( this . description ?? "Select items" ) ;
224383
@@ -236,31 +395,35 @@ export class MultipleChoice extends Root {
236395 </ span >
237396 </ div >
238397
239- < div class ="options-list ${ this . isOpen ? "open" : "" } ">
240- ${ this . options . map ( ( option ) => {
241- const label = extractStringValue (
242- option . label ,
243- this . component ,
244- this . processor ,
245- this . surfaceId
246- ) ;
247- const isSelected = currentSelections . includes ( option . value ) ;
248-
249- return html `
250- < div
251- class ="option-item ${ isSelected ? "selected" : "" } "
252- @click =${ ( e : Event ) => {
253- e . stopPropagation ( ) ;
254- this . toggleSelection ( option . value ) ;
255- } }
256- >
257- < div class ="checkbox ">
258- < span class ="checkbox-icon "> ✓</ span >
398+ < div class ="dropdown-wrapper ${ this . isOpen ? "open" : "" } ">
399+ ${ this . filterable ? this . #renderFilter( ) : nothing }
400+ < div class ="options-scroll-container ">
401+ ${ filteredOptions . map ( ( option ) => {
402+ const label = extractStringValue (
403+ option . label ,
404+ this . component ,
405+ this . processor ,
406+ this . surfaceId
407+ ) ;
408+ const isSelected = currentSelections . includes ( option . value ) ;
409+
410+ return html `
411+ < div
412+ class ="option-item ${ isSelected ? "selected" : "" } "
413+ @click =${ ( e : Event ) => {
414+ e . stopPropagation ( ) ;
415+ this . toggleSelection ( option . value ) ;
416+ } }
417+ >
418+ < div class ="checkbox ">
419+ < span class ="checkbox-icon "> ✓</ span >
420+ </ div >
421+ < span > ${ label } </ span >
259422 </ div >
260- < span > ${ label } </ span >
261- </ div >
262- ` ;
263- } ) }
423+ ` ;
424+ } ) }
425+ ${ filteredOptions . length === 0 ? html ` < div style =" padding: 16px; text-align: center; color: var(--md-sys-color-outline); " > No options found </ div > ` : nothing }
426+ </ div >
264427 </ div >
265428 </ div >
266429 ` ;
0 commit comments