1414 limitations under the License.
1515 */
1616
17+
1718import { html , css , PropertyValues , nothing } from "lit" ;
1819import { customElement , property , state } from "lit/decorators.js" ;
1920import { Root } from "./root.js" ;
@@ -35,9 +36,18 @@ export class MultipleChoice extends Root {
3536 @property ( )
3637 accessor selections : Primitives . StringValue | string [ ] = [ ] ;
3738
39+ @property ( )
40+ accessor variant : "checkbox" | "chips" = "checkbox" ;
41+
42+ @property ( { type : Boolean } )
43+ accessor filterable = false ;
44+
3845 @state ( )
3946 accessor isOpen = false ;
4047
48+ @state ( )
49+ accessor filterText = "" ;
50+
4151 static styles = [
4252 structuralStyles ,
4353 css `
@@ -95,25 +105,57 @@ export class MultipleChoice extends Root {
95105 transform: rotate(180deg);
96106 }
97107
98- /* Dropdown List */
99- .options-list {
108+ /* Dropdown Wrapper */
109+ .dropdown-wrapper {
100110 background: var(--md-sys-color-surface);
101111 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;
112+ border-radius: 8px;
113+ box-shadow: var(--md-sys-elevation-level2);
105114 padding: 0;
106115 display: none;
107116 flex-direction: column;
108- margin-top: 4px; /* Small gap */
109- max-height: 0; /* Animate height? */
110- transition: max-height 0.2s ease-out;
117+ margin-top: 4px;
118+ max-height: 300px;
119+ transition: opacity 0.2s ease-out;
120+ overflow: hidden; /* contain children */
111121 }
112122
113- .options-list .open {
123+ .dropdown-wrapper .open {
114124 display: flex;
115- max-height: 300px; /* Limit height but allow scrolling */
116- border: 1px solid var(--md-sys-color-outline-variant); /* efficient border */
125+ border: 1px solid var(--md-sys-color-outline-variant);
126+ }
127+
128+ /* Scrollable Area for Options */
129+ .options-scroll-container {
130+ overflow-y: auto;
131+ flex: 1; /* take remaining height */
132+ display: flex;
133+ flex-direction: column;
134+ }
135+
136+ /* Filter Input */
137+ .filter-container {
138+ padding: 8px;
139+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
140+ background: var(--md-sys-color-surface);
141+ z-index: 1; /* ensure top of stack */
142+ flex-shrink: 0; /* don't shrink */
143+ }
144+
145+ .filter-input {
146+ width: 100%;
147+ padding: 8px 12px;
148+ border: 1px solid var(--md-sys-color-outline);
149+ border-radius: 4px;
150+ font-family: inherit;
151+ font-size: 0.9rem;
152+ background: var(--md-sys-color-surface-container-low);
153+ color: var(--md-sys-color-on-surface);
154+ }
155+
156+ .filter-input:focus {
157+ outline: none;
158+ border-color: var(--md-sys-color-primary);
117159 }
118160
119161 /* Option Item (Checkbox style) */
@@ -164,6 +206,54 @@ export class MultipleChoice extends Root {
164206 transform: scale(1);
165207 }
166208
209+ /* Chips Layout */
210+ .chips-container {
211+ display: flex;
212+ flex-wrap: wrap;
213+ gap: 8px;
214+ padding: 4px 0;
215+ }
216+
217+ .chip {
218+ display: inline-flex;
219+ align-items: center;
220+ gap: 8px;
221+ padding: 6px 16px;
222+ border: 1px solid var(--md-sys-color-outline);
223+ border-radius: 16px;
224+ cursor: pointer;
225+ user-select: none;
226+ background: var(--md-sys-color-surface);
227+ color: var(--md-sys-color-on-surface);
228+ transition: all 0.2s ease;
229+ font-size: 0.9rem;
230+ }
231+
232+ .chip:hover {
233+ background: var(--md-sys-color-surface-container-high);
234+ }
235+
236+ .chip.selected {
237+ background: var(--md-sys-color-secondary-container);
238+ color: var(--md-sys-color-on-secondary-container);
239+ border-color: var(--md-sys-color-secondary-container);
240+ }
241+
242+ .chip.selected:hover {
243+ background: var(--md-sys-color-secondary-container-high, #e8def8);
244+ }
245+
246+ .chip-icon {
247+ display: none;
248+ width: 18px;
249+ height: 18px;
250+ }
251+
252+ .chip.selected .chip-icon {
253+ display: block;
254+ fill: currentColor;
255+ }
256+
167257 @keyframes fadeIn {
168258 from { opacity: 0; transform: translateY(-8px); }
169259 to { opacity: 1; transform: translateY(0); }
@@ -191,13 +281,14 @@ export class MultipleChoice extends Root {
191281 }
192282
193283 getCurrentSelections ( ) : string [ ] {
194- if ( ! this . processor || ! this . component ) {
195- return Array . isArray ( this . selections ) ? this . selections : [ ] ;
196- }
197284 if ( Array . isArray ( this . selections ) ) {
198285 return this . selections ;
199286 }
200287
288+ if ( ! this . processor || ! this . component ) {
289+ return [ ] ;
290+ }
291+
201292 const selectionValue = this . processor . getData (
202293 this . component ,
203294 this . selections . path ! ,
@@ -217,8 +308,82 @@ export class MultipleChoice extends Root {
217308 this . requestUpdate ( ) ;
218309 }
219310
311+ #renderCheckIcon( ) {
312+ return html `
313+ < svg class ="chip-icon " xmlns ="http://www.w3.org/2000/svg " viewBox ="0 -960 960 960 ">
314+ < path d ="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z "/>
315+ </ svg >
316+ ` ;
317+ }
318+
319+ #renderFilter( ) {
320+ return html `
321+ < div class ="filter-container ">
322+ < input
323+ type ="text "
324+ class ="filter-input "
325+ placeholder ="Filter options... "
326+ .value =${ this . filterText }
327+ @input =${ ( e : Event ) => {
328+ const target = e . target as HTMLInputElement ;
329+ this . filterText = target . value ;
330+ } }
331+ @click=${ ( e : Event ) => e . stopPropagation ( ) }
332+ />
333+ </ div >
334+ ` ;
335+ }
336+
220337 render ( ) {
221338 const currentSelections = this . getCurrentSelections ( ) ;
339+
340+ // Filter options
341+ const filteredOptions = this . options . filter ( option => {
342+ if ( ! this . filterText ) return true ;
343+ const label = extractStringValue (
344+ option . label ,
345+ this . component ,
346+ this . processor ,
347+ this . surfaceId
348+ ) ;
349+ return label . toLowerCase ( ) . includes ( this . filterText . toLowerCase ( ) ) ;
350+ } ) ;
351+
352+ // Chips Layout
353+ if ( this . variant === "chips" ) {
354+ return html `
355+ < div class ="container ">
356+ ${ this . description ? html `< div class ="header-text " style ="margin-bottom: 8px; "> ${ this . description } </ div > ` : nothing }
357+ ${ this . filterable ? this . #renderFilter( ) : nothing }
358+ < div class ="chips-container ">
359+ ${ filteredOptions . map ( ( option ) => {
360+ const label = extractStringValue (
361+ option . label ,
362+ this . component ,
363+ this . processor ,
364+ this . surfaceId
365+ ) ;
366+ const isSelected = currentSelections . includes ( option . value ) ;
367+ return html `
368+ < div
369+ class ="chip ${ isSelected ? "selected" : "" } "
370+ @click =${ ( e : Event ) => {
371+ e . stopPropagation ( ) ;
372+ this . toggleSelection ( option . value ) ;
373+ } }
374+ >
375+ ${ isSelected ? this . #renderCheckIcon( ) : nothing }
376+ < span > ${ label } </ span >
377+ </ div >
378+ ` ;
379+ } ) }
380+ </ div >
381+ ${ filteredOptions . length === 0 ? html `< div style ="padding: 8px; font-style: italic; color: var(--md-sys-color-outline); "> No options found</ div > ` : nothing }
382+ </ div >
383+ ` ;
384+ }
385+
386+ // Default Checkbox Dropdown Layout
222387 const count = currentSelections . length ;
223388 const headerText = count > 0 ? `${ count } Selected` : ( this . description ?? "Select items" ) ;
224389
@@ -236,31 +401,35 @@ export class MultipleChoice extends Root {
236401 </ span >
237402 </ div >
238403
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 >
404+ < div class ="dropdown-wrapper ${ this . isOpen ? "open" : "" } ">
405+ ${ this . filterable ? this . #renderFilter( ) : nothing }
406+ < div class ="options-scroll-container ">
407+ ${ filteredOptions . map ( ( option ) => {
408+ const label = extractStringValue (
409+ option . label ,
410+ this . component ,
411+ this . processor ,
412+ this . surfaceId
413+ ) ;
414+ const isSelected = currentSelections . includes ( option . value ) ;
415+
416+ return html `
417+ < div
418+ class ="option-item ${ isSelected ? "selected" : "" } "
419+ @click =${ ( e : Event ) => {
420+ e . stopPropagation ( ) ;
421+ this . toggleSelection ( option . value ) ;
422+ } }
423+ >
424+ < div class ="checkbox ">
425+ < span class ="checkbox-icon "> ✓</ span >
426+ </ div >
427+ < span > ${ label } </ span >
259428 </ div >
260- < span > ${ label } </ span >
261- </ div >
262- ` ;
263- } ) }
429+ ` ;
430+ } ) }
431+ ${ filteredOptions . length === 0 ? html ` < div style =" padding: 16px; text-align: center; color: var(--md-sys-color-outline); " > No options found </ div > ` : nothing }
432+ </ div >
264433 </ div >
265434 </ div >
266435 ` ;
0 commit comments