Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/webapp/app/components/AskAI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,12 @@ function ChatInterface({ initialQuery }: { initialQuery?: string }) {
/>
{isGeneratingAnswer ? (
<SimpleTooltip
asChild
tabbable
button={
<span
<button
type="button"
aria-label="Stop generating"
onClick={() => stopGeneration()}
className="group relative z-10 flex size-10 min-w-10 cursor-pointer items-center justify-center"
>
Expand All @@ -553,7 +557,7 @@ function ChatInterface({ initialQuery }: { initialQuery?: string }) {
className="absolute inset-0 animate-spin"
hoverEffect
/>
</span>
</button>
}
content="Stop generating"
/>
Expand Down
69 changes: 38 additions & 31 deletions apps/webapp/app/components/code/TSQLResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<button
type="button"
aria-label={copied ? "Copied" : "Copy"}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
copy();
}}
className={cn(
"absolute right-1 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"
)}
Comment thread
carderne marked this conversation as resolved.
>
{copied ? (
<ClipboardCheckIcon className="size-3.5" />
) : (
<ClipboardIcon className="size-3.5" />
)}
</button>
);
Comment thread
carderne marked this conversation as resolved.

return (
<div
className={cn(
Expand All @@ -856,37 +887,13 @@ function CopyableCell({
onMouseLeave={() => setIsHovered(false)}
>
<span className="flex items-center truncate">{children}</span>
{isHovered && (
<span
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
copy();
}}
className="absolute right-1 top-1/2 z-10 flex -translate-y-1/2 cursor-pointer"
>
<SimpleTooltip
button={
<span
className={cn(
"flex size-6 items-center justify-center rounded border border-border-bright bg-background-hover",
copied
? "text-green-500"
: "text-text-dimmed hover:border-border-bright hover:bg-background-raised hover:text-text-bright"
)}
>
{copied ? (
<ClipboardCheckIcon className="size-3.5" />
) : (
<ClipboardIcon className="size-3.5" />
)}
</span>
}
content={copied ? "Copied!" : "Copy"}
disableHoverableContent
/>
</span>
)}
<SimpleTooltip
asChild
tabbable
button={copyButton}
content={copied ? "Copied!" : "Copy"}
disableHoverableContent
/>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
carderne marked this conversation as resolved.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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") ||
Expand Down
13 changes: 5 additions & 8 deletions apps/webapp/app/components/primitives/ClipboardField.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -116,7 +116,7 @@ export function ClipboardField({
fullWidth = true,
}: ClipboardFieldProps) {
const [isSecure, setIsSecure] = useState(secure !== undefined && secure);
const inputIcon = useRef<HTMLInputElement>(null);
const inputId = useId();
const { container, input, buttonVariant, button, size } = variants[variant];

useEffect(() => {
Expand All @@ -128,16 +128,13 @@ export function ClipboardField({
return (
<span className={cn(container, fullWidth ? "w-full" : "max-w-fit", className)}>
{icon && (
<span
onClick={() => inputIcon.current && inputIcon.current.focus()}
className="flex items-center pl-1"
>
<label htmlFor={inputId} className="flex items-center pl-1">
{icon}
</span>
</label>
Comment thread
carderne marked this conversation as resolved.
)}
<input
id={inputId}
type="text"
ref={inputIcon}
value={isSecure ? maskedValue : value}
readOnly={true}
className={cn(
Expand Down
101 changes: 56 additions & 45 deletions apps/webapp/app/components/primitives/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,58 +44,69 @@ export function CopyButton({

const { icon: iconSize, button: buttonSize } = sizes[size];

const button =
variant === "icon" ? (
<span
onClick={copy}
className={cn(
buttonSize,
"flex shrink-0 items-center justify-center rounded border border-border-bright bg-background-hover",
copied
? "text-green-500"
: "text-text-dimmed hover:border-border-bright hover:bg-background-raised hover:text-text-bright",
buttonClassName
)}
>
{copied ? (
<ClipboardCheckIcon className={iconSize} />
) : (
<ClipboardIcon className={iconSize} />
)}
if (variant === "button") {
return (
<span className={className}>
<Button
variant={`${buttonVariant}/${size === "extra-small" ? "small" : size}`}
onClick={copy}
className={cn("shrink-0", buttonClassName)}
tooltip={showTooltip ? (copied ? "Copied!" : "Copy") : undefined}
Comment thread
carderne marked this conversation as resolved.
aria-label={children ? undefined : copied ? "Copied" : "Copy"}
Comment thread
carderne marked this conversation as resolved.
LeadingIcon={
copied ? (
<ClipboardCheckIcon
className={cn(
iconSize,
buttonVariant === "primary" ? "text-background-dimmed" : "text-green-500"
)}
/>
) : (
<ClipboardIcon
className={cn(
iconSize,
buttonVariant === "primary" ? "text-background-dimmed" : "text-text-dimmed"
)}
/>
)
}
>
{children}
</Button>
</span>
) : (
<Button
variant={`${buttonVariant}/${size === "extra-small" ? "small" : size}`}
onClick={copy}
className={cn("shrink-0", buttonClassName)}
LeadingIcon={
copied ? (
<ClipboardCheckIcon
className={cn(
iconSize,
buttonVariant === "primary" ? "text-background-dimmed" : "text-green-500"
)}
/>
) : (
<ClipboardIcon
className={cn(
iconSize,
buttonVariant === "primary" ? "text-background-dimmed" : "text-text-dimmed"
)}
/>
)
}
>
{children}
</Button>
);
}

if (!showTooltip) return <span className={className}>{button}</span>;
const iconButton = (
<button
type="button"
aria-label={copied ? "Copied" : "Copy"}
onClick={copy}
className={cn(
buttonSize,
"flex shrink-0 items-center justify-center rounded border border-border-bright bg-background-hover",
copied
? "text-green-500"
: "text-text-dimmed hover:border-border-bright hover:bg-background-raised hover:text-text-bright",
buttonClassName
)}
>
{copied ? (
<ClipboardCheckIcon className={iconSize} />
) : (
<ClipboardIcon className={iconSize} />
)}
</button>
);

if (!showTooltip) return <span className={className}>{iconButton}</span>;

return (
<span className={className}>
<SimpleTooltip
button={button}
asChild
Comment thread
carderne marked this conversation as resolved.
tabbable
button={iconButton}
content={copied ? "Copied!" : "Copy"}
Comment thread
carderne marked this conversation as resolved.
className="font-sans"
disableHoverableContent
Expand Down
17 changes: 10 additions & 7 deletions apps/webapp/app/components/primitives/CopyableText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export function CopyableText({

if (resolvedVariant === "icon-right") {
const iconButton = (
<span
<button
type="button"
aria-label={copied ? "Copied" : "Copy"}
onClick={copy}
onMouseDown={(e) => e.stopPropagation()}
Comment thread
carderne marked this conversation as resolved.
className={cn(
"ml-1 flex size-6 items-center justify-center rounded border border-border-bright bg-background-hover",
asChild && "p-1",
Expand All @@ -53,7 +57,7 @@ export function CopyableText({
) : (
<ClipboardIcon className="size-3.5" />
)}
</span>
</button>
);

return (
Expand All @@ -72,24 +76,23 @@ export function CopyableText({
{value}
</span>
<span
onClick={copy}
onMouseDown={(e) => 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
) : (
<SimpleTooltip
asChild
tabbable
button={iconButton}
content={copied ? "Copied!" : "Copy"}
className="font-sans"
disableHoverableContent
asChild={asChild}
/>
)}
</span>
Expand Down
Loading
Loading