Add awareness to Toggletip when inside Drawer given a data attribute

This commit is contained in:
tdbishop
2025-12-29 11:02:13 -06:00
parent 5b3e8690fa
commit b7010164da
4 changed files with 56 additions and 48 deletions
@@ -131,6 +131,7 @@ export function Drawer({
>
<FocusScope restoreFocus contain autoFocus>
<div
data-grafana-portal-container
aria-label={
typeof title === 'string'
? selectors.components.Drawer.General.title(title)
@@ -78,22 +78,22 @@ return (
### Usage inside Drawer
When using Toggletip inside a Drawer or other focus-trapped container, pass the container element as `portalRoot` to ensure focus management works correctly. This renders the Toggletip content inside the Drawer's DOM tree instead of the default portal container.
Use a state-based ref pattern to ensure the container is available before rendering the Toggletip:
Toggletip automatically detects when it's inside a Drawer (or other focus-trapped container with the `data-grafana-portal-container` attribute) and adjusts its behavior accordingly. No additional configuration is needed:
```tsx
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
<Drawer title="Settings" onClose={onClose}>
<div ref={setContainerEl}>
{containerEl && (
<Toggletip content={<Input placeholder="Type here..." />} portalRoot={containerEl}>
<Button>Open Toggletip</Button>
</Toggletip>
)}
</div>
</Drawer>;
<Toggletip content={<Input placeholder="Type here..." />}>
<Button>Open Toggletip</Button>
</Toggletip>
</Drawer>
```
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.
<ArgTypes of={Toggletip} />
@@ -139,41 +139,33 @@ LongContent.parameters = {
export const InsideDrawer: StoryFn<typeof Toggletip> = () => {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Use state instead of ref to trigger re-render when container is available
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
return (
<>
<Button onClick={() => setIsDrawerOpen(true)}>Open Drawer</Button>
{isDrawerOpen && (
<Drawer title="Drawer with Toggletip" onClose={() => setIsDrawerOpen(false)}>
<div ref={setContainerEl}>
<p style={{ marginBottom: '16px' }}>
This demonstrates using Toggletip inside a Drawer. The <code>portalRoot</code> prop is used to render the
Toggletip content inside the Drawer&apos;s DOM, allowing focus to work correctly with the Drawer&apos;s
focus trap.
</p>
{containerEl && (
<Toggletip
title="Interactive Form"
content={
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Field label="Name">
<Input placeholder="Enter your name" />
</Field>
<Button variant="primary" size="sm">
Submit
</Button>
</div>
}
footer="Focus should work correctly within this Toggletip"
placement="bottom-start"
portalRoot={containerEl}
>
<Button>Click to show Toggletip</Button>
</Toggletip>
)}
</div>
<p style={{ marginBottom: '16px' }}>
Toggletip automatically detects when it&apos;s inside a Drawer and renders its content within the
Drawer&apos;s DOM, allowing focus to work correctly. No manual configuration needed!
</p>
<Toggletip
title="Interactive Form"
content={
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Field label="Name">
<Input placeholder="Enter your name" />
</Field>
<Button variant="primary" size="sm">
Submit
</Button>
</div>
}
footer="Focus works correctly - auto-detected!"
placement="bottom-start"
>
<Button>Click to show Toggletip</Button>
</Toggletip>
</Drawer>
)}
</>
@@ -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<Element | null>(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 && (
<Portal root={portalRoot}>
<Portal root={autoDetectedPortalRoot}>
<FloatingFocusManager context={context} modal={true}>
<div
data-testid="toggletip-content"