fix: preserve focus across rectification turns

This commit is contained in:
Jesse_Chen
2026-07-19 11:04:25 +08:00
parent 3c1c60785b
commit 04e4ba621a
5 changed files with 30 additions and 161 deletions
@@ -23,7 +23,6 @@ export function BirthTimeCandidateResult({ journey, controller }: CandidateResul
if (!result && action.kind === "present_low_result") {
return (
<div className="birth-time-candidate-result" aria-live="polite">
<p className="birth-time-assessment-unavailable"></p>
<p className="birth-time-evidence-boundary"><span className="phrase-nowrap">使</span></p>
{terminalPath && <NewAssessmentAction controller={controller} />}
</div>
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import {
choiceQuestionGroups,
choiceSelectionIntent,
@@ -43,6 +43,12 @@ export function BirthTimeQuestionProgress({ progress }: {
export function BirthTimeChoiceQuestion(props: ChoiceQuestionProps) {
const groups = choiceQuestionGroups(props.question);
const [selectedId, setSelectedId] = useState("");
const firstChoiceRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
if (!props.pending && (props.progress.answeredCount > 0 || props.error)) {
firstChoiceRef.current?.focus();
}
}, [props.error, props.pending, props.progress.answeredCount, props.question.questionId]);
const select = (option: PublicDynamicChoiceQuestion["options"][number]) => {
const intent = choiceSelectionIntent(option);
setSelectedId(intent.optionId);
@@ -55,12 +61,13 @@ export function BirthTimeChoiceQuestion(props: ChoiceQuestionProps) {
<fieldset className="birth-time-choice-question" disabled={props.pending}>
<legend>{props.question.prompt}</legend>
<div className="birth-time-primary-choices">
{groups.primary.map((option) => (
{groups.primary.map((option, index) => (
<button
className="birth-time-choice-option is-primary"
data-selected={selectedId === option.optionId}
key={option.optionId}
onClick={() => select(option)}
ref={index === 0 ? firstChoiceRef : undefined}
type="button"
>
{option.label}
@@ -89,6 +96,12 @@ export function BirthTimeChoiceQuestion(props: ChoiceQuestionProps) {
export function BirthTimeUnmatchedClarification(props: ClarificationProps) {
const [note, setNote] = useState("");
const reframeRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
if (!props.pending && (props.progress.answeredCount > 0 || props.error)) {
reframeRef.current?.focus();
}
}, [props.error, props.pending, props.progress.answeredCount]);
return (
<div className="birth-time-choice-surface" aria-busy={props.pending}>
<BirthTimeQuestionProgress progress={props.progress} />
@@ -104,7 +117,7 @@ export function BirthTimeUnmatchedClarification(props: ClarificationProps) {
/>
</label>
<div className="birth-time-reframe-actions">
<button className="button-secondary birth-time-guided-action" onClick={() => props.onReframe("")} type="button"></button>
<button className="button-secondary birth-time-guided-action" onClick={() => props.onReframe("")} ref={reframeRef} type="button"></button>
<button className="button-primary birth-time-guided-action" onClick={() => props.onReframe(normalizeUnmatchedNote(note))} type="button"></button>
</div>
</fieldset>
@@ -1,5 +1,6 @@
"use client";
import { useEffect, useRef } from "react";
import { BirthTimeCandidateResult } from "@/components/birth-time-candidate-result";
import {
BirthTimeChoiceQuestion,
@@ -50,6 +51,17 @@ function statusCopy(action: DynamicNextAction): string {
}
export function BirthTimeRectification(props: BirthTimeRectificationProps) {
const assessmentHeadingRef = useRef<HTMLHeadingElement | null>(null);
const previousTurnVersion = useRef(props.journey.turnVersion);
useEffect(() => {
const changed = previousTurnVersion.current !== props.journey.turnVersion;
previousTurnVersion.current = props.journey.turnVersion;
if (!changed || props.journey.journeyProtocol !== "dynamic-choice-v2") return;
const action = props.journey.nextAction;
if (action.kind !== "ask_dynamic_choice" && action.kind !== "clarify_unmatched_answer") {
assessmentHeadingRef.current?.focus();
}
}, [props.journey]);
if (props.journey.journeyProtocol !== "dynamic-choice-v2") {
return <BirthTimeLegacyRectification {...props} journey={props.journey} />;
}
@@ -65,7 +77,7 @@ export function BirthTimeRectification(props: BirthTimeRectificationProps) {
return (
<section className="birth-time-rectification onboarding-card" aria-labelledby="birth-time-assessment-title">
<div className="birth-time-assessment-heading">
<div><span></span><h2 id="birth-time-assessment-title">{heading.title}</h2></div>
<div><span></span><h2 id="birth-time-assessment-title" ref={assessmentHeadingRef} tabIndex={-1}>{heading.title}</h2></div>
<span className="birth-time-status-badge">{heading.badge}</span>
</div>
{showsProgress && <BirthTimeQuestionProgress progress={props.journey.progress} />}