@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
797 lines (708 loc) • 25.6 kB
Markdown
# Spotlight
Import: `import { Spotlight } from '@neo4j-ndl/react'`
## Props
### Spotlight
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `actions` | `ReactNode` | | | The actions to be displayed in the Spotlight. Should only be TextButton or OutlinedButton components. |
| `beforeActions` | `ReactNode` | | | A ReactNode that can be put in the left corner of the Spotlight (left of action buttons). It should be used to display the current step if you have a multi-step tour |
| `children` | `ReactNode` | | | Children should be Spotlight subcomponents |
| `closeOnClickOutside` | `boolean` | | `false` | If the popover should close when the user clicks outside of it |
| `initialFocus` | `number \| RefObject<HTMLElement \| null>` | | | Which element to initially focus. * |
| `onClose` | `((action: SpotlightCloseAction) => void)` | | | A callback function that is called if the Spotlight is closed by either clicking outside the Spotlight, or by pressing the Escape key. The function has one argument: action (string that can be either "clickOutside" or "escapeKeyDown") |
| `onOpen` | `(() => void)` | | | Callback function called when the spotlight is opened |
| `placement` | `'bottom-end-bottom-start' \| 'bottom-end-top-end' \| 'bottom-middle-top-middle' \| 'bottom-start-bottom-end' \| 'bottom-start-top-start' \| 'middle-end-middle-start' \| 'middle-start-middle-end' \| 'top-end-bottom-end' \| 'top-end-top-start' \| 'top-middle-bottom-middle' \| 'top-start-bottom-start' \| 'top-start-top-end'` | | | The placement of the floating element is determined by two sets of words. The first set of words specifies the point on the anchor element where the floating element will be attached. The second set of coordinates specifies the point on the floating element that will attach to the anchor element. |
| `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
| `target` | `string` | ✅ | | The id of the `SpotlightTarget` that this `Spotlight` should be rendered next to |
### Spotlight.Body
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `ElementType<any, keyof IntrinsicElements>` | | | An override of the default HTML tag of the root of the component. Can also be another React component. |
| `children` | `ReactNode` | | | The spotlight body content |
| `ref` | `any` | | | A ref to apply to the root element. |
### Spotlight.Header
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `ElementType<any, keyof IntrinsicElements>` | | | An override of the default HTML tag of the root of the component. Can also be another React component. |
| `children` | `ReactNode` | | | The spotlight header content |
| `ref` | `any` | | | A ref to apply to the root element. |
### Spotlight.IconWrapper
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `ElementType<any, keyof IntrinsicElements>` | | | An override of the default HTML tag of the root of the component. Can also be another React component. |
| `children` | `ReactNode` | | | Content displayed in the icon wrapper |
| `ref` | `any` | | | A ref to apply to the root element. |
### Spotlight.Image
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `alt` | `string` | ✅ | | A string that represents the alt text of the image |
| `ref` | `Ref<HTMLImageElement>` | | | A ref to apply to the root element. |
| `src` | `string` | ✅ | | A string that represents the URL of the image |
### Spotlight.Label
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `("symbol" & ElementType<any, keyof IntrinsicElements>) \| ("object" & ElementType<any, keyof IntrinsicElements>) \| ... 177 more ... \| (FunctionComponent<...> & ElementType<...>)` | | | An override of the default HTML tag of the root of the component. Can also be another React component. |
| `children` | `ReactNode` | ✅ | | The content displayed inside the status label |
| `hasIcon` | `boolean` | | | Whether the status label have an icon |
| `ref` | `any` | | | A ref to apply to the root element. |
| `size` | `'large' \| 'small'` | | | Size of the status label |
| `variant` | `'danger' \| 'default' \| 'discovery' \| 'info' \| 'success' \| 'warning'` | | | Color of the status label that indicates its purpose. |
## Accessibility
## Implementation guidelines
The Spotlight component follows WAI-ARIA dialog pattern guidelines. Includes features such as focus trapping, keyboard navigation support, and proper ARIA roles for enhanced accessibility.
Please note that you should always provide either a Spotlight.Header, aria-label or aria-labelledby.
## Examples
### Fixed Positioning
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
FilledButton,
OutlinedButton,
Spotlight,
SpotlightTarget,
useNeedleTheme,
useSpotlightContext,
} from '@neo4j-ndl/react';
import { useEffect } from 'react';
const Component = (props: { isChromatic: boolean }) => {
const { themeClassName } = useNeedleTheme();
const { setIsOpen, setActiveSpotlight } = useSpotlightContext();
const targetId = `test-target-${themeClassName}`;
useEffect(() => {
if (props.isChromatic) {
setActiveSpotlight(targetId);
setIsOpen(true);
}
}, [setActiveSpotlight, setIsOpen, targetId, props.isChromatic]);
return (
<div className="n-flex n-justify-center n-gap-token-64 n-h-[100px]">
<FilledButton onClick={() => setActiveSpotlight(targetId)}>
Activate Spotlight
</FilledButton>
<div className="n-left-[auto] n-top-[110px] n-fixed">
<SpotlightTarget
id={targetId}
hasPulse={!props.isChromatic}
asChild
indicatorVariant="border"
indicatorPlacement="middle-middle"
>
<FilledButton onClick={() => alert('Target button clicked')}>
Target
</FilledButton>
</SpotlightTarget>
</div>
<Spotlight
placement="bottom-end-top-end"
target={targetId}
actions={
<OutlinedButton
onClick={() => {
setIsOpen(false);
setActiveSpotlight(null);
}}
>
Close
</OutlinedButton>
}
>
<Spotlight.Header>This is a header</Spotlight.Header>
<Spotlight.Body>This is content</Spotlight.Body>
</Spotlight>
</div>
);
};
export default Component;
```
### In Dialog
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
Dialog,
FilledButton,
OutlinedButton,
Spotlight,
SpotlightTarget,
Typography,
useNeedleTheme,
useSpotlightContext,
} from '@neo4j-ndl/react';
import { useEffect, useState } from 'react';
const Component = (props: { isChromatic: boolean }) => {
const { themeClassName } = useNeedleTheme();
const { setIsOpen, setActiveSpotlight } = useSpotlightContext();
const [isModalOpen, setIsModalOpen] = useState<boolean>(props.isChromatic);
const targetId = `test-target-${themeClassName}`;
useEffect(() => {
if (props.isChromatic && isModalOpen) {
setActiveSpotlight(targetId);
setIsOpen(true);
}
}, [isModalOpen, setActiveSpotlight, setIsOpen, targetId, props.isChromatic]);
return (
<div className="n-flex n-justify-center n-gap-token-64">
<FilledButton onClick={() => setIsModalOpen(true)}>
Open Dialog
</FilledButton>
<Dialog
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
className={themeClassName}
>
<Dialog.Header>Using Spotlight inside a dialog</Dialog.Header>
<Dialog.Content className="n-flex n-flex-col n-gap-token-32">
<Typography variant="body-medium">
Spotlight and SpotlightTarget should behave automatically correct
when used inside a dialog. If positioning is not on top of
everything, you might want to override the
<code>--spotlight-z-index</code> css variable.
</Typography>
<div className="n-flex n-justify-center n-gap-token-64">
<FilledButton onClick={() => setActiveSpotlight(targetId)}>
Activate Spotlight
</FilledButton>
<SpotlightTarget
id={targetId}
hasPulse={!props.isChromatic}
asChild
indicatorVariant="border"
>
<FilledButton onClick={() => alert('Target button clicked')}>
Target
</FilledButton>
</SpotlightTarget>
</div>
</Dialog.Content>
<Spotlight
placement="bottom-end-top-end"
target={targetId}
actions={
<OutlinedButton
onClick={() => {
setIsOpen(false);
setActiveSpotlight(null);
}}
>
Close
</OutlinedButton>
}
>
<Spotlight.Header>This is a header</Spotlight.Header>
<Spotlight.Body>This is content</Spotlight.Body>
</Spotlight>
</Dialog>
</div>
);
};
export default Component;
```
### In Side Nav
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
FilledButton,
OutlinedButton,
SideNavigation,
Spotlight,
SpotlightTarget,
useNeedleTheme,
useSpotlightContext,
} from '@neo4j-ndl/react';
import { useEffect, useState } from 'react';
const Component = (props: { isChromatic: boolean }) => {
const { themeClassName } = useNeedleTheme();
const { setActiveSpotlight, setIsOpen } = useSpotlightContext();
const [isSelected, setIsSelected] = useState<string | null>(
'aura-db-instances',
);
const targetId = `test-target-${themeClassName}`;
const handleClick = (item: string) => {
setIsSelected(item);
};
useEffect(() => {
if (props.isChromatic) {
setActiveSpotlight(targetId);
setIsOpen(true);
}
}, [setActiveSpotlight, setIsOpen, targetId, props.isChromatic]);
return (
<div className="n-h-[500px] n-flex">
<SideNavigation isExpanded={true} ariaLabel="Main side navigation">
<SideNavigation.CategoryHeader>AuraDB</SideNavigation.CategoryHeader>
<SideNavigation.ListItem>
<SideNavigation.NavItem
onClick={() => handleClick('aura-db-instances')}
isActive={isSelected === 'aura-db-instances'}
label="Instances"
/>
</SideNavigation.ListItem>
<SideNavigation.ListItem>
<SideNavigation.NavItem
onClick={() => handleClick('aura-db-connect')}
isActive={isSelected === 'aura-db-connect'}
label="Connect"
/>
</SideNavigation.ListItem>
<SideNavigation.ListItem>
<SideNavigation.NavItem
onClick={() => handleClick('aura-db-python')}
isActive={isSelected === 'aura-db-python'}
label="Python"
/>
</SideNavigation.ListItem>
<SideNavigation.ListItem>
<SpotlightTarget
id={targetId}
hasPulse={!props.isChromatic}
borderRadius={4}
indicatorVariant="point"
indicatorPlacement="top-right"
asChild
>
<SideNavigation.NavItem
onClick={() => handleClick('aura-db-javascript')}
isActive={isSelected === 'aura-db-javascript'}
label="JavaScript"
/>
</SpotlightTarget>
</SideNavigation.ListItem>
<SideNavigation.CategoryHeader>AuraDS</SideNavigation.CategoryHeader>
<SideNavigation.ListItem>
<SideNavigation.NavItem
onClick={() => handleClick('aura-ds-instances')}
isActive={isSelected === 'aura-ds-instances'}
label="Instances"
/>
</SideNavigation.ListItem>
<SideNavigation.ListItem>
<SideNavigation.NavItem
onClick={() => handleClick('aura-ds-getting-started')}
isActive={isSelected === 'aura-ds-getting-started'}
label="Getting Started"
/>
</SideNavigation.ListItem>
</SideNavigation>
<div className="n-w-full n-h-full n-flex n-flex-col n-items-center n-justify-center n-rounded-md n-p-token-16">
<FilledButton onClick={() => setActiveSpotlight(targetId)}>
Activate Spotlight
</FilledButton>
</div>
<Spotlight
placement="top-end-top-start"
target={targetId}
actions={
<OutlinedButton
onClick={() => {
setIsOpen(false);
setActiveSpotlight(null);
}}
>
Close
</OutlinedButton>
}
>
<Spotlight.Header>This is a header</Spotlight.Header>
<Spotlight.Body>This is content</Spotlight.Body>
</Spotlight>
</div>
);
};
export default Component;
```
### On Tabs
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
FilledButton,
OutlinedButton,
Spotlight,
SpotlightTarget,
Tabs,
Typography,
useNeedleTheme,
useSpotlightContext,
} from '@neo4j-ndl/react';
import { useState } from 'react';
const Component = ({ hasPulse = true }: { hasPulse?: boolean }) => {
const { themeClassName } = useNeedleTheme();
const { setIsOpen, setActiveSpotlight } = useSpotlightContext();
const [tabsValue, setTabsValue] = useState('0');
const targetId = `test-target-${themeClassName}`;
return (
<div className="n-flex n-justify-center n-gap-token-10">
<FilledButton onClick={() => setActiveSpotlight(targetId)}>
Activate Spotlight
</FilledButton>
<div>
<Tabs value={tabsValue} onChange={setTabsValue}>
<SpotlightTarget
id={targetId}
hasPulse={hasPulse}
borderRadius={4}
indicatorVariant="point"
>
<Tabs.Tab id="0">Tab 1</Tabs.Tab>
</SpotlightTarget>
<Tabs.Tab id="1">Tab 2</Tabs.Tab>
<Tabs.Tab id="2">Tab 3</Tabs.Tab>
</Tabs>
<Tabs.TabPanel className="n-p-token-4" value={tabsValue} tabId="0">
<Typography variant="body-medium">Tab 1 content</Typography>
</Tabs.TabPanel>
<Tabs.TabPanel className="n-p-token-4" value={tabsValue} tabId="1">
<Typography variant="body-medium">Tab 2 content</Typography>
</Tabs.TabPanel>
<Tabs.TabPanel className="n-p-token-4" value={tabsValue} tabId="2">
<Typography variant="body-medium">Tab 3 content</Typography>
</Tabs.TabPanel>
</div>
<Spotlight
placement="bottom-end-top-end"
target={targetId}
actions={
<OutlinedButton
onClick={() => {
setIsOpen(false);
setActiveSpotlight(null);
}}
>
Close
</OutlinedButton>
}
>
<Spotlight.Header>This is a header</Spotlight.Header>
<Spotlight.Body>This is content</Spotlight.Body>
</Spotlight>
</div>
);
};
export default Component;
```
### Tour
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
FilledButton,
OutlinedButton,
Spotlight,
SpotlightTarget,
SpotlightTour,
useNeedleTheme,
useSpotlightContext,
} from '@neo4j-ndl/react';
import { FingerPrintIconOutline } from '@neo4j-ndl/react/icons';
const Component = () => {
const { themeClassName } = useNeedleTheme();
const { setActiveSpotlight } = useSpotlightContext();
return (
<div className="n-flex n-justify-center n-gap-token-64">
<OutlinedButton
onClick={() => setActiveSpotlight(`test-target-1-${themeClassName}`)}
>
Start tour
</OutlinedButton>
<SpotlightTarget id={`test-target-1-${themeClassName}`} hasPulse asChild>
<FilledButton onClick={() => alert('Target 1 clicked')}>
Target 1
</FilledButton>
</SpotlightTarget>
<SpotlightTarget id={`test-target-2-${themeClassName}`} hasPulse asChild>
<FilledButton onClick={() => alert('Target 2 clicked')}>
Target 2
</FilledButton>
</SpotlightTarget>
<SpotlightTarget id={`test-target-3-${themeClassName}`} hasPulse asChild>
<FilledButton onClick={() => alert('Target 3 clicked')}>
Target 3
</FilledButton>
</SpotlightTarget>
<SpotlightTour
onAction={(target, action) =>
console.info(`Action ${action} was performed in spotlight ${target}`)
}
spotlights={[
{
children: (
<>
<Spotlight.Label>NEW</Spotlight.Label>
<Spotlight.Header>This is a header</Spotlight.Header>
<Spotlight.Body>This is content</Spotlight.Body>
</>
),
target: `test-target-1-${themeClassName}`,
},
{
children: (
<>
<Spotlight.IconWrapper>
<FingerPrintIconOutline />
</Spotlight.IconWrapper>
<Spotlight.Header>This is a header</Spotlight.Header>
<Spotlight.Body>This is content</Spotlight.Body>
</>
),
target: `test-target-2-${themeClassName}`,
},
{
children: (
<>
<Spotlight.Image
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzMzMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPkV4YW1wbGUgSW1hZ2U8L3RleHQ+PC9zdmc+"
alt="example image"
/>
<Spotlight.Body>This is content</Spotlight.Body>
</>
),
target: `test-target-3-${themeClassName}`,
},
]}
/>
</div>
);
};
export default Component;
```
### With Icon
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
FilledButton,
OutlinedButton,
Spotlight,
SpotlightTarget,
TextButton,
useNeedleTheme,
useSpotlightContext,
} from '@neo4j-ndl/react';
import { FingerPrintIconOutline } from '@neo4j-ndl/react/icons';
import { useEffect } from 'react';
const Component = (props: { isChromatic: boolean }) => {
const { themeClassName } = useNeedleTheme();
const { setIsOpen, setActiveSpotlight } = useSpotlightContext();
const targetId = `test-target-${themeClassName}`;
useEffect(() => {
if (props.isChromatic) {
setActiveSpotlight(targetId);
setIsOpen(true);
}
}, [setActiveSpotlight, setIsOpen, targetId, props.isChromatic]);
return (
<div className="n-flex n-justify-center n-gap-token-64">
<FilledButton onClick={() => setActiveSpotlight(targetId)}>
Activate Spotlight
</FilledButton>
<SpotlightTarget
id={targetId}
hasPulse={!props.isChromatic}
indicatorVariant="border"
asChild
>
<TextButton onClick={() => alert('Target button clicked')}>
Target
</TextButton>
</SpotlightTarget>
<Spotlight
placement="bottom-end-top-end"
target={targetId}
actions={
<OutlinedButton
onClick={() => {
setIsOpen(false);
setActiveSpotlight(null);
}}
>
Close
</OutlinedButton>
}
>
<Spotlight.IconWrapper>
<FingerPrintIconOutline />
</Spotlight.IconWrapper>
<Spotlight.Header>This is a header</Spotlight.Header>
<Spotlight.Body>This is content</Spotlight.Body>
</Spotlight>
</div>
);
};
export default Component;
```
### With Image
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
FilledButton,
OutlinedButton,
Spotlight,
SpotlightTarget,
TextButton,
useNeedleTheme,
useSpotlightContext,
} from '@neo4j-ndl/react';
import { useEffect } from 'react';
const Component = (props: { isChromatic: boolean }) => {
const { themeClassName } = useNeedleTheme();
const { setIsOpen, setActiveSpotlight } = useSpotlightContext();
const targetId = `test-target-${themeClassName}`;
useEffect(() => {
if (props.isChromatic) {
setActiveSpotlight(targetId);
setIsOpen(true);
}
}, [setActiveSpotlight, setIsOpen, targetId, props.isChromatic]);
return (
<div className="n-flex n-justify-center n-gap-token-64">
<FilledButton onClick={() => setActiveSpotlight(targetId)}>
Activate Spotlight
</FilledButton>
<SpotlightTarget id={targetId} hasPulse={!props.isChromatic} asChild>
<TextButton onClick={() => alert('Target button clicked')}>
Target
</TextButton>
</SpotlightTarget>
<Spotlight
placement="bottom-end-top-end"
target={targetId}
actions={
<OutlinedButton
onClick={() => {
setIsOpen(false);
setActiveSpotlight(null);
}}
>
Close
</OutlinedButton>
}
>
<Spotlight.Image
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzMzMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPkV4YW1wbGUgSW1hZ2U8L3RleHQ+PC9zdmc+"
alt="example image"
/>
<Spotlight.Header>This is a header</Spotlight.Header>
<Spotlight.Body>This is content</Spotlight.Body>
</Spotlight>
</div>
);
};
export default Component;
```
### With Label
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
FilledButton,
OutlinedButton,
Spotlight,
SpotlightTarget,
TextButton,
useNeedleTheme,
useSpotlightContext,
} from '@neo4j-ndl/react';
import { useEffect } from 'react';
const Component = (props: { isChromatic: boolean }) => {
const { themeClassName } = useNeedleTheme();
const { setIsOpen, setActiveSpotlight } = useSpotlightContext();
const targetId = `test-target-${themeClassName}`;
useEffect(() => {
if (props.isChromatic) {
setActiveSpotlight(targetId);
setIsOpen(true);
}
}, [setActiveSpotlight, setIsOpen, targetId, props.isChromatic]);
return (
<div className="n-flex n-justify-center n-gap-token-64">
<FilledButton onClick={() => setActiveSpotlight(targetId)}>
Activate Spotlight
</FilledButton>
<SpotlightTarget
id={targetId}
hasPulse={!props.isChromatic}
indicatorVariant="border"
asChild
>
<TextButton onClick={() => alert('Target button clicked')}>
Target
</TextButton>
</SpotlightTarget>
<Spotlight
placement="bottom-end-top-end"
target={targetId}
actions={
<OutlinedButton
onClick={() => {
setIsOpen(false);
setActiveSpotlight(null);
}}
>
Close
</OutlinedButton>
}
>
<Spotlight.Label>NEW</Spotlight.Label>
<Spotlight.Header>This is a header</Spotlight.Header>
<Spotlight.Body>This is content</Spotlight.Body>
</Spotlight>
</div>
);
};
export default Component;
```
### With Pointer
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
FilledButton,
OutlinedButton,
Spotlight,
SpotlightTarget,
useNeedleTheme,
useSpotlightContext,
} from '@neo4j-ndl/react';
import { useEffect } from 'react';
const Component = (props: { isChromatic: boolean }) => {
const { themeClassName } = useNeedleTheme();
const { setIsOpen, setActiveSpotlight } = useSpotlightContext();
const targetId = `test-target-${themeClassName}`;
useEffect(() => {
if (props.isChromatic) {
setActiveSpotlight(targetId);
setIsOpen(true);
}
}, [setActiveSpotlight, setIsOpen, targetId, props.isChromatic]);
return (
<div className="n-flex n-justify-center n-gap-token-64">
<FilledButton onClick={() => setActiveSpotlight(targetId)}>
Activate Spotlight
</FilledButton>
<SpotlightTarget
id={targetId}
hasPulse={!props.isChromatic}
asChild
indicatorVariant="point"
indicatorPlacement="top-right"
>
<FilledButton onClick={() => alert('Target button clicked')}>
Target
</FilledButton>
</SpotlightTarget>
<Spotlight
placement="bottom-end-top-end"
target={targetId}
actions={
<OutlinedButton
onClick={() => {
setIsOpen(false);
setActiveSpotlight(null);
}}
>
Close
</OutlinedButton>
}
>
<Spotlight.Header>This is a header</Spotlight.Header>
<Spotlight.Body>This is content</Spotlight.Body>
</Spotlight>
</div>
);
};
export default Component;
```