Skip to content

Commit 2856a6e

Browse files
committed
fix(webapp): preserve focus across copy tooltips
1 parent cccebe6 commit 2856a6e

3 files changed

Lines changed: 33 additions & 33 deletions

File tree

apps/webapp/app/components/code/TSQLResultsTable.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -843,10 +843,11 @@ function CopyableCell({
843843
const [isHovered, setIsHovered] = useState(false);
844844
const { copy, copied } = useCopy(value);
845845

846-
// The button (with its aria-label) stays mounted at all times so keyboard users can always
847-
// reach it. The Radix tooltip subtree is comparatively expensive to keep alive for every
848-
// visible cell of a virtualized grid, so it's only mounted while the cell is hovered - the
849-
// tooltip is a hover affordance, not required for the button's accessible name.
846+
// The button (with its aria-label) always sits in the same position in the tree, wrapped by
847+
// the same SimpleTooltip, so it is never unmounted/remounted on hover (which would drop
848+
// keyboard focus). Only the tooltip's open state, not its mounted tree, follows hover; Radix
849+
// still only renders the (comparatively expensive) tooltip content into the DOM of this
850+
// virtualized grid while `open` is true.
850851
const copyButton = (
851852
<button
852853
type="button"
@@ -885,17 +886,14 @@ function CopyableCell({
885886
onMouseLeave={() => setIsHovered(false)}
886887
>
887888
<span className="flex items-center truncate">{children}</span>
888-
{isHovered ? (
889-
<SimpleTooltip
890-
asChild
891-
tabbable
892-
button={copyButton}
893-
content={copied ? "Copied!" : "Copy"}
894-
disableHoverableContent
895-
/>
896-
) : (
897-
copyButton
898-
)}
889+
<SimpleTooltip
890+
asChild
891+
tabbable
892+
open={isHovered}
893+
button={copyButton}
894+
content={copied ? "Copied!" : "Copy"}
895+
disableHoverableContent
896+
/>
899897
</div>
900898
);
901899
}

apps/webapp/app/components/primitives/ClipboardField.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState } from "react";
1+
import { useEffect, useId, useState } from "react";
22
import { cn } from "~/utils/cn";
33
import { CopyButton } from "./CopyButton";
44

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

122122
useEffect(() => {
@@ -127,10 +127,14 @@ export function ClipboardField({
127127

128128
return (
129129
<span className={cn(container, fullWidth ? "w-full" : "max-w-fit", className)}>
130-
{icon && <span className="flex items-center pl-1">{icon}</span>}
130+
{icon && (
131+
<label htmlFor={inputId} className="flex items-center pl-1">
132+
{icon}
133+
</label>
134+
)}
131135
<input
136+
id={inputId}
132137
type="text"
133-
ref={inputIcon}
134138
value={isSecure ? maskedValue : value}
135139
readOnly={true}
136140
className={cn(

apps/webapp/app/components/primitives/Table.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -478,9 +478,10 @@ export const CopyableTableCell = forwardRef<HTMLTableCellElement, CopyableTableC
478478
const [isHovered, setIsHovered] = useState(false);
479479
const { copy, copied } = useCopy(value);
480480

481-
// The button (with its aria-label) stays mounted at all times so keyboard users can always
482-
// reach it. The Radix tooltip subtree is only mounted while the cell is hovered - the
483-
// tooltip is a hover affordance, not required for the button's accessible name.
481+
// The button (with its aria-label) always sits in the same position in the tree, wrapped by
482+
// the same SimpleTooltip, so it is never unmounted/remounted on hover (which would drop
483+
// keyboard focus). Only the tooltip's open state, not its mounted tree, follows hover; Radix
484+
// still only renders the tooltip content into the DOM while `open` is true.
484485
const copyButton = (
485486
<button
486487
type="button"
@@ -514,17 +515,14 @@ export const CopyableTableCell = forwardRef<HTMLTableCellElement, CopyableTableC
514515
onMouseLeave={() => setIsHovered(false)}
515516
>
516517
{children}
517-
{isHovered ? (
518-
<SimpleTooltip
519-
asChild
520-
tabbable
521-
button={copyButton}
522-
content={copied ? "Copied!" : "Copy"}
523-
disableHoverableContent
524-
/>
525-
) : (
526-
copyButton
527-
)}
518+
<SimpleTooltip
519+
asChild
520+
tabbable
521+
open={isHovered}
522+
button={copyButton}
523+
content={copied ? "Copied!" : "Copy"}
524+
disableHoverableContent
525+
/>
528526
</div>
529527
</TableCell>
530528
);

0 commit comments

Comments
 (0)