From b7010164dac61f52055a6035d0103e71934931dd Mon Sep 17 00:00:00 2001 From: tdbishop Date: Mon, 29 Dec 2025 11:02:13 -0600 Subject: [PATCH] Add awareness to Toggletip when inside Drawer given a data attribute --- .../src/components/Drawer/Drawer.tsx | 1 + .../src/components/Toggletip/Toggletip.mdx | 26 +++++----- .../components/Toggletip/Toggletip.story.tsx | 50 ++++++++----------- .../src/components/Toggletip/Toggletip.tsx | 27 +++++++--- 4 files changed, 56 insertions(+), 48 deletions(-) diff --git a/packages/grafana-ui/src/components/Drawer/Drawer.tsx b/packages/grafana-ui/src/components/Drawer/Drawer.tsx index 00039adfc3f..7955b324aa8 100644 --- a/packages/grafana-ui/src/components/Drawer/Drawer.tsx +++ b/packages/grafana-ui/src/components/Drawer/Drawer.tsx @@ -131,6 +131,7 @@ export function Drawer({ >
(null); - -
- {containerEl && ( - } portalRoot={containerEl}> - - - )} -
-
; + }> + + + ``` +When auto-detected inside a focus-trapped container: + +- The Toggletip content renders inside the Drawer's DOM tree +- Focus management defers to the parent container's focus trap +- Interactive elements like inputs work correctly + +If you need to override auto-detection or specify a custom container, use the `portalRoot` prop. + diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.story.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.story.tsx index 54a8b855520..de60bc57ff1 100644 --- a/packages/grafana-ui/src/components/Toggletip/Toggletip.story.tsx +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.story.tsx @@ -139,41 +139,33 @@ LongContent.parameters = { export const InsideDrawer: StoryFn = () => { const [isDrawerOpen, setIsDrawerOpen] = useState(false); - // Use state instead of ref to trigger re-render when container is available - const [containerEl, setContainerEl] = useState(null); return ( <> {isDrawerOpen && ( setIsDrawerOpen(false)}> -
-

- This demonstrates using Toggletip inside a Drawer. The portalRoot prop is used to render the - Toggletip content inside the Drawer's DOM, allowing focus to work correctly with the Drawer's - focus trap. -

- {containerEl && ( - - - - - -
- } - footer="Focus should work correctly within this Toggletip" - placement="bottom-start" - portalRoot={containerEl} - > - - - )} -
+

+ Toggletip automatically detects when it's inside a Drawer and renders its content within the + Drawer's DOM, allowing focus to work correctly. No manual configuration needed! +

+ + + + + + + } + footer="Focus works correctly - auto-detected!" + placement="bottom-start" + > + + )} diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx index 6bda1602385..f569d6aa400 100644 --- a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx @@ -11,7 +11,7 @@ import { useInteractions, } from '@floating-ui/react'; import { Placement } from '@popperjs/core'; -import { memo, cloneElement, isValidElement, useRef, useState, type JSX } from 'react'; +import { memo, cloneElement, isValidElement, useRef, useState, useMemo, type JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -47,9 +47,10 @@ export interface ToggletipProps { show?: boolean; /** Callback function to be called when the toggletip is opened */ onOpen?: () => void; - /** Optional root element for the portal. Use when Toggletip is inside a focus-trapped container like Drawer. - * When provided, the Toggletip will render inside this element and disable its own modal focus trap, - * deferring focus management to the parent container. */ + /** Optional root element for the portal. When Toggletip is inside a focus-trapped container like Drawer, + * the portal root is auto-detected via the `data-grafana-portal-container` attribute. Use this prop + * to override auto-detection or specify a custom container. When inside a focus-trapped container, + * the Toggletip disables its own modal focus trap, deferring focus management to the parent. */ portalRoot?: HTMLElement; } @@ -115,16 +116,30 @@ export const Toggletip = memo( const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]); + // Auto-detect portal container from reference element's ancestors + // This allows Toggletip to work automatically inside Drawer and other focus-trapped containers + const [referenceElement, setReferenceElement] = useState(null); + const autoDetectedPortalRoot = useMemo(() => { + if (portalRoot) { + return portalRoot; + } + const container = referenceElement?.closest('[data-grafana-portal-container]'); + return container instanceof HTMLElement ? container : undefined; + }, [portalRoot, referenceElement]); + return ( <> {cloneElement(children, { - ref: refs.setReference, + ref: (node: Element | null) => { + refs.setReference(node); + setReferenceElement(node); + }, tabIndex: 0, 'aria-expanded': isOpen, ...getReferenceProps(), })} {isOpen && ( - +