Skip to content

Commit ef861af

Browse files
committed
fix(ui): Preserve AskUser selection and focus
Keep selected radio styling on hover and retain the Other selection and input focus when custom text is cleared. Show selection counts only for multiple-choice questions. Cover empty custom input and focus retention for single and multiple selection with and without session drafts.
1 parent dde7566 commit ef861af

6 files changed

Lines changed: 44 additions & 61 deletions

File tree

‎design-system/packages/ui/src/components/Radio/Radio.module.css‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
transform: scale(1);
7272
}
7373

74-
.input:hover:not(:disabled) + .circle {
74+
.input:hover:not(:checked, :disabled) + .circle {
7575
border-color: var(--openbitfun-color-field-border-hover);
7676
background-color: var(--openbitfun-color-field-background-hover);
7777
}

‎design-system/packages/ui/src/flow-chat/ask-user/AskUser.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ export const AskUser = forwardRef<HTMLDivElement, AskUserProps>(function AskUser
615615

616616
{showFooter && resolvedExpanded && (
617617
<div className={styles.footer} data-openbitfun-part="footer">
618-
{paginated && navigation && activeQuestion && (
618+
{paginated && navigation && activeQuestion?.selectionMode === "multiple" && (
619619
<span aria-live="polite" className={styles.counter} data-openbitfun-part="selection-count">
620620
{navigation.selectionLabel(
621621
selectedCount,

‎src/web-ui/src/flow_chat/store/askUserQuestionDraftStore.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ describe('askUserQuestionDraftStore', () => {
9999
expect(askUserQuestionDraftStore.getState().drafts[key]).toBeUndefined();
100100
});
101101

102-
it('removes the Other marker when custom input becomes blank', () => {
102+
it('preserves the Other marker when custom input becomes blank', () => {
103103
const multiKey = askUserQuestionDraftKey('session-a', 'tool-multi');
104104
const singleKey = askUserQuestionDraftKey('session-a', 'tool-single');
105105
const store = askUserQuestionDraftStore.getState();
@@ -114,21 +114,21 @@ describe('askUserQuestionDraftStore', () => {
114114
store.setOtherInput(singleKey, 0, '');
115115

116116
expect(askUserQuestionDraftStore.getState().drafts[multiKey]).toMatchObject({
117-
answers: { 0: ['PostgreSQL'] },
117+
answers: { 0: ['PostgreSQL', 'Other'] },
118118
otherInputs: { 0: '' },
119119
});
120120
expect(askUserQuestionDraftStore.getState().drafts[singleKey]).toMatchObject({
121-
answers: {},
121+
answers: { 0: 'Other' },
122122
otherInputs: { 0: '' },
123123
});
124124
});
125125

126-
it('preserves the Other marker for transient blank IME composition values', () => {
126+
it('preserves the Other marker for an initially empty custom input', () => {
127127
const key = askUserQuestionDraftKey('session-a', 'tool-ime');
128128
const store = askUserQuestionDraftStore.getState();
129129

130130
store.setSingleAnswer(key, 0, 'Other');
131-
store.setOtherInput(key, 0, '', true);
131+
store.setOtherInput(key, 0, '');
132132

133133
expect(askUserQuestionDraftStore.getState().drafts[key]).toMatchObject({
134134
answers: { 0: 'Other' },

‎src/web-ui/src/flow_chat/store/askUserQuestionDraftStore.ts‎

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ interface AskUserQuestionDraftState {
2929
key: string,
3030
questionIndex: number,
3131
value: string,
32-
preserveOtherSelection?: boolean,
3332
) => void;
3433
setSubmissionPhase: (key: string, phase: AskUserQuestionSubmissionPhase) => void;
3534
clearDraft: (key: string) => void;
@@ -103,28 +102,6 @@ function updateDraft(
103102
};
104103
}
105104

106-
function removeOtherAnswer(
107-
answers: Record<number, AskUserQuestionAnswer>,
108-
questionIndex: number,
109-
): Record<number, AskUserQuestionAnswer> {
110-
const current = answers[questionIndex];
111-
if (Array.isArray(current)) {
112-
if (!current.includes('Other')) {
113-
return answers;
114-
}
115-
return {
116-
...answers,
117-
[questionIndex]: current.filter(value => value !== 'Other'),
118-
};
119-
}
120-
if (current !== 'Other') {
121-
return answers;
122-
}
123-
const nextAnswers = { ...answers };
124-
delete nextAnswers[questionIndex];
125-
return nextAnswers;
126-
}
127-
128105
function removeMatchingDrafts(
129106
state: AskUserQuestionDraftState,
130107
shouldRemove: (key: string) => boolean,
@@ -170,14 +147,11 @@ export const useAskUserQuestionDraftStore = create<AskUserQuestionDraftState>((s
170147
}));
171148
},
172149

173-
setOtherInput: (key, questionIndex, value, preserveOtherSelection = false) => {
150+
setOtherInput: (key, questionIndex, value) => {
174151
set(state => updateDraft(state, key, draft => {
175152
const isEmpty = value.trim().length === 0;
176153
return {
177154
...draft,
178-
answers: isEmpty && !preserveOtherSelection
179-
? removeOtherAnswer(draft.answers, questionIndex)
180-
: draft.answers,
181155
otherInputs: {
182156
...draft.otherInputs,
183157
[questionIndex]: isEmpty ? '' : value,

‎src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.test.tsx‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,39 @@ describe('AskUserQuestionCard', () => {
550550
expect(document.activeElement).toBe(customInput);
551551
});
552552

553-
it('deselects a blank multi-select Other answer and omits it from submission', async () => {
553+
it.each([
554+
{ sessionId: 'session-a', multiSelect: false },
555+
{ sessionId: undefined, multiSelect: false },
556+
{ sessionId: 'session-a', multiSelect: true },
557+
{ sessionId: undefined, multiSelect: true },
558+
])('retains Other selection and input focus after clearing text ($sessionId, multiple=$multiSelect)', async ({ sessionId, multiSelect }) => {
559+
act(() => root.render(
560+
<AskUserQuestionCard
561+
toolItem={questionTool('pending_confirmation', multiSelect)}
562+
config={config}
563+
sessionId={sessionId}
564+
/>,
565+
));
566+
await act(async () => container.querySelector<HTMLInputElement>('input[value="Other"]')!.click());
567+
const input = container.querySelector<HTMLInputElement>('[data-openbitfun-part="custom-input"] input')!;
568+
expect(document.activeElement).toBe(input);
569+
act(() => setInputValue(input, 'Custom database'));
570+
571+
for (const value of ['', ' ']) {
572+
act(() => setInputValue(input, value));
573+
expect(container.querySelector('[data-openbitfun-part="custom-input"] input')).toBe(input);
574+
expect(document.activeElement).toBe(input);
575+
expect(container.querySelector<HTMLInputElement>('input[value="Other"]')?.checked).toBe(true);
576+
expect(container.querySelector<HTMLButtonElement>('[data-openbitfun-part="submit"] button')?.disabled).toBe(true);
577+
}
578+
579+
act(() => setInputValue(input, 'New database'));
580+
expect(input.value).toBe('New database');
581+
expect(document.activeElement).toBe(input);
582+
expect(container.querySelector<HTMLButtonElement>('[data-openbitfun-part="submit"] button')?.disabled).toBe(false);
583+
});
584+
585+
it('keeps a blank multi-select Other answer selected and omits it from submission', async () => {
554586
act(() => {
555587
root.render(
556588
<AskUserQuestionCard
@@ -578,7 +610,7 @@ describe('AskUserQuestionCard', () => {
578610
}
579611
});
580612

581-
expect(container.querySelector<HTMLInputElement>('input[value="Other"]')?.checked).toBe(false);
613+
expect(container.querySelector<HTMLInputElement>('input[value="Other"]')?.checked).toBe(true);
582614
expect(container.querySelector<HTMLInputElement>('input[value="PostgreSQL"]')?.checked).toBe(true);
583615

584616
const submitButton = container.querySelector<HTMLButtonElement>('[data-openbitfun-part="submit"] button');

‎src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx‎

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -367,42 +367,19 @@ export const AskUserQuestionCard: React.FC<ToolCardProps> = ({
367367
const handleOtherInputChange = useCallback((
368368
questionIndex: number,
369369
value: string,
370-
preserveOtherSelection = false,
371370
) => {
372371
if (draftKey) {
373372
askUserQuestionDraftStore.getState().setOtherInput(
374373
draftKey,
375374
questionIndex,
376375
value,
377-
preserveOtherSelection,
378376
);
379377
return;
380378
}
381379
setLocalDraft((current) => {
382380
const isEmpty = value.trim().length === 0;
383-
const currentAnswer = current.answers[questionIndex];
384-
let nextAnswers = current.answers;
385-
if (
386-
isEmpty
387-
&& !preserveOtherSelection
388-
&& Array.isArray(currentAnswer)
389-
&& currentAnswer.includes(OTHER_OPTION_VALUE)
390-
) {
391-
nextAnswers = {
392-
...current.answers,
393-
[questionIndex]: currentAnswer.filter((answer) => answer !== OTHER_OPTION_VALUE),
394-
};
395-
} else if (
396-
isEmpty
397-
&& !preserveOtherSelection
398-
&& currentAnswer === OTHER_OPTION_VALUE
399-
) {
400-
nextAnswers = { ...current.answers };
401-
delete nextAnswers[questionIndex];
402-
}
403381
return {
404382
...current,
405-
answers: nextAnswers,
406383
otherInputs: {
407384
...current.otherInputs,
408385
[questionIndex]: isEmpty ? '' : value,
@@ -707,8 +684,8 @@ export const AskUserQuestionCard: React.FC<ToolCardProps> = ({
707684
) : undefined}
708685
onFocusCapture={handleInteraction}
709686
onAnswersChange={handleAnswersChange}
710-
onCustomAnswerChange={(questionId, value, meta) => {
711-
handleOtherInputChange(Number(questionId), value, meta.isComposing);
687+
onCustomAnswerChange={(questionId, value) => {
688+
handleOtherInputChange(Number(questionId), value);
712689
}}
713690
onSubmit={handleSubmit}
714691
questions={designQuestions}

0 commit comments

Comments
 (0)