1- import { useState , useCallback , useEffect , useId , memo } from 'react' ;
2- import type { Types , Primitives } from '@a2ui/lit/0.8' ;
1+ import { useCallback , useId , memo } from 'react' ;
2+ import type { Types } from '@a2ui/lit/0.8' ;
33import type { A2UIComponentProps } from '../../types' ;
44import { useA2UIComponent } from '../../hooks/useA2UIComponent' ;
55import { classMapToString , stylesToObject } from '../../lib/utils' ;
66
7- interface Option {
8- label : Primitives . StringValue ;
9- value : string ;
10- }
11-
127/**
13- * MultipleChoice component - a selection component for single or multiple options .
8+ * MultipleChoice component - a selection component using a dropdown .
149 *
15- * When maxAllowedSelections is 1, renders as radio buttons .
16- * Otherwise, renders as checkboxes .
10+ * Renders a <select> element with options, matching the Lit renderer's behavior .
11+ * Supports two-way data binding for the selected value .
1712 */
1813export const MultipleChoice = memo ( function MultipleChoice ( {
1914 node,
2015 surfaceId,
2116} : A2UIComponentProps < Types . MultipleChoiceNode > ) {
22- const { theme, resolveString, setValue, getValue } = useA2UIComponent ( node , surfaceId ) ;
17+ const { theme, resolveString, setValue } = useA2UIComponent ( node , surfaceId ) ;
2318 const props = node . properties ;
24- const groupId = useId ( ) ;
19+ const id = useId ( ) ;
2520
26- const options = ( props . options as Option [ ] ) ?? [ ] ;
27- const maxSelections = props . maxAllowedSelections ?? 1 ;
21+ const options = ( props . options as { label : { literalString ?: string ; path ?: string } ; value : string } [ ] ) ?? [ ] ;
2822 const selectionsPath = props . selections ?. path ;
2923
30- // Initialize selections from data model or literal
31- const getInitialSelections = ( ) : string [ ] => {
32- if ( selectionsPath ) {
33- const data = getValue ( selectionsPath ) ;
34- if ( Array . isArray ( data ) ) return data . map ( String ) ;
35- if ( data !== null ) return [ String ( data ) ] ;
36- }
37- return [ ] ;
38- } ;
39-
40- const [ selections , setSelections ] = useState < string [ ] > ( getInitialSelections ) ;
41-
42- // Sync with external data model changes
43- useEffect ( ( ) => {
44- if ( selectionsPath ) {
45- const externalValue = getValue ( selectionsPath ) ;
46- if ( externalValue !== null ) {
47- const newSelections = Array . isArray ( externalValue )
48- ? externalValue . map ( String )
49- : [ String ( externalValue ) ] ;
50- setSelections ( newSelections ) ;
51- }
52- }
53- } , [ selectionsPath , getValue ] ) ;
24+ // Access description from props (Lit component supports it)
25+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26+ const description = resolveString ( ( props as any ) . description ) ?? 'Select an item' ;
5427
5528 const handleChange = useCallback (
56- ( optionValue : string , checked : boolean ) => {
57- let newSelections : string [ ] ;
58-
59- if ( maxSelections === 1 ) {
60- // Radio behavior
61- newSelections = checked ? [ optionValue ] : [ ] ;
62- } else {
63- // Checkbox behavior
64- if ( checked ) {
65- newSelections = [ ...selections , optionValue ] . slice ( 0 , maxSelections ) ;
66- } else {
67- newSelections = selections . filter ( ( v ) => v !== optionValue ) ;
68- }
69- }
70-
71- setSelections ( newSelections ) ;
72-
73- // Two-way binding: update data model
29+ ( e : React . ChangeEvent < HTMLSelectElement > ) => {
30+ // Two-way binding: update data model with array (matches Lit behavior)
7431 if ( selectionsPath ) {
75- setValue (
76- selectionsPath ,
77- maxSelections === 1 ? newSelections [ 0 ] ?? '' : newSelections
78- ) ;
32+ setValue ( selectionsPath , [ e . target . value ] ) ;
7933 }
8034 } ,
81- [ maxSelections , selections , selectionsPath , setValue ]
35+ [ selectionsPath , setValue ]
8236 ) ;
8337
84- const isRadio = maxSelections === 1 ;
85-
8638 // Apply --weight CSS variable on root div (:host equivalent) for flex layouts
8739 const hostStyle : React . CSSProperties = node . weight !== undefined
8840 ? { '--weight' : node . weight } as React . CSSProperties
@@ -91,43 +43,38 @@ export const MultipleChoice = memo(function MultipleChoice({
9143 // Structure mirrors Lit's MultipleChoice component:
9244 // <div class="a2ui-multiplechoice"> ← :host equivalent
9345 // <section class="..."> ← container theme classes
94- // ...options...
46+ // <label>...</label> ← description label
47+ // <select>...</select> ← dropdown element
9548 // </section>
9649 // </div>
9750 return (
9851 < div className = "a2ui-multiplechoice" style = { hostStyle } >
99- < section
100- className = { classMapToString ( theme . components . MultipleChoice . container ) }
101- style = { stylesToObject ( theme . additionalStyles ?. MultipleChoice ) }
102- role = { isRadio ? 'radiogroup' : 'group' }
103- >
104- { options . map ( ( option , index ) => {
105- const label = resolveString ( option . label ) ;
106- const optionId = `${ groupId } -${ index } ` ;
107- const isSelected = selections . includes ( option . value ) ;
108-
109- return (
110- < label
111- key = { option . value }
112- className = { classMapToString ( theme . components . MultipleChoice . element ) }
113- style = { { cursor : 'pointer' , display : 'flex' , flexDirection : 'row' , gap : '0.5rem' , alignItems : 'center' } }
114- >
115- < input
116- type = { isRadio ? 'radio' : 'checkbox' }
117- id = { optionId }
118- name = { groupId }
119- value = { option . value }
120- checked = { isSelected }
121- onChange = { ( e ) => handleChange ( option . value , e . target . checked ) }
122- style = { { cursor : 'pointer' } }
123- />
124- < span className = { classMapToString ( theme . components . MultipleChoice . label ) } >
125- { label }
126- </ span >
127- </ label >
128- ) ;
129- } ) }
130- </ section >
52+ < section
53+ className = { classMapToString ( theme . components . MultipleChoice . container ) }
54+ >
55+ < label
56+ htmlFor = { id }
57+ className = { classMapToString ( theme . components . MultipleChoice . label ) }
58+ >
59+ { description }
60+ </ label >
61+ < select
62+ name = "data"
63+ id = { id }
64+ className = { classMapToString ( theme . components . MultipleChoice . element ) }
65+ style = { stylesToObject ( theme . additionalStyles ?. MultipleChoice ) }
66+ onChange = { handleChange }
67+ >
68+ { options . map ( ( option ) => {
69+ const label = resolveString ( option . label ) ;
70+ return (
71+ < option key = { option . value } value = { option . value } >
72+ { label }
73+ </ option >
74+ ) ;
75+ } ) }
76+ </ select >
77+ </ section >
13178 </ div >
13279 ) ;
13380} ) ;
0 commit comments