@@ -118,26 +118,44 @@ export const MultipleChoiceForm: React.FC<MultipleChoiceFormProps> = ({
118118 [ questions , answers ] ,
119119 )
120120
121+ const setAnswerForQuestion = useCallback (
122+ (
123+ questionIndex : number ,
124+ updater : ( previous : AccordionAnswer | undefined ) => AccordionAnswer ,
125+ ) => {
126+ setAnswers ( ( prev ) => {
127+ const nextAnswers = new Map ( prev )
128+ const previousAnswer = prev . get ( questionIndex )
129+ nextAnswers . set ( questionIndex , updater ( previousAnswer ) )
130+ return nextAnswers
131+ } )
132+ } ,
133+ [ ] ,
134+ )
135+
136+ const goToNextUnanswered = useCallback (
137+ ( questionIndex : number ) => {
138+ const nextUnanswered = findNextUnanswered ( questionIndex )
139+ setExpandedIndex ( nextUnanswered )
140+ } ,
141+ [ findNextUnanswered ] ,
142+ )
143+
121144 // Handle setting "Other" text (with cursor position)
122145 const handleSetOtherText = useCallback (
123146 ( questionIndex : number , text : string , cursorPosition : number ) => {
124- setAnswers ( ( prev ) => {
125- const newAnswers = new Map ( prev )
126- const currentAnswer = prev . get ( questionIndex )
127- newAnswers . set ( questionIndex , {
128- ...currentAnswer ,
129- isOther : true ,
130- otherText : text ,
131- } )
132- return newAnswers
133- } )
147+ setAnswerForQuestion ( questionIndex , ( currentAnswer ) => ( {
148+ ...currentAnswer ,
149+ isOther : true ,
150+ otherText : text ,
151+ } ) )
134152 setOtherCursorPositions ( ( prev ) => {
135153 const newPositions = new Map ( prev )
136154 newPositions . set ( questionIndex , cursorPosition )
137155 return newPositions
138156 } )
139157 } ,
140- [ ] ,
158+ [ setAnswerForQuestion ] ,
141159 )
142160
143161 // Handle "Other" text submit (Enter key)
@@ -149,154 +167,146 @@ export const MultipleChoiceForm: React.FC<MultipleChoiceFormProps> = ({
149167 setIsTypingOther ( false )
150168 // If text is entered, move to next question
151169 if ( currentText . trim ( ) ) {
152- const nextUnanswered = findNextUnanswered ( questionIndex )
153- setExpandedIndex ( nextUnanswered )
170+ goToNextUnanswered ( questionIndex )
154171 }
155172 } ,
156- [ answers , findNextUnanswered ] ,
173+ [ answers , goToNextUnanswered ] ,
157174 )
158175
159176 // Handle "Other" text cancel (Escape key) - deselect Custom option entirely
160177 const handleOtherCancel = useCallback (
161178 ( questionIndex : number ) => {
162179 // Clear text, deselect "Custom" option, and exit typing mode
163- setAnswers ( ( prev ) => {
164- const newAnswers = new Map ( prev )
165- const currentAnswer = prev . get ( questionIndex )
166- // Deselect "Custom" by setting isOther to false and clearing text
167- newAnswers . set ( questionIndex , {
168- ...currentAnswer ,
169- isOther : false ,
170- otherText : '' ,
171- } )
172- return newAnswers
173- } )
180+ setAnswerForQuestion ( questionIndex , ( currentAnswer ) => ( {
181+ ...currentAnswer ,
182+ isOther : false ,
183+ otherText : '' ,
184+ } ) )
174185 setOtherCursorPositions ( ( prev ) => {
175186 const newPositions = new Map ( prev )
176187 newPositions . set ( questionIndex , 0 )
177188 return newPositions
178189 } )
179190 setIsTypingOther ( false )
180191 } ,
181- [ ] ,
192+ [ setAnswerForQuestion ] ,
182193 )
183194
184195 // Handle selecting an option (single-select)
185196 const handleSelectOption = useCallback (
186197 ( questionIndex : number , optionIndex : number ) => {
187- setAnswers ( ( prev ) => {
188- const newAnswers = new Map ( prev )
189- if ( optionIndex === OTHER_OPTION_INDEX ) {
190- // "Other" option - enter typing mode
191- newAnswers . set ( questionIndex , {
192- isOther : true ,
193- otherText : prev . get ( questionIndex ) ?. otherText || '' ,
194- } )
195- } else {
196- newAnswers . set ( questionIndex , {
197- selectedIndex : optionIndex ,
198- isOther : false ,
199- } )
200- }
201- return newAnswers
202- } )
198+ const isOtherOption = optionIndex === OTHER_OPTION_INDEX
199+ setAnswerForQuestion ( questionIndex , ( currentAnswer ) =>
200+ isOtherOption
201+ ? {
202+ ...currentAnswer ,
203+ isOther : true ,
204+ otherText : currentAnswer ?. otherText || '' ,
205+ }
206+ : {
207+ selectedIndex : optionIndex ,
208+ isOther : false ,
209+ } ,
210+ )
203211
204212 // For "Other" option, enter typing mode
205- if ( optionIndex === OTHER_OPTION_INDEX ) {
213+ if ( isOtherOption ) {
206214 setIsTypingOther ( true )
207215 } else {
208216 // For regular options, collapse and move to next unanswered
209- const nextUnanswered = findNextUnanswered ( questionIndex )
210- setExpandedIndex ( nextUnanswered )
217+ goToNextUnanswered ( questionIndex )
211218 }
212219 } ,
213- [ findNextUnanswered ] ,
220+ [ goToNextUnanswered , setAnswerForQuestion ] ,
214221 )
215222
216223 // Handle toggling an option (multi-select)
217224 const handleToggleOption = useCallback (
218225 ( questionIndex : number , optionIndex : number ) => {
226+ let toggledOtherOn = false
227+
219228 setAnswers ( ( prev ) => {
220229 const newAnswers = new Map ( prev )
221230 const currentAnswer = prev . get ( questionIndex )
222- const currentIndices = currentAnswer ?. selectedIndices ?? new Set ( )
223231
224232 if ( optionIndex === OTHER_OPTION_INDEX ) {
225- // "Other" option toggle
226- const wasOther = currentAnswer ?. isOther ?? false
233+ toggledOtherOn = ! ( currentAnswer ?. isOther ?? false )
227234 newAnswers . set ( questionIndex , {
228235 ...currentAnswer ,
229- selectedIndices : currentIndices ,
230- isOther : ! wasOther ,
236+ selectedIndices : new Set ( currentAnswer ?. selectedIndices ?? [ ] ) ,
237+ isOther : ! currentAnswer ?. isOther ,
231238 otherText : currentAnswer ?. otherText || '' ,
232239 } )
240+ return newAnswers
241+ }
242+
243+ const newIndices = new Set ( currentAnswer ?. selectedIndices ?? [ ] )
244+ if ( newIndices . has ( optionIndex ) ) {
245+ newIndices . delete ( optionIndex )
233246 } else {
234- const newIndices = new Set ( currentIndices )
235- if ( newIndices . has ( optionIndex ) ) {
236- newIndices . delete ( optionIndex )
237- } else {
238- newIndices . add ( optionIndex )
239- }
240- newAnswers . set ( questionIndex , {
241- ...currentAnswer ,
242- selectedIndices : newIndices ,
243- isOther : currentAnswer ?. isOther ?? false ,
244- } )
247+ newIndices . add ( optionIndex )
245248 }
249+ newAnswers . set ( questionIndex , {
250+ ...currentAnswer ,
251+ selectedIndices : newIndices ,
252+ isOther : currentAnswer ?. isOther ?? false ,
253+ } )
246254 return newAnswers
247255 } )
248256
249257 // For "Other" option in multi-select, also enter typing mode
250258 if ( optionIndex === OTHER_OPTION_INDEX ) {
251- // Check if we're toggling ON (not off)
252- const currentAnswer = answers . get ( questionIndex )
253- if ( ! currentAnswer ?. isOther ) {
254- setIsTypingOther ( true )
255- } else {
256- setIsTypingOther ( false )
257- }
259+ setIsTypingOther ( toggledOtherOn )
258260 }
259261 } ,
260- [ answers ] ,
262+ [ ] ,
261263 )
262264
263- // Handle submit
264- const handleSubmit = useCallback ( ( ) => {
265- const formattedAnswers = questions . map (
266- ( question : AskUserQuestion , index : number ) => {
267- const answer = answers . get ( index )
268- if ( ! answer ) {
269- return { question : question . question , answer : 'Skipped' }
270- }
271-
272- if ( answer . isOther && answer . otherText ) {
273- return { question : question . question , answer : answer . otherText }
274- }
265+ const formatAnswer = useCallback (
266+ (
267+ question : AskUserQuestion ,
268+ answer : AccordionAnswer | undefined ,
269+ ) => {
270+ if ( ! answer ) {
271+ return { question : question . question , answer : 'Skipped' }
272+ }
275273
276- if ( question . multiSelect && answer . selectedIndices ) {
277- const selectedLabels = Array . from ( answer . selectedIndices )
274+ const selectedOptions = question . multiSelect
275+ ? Array . from ( answer . selectedIndices ?? [ ] )
278276 . map ( ( idx : number ) => getOptionLabel ( question . options [ idx ] ) )
279277 . filter ( Boolean )
280- return {
281- question : question . question ,
282- answer : selectedLabels . join ( ', ' ) ,
283- }
284- }
278+ : answer . selectedIndex !== undefined
279+ ? [ getOptionLabel ( question . options [ answer . selectedIndex ] ) ]
280+ : [ ]
285281
286- if ( answer . selectedIndex !== undefined ) {
287- const label = getOptionLabel ( question . options [ answer . selectedIndex ] )
288- return {
289- question : question . question ,
290- answer : label || 'Unknown' ,
291- }
292- }
282+ const customText =
283+ answer . isOther && ( answer . otherText ?. trim ( ) . length ?? 0 ) > 0
284+ ? ( answer . otherText ?? '' ) . trim ( )
285+ : ''
293286
287+ const parts = customText ? [ ...selectedOptions , customText ] : selectedOptions
288+ if ( parts . length === 0 ) {
294289 return { question : question . question , answer : 'Skipped' }
290+ }
291+
292+ return {
293+ question : question . question ,
294+ answer : question . multiSelect ? parts . join ( ', ' ) : parts [ 0 ] ,
295+ }
296+ } ,
297+ [ ] ,
298+ )
299+
300+ // Handle submit
301+ const handleSubmit = useCallback ( ( ) => {
302+ const formattedAnswers = questions . map (
303+ ( question : AskUserQuestion , index : number ) => {
304+ return formatAnswer ( question , answers . get ( index ) )
295305 } ,
296306 )
297307
298308 onSubmit ( formattedAnswers )
299- } , [ questions , answers , onSubmit ] )
309+ } , [ questions , answers , onSubmit , formatAnswer ] )
300310
301311 // Keyboard navigation using OpenTUI's useKeyboard hook
302312 useKeyboard (
0 commit comments