1515 */
1616
1717import { html , css , PropertyValues , nothing } from "lit" ;
18- import { customElement , property } from "lit/decorators.js" ;
18+ import { customElement , property , state } from "lit/decorators.js" ;
1919import { Root } from "./root.js" ;
2020import { A2uiMessageProcessor } from "@a2ui/web_core/data/model-processor" ;
2121import * as Primitives from "@a2ui/web_core/types/primitives" ;
@@ -35,6 +35,9 @@ export class MultipleChoice extends Root {
3535 @property ( )
3636 accessor selections : Primitives . StringValue | string [ ] = [ ] ;
3737
38+ @state ( )
39+ accessor isOpen = false ;
40+
3841 static styles = [
3942 structuralStyles ,
4043 css `
@@ -46,20 +49,129 @@ export class MultipleChoice extends Root {
4649 display: block;
4750 flex: var(--weight);
4851 min-height: 0;
49- overflow: auto;
52+ position: relative;
53+ font-family: 'Google Sans', 'Roboto', sans-serif;
54+ }
55+
56+ .container {
57+ display: flex;
58+ flex-direction: column;
59+ gap: 4px;
60+ position: relative;
61+ }
62+
63+ /* Header / Trigger */
64+ .dropdown-header {
65+ display: flex;
66+ align-items: center;
67+ justify-content: space-between;
68+ padding: 12px 16px;
69+ background: var(--md-sys-color-surface);
70+ border: 1px solid var(--md-sys-color-outline-variant);
71+ border-radius: 8px;
72+ cursor: pointer;
73+ user-select: none;
74+ transition: background-color 0.2s;
75+ box-shadow: var(--md-sys-elevation-level1);
76+ }
77+
78+ .dropdown-header:hover {
79+ background: var(--md-sys-color-surface-container-low);
80+ }
81+
82+ .header-text {
83+ font-size: 1rem;
84+ color: var(--md-sys-color-on-surface);
85+ font-weight: 400;
86+ }
87+
88+ .chevron {
89+ color: var(--md-sys-color-primary);
90+ font-size: 1.2rem;
91+ transition: transform 0.2s ease;
92+ }
93+
94+ .chevron.open {
95+ transform: rotate(180deg);
96+ }
97+
98+ /* Dropdown List */
99+ .options-list {
100+ background: var(--md-sys-color-surface);
101+ 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;
105+ padding: 0;
106+ display: none;
107+ flex-direction: column;
108+ margin-top: 4px; /* Small gap */
109+ max-height: 0; /* Animate height? */
110+ transition: max-height 0.2s ease-out;
50111 }
51112
52- select {
53- width: 100%;
113+ .options-list.open {
114+ display: flex;
115+ max-height: 300px; /* Limit height but allow scrolling */
116+ border: 1px solid var(--md-sys-color-outline-variant); /* efficient border */
54117 }
55118
56- .description {
119+ /* Option Item (Checkbox style) */
120+ .option-item {
121+ display: flex;
122+ align-items: center;
123+ gap: 12px;
124+ padding: 12px 16px;
125+ cursor: pointer;
126+ color: var(--md-sys-color-on-surface);
127+ font-size: 0.95rem;
128+ transition: background-color 0.1s;
129+ }
130+
131+ .option-item:hover {
132+ background: var(--md-sys-color-surface-container-highest);
133+ }
134+
135+ /* Custom Checkbox */
136+ .checkbox {
137+ width: 18px;
138+ height: 18px;
139+ border: 2px solid var(--md-sys-color-outline);
140+ border-radius: 2px;
141+ display: flex;
142+ align-items: center;
143+ justify-content: center;
144+ transition: all 0.2s;
145+ flex-shrink: 0;
146+ }
147+
148+ .option-item.selected .checkbox {
149+ background: var(--md-sys-color-primary);
150+ border-color: var(--md-sys-color-primary);
151+ }
152+
153+ .checkbox-icon {
154+ color: var(--md-sys-color-on-primary);
155+ font-size: 14px;
156+ font-weight: bold;
157+ opacity: 0;
158+ transform: scale(0.5);
159+ transition: all 0.2s;
160+ }
161+
162+ .option-item.selected .checkbox-icon {
163+ opacity: 1;
164+ transform: scale(1);
165+ }
166+
167+ @keyframes fadeIn {
168+ from { opacity: 0; transform: translateY(-8px); }
169+ to { opacity: 1; transform: translateY(0); }
57170 }
58171 ` ,
59172 ] ;
60173
61174 #setBoundValue( value : string [ ] ) {
62- console . log ( value ) ;
63175 if ( ! this . selections || ! this . processor ) {
64176 return ;
65177 }
@@ -78,65 +190,76 @@ export class MultipleChoice extends Root {
78190 ) ;
79191 }
80192
81- protected willUpdate ( changedProperties : PropertyValues < this> ) : void {
82- const shouldUpdate = changedProperties . has ( "options" ) ;
83- if ( ! shouldUpdate ) {
84- return ;
85- }
86-
193+ getCurrentSelections ( ) : string [ ] {
87194 if ( ! this . processor || ! this . component || Array . isArray ( this . selections ) ) {
88- return ;
195+ return [ ] ;
89196 }
90197
91- this . selections ;
92-
93198 const selectionValue = this . processor . getData (
94199 this . component ,
95200 this . selections . path ! ,
96201 this . surfaceId ?? A2uiMessageProcessor . DEFAULT_SURFACE_ID
97202 ) ;
98203
99- if ( ! Array . isArray ( selectionValue ) ) {
100- return ;
101- }
204+ return Array . isArray ( selectionValue ) ? ( selectionValue as string [ ] ) : [ ] ;
205+ }
102206
103- this . #setBoundValue( selectionValue as string [ ] ) ;
207+ toggleSelection ( value : string ) {
208+ const current = this . getCurrentSelections ( ) ;
209+ if ( current . includes ( value ) ) {
210+ this . #setBoundValue( current . filter ( ( v ) => v !== value ) ) ;
211+ } else {
212+ this . #setBoundValue( [ ...current , value ] ) ;
213+ }
214+ this . requestUpdate ( ) ;
104215 }
105216
106217 render ( ) {
107- return html `< section class =${ classMap (
108- this . theme . components . MultipleChoice . container
109- ) } >
110- < label class =${ classMap (
111- this . theme . components . MultipleChoice . label
112- ) } for ="data"> ${ this . description ?? "Select an item" } </ label >
113- < select
114- name ="data "
115- id ="data "
116- class =${ classMap ( this . theme . components . MultipleChoice . element ) }
117- style =${
118- this . theme . additionalStyles ?. MultipleChoice
119- ? styleMap ( this . theme . additionalStyles ?. MultipleChoice )
120- : nothing
121- }
122- @change=${ ( evt : Event ) => {
123- if ( ! ( evt . target instanceof HTMLSelectElement ) ) {
124- return ;
125- }
126-
127- this . #setBoundValue( [ evt . target . value ] ) ;
128- } }
129- >
130- ${ this . options . map ( ( option ) => {
131- const label = extractStringValue (
132- option . label ,
133- this . component ,
134- this . processor ,
135- this . surfaceId
136- ) ;
137- return html `< option ${ option . value } > ${ label } </ option > ` ;
138- } ) }
139- </ select >
140- </ section > ` ;
218+ const currentSelections = this . getCurrentSelections ( ) ;
219+ const count = currentSelections . length ;
220+ const headerText = count > 0 ? `${ count } Selected` : ( this . description ?? "Select items" ) ;
221+
222+ return html `
223+ < div class ="container ">
224+ < div
225+ class ="dropdown-header "
226+ @click =${ ( ) => this . isOpen = ! this . isOpen }
227+ >
228+ < span class ="header-text "> ${ headerText } </ span >
229+ < span class ="chevron ${ this . isOpen ? "open" : "" } ">
230+ < svg xmlns ="http://www.w3.org/2000/svg " height ="24 " viewBox ="0 -960 960 960 " width ="24 " fill ="currentColor ">
231+ < path d ="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z "/>
232+ </ svg >
233+ </ span >
234+ </ div >
235+
236+ < div class ="options-list ${ this . isOpen ? "open" : "" } ">
237+ ${ this . options . map ( ( option ) => {
238+ const label = extractStringValue (
239+ option . label ,
240+ this . component ,
241+ this . processor ,
242+ this . surfaceId
243+ ) ;
244+ const isSelected = currentSelections . includes ( option . value ) ;
245+
246+ return html `
247+ < div
248+ class ="option-item ${ isSelected ? "selected" : "" } "
249+ @click =${ ( e : Event ) => {
250+ e . stopPropagation ( ) ;
251+ this . toggleSelection ( option . value ) ;
252+ } }
253+ >
254+ < div class ="checkbox ">
255+ < span class ="checkbox-icon "> ✓</ span >
256+ </ div >
257+ < span > ${ label } </ span >
258+ </ div >
259+ ` ;
260+ } ) }
261+ </ div >
262+ </ div >
263+ ` ;
141264 }
142265}
0 commit comments