Popover
A Popover is essentially an interactive
Tooltip for displaying an overlay anchored to a
particular element. It can set up to open automatically using a trigger
element that also serves as its anchor point, or it can be opened manually
positioned relative to an anchor element.
React
import { Popover } from '@hoverinc/design-system-react-web';Basic usage
It is used to display contextual information to the user, and should be paired with a clickable trigger element.
When Popover opens, focus is sent to the popover content. When it closes, focus is returned to the trigger.
<Popover header="Confirmation!" trigger={<Button>Trigger</Button>} inPortal>
Are you sure you want to have that milkshake?
</Popover>Close button
Pass isClosable to enable the built-in close button.
<Popover
isClosable
header="Confirmation!"
trigger={<Button>Trigger</Button>}
inPortal
>
Are you sure you want to have that milkshake?
</Popover>Action
When there is a single action associated with the popover, render a
PopoverButton in the footer.
<Popover
header="With Action"
trigger={<Button>Trigger</Button>}
footer={<PopoverButton>Action</PopoverButton>}
inPortal
>
Popover with a single action. Lorem ipsum dolor sit amet, consectetur
adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore.
</Popover>Hover
Pass isHover to open the popover when the trigger is hovered instead of
clicked.
<Popover
isHover
header="Confirmation!"
trigger={<Button>Trigger</Button>}
inPortal
>
Are you sure you want to have that milkshake?
</Popover>Color mode
By default, Popover uses the color mode context to determine whether to
display a dark or light placeholder animation. Pass colorMode to manually
force a color mode.
<HStack>
<Popover
colorMode="light"
header="Confirmation!"
trigger={<Button>Light</Button>}
footer={<PopoverButton>Yes!</PopoverButton>}
isClosable
inPortal
>
Are you sure you want to have that milkshake?
</Popover>
<Popover
colorMode="dark"
header="Confirmation!"
trigger={<Button>Dark</Button>}
footer={<PopoverButton>Yes!</PopoverButton>}
isClosable
inPortal
>
Are you sure you want to have that milkshake?
</Popover>
</HStack>Variant
By default, Popover uses a solid background. Use the glass variant when
displaying popovers over media, such as images.
<HStack>
<Popover
colorMode="light"
variant="glass"
header="Confirmation!"
trigger={<Button>Light Glass</Button>}
footer={<PopoverButton>Yes!</PopoverButton>}
isClosable
inPortal
>
Are you sure you want to have that milkshake?
</Popover>
<Popover
colorMode="dark"
variant="glass"
header="Confirmation!"
trigger={<Button>Dark Glass</Button>}
footer={<PopoverButton>Yes!</PopoverButton>}
isClosable
inPortal
>
Are you sure you want to have that milkshake?
</Popover>
</HStack>Rendering the Popover in a Portal
To render a popover in a Portal, pass the
inPortal prop.
<Popover
inPortal
header="Header"
footer="This is the footer"
trigger={<Button>Trigger</Button>}
>
Are you sure you want to have that milkshake?
</Popover>Initial focus
By default, focus is to sent to the popover content when it opens. Pass the
initialFocusRef prop to send focus to a specific element instead.
() => {
const initialFocusRef = React.useRef();
return (
<Popover
initialFocusRef={initialFocusRef}
placement="bottom"
trigger={<Button>Trigger</Button>}
closeOnBlur={false}
header="Manage Your Channels"
width="750"
footer={
<HStack justifyContent="space-between" mt="300" width="100%">
<Body size="small" color="neutral.600" fontWeight="bodyBold">
Step 2 of 4
</Body>
<HStack>
<Button variant="secondary">Setup Email</Button>
<Button variant="primary" ref={initialFocusRef}>
Next
</Button>
</HStack>
</HStack>
}
inPortal
>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore.
</Popover>
);
};Controlled usage
You can control the opening and closing of the popover by passing the isOpen,
and onClose props.
Sometimes you might need to set the returnFocusOnClose prop to false to
prevent popover from returning focus to PopoverTrigger's children.
() => {
const { isOpen, onToggle, onClose } = useDisclosure();
return (
<VStack alignItems="stretch" width="fit-content">
<Button onClick={onToggle}>External Trigger</Button>
<Popover
returnFocusOnClose={false}
isOpen={isOpen}
onClose={onClose}
placement="right"
closeOnBlur={false}
trigger={<Button variant="secondary">Internal Trigger</Button>}
header="Confirmation"
footer={
<>
<Button variant="secondary" flex={1} onClick={onClose}>
Cancel
</Button>
<Button colorScheme="danger" flex={1}>
Apply
</Button>
</>
}
inPortal
>
Are you sure you want to continue with your action?
</Popover>
</VStack>
);
};Anchor
Pass anchor instead of trigger to prevent trigger any action. The wrapped
component will become a position reference.
When passing an anchor instead of a trigger, the state of the Popover must
be controlled with external state (controlled mode).
Anchor must accept a ref
As with trigger, if you pass a custom component inside
anchor ensure that it uses forwardRef so the popover is triggered
successfully.
() => {
const [isEditing, setIsEditing] = useBoolean();
const [color, setColor] = React.useState('salmon');
return (
<HStack>
<Popover
isOpen={isEditing}
onOpen={setIsEditing.on}
onClose={setIsEditing.off}
closeOnBlur={false}
isLazy
lazyBehavior="keepMounted"
anchor={
<TextInput
color={color}
w="auto"
display="inline-flex"
isDisabled={!isEditing}
size="small"
defaultValue="Popover Anchor"
/>
}
inPortal
width="fit-content"
bodyProps={{ display: 'flex', flexDirection: 'row' }}
>
<Label htmlFor="#colors" isSemiBold mr="300">
Colors:
</Label>
<RadioGroup
value={color}
onChange={newColor => setColor(newColor)}
id="colors"
>
<HStack>
<Radio value="salmon">salmon</Radio>
<Radio value="royalblue">royalblue</Radio>
<Radio value="turquoise">turquoise</Radio>
<Radio value="violet">violet</Radio>
</HStack>
</RadioGroup>
</Popover>
<Button onClick={setIsEditing.toggle}>
{isEditing ? 'Save' : 'Edit'}
</Button>
</HStack>
);
};Accessing internal state
All Popover elements have access to the internal popover state (isOpen and
onClose). Use the render prop pattern to gain access to them.
() => {
const initialRef = React.useRef();
return (
<Popover
closeOnBlur={false}
placement="left"
initialFocusRef={initialRef}
trigger={({ isOpen }) => (
<Button iconAfter={isOpen ? iconArrowLeft : iconArrowLeft}>
Trigger
</Button>
)}
header={({ onClose }) => (
<>
This is the <em>header</em>,
<Link as="button" onClick={onClose}>
close me
</Link>.
</>
)}
footer={({ onClose }) => (
<>
This is the <em>footer</em>,
<Link as="button" onClick={onClose}>
close me
</Link>.
</>
)}
inPortal
>
{({ onClose }) => (
<Body mt="0" mb="200">
Hello. Nice to meet you! This is the <em>body</em> of the popover.
<Link as="button" onClick={onClose} ref={initialRef}>
Click here to close me.
</Link>
</Body>
)}
</Popover>
);
};Placement
Since popover is powered by Popper, you can change the
placement of the popover by passing the placement prop.
The possible values are:
| Start | Center | End |
|---|---|---|
bottom-start | bottom (default) | bottom-end |
auto-start | auto | auto-end |
top-start | top | top-end |
left-start | left | left-end |
right-start | right | right-end |
() => {
const [placement, setPlacement] = React.useState('top-start');
return (
<HStack>
<Spacer flex={1} />
<Popover
placement={placement}
header="Popover placement"
trigger={<Button>Click Me</Button>}
inPortal
>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore.
</Popover>
<Select
flex={1}
onChange={event => setPlacement(event.target.value)}
size="small"
value={placement}
>
{[
'bottom-start',
'bottom',
'bottom-end',
'auto-start',
'auto',
'auto-end',
'top-start',
'top',
'top-end',
'left-start',
'left',
'left-end',
'right-start',
'right',
'right-end',
].map(p => (
<option key={p} value={p}>
{p}
</option>
))}
;
</Select>
</HStack>
);
};Lazily mounting Popover
By default, the Popover component renders its content to the DOM, meaning that
invisible popover contents are still rendered but are hidden by styles.
If you want to defer rendering of popover content until that Popover is
opened, you can use the isLazy prop. This is useful if your content needs to
be extra performant, or make network calls on mount that should only happen when
the component is displayed.
<Popover
isLazy
inPortal
header="Lazy Content"
trigger={<Button>Click Me</Button>}
>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore.
</Popover>Accessibility
Keyboard and Focus
- When the popover is opened, focus is moved to the
PopoverContent. If theinitialFocusRefis set, then focus moves to the element with thatref. - When the popover is closed, focus returns to the trigger. If you set
returnFocusOnClosetofalse, focus will not return. - If trigger is set to
hover:- Focusing on or mousing over the trigger will open the popover.
- Blurring or mousing out of the trigger will close the popover. If you move
your mouse into the
PopoverContent, it'll remain visible.
- If trigger is set to
click:- Clicking the trigger or using the
SpaceorEnterwhen focus is on the trigger will open the popover. - Clicking the trigger again will close the popover.
- Clicking the trigger or using the
- Hitting the
Esckey while the popover is open and focus is within thePopoverContent, will close the popover. If you setcloseOnEsctofalse, it will not close. - Clicking outside or blurring out of the
PopoverContentcloses the popover. If you setcloseOnBlurtofalse, it will not close.
ARIA Attributes
- If the trigger is set to
click, thePopoverContentelement has role set todialog. If the trigger is set tohover, thePopoverContenthasroleset totooltip. - The
PopoverContenthasaria-labelledbyset to theidof thePopoverHeader. - The
PopoverContenthasaria-describedbyset to theidof thePopoverBody. - The
PopoverContenthasaria-hiddenset totrueorfalsedepending on the open/closed state of the popover. - The trigger has
aria-haspopupset totrueto denote that it triggers a popover. - The trigger has
aria-controlsset to theidof thePopoverContentto associate the popover and the trigger. - The trigger has
aria-expandedset totrueorfalsedepending on the open/closed state of the popover.