Compare commits

...

2 Commits

4 changed files with 90 additions and 3 deletions

View File

@@ -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)

View File

@@ -76,4 +76,24 @@ return (
);
```
### Usage inside Drawer
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
<Drawer title="Settings" onClose={onClose}>
<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} />

View File

@@ -1,6 +1,10 @@
import { Meta, StoryFn } from '@storybook/react';
import { useState } from 'react';
import { Button } from '../Button/Button';
import { Drawer } from '../Drawer/Drawer';
import { Field } from '../Forms/Field';
import { Input } from '../Input/Input';
import { ScrollContainer } from '../ScrollContainer/ScrollContainer';
import mdx from '../Toggletip/Toggletip.mdx';
@@ -133,4 +137,46 @@ LongContent.parameters = {
},
};
export const InsideDrawer: StoryFn<typeof Toggletip> = () => {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
return (
<>
<Button onClick={() => setIsDrawerOpen(true)}>Open Drawer</Button>
{isDrawerOpen && (
<Drawer title="Drawer with Toggletip" onClose={() => setIsDrawerOpen(false)}>
<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>
)}
</>
);
};
InsideDrawer.parameters = {
controls: {
hideNoControlsWarning: true,
exclude: ['title', 'content', 'footer', 'children', 'placement', 'theme', 'closeButton', 'portalRoot'],
},
};
export default meta;

View File

@@ -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,6 +47,11 @@ export interface ToggletipProps {
show?: boolean;
/** Callback function to be called when the toggletip is opened */
onOpen?: () => void;
/** 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;
}
/**
@@ -67,6 +72,7 @@ export const Toggletip = memo(
fitContent = false,
onOpen,
show,
portalRoot,
}: ToggletipProps) => {
const arrowRef = useRef(null);
const grafanaTheme = useTheme2();
@@ -110,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>
<Portal root={autoDetectedPortalRoot}>
<FloatingFocusManager context={context} modal={true}>
<div
data-testid="toggletip-content"