diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index bc64a7e0c42..96be9dd7075 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -543,8 +543,12 @@ function ChatInterface({ initialQuery }: { initialQuery?: string }) { /> {isGeneratingAnswer ? ( stopGeneration()} className="group relative z-10 flex size-10 min-w-10 cursor-pointer items-center justify-center" > @@ -553,7 +557,7 @@ function ChatInterface({ initialQuery }: { initialQuery?: string }) { className="absolute inset-0 animate-spin" hoverEffect /> - + } content="Stop generating" /> diff --git a/apps/webapp/app/components/code/TSQLResultsTable.tsx b/apps/webapp/app/components/code/TSQLResultsTable.tsx index 6da3caee0df..c1a627b8db8 100644 --- a/apps/webapp/app/components/code/TSQLResultsTable.tsx +++ b/apps/webapp/app/components/code/TSQLResultsTable.tsx @@ -843,6 +843,37 @@ function CopyableCell({ const [isHovered, setIsHovered] = useState(false); const { copy, copied } = useCopy(value); + // The button (with its aria-label) always sits in the same position in the tree, wrapped by + // the same SimpleTooltip, so it is never unmounted/remounted on hover (which would drop + // keyboard focus). The tooltip is left uncontrolled so Radix opens it only when the pointer or + // keyboard focus is actually on the button, not whenever the pointer is anywhere in this + // virtualized grid's cell. `focus-visible:` (not `focus:`) ensures keyboard focus reveals the + // button without leaving it visible after a mouse click moves outside the cell. + const copyButton = ( + + ); + return (
setIsHovered(false)} > {children} - {isHovered && ( - { - e.stopPropagation(); - e.preventDefault(); - copy(); - }} - className="absolute right-1 top-1/2 z-10 flex -translate-y-1/2 cursor-pointer" - > - - {copied ? ( - - ) : ( - - )} - - } - content={copied ? "Copied!" : "Copy"} - disableHoverableContent - /> - - )} +
); } diff --git a/apps/webapp/app/components/dashboard-agent/tooltip-accessible-name.test.ts b/apps/webapp/app/components/dashboard-agent/tooltip-accessible-name.test.ts index 361d20c456e..5c0379b1b3e 100644 --- a/apps/webapp/app/components/dashboard-agent/tooltip-accessible-name.test.ts +++ b/apps/webapp/app/components/dashboard-agent/tooltip-accessible-name.test.ts @@ -60,7 +60,6 @@ const NO_AS_CHILD_BASELINE = new Set([ "app/components/GitMetadata.tsx::LinkButton", "app/components/code/TSQLResultsTable.tsx::TextLink", "app/components/integrations/VercelLink.tsx::LinkButton", - "app/components/primitives/CopyButton.tsx::Button", "app/components/runs/v3/RunTag.tsx::Link", "app/components/runs/v3/TaskRunsTable.tsx::DialogTrigger", "app/routes/account.tokens/route.tsx::DialogTrigger", @@ -90,6 +89,16 @@ function attrOf(node: JsxNode, name: string) { return open.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText() === name); } +function hasStaticTrueAttribute(node: JsxNode, name: string): boolean { + const attribute = attrOf(node, name); + if (!attribute || !ts.isJsxAttribute(attribute)) return false; + if (!attribute.initializer) return true; + return ( + ts.isJsxExpression(attribute.initializer) && + attribute.initializer.expression?.kind === ts.SyntaxKind.TrueKeyword + ); +} + /** Text anywhere under the element, ignoring an expression that can render nothing. */ function hasText(node: TsNode): boolean { if (!ts.isJsxElement(node)) return false; @@ -179,7 +188,7 @@ function scanFile(file: string, relative: string): Violation[] { const initializer = buttonAttr && ts.isJsxAttribute(buttonAttr) ? buttonAttr.initializer : undefined; if (initializer && ts.isJsxExpression(initializer)) { - const asChild = !!attrOf(node, "asChild"); + const asChild = hasStaticTrueAttribute(node, "asChild"); for (const trigger of resolve(initializer.expression).flatMap(triggersIn)) { const named = !!attrOf(trigger, "aria-label") || diff --git a/apps/webapp/app/components/primitives/ClipboardField.tsx b/apps/webapp/app/components/primitives/ClipboardField.tsx index 622c5bde430..73facf3bcc0 100644 --- a/apps/webapp/app/components/primitives/ClipboardField.tsx +++ b/apps/webapp/app/components/primitives/ClipboardField.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useId, useState } from "react"; import { cn } from "~/utils/cn"; import { CopyButton } from "./CopyButton"; @@ -116,7 +116,7 @@ export function ClipboardField({ fullWidth = true, }: ClipboardFieldProps) { const [isSecure, setIsSecure] = useState(secure !== undefined && secure); - const inputIcon = useRef(null); + const inputId = useId(); const { container, input, buttonVariant, button, size } = variants[variant]; useEffect(() => { @@ -128,16 +128,13 @@ export function ClipboardField({ return ( {icon && ( - inputIcon.current && inputIcon.current.focus()} - className="flex items-center pl-1" - > + + )} - {copied ? ( - - ) : ( - - )} + if (variant === "button") { + return ( + + - ) : ( - ); + } - if (!showTooltip) return {button}; + const iconButton = ( + + ); + + if (!showTooltip) return {iconButton}; return ( e.stopPropagation()} className={cn( "ml-1 flex size-6 items-center justify-center rounded border border-border-bright bg-background-hover", asChild && "p-1", @@ -53,7 +57,7 @@ export function CopyableText({ ) : ( )} - + ); return ( @@ -72,24 +76,23 @@ export function CopyableText({ {value} e.stopPropagation()} className={cn( - "absolute top-0 z-10 size-6 font-sans", + "absolute top-0 z-10 flex size-6 font-sans transition-opacity has-focus-visible:pointer-events-auto has-focus-visible:opacity-100", // Truncated values reserve a right gutter, so the button sits inside it truncate ? "right-0" : "-right-6", - isHovered ? "flex" : "hidden" + isHovered ? "opacity-100" : "pointer-events-none opacity-0" )} > {hideTooltip ? ( iconButton ) : ( )} diff --git a/apps/webapp/app/components/primitives/Table.tsx b/apps/webapp/app/components/primitives/Table.tsx index 9ade6c34bc2..5eaf30f89e9 100644 --- a/apps/webapp/app/components/primitives/Table.tsx +++ b/apps/webapp/app/components/primitives/Table.tsx @@ -478,6 +478,37 @@ export const CopyableTableCell = forwardRef { + e.stopPropagation(); + e.preventDefault(); + copy(); + }} + className={cn( + "absolute -right-2 top-1/2 z-10 flex size-6 -translate-y-1/2 items-center justify-center rounded border border-border-bright bg-background-hover transition-opacity focus-visible:pointer-events-auto focus-visible:opacity-100", + isHovered ? "opacity-100" : "pointer-events-none opacity-0", + copied + ? "text-green-500" + : "text-text-dimmed hover:border-border-bright hover:bg-background-raised hover:text-text-bright" + )} + > + {copied ? ( + + ) : ( + + )} + + ); + return (
setIsHovered(false)} > {children} - {isHovered && ( - { - e.stopPropagation(); - e.preventDefault(); - copy(); - }} - className="absolute -right-2 top-1/2 z-10 flex -translate-y-1/2 cursor-pointer" - > - - {copied ? ( - - ) : ( - - )} - - } - content={copied ? "Copied!" : "Copy"} - disableHoverableContent - /> - - )} +
); diff --git a/apps/webapp/app/components/runs/v3/RunTag.tsx b/apps/webapp/app/components/runs/v3/RunTag.tsx index 16d2333632e..379c89b073b 100644 --- a/apps/webapp/app/components/runs/v3/RunTag.tsx +++ b/apps/webapp/app/components/runs/v3/RunTag.tsx @@ -113,13 +113,17 @@ function CopyButton({ textToCopy, isHovered }: { textToCopy: string; isHovered: return ( e.stopPropagation()} className={cn( - "absolute -right-6 top-0 z-10 size-6 items-center justify-center rounded-r-sm border-y border-r border-border-bright bg-background-hover", - isHovered ? "flex" : "hidden", + "absolute -right-6 top-0 z-10 flex size-6 items-center justify-center rounded-r-sm border-y border-r border-border-bright bg-background-hover transition-opacity focus-visible:pointer-events-auto focus-visible:opacity-100", + isHovered ? "opacity-100" : "pointer-events-none opacity-0", copied ? "text-green-500" : "text-text-dimmed hover:border-border-bright hover:bg-background-raised hover:text-text-bright" @@ -130,7 +134,7 @@ function CopyButton({ textToCopy, isHovered }: { textToCopy: string; isHovered: ) : ( )} - + } content={copied ? "Copied!" : "Copy tag"} disableHoverableContent @@ -158,18 +162,22 @@ function DeleteButton({ return ( e.stopPropagation()} className={cn( - "absolute -right-6 top-0 z-10 size-6 items-center justify-center rounded-r-sm border-y border-r border-border-bright bg-background-hover", - isHovered ? "flex" : "hidden", + "absolute -right-6 top-0 z-10 flex size-6 items-center justify-center rounded-r-sm border-y border-r border-border-bright bg-background-hover transition-opacity focus-visible:pointer-events-auto focus-visible:opacity-100", + isHovered ? "opacity-100" : "pointer-events-none opacity-0", "text-text-dimmed hover:border-border-bright hover:bg-background-raised hover:text-rose-400" )} > - + } content="Remove tag" disableHoverableContent