|
10 | 10 | */ |
11 | 11 | import { ArrowRight, Check, ChevronLeft, ChevronRight, X } from 'lucide-react'; |
12 | 12 | import { Outlined } from 'bisheng-icons'; |
13 | | -import { useEffect, useMemo, useRef, useState } from 'react'; |
| 13 | +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; |
14 | 14 | import { Textarea } from '~/components/ui'; |
15 | 15 | import { useLocalize } from '~/hooks'; |
16 | 16 | import { cn } from '~/utils'; |
@@ -168,6 +168,19 @@ function ClarifyCardInteractive({ data, disabled = false, onSubmit }: ClarifyCar |
168 | 168 | return () => window.removeEventListener('keydown', onKey); |
169 | 169 | }, []); |
170 | 170 |
|
| 171 | + // The custom-answer <textarea> is one reused DOM node across pages, and its |
| 172 | + // grown height lives in an inline style set on input. On page change that |
| 173 | + // stale height would leak into the next question (or a saved multi-line answer |
| 174 | + // would show clipped). Re-fit it to the CURRENT question's content whenever the |
| 175 | + // page changes — empty content collapses back to one row. |
| 176 | + const customTaRef = useRef<HTMLTextAreaElement>(null); |
| 177 | + useLayoutEffect(() => { |
| 178 | + const el = customTaRef.current; |
| 179 | + if (!el) return; |
| 180 | + el.style.height = 'auto'; |
| 181 | + el.style.height = `${el.scrollHeight}px`; |
| 182 | + }, [page]); |
| 183 | + |
171 | 184 | return ( |
172 | 185 | <div |
173 | 186 | className="my-3 w-full rounded-2xl border border-[#EEF2F6] bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.03)]" |
@@ -269,52 +282,69 @@ function ClarifyCardInteractive({ data, disabled = false, onSubmit }: ClarifyCar |
269 | 282 | <li> |
270 | 283 | <div |
271 | 284 | className={cn( |
272 | | - 'flex h-9 items-center gap-2 rounded-lg px-4 transition-all duration-200', |
| 285 | + // min-h (not fixed h) so the textarea can grow past one |
| 286 | + // line when the user inserts Shift+Enter newlines. |
| 287 | + // items-start: once grown, the number stays pinned to the |
| 288 | + // FIRST line (top) instead of centering on the tall box. |
| 289 | + // py-2 (8px) + a 20px line = 36px (h-9) when single-line. |
| 290 | + 'flex min-h-9 items-start gap-2 rounded-lg px-4 py-2 transition-all duration-200', |
273 | 291 | // No box by default (matches the other options); the |
274 | 292 | // input-box background only appears once it's active. |
275 | 293 | customSelected ? 'bg-[#EEE]' : 'hover:bg-gray-50/80', |
276 | 294 | )} |
277 | 295 | > |
278 | | - <span className="shrink-0 text-sm font-medium text-[#8C8C8C]"> |
| 296 | + <span className="shrink-0 text-sm font-medium leading-5 text-[#8C8C8C]"> |
279 | 297 | {q.options.length + 1}. |
280 | 298 | </span> |
281 | | - <input |
282 | | - type="text" |
| 299 | + <textarea |
| 300 | + ref={customTaRef} |
| 301 | + rows={1} |
283 | 302 | disabled={disabled || submitted} |
284 | 303 | value={customText[q.id] || ''} |
285 | | - placeholder={localize('com_linsight_clarify_custom')} |
| 304 | + // Default placeholder is the short "自行输入"; once the row is |
| 305 | + // highlighted (customSelected, i.e. focused/active) it grows the |
| 306 | + // Shift+Enter hint — matching when the bg-[#EEE] highlight shows. |
| 307 | + placeholder={localize( |
| 308 | + customSelected |
| 309 | + ? 'com_linsight_clarify_custom_active' |
| 310 | + : 'com_linsight_clarify_custom', |
| 311 | + )} |
286 | 312 | onFocus={() => !customSelected && handleSelect(q, CUSTOM_KEY)} |
287 | 313 | onChange={(e) => { |
288 | 314 | setCustomText((prev) => ({ ...prev, [q.id]: e.target.value })); |
289 | 315 | if (!customSelected) handleSelect(q, CUSTOM_KEY); |
| 316 | + // Auto-grow to fit its content (Shift+Enter newlines). |
| 317 | + e.currentTarget.style.height = 'auto'; |
| 318 | + e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`; |
290 | 319 | }} |
291 | 320 | onKeyDown={(e) => { |
292 | | - // Enter confirms the typed custom answer and advances to |
293 | | - // the next question (both single- AND multi-select), |
294 | | - // matching the "下一题 ↵" hint — the input is focused, so |
295 | | - // the window-level Enter handler is bypassed and this is the |
296 | | - // only place that can advance. Guard the IME composition |
297 | | - // Enter so committing pinyin doesn't skip the question. |
| 321 | + // Enter (without Shift) confirms the typed custom answer and |
| 322 | + // advances to the next question (both single- AND |
| 323 | + // multi-select), matching the "下一题 ↵" hint — the field is |
| 324 | + // focused, so the window-level Enter handler is bypassed and |
| 325 | + // this is the only place that can advance. Shift+Enter falls |
| 326 | + // through to the textarea's native newline. Guard the IME |
| 327 | + // composition Enter so committing pinyin doesn't skip. |
298 | 328 | if ( |
299 | 329 | e.key === 'Enter' && |
| 330 | + !e.shiftKey && |
300 | 331 | !e.nativeEvent.isComposing && |
301 | 332 | customText[q.id]?.trim() |
302 | 333 | ) { |
303 | 334 | e.preventDefault(); |
304 | 335 | handleConfirm(); |
305 | 336 | } |
306 | 337 | }} |
307 | | - className={cn( |
308 | | - 'flex-1 bg-transparent text-sm outline-none placeholder:text-[#8C8C8C]', |
309 | | - customSelected ? 'text-[#1A1A1A] font-medium' : 'text-[#1A1A1A]', |
310 | | - )} |
| 338 | + className="flex-1 resize-none border-0 bg-transparent p-0 text-sm font-normal leading-5 text-[#1A1A1A] outline-none placeholder:text-[#8C8C8C]" |
311 | 339 | /> |
312 | 340 | {!q.multiple && customSelected && customText[q.id]?.trim() && ( |
313 | 341 | <button |
314 | 342 | type="button" |
315 | 343 | disabled={disabled || submitted} |
316 | 344 | onClick={handleConfirm} |
317 | | - className="flex shrink-0 items-center gap-1 text-sm font-medium text-[#8C8C8C] hover:text-[#212121] disabled:opacity-50 transition-colors" |
| 345 | + // self-end pins 确定 to the bottom-right of the (possibly |
| 346 | + // grown) box, while the number stays top-left. |
| 347 | + className="flex shrink-0 self-end items-center gap-1 text-sm font-medium text-[#8C8C8C] hover:text-[#212121] disabled:opacity-50 transition-colors" |
318 | 348 | > |
319 | 349 | {localize('com_linsight_clarify_submit')} |
320 | 350 | <Outlined.CornerDownLeft size={14} className="shrink-0" /> |
|
0 commit comments