@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
479 lines (374 loc) • 16.7 kB
Markdown
# Tooltip
Import: `import { Tooltip } from '@neo4j-ndl/react'`
## Props
### Tooltip
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `autoUpdateOptions` | `AutoUpdateOptions` | | | |
| `children` | `ReactNode` | ✅ | | Content displayed inside the tooltip. Wraps `Tooltip.Trigger`, `Tooltip.Content`, and optional parts |
| `floatingStrategy` | `'absolute' \| 'fixed'` | | | Positioning strategy of the tooltip. Defaults to `fixed` when inside a dialog, otherwise `absolute` |
| `followCursor` | `boolean` | | | Whether the tooltip should follow the mouse cursor. Only applies to 'simple' tooltips with mouse hover. |
| `hoverDelay` | `{ open: number; close: number; }` | | | Hover open/close delays in milliseconds |
| `isDisabled` | `boolean` | | `false` | Whether the tooltip is disabled |
| `isInitialOpen` | `boolean` | | | Whether the tooltip should be open on first render |
| `isOpen` | `boolean` | | | Controls the open state . If omitted, the tooltip manages its own state |
| `isPortaled` | `boolean` | | | Whether the content is rendered in a portal. |
| `onOpenChange` | `((open: boolean, event?: Event, reason?: OpenChangeReason) => void) \| undefined` | | | Callback fired when the open state changes, if omitted, the tooltip manages its own state using internal hooks |
| `placement` | `'bottom-end' \| 'bottom-start' \| 'bottom' \| 'left-end' \| 'left-start' \| 'left' \| 'right-end' \| 'right-start' \| 'right' \| 'top-end' \| 'top-start' \| 'top'` | | | Placement of the tooltip content relative to the trigger |
| `shouldCloseOnReferenceClick` | `boolean` | | | Whether the tooltip should close when the reference element is clicked. Useful when the trigger moves after click. |
| `type` | `'rich' \| 'simple'` | ✅ | | Visual style of the tooltip content. `simple` renders plain text, `rich` renders a composable panel rich type will be removed in v5, use simple type instead. |
### Tooltip.Actions
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | ✅ | | Content displayed inside the actions container |
| `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
### Tooltip.Body
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | ✅ | | Content displayed inside the body |
| `passThroughProps` | `Partial<Omit<TypographyProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>, "ref">>` | | | Additional props to pass to the Typography component |
| `ref` | `Ref<HTMLSpanElement>` | | | A ref to apply to the root element. |
### Tooltip.Content
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | ✅ | | Content displayed inside the tooltip |
| `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
### Tooltip.Header
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | ✅ | | Content displayed inside the header |
| `passThroughProps` | `Partial<Omit<TypographyProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>, "ref">>` | | | Use `typographyVariant` instead |
| `ref` | `Ref<HTMLSpanElement>` | | | A ref to apply to the root element. |
| `typographyVariant` | `'body-large' \| 'body-medium' \| 'body-small' \| 'code' \| 'display' \| 'label' \| 'subheading-large' \| 'subheading-medium' \| 'subheading-small' \| 'title-1' \| 'title-2' \| 'title-3' \| 'title-4'` | | `subheading-medium` | |
### Tooltip.Trigger
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | ✅ | | Content displayed inside the trigger element |
| `hasButtonWrapper` | `boolean` | | `false` | Whether the trigger element can be any element without an extra button wrapper. The child element will be cloned with the necessary trigger props and merged refs |
| `ref` | `Ref<HTMLButtonElement>` | | | A ref to apply to the root element. |
## Accessibility
## Keyboard interactions
Behavior depends on the `type` prop. Simple and rich tooltips follow different interaction models.
### Simple tooltips (`type="simple"`)
Implements the keyboard interactions defined in the [WAI-ARIA tooltip pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/).
| Key | Description |
|-----|-------------|
| `Tab` | Moves focus to the trigger; the tooltip opens when the trigger receives focus |
| `Shift + Tab` | Moves focus away from the trigger; the tooltip closes when focus leaves |
| `Escape` | Closes the tooltip while focus remains on the trigger |
Simple tooltips also open on mouse hover and close when the pointer leaves the trigger and tooltip area.
### Rich tooltips (`type="rich"`)
Rich tooltips use a light-dismiss dialog interaction model for panels with actions or structured content. It doesn't trap focus, and clicking outside it will immediately close the component.
| Key | Description |
|-----|-------------|
| `Tab` | When closed, moves focus to the trigger. When open, moves focus between focusable elements inside the tooltip |
| `Shift + Tab` | When open, moves focus between focusable elements. Tabbing past the last element closes the tooltip and returns focus to the trigger |
| `Space` | On the trigger, toggles the tooltip open and closed. When open, activates the focused button |
| `Enter` | On the trigger, toggles the tooltip open (same as click) |
| `Escape` | Closes the tooltip and returns focus to the trigger |
## WAI-ARIA roles and attributes
### Simple tooltips
Implements the [WAI-ARIA tooltip pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/).
- The popup (`Tooltip.Content`) has `role="tooltip"`
- When open, the trigger is described via `aria-describedby`, wired by Floating UI to the portaled tooltip element
### Rich tooltips
- The popup (`Tooltip.Content`) has `role="dialog"`
- The popup is labeled via `aria-labelledby` (auto-wired from `Tooltip.Header`) or `aria-label`
## Implementation guidelines
- Rich tooltips must have an accessible name — provide `Tooltip.Header` or set `aria-label` on `Tooltip.Content` via `htmlAttributes`. A console warning is emitted if neither is provided
- Use `hasButtonWrapper` on `Tooltip.Trigger` with Needle trigger buttons (`IconButton`, `FilledButton`, etc.) — accessible names are already provided via `description` or visible label text
- Do not rely on a simple tooltip as the only source of essential information — provide a persistent accessible name or description on the trigger when the content is required to operate the control
- Rich tooltips with `Tooltip.Actions` should use Needle `Button` variants (e.g. `OutlinedButton`, `TextButton`) or `TextLink` for actions so they are keyboard focusable, styled consistently, and operable
- Set `isDisabled` when the tooltip would open alongside another overlay (e.g. an open menu), which can create confusing or conflicting information for assistive technology users
### Related WCAG criteria
- [1.4.13 Content on Hover or Focus](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html) (AA): Simple and rich tooltips can be dismissed with `Escape`; rich tooltips can also be dismissed by activating the trigger again
- [2.1.1 Keyboard](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) (A): Simple tooltips are available to keyboard users via focus; rich tooltips and their actions are fully operable via keyboard
- [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) (A): `role="tooltip"` / `role="dialog"`, accessible name (`aria-labelledby` / `aria-label` for rich tooltips), and accessible description (`aria-describedby` for simple tooltips when open) are set automatically by the component
## Examples
### Default
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { Tooltip } from '-ndl/react';
const Component = () => {
return (
<Tooltip type="simple">
<Tooltip.Trigger>Hover me!</Tooltip.Trigger>
<Tooltip.Content>This is a tooltip!</Tooltip.Content>
</Tooltip>
);
};
export default Component;
```
### Controlled
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { FilledButton, Tooltip } from '-ndl/react';
import { useState } from 'react';
const Component = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<Tooltip type="simple" isOpen={isOpen}>
<Tooltip.Trigger hasButtonWrapper>
<FilledButton size="medium" onClick={() => setIsOpen((prev) => !prev)}>
Click me
</FilledButton>
</Tooltip.Trigger>
<Tooltip.Content>This is a controlled tooltip</Tooltip.Content>
</Tooltip>
);
};
export default Component;
```
### Disabled
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { Tooltip } from '-ndl/react';
const Component = () => {
return (
<Tooltip type="simple" isDisabled={true}>
<Tooltip.Trigger>Hover me!</Tooltip.Trigger>
<Tooltip.Content>This is a tooltip!</Tooltip.Content>
</Tooltip>
);
};
export default Component;
```
### Follow Cursor
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { Tooltip } from '-ndl/react';
const triggerStyle: React.CSSProperties = {
alignItems: 'center',
backgroundColor: 'var(--theme-color-primary-bg-weak)',
borderRadius: '4px',
color: 'var(--theme-color-primary-text-inverse)',
display: 'flex',
height: '300px',
justifyContent: 'center',
padding: '8px',
width: '300px',
};
const Component = () => {
return (
<div style={{ display: 'flex', gap: '24px' }}>
<Tooltip type="simple" followCursor placement="bottom">
<Tooltip.Trigger>
<div style={triggerStyle}>Hover me (x-axis)</div>
</Tooltip.Trigger>
<Tooltip.Content>Follows cursor along x-axis</Tooltip.Content>
</Tooltip>
<Tooltip type="simple" followCursor placement="right">
<Tooltip.Trigger>
<div style={triggerStyle}>Hover me (y-axis)</div>
</Tooltip.Trigger>
<Tooltip.Content>Follows cursor along y-axis</Tooltip.Content>
</Tooltip>
</div>
);
};
export default Component;
```
### Hover Delay
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { Tooltip } from '-ndl/react';
const Component = () => {
return (
<Tooltip type="simple" hoverDelay={{ close: 200, open: 200 }}>
<Tooltip.Trigger>Hover me!</Tooltip.Trigger>
<Tooltip.Content>This is a tooltip!</Tooltip.Content>
</Tooltip>
);
};
export default Component;
```
### In Dialog
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { Dialog, FilledButton, Tooltip, Typography } from '-ndl/react';
import { useState } from 'react';
const Component = () => {
const [isDialogOpen, setDialogOpen] = useState(false);
const closeDialog = () => setDialogOpen(false);
return (
<div className="n-flex n-justify-center">
<FilledButton onClick={() => setDialogOpen(true)}>
Open Dialog
</FilledButton>
<Dialog isOpen={isDialogOpen} onClose={closeDialog}>
<Dialog.Header>Header</Dialog.Header>
<Dialog.Content>
<Tooltip type="simple">
<Tooltip.Trigger>
<Typography variant="body-medium">Hover me!</Typography>
</Tooltip.Trigger>
<Tooltip.Content>This is a long tooltip!</Tooltip.Content>
</Tooltip>
</Dialog.Content>
</Dialog>
</div>
);
};
export default Component;
```
### Initial Open
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { Tooltip } from '-ndl/react';
const Component = () => {
return (
<Tooltip type="simple" isInitialOpen={true}>
<Tooltip.Trigger>Hover me!</Tooltip.Trigger>
<Tooltip.Content>This is a tooltip!</Tooltip.Content>
</Tooltip>
);
};
export default Component;
```
### Placements
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { FilledButton, Tooltip } from '-ndl/react';
const Component = () => {
const placements = [
{ label: 'Top', placement: 'top' as const },
{ label: 'Bottom', placement: 'bottom' as const },
{ label: 'Left', placement: 'left' as const },
{ label: 'Right', placement: 'right' as const },
{ label: 'Top Start', placement: 'top-start' as const },
{ label: 'Top End', placement: 'top-end' as const },
{ label: 'Bottom Start', placement: 'bottom-start' as const },
{ label: 'Bottom End', placement: 'bottom-end' as const },
];
return (
<div className="n-grid n-grid-cols-4 n-gap-token-16 n-p-token-16 n-place-items-center">
{placements.map(({ placement, label }) => (
<Tooltip key={placement} type="simple" placement={placement}>
<Tooltip.Trigger hasButtonWrapper>
<FilledButton size="small">{label}</FilledButton>
</Tooltip.Trigger>
<Tooltip.Content>Tooltip on {label.toLowerCase()}</Tooltip.Content>
</Tooltip>
))}
</div>
);
};
export default Component;
```
### Rich
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { FilledButton, Tooltip } from '-ndl/react';
const Component = () => {
return (
<Tooltip type="rich">
<Tooltip.Trigger hasButtonWrapper>
<FilledButton size="medium">Click me!</FilledButton>
</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Header>Header</Tooltip.Header>
<Tooltip.Body>
This rich tooltip demonstrates all the available subcomponents: Header
and Body, without Actions.
</Tooltip.Body>
</Tooltip.Content>
</Tooltip>
);
};
export default Component;
```
### Rich Actions
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import {
FilledButton,
OutlinedButton,
TextButton,
Tooltip,
} from '-ndl/react';
const Component = () => {
return (
<Tooltip type="rich">
<Tooltip.Trigger hasButtonWrapper>
<FilledButton size="medium">Click me!</FilledButton>
</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Header>Header</Tooltip.Header>
<Tooltip.Body>
This rich tooltip demonstrates all the available subcomponents:
Header, Body, and Actions.
</Tooltip.Body>
<Tooltip.Actions>
<OutlinedButton onClick={() => alert('Primary action')}>
Action
</OutlinedButton>
<TextButton onClick={() => alert('Secondary action')}>
Action
</TextButton>
</Tooltip.Actions>
</Tooltip.Content>
</Tooltip>
);
};
export default Component;
```
### Rich Link
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { FilledButton, TextLink, Tooltip } from '-ndl/react';
const Component = () => {
return (
<Tooltip type="rich">
<Tooltip.Trigger hasButtonWrapper>
<FilledButton size="medium">Click me!</FilledButton>
</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Header>Header</Tooltip.Header>
<Tooltip.Body>
This rich tooltip demonstrates all the available subcomponents:
Header, Body, and Actions.
</Tooltip.Body>
<Tooltip.Actions>
<TextLink type="external" href="#">
External link
</TextLink>
</Tooltip.Actions>
</Tooltip.Content>
</Tooltip>
);
};
export default Component;
```
### Trigger Button Wrapper
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { FilledButton, Tooltip } from '-ndl/react';
const Component = () => {
return (
<Tooltip type="simple">
<Tooltip.Trigger hasButtonWrapper>
<FilledButton size="medium">Click me!</FilledButton>
</Tooltip.Trigger>
<Tooltip.Content>This is a tooltip!</Tooltip.Content>
</Tooltip>
);
};
export default Component;
```
### With Keyboard Display
```tsx
import '-ndl/base/lib/neo4j-ds-styles.css';
import { Kbd, Tooltip } from '-ndl/react';
const Component = () => {
return (
<Tooltip type="simple">
<Tooltip.Trigger>Hover me!</Tooltip.Trigger>
<Tooltip.Content>
This is a keyboard shortcut:{' '}
<Kbd modifierKeys={['meta']} keys={['B']} />
</Tooltip.Content>
</Tooltip>
);
};
export default Component;
```