diff --git a/public/app/plugins/panel/piechart/PieChart.tsx b/public/app/plugins/panel/piechart/PieChart.tsx
index 748119ebe95..3a0c30db28f 100644
--- a/public/app/plugins/panel/piechart/PieChart.tsx
+++ b/public/app/plugins/panel/piechart/PieChart.tsx
@@ -122,21 +122,18 @@ export const PieChart = ({
if (arc.data.hasLinks && arc.data.getLinks) {
return (
-
- {(api) => (
-
- )}
-
+
);
} else {
return (
@@ -203,6 +200,183 @@ interface SliceProps {
innerRadius: number;
}
+interface PieSliceWithDataLinksProps extends Omit {
+ links: () => any[];
+}
+
+interface PieChartDataLinksContextMenuProps {
+ links: () => any[];
+ children: (props: { openMenu?: React.MouseEventHandler }) => React.ReactElement;
+ elementRef: React.RefObject;
+ publishDataHoverEvent: (raw: Event | React.SyntheticEvent) => void;
+ publishDataHoverClearEvent: (raw: Event | React.SyntheticEvent) => void;
+}
+
+/**
+ * Wrapper around DataLinksContextMenu that adds event handlers to the anchor element
+ * when it's created (for single-link case). This keeps event handler logic co-located
+ * with component creation rather than in a separate useEffect in PieSlice.
+ */
+function PieChartDataLinksContextMenu({
+ links,
+ children,
+ elementRef,
+ publishDataHoverEvent,
+ publishDataHoverClearEvent,
+}: PieChartDataLinksContextMenuProps) {
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) {
+ return;
+ }
+
+ const parentAnchor = container.querySelector('a');
+ if (!parentAnchor) {
+ return;
+ }
+
+ // Ensure anchor is focusable
+ if (parentAnchor.getAttribute('tabIndex') === '-1') {
+ parentAnchor.removeAttribute('tabIndex');
+ }
+
+ // Ensure SVG element is not focusable
+ if (elementRef.current) {
+ const ensureNotFocusable = () => {
+ if (elementRef.current && elementRef.current.getAttribute('tabIndex') !== '-1') {
+ elementRef.current.setAttribute('tabIndex', '-1');
+ }
+ };
+ ensureNotFocusable();
+ setTimeout(ensureNotFocusable, 0);
+ }
+
+ const handleAnchorFocus = (e: FocusEvent) => {
+ publishDataHoverEvent(e);
+ };
+
+ const handleAnchorBlur = (e: FocusEvent) => {
+ publishDataHoverClearEvent(e);
+ };
+
+ const handleAnchorKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Tab' && elementRef.current) {
+ elementRef.current.setAttribute('tabIndex', '-1');
+ }
+ };
+
+ const handleAnchorFocusIn = (e: FocusEvent) => {
+ const target = e.target;
+ if (target instanceof Element && target !== parentAnchor && parentAnchor.contains(target)) {
+ e.stopPropagation();
+ parentAnchor.focus();
+ }
+ };
+
+ const handleFocusIn = (e: FocusEvent) => {
+ const target = e.target;
+ if (target === parentAnchor || (target instanceof Node && parentAnchor.contains(target))) {
+ handleAnchorFocus(e);
+ }
+ };
+
+ parentAnchor.addEventListener('focus', handleAnchorFocus, true);
+ parentAnchor.addEventListener('focusin', handleFocusIn, true);
+ parentAnchor.addEventListener('blur', handleAnchorBlur, true);
+ parentAnchor.addEventListener('keydown', handleAnchorKeyDown, true);
+ parentAnchor.addEventListener('focusin', handleAnchorFocusIn, true);
+
+ return () => {
+ parentAnchor.removeEventListener('focus', handleAnchorFocus, true);
+ parentAnchor.removeEventListener('focusin', handleFocusIn, true);
+ parentAnchor.removeEventListener('blur', handleAnchorBlur, true);
+ parentAnchor.removeEventListener('keydown', handleAnchorKeyDown, true);
+ parentAnchor.removeEventListener('focusin', handleAnchorFocusIn, true);
+ };
+ }, [elementRef, publishDataHoverEvent, publishDataHoverClearEvent]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Component that wraps PieSlice with DataLinksContextMenu and handles event listeners
+ * for the anchor element. This keeps event handler logic co-located with component creation.
+ */
+function PieSliceWithDataLinks({
+ arc,
+ pie,
+ highlightState,
+ fill,
+ tooltip,
+ tooltipOptions,
+ outerRadius,
+ innerRadius,
+ links,
+}: PieSliceWithDataLinksProps) {
+ const { eventBus } = usePanelContext();
+ const elementRef = useRef(null);
+
+ const publishDataHoverEvent = useCallback(
+ (raw: Event | React.SyntheticEvent) => {
+ eventBus?.publish({
+ type: DataHoverEvent.type,
+ payload: {
+ raw,
+ x: 0,
+ y: 0,
+ dataId: arc.data.display.title,
+ },
+ });
+ },
+ [eventBus, arc.data.display.title]
+ );
+
+ const publishDataHoverClearEvent = useCallback(
+ (raw: Event | React.SyntheticEvent) => {
+ eventBus?.publish({
+ type: DataHoverClearEvent.type,
+ payload: {
+ raw,
+ x: 0,
+ y: 0,
+ dataId: arc.data.display.title,
+ },
+ });
+ },
+ [eventBus, arc.data.display.title]
+ );
+
+ return (
+
+ {(api) => (
+
+ )}
+
+ );
+}
+
function PieSlice({
arc,
pie,
@@ -213,11 +387,13 @@ function PieSlice({
tooltipOptions,
outerRadius,
innerRadius,
-}: SliceProps) {
+ elementRef: externalElementRef,
+}: SliceProps & { elementRef?: React.RefObject }) {
const theme = useTheme2();
const styles = useStyles2(getStyles);
const { eventBus } = usePanelContext();
- const elementRef = useRef(null);
+ const internalElementRef = useRef(null);
+ const elementRef = externalElementRef || internalElementRef;
const blurTimeoutRef = useRef | null>(null);
const hasDataLinksDirect = Boolean(arc.data.hasLinks && arc.data.getLinks);
@@ -254,74 +430,6 @@ function PieSlice({
[eventBus, arc.data.display.title]
);
- useEffect(() => {
- if (hasDataLinks && !openMenu && elementRef.current) {
- const parentAnchor = elementRef.current.closest('a');
- if (parentAnchor) {
- if (parentAnchor.getAttribute('tabIndex') === '-1') {
- parentAnchor.removeAttribute('tabIndex');
- }
-
- if (elementRef.current) {
- // Ensure the SVG element is not focusable when parent anchor handles focus
- // Use setTimeout(0) to ensure this runs after React has applied JSX attributes
- const ensureNotFocusable = () => {
- if (elementRef.current && elementRef.current.getAttribute('tabIndex') !== '-1') {
- elementRef.current.setAttribute('tabIndex', '-1');
- }
- };
- // Run immediately and after React's render cycle to catch any timing issues
- ensureNotFocusable();
- setTimeout(ensureNotFocusable, 0);
- }
-
- const handleAnchorFocus = (e: FocusEvent) => {
- publishDataHoverEvent(e);
- };
-
- const handleAnchorBlur = (e: FocusEvent) => {
- publishDataHoverClearEvent(e);
- };
-
- const handleAnchorKeyDown = (e: KeyboardEvent) => {
- if (e.key === 'Tab' && elementRef.current) {
- elementRef.current.setAttribute('tabIndex', '-1');
- }
- };
-
- const handleAnchorFocusIn = (e: FocusEvent) => {
- const target = e.target;
- if (target instanceof Element && target !== parentAnchor && parentAnchor.contains(target)) {
- e.stopPropagation();
- parentAnchor.focus();
- }
- };
-
- const handleFocusIn = (e: FocusEvent) => {
- const target = e.target;
- if (target === parentAnchor || (target instanceof Node && parentAnchor.contains(target))) {
- handleAnchorFocus(e);
- }
- };
-
- parentAnchor.addEventListener('focus', handleAnchorFocus, true);
- parentAnchor.addEventListener('focusin', handleFocusIn, true);
- parentAnchor.addEventListener('blur', handleAnchorBlur, true);
- parentAnchor.addEventListener('keydown', handleAnchorKeyDown, true);
- parentAnchor.addEventListener('focusin', handleAnchorFocusIn, true);
-
- return () => {
- parentAnchor.removeEventListener('focus', handleAnchorFocus, true);
- parentAnchor.removeEventListener('focusin', handleFocusIn, true);
- parentAnchor.removeEventListener('blur', handleAnchorBlur, true);
- parentAnchor.removeEventListener('keydown', handleAnchorKeyDown, true);
- parentAnchor.removeEventListener('focusin', handleAnchorFocusIn, true);
- };
- }
- }
- return undefined;
- }, [hasDataLinks, openMenu, publishDataHoverEvent, publishDataHoverClearEvent]);
-
const onMouseOut = useCallback(
(event: React.MouseEvent) => {
publishDataHoverClearEvent(event);