From caa2f6a7c250112343b2c4a503287c505e1b1bfd Mon Sep 17 00:00:00 2001 From: big-sheq Date: Thu, 5 Nov 2026 22:15:58 -0600 Subject: [PATCH] feat(native-chat): step between a question card's questions with Left and Right (#38335) On a multi-question card only a click on a step tab changed question. Left and Right now step to the previous or next question while an option row or a step tab has focus, keep every answer, and stop at the first and last question. Inside the answer field they still move the caret. Co-authored-by: Michael Schaecher <276865230+big-sheq@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- .../NativeChatQuestionCard.test.tsx | 28 +++++++++++++++++++ .../native-chat/NativeChatQuestionCard.tsx | 16 ++++++++++- .../use-native-chat-question-keys.ts | 10 ++++++- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/native-chat/NativeChatQuestionCard.test.tsx b/src/renderer/src/components/native-chat/NativeChatQuestionCard.test.tsx index 6cd7ace1591..563cb7b9ba8 100644 --- a/src/renderer/src/components/native-chat/NativeChatQuestionCard.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatQuestionCard.test.tsx @@ -304,6 +304,34 @@ describe('NativeChatQuestionCard', () => { ]) }) + it('moves between questions with Left and Right from an option or step tab, stopping at the ends', () => { + vi.useFakeTimers() + container.setAttribute('data-native-chat-root', 'true') + render(indentThenFruits, vi.fn(), true, true) + clickOption('Spaces') + + expect(pressKey({ key: 'ArrowLeft' }, document.activeElement!).defaultPrevented).toBe(false) + expect(cardTitle()).toBe('Do you prefer tabs or spaces?') + expect(pressKey({ key: 'ArrowRight' }, document.activeElement!).defaultPrevented).toBe(true) + expect(cardTitle()).toBe('Which fruit?') + expect(document.activeElement?.textContent).toContain('Apple') + expect(pressKey({ key: 'ArrowRight' }, document.activeElement!).defaultPrevented).toBe(false) + expect(cardTitle()).toBe('Which fruit?') + + const cancel = container.querySelector('button[aria-label="Cancel"]')! + act(() => cancel.focus()) + expect(pressKey({ key: 'ArrowLeft' }, cancel).defaultPrevented).toBe(false) + expect(cardTitle()).toBe('Which fruit?') + + const stepTab = [...container.querySelectorAll('button')].find((b) => + b.textContent?.includes('Step 1') + )! + act(() => stepTab.focus()) + expect(pressKey({ key: 'ArrowLeft' }, stepTab).defaultPrevented).toBe(true) + expect(cardTitle()).toBe('Do you prefer tabs or spaces?') + expect(optionPressed('Spaces')).toBe('true') + }) + it('sends nothing when the question is cancelled before the pick moves on', () => { vi.useFakeTimers() const onAnswer = vi.fn() diff --git a/src/renderer/src/components/native-chat/NativeChatQuestionCard.tsx b/src/renderer/src/components/native-chat/NativeChatQuestionCard.tsx index 5e1c635fb1b..fbfd5651bc6 100644 --- a/src/renderer/src/components/native-chat/NativeChatQuestionCard.tsx +++ b/src/renderer/src/components/native-chat/NativeChatQuestionCard.tsx @@ -166,7 +166,6 @@ export function NativeChatQuestionCard({ autoAdvance.schedule(() => advanceOrSubmit(next, otherText)) } } - useNativeChatQuestionKeys(cardRef, shouldFocus && !isSubmitting, q.options.length, pickOption) const confirm = (fromKeyboard = false): void => { autoAdvance.cancel() @@ -184,6 +183,20 @@ export function NativeChatQuestionCard({ autoAdvance.cancel() setIndex(questionIndex) } + useNativeChatQuestionKeys( + cardRef, + shouldFocus && !isSubmitting, + q.options.length, + pickOption, + (delta) => { + const next = index + delta + if (next < 0 || next >= total) { + return false + } + goTo(next) + return true + } + ) const cancel = (): void => { autoAdvance.cancel() onCancel() @@ -226,6 +239,7 @@ export function NativeChatQuestionCard({