Skip to content
Open
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
71 changes: 44 additions & 27 deletions src/internal/components/chart-legend/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import clsx from "clsx";

import {
circleIndex,
getAllFocusables,
handleKey,
KeyCode,
SingleTabStopNavigationAPI,
Expand Down Expand Up @@ -70,6 +71,7 @@ export const ChartLegend = ({
const elementsByIndexRef = useRef<Record<number, HTMLElement>>([]);
const elementsByIdRef = useRef<Record<string, HTMLElement>>({});
const tooltipRef = useRef<HTMLElement>(null);
const tooltipWrapperRef = useRef<HTMLDivElement>(null);
const highlightControl = useMemo(() => new DebouncedCall(), []);
const scrollIntoViewControl = useMemo(() => new DebouncedCall(), []);
const [selectedIndex, setSelectedIndex] = useState<number>(0);
Expand Down Expand Up @@ -103,7 +105,8 @@ export const ChartLegend = ({
return () => {
document.removeEventListener("keydown", onDocumentKeyDown, true);
};
}, [items, tooltipItemId, hideTooltip]);
}, [tooltipItemId, hideTooltip]);

const isMouseInContainer = useRef<boolean>(false);

// Scrolling to the highlighted legend item.
Expand Down Expand Up @@ -147,8 +150,13 @@ export const ChartLegend = ({
function onBlur(event: React.FocusEvent) {
navigationAPI.current!.updateFocusTarget();

// Hide tooltip and clear highlight unless focus moves inside tooltip;
if (tooltipRef.current && event.relatedTarget && !tooltipRef.current.contains(event.relatedTarget)) {
// Hide tooltip and clear highlight unless focus moves inside the tooltip wrapper
// (which contains the tooltip itself and its entry/exit tab traps).
const next = event.relatedTarget as Node | null;
if (next && tooltipWrapperRef.current?.contains(next)) {
return;
}
if (next) {
clearHighlight();
hideTooltip();
}
Expand Down Expand Up @@ -217,25 +225,25 @@ export const ChartLegend = ({
const tooltipPosition = isVertical ? "left" : "bottom";

return (
<SingleTabStopNavigationProvider
ref={navigationAPI}
navigationActive={true}
getNextFocusTarget={() => getNextFocusTarget()}
onUnregisterActive={(element: HTMLElement) => onUnregisterActive(element, navigationAPI)}
<div
role="toolbar"
aria-label={legendTitle || ariaLabel}
className={clsx(testClasses.root, styles.root, className)}
data-axisid={axisId}
onMouseEnter={() => (isMouseInContainer.current = true)}
onMouseLeave={() => (isMouseInContainer.current = false)}
>
<div
role="toolbar"
aria-label={legendTitle || ariaLabel}
className={clsx(testClasses.root, styles.root, className)}
data-axisid={axisId}
onMouseEnter={() => (isMouseInContainer.current = true)}
onMouseLeave={() => (isMouseInContainer.current = false)}
{legendTitle && (
<Box fontWeight="bold" className={testClasses.title} textAlign={getBoxTextAlignment(horizontalAlignment)}>
{legendTitle}
</Box>
)}
<SingleTabStopNavigationProvider
ref={navigationAPI}
navigationActive={true}
getNextFocusTarget={() => getNextFocusTarget()}
onUnregisterActive={(element: HTMLElement) => onUnregisterActive(element, navigationAPI)}
>
{legendTitle && (
<Box fontWeight="bold" className={testClasses.title} textAlign={getBoxTextAlignment(horizontalAlignment)}>
{legendTitle}
</Box>
)}
<div
// The list element is not focusable. However, the focus lands on it regardless, when testing in Firefox.
// Setting the tab index to -1 does fix the problem.
Expand Down Expand Up @@ -306,19 +314,27 @@ export const ChartLegend = ({
);
})}
</div>
{tooltipContent && (
</SingleTabStopNavigationProvider>
{tooltipContent && (
<div ref={tooltipWrapperRef}>
{/* [1] skips FocusLock's leading TabTrap to target the dismiss button. */}
Copy link
Copy Markdown
Member

@pan-kot pan-kot May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a hack that we better avoid. Besides, we don't really need focus traps around the tooltip because tooltip itself has some. The only issue is that when the tooltip's first focus trap gets focused - the focus goes to the last element. I believe we can fix it in the tooltip itself - let me check it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should address it: cloudscape-design/components#4518

<div tabIndex={0} onFocus={() => tooltipRef.current && getAllFocusables(tooltipRef.current)[1]?.focus()} />
<InternalChartTooltip
ref={tooltipRef}
trackRef={tooltipTrack}
triggerClampRef={containerRef}
trackKey={tooltipTarget.id}
container={null}
dismissButton={false}
onDismiss={() => {}}
dismissButton={true}
disableDismissAutoFocus={true}
onDismiss={() => {
hideTooltip(true);
elementsByIdRef.current[tooltipTarget.id]?.focus();
}}
position={tooltipPosition}
title={tooltipContent.header}
onMouseEnter={() => showTooltip(tooltipTarget.id)}
onMouseLeave={() => hideTooltip()}
onBlur={() => hideTooltip()}
footer={
tooltipContent.footer && (
<>
Expand All @@ -330,9 +346,10 @@ export const ChartLegend = ({
>
{tooltipContent.body}
</InternalChartTooltip>
)}
</div>
</SingleTabStopNavigationProvider>
<div tabIndex={0} onFocus={() => tooltipRef.current && getAllFocusables(tooltipRef.current)[1]?.focus()} />
</div>
)}
</div>
);
};

Expand Down
Loading