Add awareness to Toggletip when inside Drawer given a data attribute
This commit is contained in:
@@ -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's DOM, allowing focus to work correctly with the Drawer'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's inside a Drawer and renders its content within the
|
||||
Drawer'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"
|
||||
|
||||
Reference in New Issue
Block a user