fix(rectification): keep the choice selected state inside the button
The outer focus ring overflowed the message bubble and stacked on the existing selected border. Draw keyboard focus inset and skip auto-focusing the first option. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7617,3 +7617,19 @@
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-495 | 选择题选中项外圈粗描边溢出气泡
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-02
|
||||
- 最近更新:2026-09-02
|
||||
- 影响面:`.birth-time-choice-option:focus-visible`、嵌入 `RectificationChoiceCard`、`.message-bubble`
|
||||
- 用户现象:点选 A 后(或新题自动聚焦第一项时),选项外侧出现一圈粗的红棕色描边,圆角对不齐,看起来像边框溢出。已有选中底色和边框已经够用。
|
||||
- 触发条件:校正聊天里嵌入选择题;选项在 `.message-bubble { overflow: hidden }` 内。新题会 `focus()` 第一项,点击后按钮仍保持 `:focus-visible`。
|
||||
- 根因:`:focus-visible` 使用 `outline-offset: 2px`(会话页还有 `3px`),描边画在按钮外面,被气泡裁切。选中态 `border-color: var(--color-action)` 与焦点色相同,叠成双层粗边。
|
||||
- 修复:焦点环改为 `outline-offset: -2px` 画在按钮内。已选中项不再叠加 outline(高对比模式除外)。嵌入卡不再自动聚焦第一项。
|
||||
- 验证:`rectification-varga-style-copy` 锁 inset offset、选中无 outline、卡片不 `.focus()`。
|
||||
- 防复发:选择题焦点环不得使用正 `outline-offset`。选中态只保留 `data-selected` 的边框和底色。
|
||||
- 相关记录:BUG-490
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
.birth-time-choice-option:hover:not(:disabled) { border-color: var(--color-border-strong); background: var(--color-canvas-soft); }
|
||||
.birth-time-choice-option:active:not(:disabled) { transform: scale(.96); }
|
||||
.birth-time-choice-option[data-selected="true"] { border-color: var(--color-action); background: var(--color-action-soft); color: var(--color-ink); }
|
||||
.birth-time-choice-option:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
|
||||
.birth-time-choice-option:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
|
||||
.birth-time-choice-option[data-selected="true"]:focus-visible { outline: none; }
|
||||
@media (forced-colors: active) {
|
||||
.birth-time-choice-option[data-selected="true"]:focus-visible { outline: 2px solid Highlight; outline-offset: -2px; }
|
||||
}
|
||||
.birth-time-choice-question:disabled .birth-time-choice-option { cursor: wait; opacity: .62; }
|
||||
.birth-time-choice-pending { margin: 0; color: var(--color-action); font-size: var(--type-caption); }
|
||||
.birth-time-unmatched-note { display: grid; gap: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 600; }
|
||||
|
||||
@@ -930,6 +930,8 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.conversational-rectification > *, .conversational-rectification form, .conversational-rectification fieldset { min-width: 0; max-width: 100%; }
|
||||
.conversational-rectification button { min-height: 44px; max-width: 100%; overflow-wrap: anywhere; }
|
||||
.conversational-rectification :where(button, textarea):focus-visible { outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent); outline-offset: 3px; }
|
||||
.conversational-rectification .birth-time-choice-option:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
|
||||
.conversational-rectification .birth-time-choice-option[data-selected="true"]:focus-visible { outline: none; }
|
||||
.conversational-narrative { overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); box-shadow: var(--shadow-soft); }
|
||||
.conversational-narrative-body { padding: var(--space-4) var(--space-5); }
|
||||
.conversational-narrative .message-markdown { overflow-wrap: anywhere; color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.8; }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import type {
|
||||
ChoiceKey,
|
||||
RectificationChoiceCard as ChoiceCard,
|
||||
@@ -20,14 +20,8 @@ type RectificationChoiceCardProps = Readonly<{
|
||||
export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
|
||||
const [localSelected, setLocalSelected] = useState<ChoiceKey | "stop" | "">("");
|
||||
const selectedKey = props.selectedKey || localSelected;
|
||||
const firstChoiceRef = useRef<HTMLButtonElement | null>(null);
|
||||
const answered = Boolean(selectedKey);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.pending || answered) return;
|
||||
firstChoiceRef.current?.focus();
|
||||
}, [answered, props.card.question_id, props.pending]);
|
||||
|
||||
function select(key: ChoiceKey) {
|
||||
if (props.pending || props.disabled || selectedKey) return;
|
||||
setLocalSelected(key);
|
||||
@@ -58,10 +52,9 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
|
||||
: <legend>{props.card.prompt}</legend>}
|
||||
{!hidePrompt && props.card.why ? <p className="rectification-choice-why">{props.card.why}</p> : null}
|
||||
<div className="birth-time-primary-choices">
|
||||
{props.card.options.map((option, index) => (
|
||||
{props.card.options.map((option) => (
|
||||
<button
|
||||
key={option.key}
|
||||
ref={index === 0 ? firstChoiceRef : undefined}
|
||||
type="button"
|
||||
className="birth-time-choice-option is-primary"
|
||||
data-selected={selectedKey === option.key ? "true" : "false"}
|
||||
|
||||
@@ -353,3 +353,26 @@ test("embedded choice cards hide the duplicate legend; standalone cards keep the
|
||||
assert.match(card, /className="rectification-choice-why"/);
|
||||
assert.doesNotMatch(card, /choice-question-wrap|choice-stem-wrap|legend-spacer/);
|
||||
});
|
||||
|
||||
test("choice option focus ring stays inside the button and does not stack on the selected border", () => {
|
||||
const choiceCss = readFileSync(new URL("../src/app/birth-time-choice.css", import.meta.url), "utf8");
|
||||
const globals = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
const card = readFileSync(new URL("../src/components/rectification-choice-card.tsx", import.meta.url), "utf8");
|
||||
const focusRule = choiceCss.match(/\.birth-time-choice-option:focus-visible \{[^}]+\}/)?.[0] ?? "";
|
||||
const selectedFocusRule = choiceCss.match(
|
||||
/\.birth-time-choice-option\[data-selected="true"\]:focus-visible \{[^}]+\}/,
|
||||
)?.[0] ?? "";
|
||||
assert.match(focusRule, /outline-offset:\s*-2px/);
|
||||
assert.doesNotMatch(focusRule, /outline-offset:\s*[1-9]/);
|
||||
assert.match(selectedFocusRule, /outline:\s*none/);
|
||||
assert.match(
|
||||
globals,
|
||||
/\.conversational-rectification \.birth-time-choice-option:focus-visible \{[^}]*outline-offset:\s*-2px/,
|
||||
);
|
||||
assert.match(
|
||||
globals,
|
||||
/\.conversational-rectification \.birth-time-choice-option\[data-selected="true"\]:focus-visible \{[^}]*outline:\s*none/,
|
||||
);
|
||||
assert.doesNotMatch(card, /firstChoiceRef/);
|
||||
assert.doesNotMatch(card, /\.focus\(/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user