fix(webview-ui): accessible DisclosureHeader and refactor error/diff_error headers (aria, keyboard, chevron consistency)

This commit is contained in:
Merge Resolver 2025-08-21 17:08:26 -06:00
parent eb4bd66aff
commit d3031380a3
2 changed files with 115 additions and 105 deletions

View file

@ -33,7 +33,8 @@ import MarkdownBlock from "../common/MarkdownBlock"
import { ReasoningBlock } from "./ReasoningBlock"
import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
import { IconButton } from "./IconButton"
import { DisclosureHeader } from "./DisclosureHeader"
import { Mention } from "./Mention"
import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
@ -864,60 +865,29 @@ export const ChatRowContent = ({
overflow: "hidden",
marginBottom: "8px",
}}>
<div
style={{
borderBottom: isDiffErrorExpanded
? "1px solid var(--vscode-editorGroup-border)"
: "none",
fontWeight: "normal",
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-editor-foreground)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
<DisclosureHeader
contentId={`diff-error-${message.ts}`}
iconClass="codicon-warning"
iconStyle={{ color: "var(--vscode-editorWarning-foreground)", opacity: 0.8 }}
title={<span style={{ fontWeight: "bold" }}>{t("chat:diffError.title")}</span>}
expanded={isDiffErrorExpanded}
onToggle={() => setIsDiffErrorExpanded(!isDiffErrorExpanded)}
onCopy={() => {
copyWithFeedback(message.text || "").then((success) => {
if (success) {
setShowCopySuccess(true)
setTimeout(() => {
setShowCopySuccess(false)
}, 1000)
}
})
}}
onClick={() => setIsDiffErrorExpanded(!isDiffErrorExpanded)}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
flexGrow: 1,
}}>
<span
className="codicon codicon-warning"
style={{
color: "var(--vscode-editorWarning-foreground)",
opacity: 0.8,
fontSize: 16,
marginBottom: "-1.5px",
}}></span>
<span style={{ fontWeight: "bold" }}>{t("chat:diffError.title")}</span>
</div>
<div style={{ display: "flex", alignItems: "center" }}>
<IconButton
iconClass={showCopySuccess ? "codicon-check" : "codicon-copy"}
title={t("chat:codeblock.tooltips.copy_code")}
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
copyWithFeedback(message.text || "").then((success) => {
if (success) {
setShowCopySuccess(true)
setTimeout(() => {
setShowCopySuccess(false)
}, 1000)
}
})
}}
style={{ marginRight: "4px" }}
/>
<span
className={`codicon codicon-chevron-${isDiffErrorExpanded ? "up" : "down"}`}></span>
</div>
</div>
copyTitle={t("chat:codeblock.tooltips.copy_code")}
copyIconClass={showCopySuccess ? "codicon-check" : "codicon-copy"}
/>
{isDiffErrorExpanded && (
<div
id={`diff-error-${message.ts}`}
style={{
padding: "8px",
backgroundColor: "var(--vscode-editor-background)",
@ -1127,62 +1097,29 @@ export const ChatRowContent = ({
overflow: "hidden",
marginBottom: "8px",
}}>
<div
style={{
borderBottom: isErrorExpanded
? "1px solid var(--vscode-editorGroup-border)"
: "none",
fontWeight: "normal",
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-editor-foreground)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
<DisclosureHeader
contentId={`error-${message.ts}`}
iconClass="codicon-error"
iconStyle={{ color: "var(--vscode-errorForeground)", opacity: 0.8 }}
title={<span style={{ fontWeight: "bold" }}>{t("chat:error")}</span>}
expanded={isErrorExpanded}
onToggle={() => setIsErrorExpanded(!isErrorExpanded)}
onCopy={() => {
copyWithFeedback(message.text || "").then((success) => {
if (success) {
setShowErrorCopySuccess(true)
setTimeout(() => {
setShowErrorCopySuccess(false)
}, 1000)
}
})
}}
onClick={() => setIsErrorExpanded(!isErrorExpanded)}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
flexGrow: 1,
}}>
<span
className="codicon codicon-error"
style={{
color: "var(--vscode-errorForeground)",
opacity: 0.8,
fontSize: 16,
marginBottom: "-1.5px",
}}></span>
<span style={{ fontWeight: "bold", color: "var(--vscode-errorForeground)" }}>
{t("chat:error")}
</span>
</div>
<div style={{ display: "flex", alignItems: "center" }}>
<IconButton
iconClass={showErrorCopySuccess ? "codicon-check" : "codicon-copy"}
title={t("chat:codeblock.tooltips.copy_code")}
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
copyWithFeedback(message.text || "").then((success) => {
if (success) {
setShowErrorCopySuccess(true)
setTimeout(() => {
setShowErrorCopySuccess(false)
}, 1000)
}
})
}}
style={{ marginRight: "4px" }}
/>
<span
className={`codicon codicon-chevron-${isErrorExpanded ? "up" : "down"}`}></span>
</div>
</div>
copyTitle={t("chat:codeblock.tooltips.copy_code")}
copyIconClass={showErrorCopySuccess ? "codicon-check" : "codicon-copy"}
/>
{isErrorExpanded && (
<div
id={`error-${message.ts}`}
style={{
padding: "8px",
backgroundColor: "var(--vscode-editor-background)",

View file

@ -0,0 +1,73 @@
import React from "react"
import { cn } from "@/lib/utils"
import { IconButton } from "./IconButton"
interface DisclosureHeaderProps {
contentId: string
iconClass: string
iconStyle?: React.CSSProperties
title: React.ReactNode
expanded: boolean
onToggle: () => void
onCopy?: (e: React.MouseEvent) => void
copyTitle?: string
copyIconClass?: string // e.g. "codicon-copy" | "codicon-check"
className?: string
}
export const DisclosureHeader: React.FC<DisclosureHeaderProps> = ({
contentId,
iconClass,
iconStyle,
title,
expanded,
onToggle,
onCopy,
copyTitle,
copyIconClass,
className,
}) => {
return (
<div
className={cn("flex items-center justify-between", className)}
style={{
fontWeight: "normal",
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-editor-foreground)",
borderBottom: expanded ? "1px solid var(--vscode-editorGroup-border)" : "none",
}}>
<button
type="button"
aria-expanded={expanded}
aria-controls={contentId}
onClick={onToggle}
className={cn(
"flex items-center justify-between gap-2",
"bg-transparent border-0 p-0 m-0 cursor-pointer",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
)}
style={{ width: "100%", textAlign: "left" }}>
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
<span
className={cn("codicon", iconClass)}
style={{ fontSize: 16, marginBottom: "-1.5px", ...iconStyle }}
/>
<span>{title}</span>
</div>
<span className={`codicon codicon-chevron-${expanded ? "down" : "right"}`} />
</button>
{onCopy && (
<IconButton
iconClass={copyIconClass ?? "codicon-copy"}
title={copyTitle ?? "Copy"}
onClick={(e) => {
e.stopPropagation()
onCopy(e)
}}
style={{ marginLeft: 4 }}
/>
)}
</div>
)
}