Popovers are general components with multiple purposes. For navigation or actions, use an Action Menu. For data input, use a Select.
<Stack alignItems='center' justifyContent='center' className='p-4' style={{ height: '200px' }}>
<Popover
trigger={<Button inactive>Element</Button>}
open
direction="r"
header="Popover Header"
width="xs"
padding="s"
>
<BodyText size="small">Popover content.</BodyText>
</Popover>
</Stack>
Sizes
Popovers have a set of fixed width options, ranging from extra small (192px) to a large (640px). This is similar in how the Modal's width operates. There is also an option for the popover to extend to 100% the width of the element it is wrapped around.
<State>
{([open, setOpen]) => (
<>
<ButtonGroup>
{[['xs', 'Extra Small'], ['s', 'Small'], ['m', 'Medium'], ['l', 'Large']].map((item, index) => (
<Popover
key={index}
portal
trigger={<Button onClick={() => setOpen(open !== index ? index : null)}>{item[1]}</Button>}
open={open === index}
direction="t"
width={item[0]}
>
<BodyText size="small">{`${item[1]} Popover.`}</BodyText>
<BodyText size="small">Second line of text.</BodyText>
</Popover>
))}
</ButtonGroup>
<br />
<Popover
portal
trigger={
<Card onClick={() => setOpen(open !== 4 ? 4 : null)}>
Match the width of the wrapped element (click to open)
</Card>
}
open={open === 4}
direction="b"
width="100"
>
<BodyText size="small">Full-width Popover.</BodyText>
<BodyText size="small">Second line of text.</BodyText>
</Popover>
</>
)}
</State>
Padding
There are three configurable padding options for the overall modal: small, medium, and large. These options are independent of the width properties.
Small
Made for small, simple dropdowns.
<Popover
trigger={<Button inactive iconName='more_vert' />}
open
sharp
direction="br"
header="Popover Header"
width="auto"
padding="s"
>
<BodyText size="small">Popover content.</BodyText>
<BodyText size="small">Second line of text.</BodyText>
</Popover>
Medium
Default — for most use cases. Complex dropdowns, small–medium sized interactive overlays on screen.
<Stack alignItems="center" style={{ height: '320px' }}>
<Popover
trigger={<Button inactive iconName='more_vert' />}
header={
<Stack justifyContent="space-between" alignItems="center" className="w-100">
<Headline size="small">Popover Header</Headline>
<BodyText size="small" className="c-neutral-90">#12345</BodyText>
</Stack>
}
headerAlign="space-between"
footer={
<ButtonGroup>
<Button xsmall>Cancel</Button>
<Button xsmall primary>Save</Button>
</ButtonGroup>
}
footerAlign="right"
open
direction="r"
width="s"
padding="m"
>
<TagGroup style={{ marginLeft: "-4px", marginTop: "-4px" }}>
<Tag>A Tag</Tag>
<Tag>Another Tag</Tag>
<Tag>Maintence</Tag>
</TagGroup>
<BodyText size="small" className="m-t-1">To be in the same Business Unit group, Business Units must have the same arrival windows. Please select which business units to remove and/or adjust your arrival windows in settings.</BodyText>
</Popover>
</Stack>
Large
Similiar in nature to a modal, these are for larger flows.
<Stack alignItems="center" style={{ height: '420px' }}>
<Popover
trigger={<Button inactive iconName='warning' />}
header={
<Stack justifyContent="space-between" alignItems="center" className="w-100">
<Headline>Fix Inconsistent Arrival Windows</Headline>
<BodyText className="c-neutral-90">#12345</BodyText>
</Stack>
}
headerAlign="space-between"
footer={
<ButtonGroup>
<Button small>Cancel</Button>
<Button small negative>Remove Selected</Button>
</ButtonGroup>
}
footerAlign="right"
open
direction="r"
width="m"
padding="l"
>
<BodyText subdued>To be in the same Business Unit group, Business Units must have the same arrival windows. Please select which business units to remove and/or adjust your arrival windows in <Link primary>settings</Link>.</BodyText>
<Form className="m-t-3">
<Form.Group grouped>
<Form.Radio
label={<label><strong>Rebel Alliance</strong><BodyText el="div" size="small" className="m-b-half">The Good Guys</BodyText></label>}
value="1"
/>
<Form.Radio
label={<label><strong>Galactic Empire</strong><BodyText el="div" size="small" className="m-b-half">The Bad Guys</BodyText></label>}
value="2"
/>
</Form.Group>
</Form>
</Popover>
</Stack>
Popovers have optional sections for a header and footer. This can be useful when the body section is a scrollable area.
Body Sections
The body section is where most or all of the content of a popover exists. It exists between the optional popover header and footer.
Line Divider
A divider can be used to separate out sections within a popover. By default, dividers extend the full width of the popover, and do not provide any built in padding above or below it.
<Stack alignItems='center' justifyContent='center' spacing='4' className='p-4' style={{ height: '200px' }}>
<Popover
trigger={<Button inactive>Element</Button>}
open
direction="r"
width="xs"
padding="s"
>
<Form className="w-100 m-b-2">
<Form.Input placeholder="Message Jane Doe" label="Label" className="w-100" />
</Form>
<Popover.Divider />
<Form className="w-100 m-y-2">
<Form.Input placeholder="Message Jane Doe" label="Label" className="w-100" />
</Form>
<Popover.Divider />
<Form className="w-100 m-t-2">
<Form.Input placeholder="Message Jane Doe" label="Label" className="w-100" />
</Form>
</Popover>
</Stack>
Full-width Image
A full-width image can be applied to the header section of a popover.
<Stack alignItems="center" className="example-full" style={{ height: '460px' }}>
<style>{`
.demo-shadow {
box-sizing: border-box;
position: absolute;
bottom: 0;
left: 0;
z-index: 2;
background: linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 100%);
width: calc(100% + 48px);
height: calc(100% + 32px);
display: flex;
align-items: flex-end;
padding: 16px 24px;
transition: .1s all ease;
opacity: 0.01;
}
.demo-shadow:hover {
opacity: 1;
}
.demo-shadow:hover .Text { opacity: 1; }
`}</style>
<Popover
trigger={<Button inactive iconName='more_vert' />}
header={
<div className="d-f align-items-start" style={{ position: 'relative' }}>
<div className="demo-shadow">
<Headline className="c-white" size="small">Jane Doe</Headline>
</div>
<img src={IG88} alt="Stock Image" style={{maxWidth: '100%'}}/>
</div>
}
headerNoPadding
open
direction="r"
width="s"
padding="m"
className="of-hidden"
>
<BodyText size="small" subdued className="m-b-1" style={{marginTop: "-8px"}}>10:22 AM Local Time</BodyText>
<Popover.Divider />
<BodyText size="small" className="m-b-1 m-t-1">Clock In</BodyText>
<BodyText size="small" className="m-b-1">Start Meal</BodyText>
<BodyText size="small" className="m-b-1">Non-job timesheets</BodyText>
<BodyText size="small" className="m-b-1">Non-job purchase orders</BodyText>
<Popover.Divider />
<Form className="w-100 p-t-2">
<Form.Input placeholder="Message Jane Doe" className="w-100" />
</Form>
</Popover>
</Stack>
No Padding Sections
Overrides the popover's built in padding (via negative margins) for a particular section. This can be used to introduce a full-width background color to a section.
<Stack alignItems="flex-start" justifyContent="space-between">
<Popover
trigger={<Button inactive iconName='more_vert' />}
headerNoPadding
open
header="Popover Header"
direction="br"
width="xs"
padding="m"
className="of-hidden"
>
No padding on header area
</Popover>
<Popover
trigger={<Button inactive iconName='more_vert' />}
contentNoPadding
open
header="Popover Header"
direction="bl"
width="xs"
padding="m"
className="of-hidden"
>
No padding on content area
</Popover>
</Stack>
The body content of a popover can be given a fixed height and become scrollable. The header and footer content will remain fixed at the bottom.
<Stack alignItems="center" style={{ height: '520px' }}>
<Popover
header={
<Stack justifyContent="space-between" alignItems="center" className="w-100">
<Headline size="small">Popover Header</Headline>
<BodyText size="small" className="c-neutral-90">#12345</BodyText>
</Stack>
}
headerAlign="space-between"
footer={
<ButtonGroup>
<Button small>Cancel</Button>
<Button small primary>Remove Selected</Button>
</ButtonGroup>
}
scrollHeight="320px"
footerAlign="right"
open
direction="r"
width="m"
padding="l"
>
<BodyText>
Pop-up kale chips four dollar toast gastropub you probably haven't heard of them prism tote bag.
Paleo thundercats godard glossier +1, iceland anim.
Readymade sriracha occaecat, crucifix bicycle rights retro seitan exercitation craft beer kale
chips minim.
Do post-ironic wayfarers, seitan etsy small batch hammock green juice hexagon whatever hoodie
ipsum fashion axe copper mug.
Fingerstache put a bird on it palo santo craft beer.
</BodyText>
<BodyText>
Adaptogen officia cred ut enamel pin.
Man bun fixie blue bottle minim proident franzen raw denim fanny pack, church-key edison bulb
butcher lumbersexual vaporware ethical YOLO.
Mlkshk elit austin succulents live-edge poke esse pork belly williamsburg consectetur
helvetica craft beer put a bird on it pop-up aliqua.
Viral irure synth laboris laborum.
</BodyText>
<BodyText>
Pop-up kale chips four dollar toast gastropub you probably haven't heard of them prism tote bag.
Paleo thundercats godard glossier +1, iceland anim.
Readymade sriracha occaecat, crucifix bicycle rights retro seitan exercitation craft beer kale
chips minim.
Do post-ironic wayfarers, seitan etsy small batch hammock green juice hexagon whatever hoodie
ipsum fashion axe copper mug.
Fingerstache put a bird on it palo santo craft beer.
</BodyText>
</Popover>
</Stack>
Directions
Popovers can appear in many directions relative to an element. Direction is manually controlled. The default is to appear above the element.
<State initial={true}>
{([open, setOpen]) => {
const Trigger = (props) => (
<Button onClick={() => setOpen(!open)} style={{ width: '130px' }}>
{props.children}
</Button>
);
const popoverContent = (
<>
<BodyText size="small">Popover content.</BodyText>
<BodyText size="small">Second line of text.</BodyText>
</>
)
const footerContent = <BodyText size="small">Popover Footer</BodyText>;
const popoverProps = {
children: popoverContent,
header: 'Popover Header',
footer: footerContent,
open: open,
portal: true,
width: 'xs',
};
return (
<>
<Stack className="w-100 m-b-8" alignItems='flex-end' justifyContent='space-around' style={{ height: '240px' }}>
<Popover
{...popoverProps}
trigger={<Trigger>Top Left</Trigger>}
direction="tl"
/>
<Popover
{...popoverProps}
trigger={<Trigger>Top</Trigger>}
direction="t"
/>
<Popover
{...popoverProps}
trigger={<Trigger>Top Right</Trigger>}
direction="tr"
/>
</Stack>
<Stack justifyContent="center" className="d-f w-100 justify-content-around" spacing={5}>
<Stack direction='column' alignItems='center' justifyContent='space-around' style={{ height: '500px' }}>
<Popover
{...popoverProps}
trigger={<Trigger>Left Top</Trigger>}
direction="lt"
/>
<Popover
{...popoverProps}
trigger={<Trigger>Left</Trigger>}
direction="l"
/>
<Popover
{...popoverProps}
trigger={<Trigger>Left Bottom</Trigger>}
direction="lb"
/>
</Stack>
<Stack direction='column' alignItems='center' justifyContent='space-around' style={{ height: '500px' }}>
<Popover
{...popoverProps}
trigger={<Trigger>Right Top</Trigger>}
direction="rt"
/>
<Popover
{...popoverProps}
trigger={<Trigger>Right</Trigger>}
direction="r"
/>
<Popover
{...popoverProps}
trigger={<Trigger>Right Bottom</Trigger>}
direction="rb"
/>
</Stack>
</Stack>
<Stack className="m-t-8" alignItems='flex-start' justifyContent='space-around' style={{ height: '240px' }}>
<Popover
{...popoverProps}
trigger={<Trigger>Bottom Left</Trigger>}
direction="bl"
/>
<Popover
{...popoverProps}
trigger={<Trigger>Bottom</Trigger>}
direction="b"
/>
<Popover
{...popoverProps}
trigger={<Trigger>Bottom Right</Trigger>}
direction="br"
/>
</Stack>
</>
)
}}
</State>
Open on Hover
The popover can also be triggered through hover. In this scenario, they behave similar to the tooltip, but with more potential information to display.
<State initial={false}>
{([open, setOpen]) => (
<span>
<Popover
trigger={
<Button
onMouseOver={() => setOpen(true)}
onMouseOut={() => setOpen(false)}
>
Hover me!
</Button>
}
open={open}
direction="r"
width="xs"
el="span"
>
<BodyText size="small">Hello</BodyText>
</Popover>
</span>
)}
</State>
Close on Body and Button Click
More complex open and close interactions are possible. For example, a popover can be opened with both a hover or a mouse click, and a popover can be closed by clicking outside body content.
<State initial={false}>
{([open, setOpen]) => {
const handleClick = (e) => !this.node.contains(e.target) ? closePopover() : null;
const openPopover = () => setOpen(true);
const closePopover = () => setOpen(false);
const toggleOpen = () => setOpen(!open);
React.useEffect(() => {
document.addEventListener('mousedown', handleClick, false);
return () => {
document.removeEventListener('mousedown', handleClick, false);
}
}, []);
return (
<span ref={node => this.node = node}>
<Popover
trigger={
<Button
onMouseOver={openPopover}
onClick={toggleOpen}
>
Hover or click me!
</Button>
}
open={open}
direction="r"
width="xs"
el="span"
>
<BodyText size="small">Hello</BodyText>
</Popover>
</span>
)}
}
</State>
Open through a React Portal
Popovers are typically wrapped in another element. However they can be opened outside of the DOM hierarchy through a React portal.
<State initial={true}>
{([portal, setPortal]) => (
<Popover
open
portal={portal}
direction="t"
width="xs"
el="span"
trigger={
<ToggleSwitch
onClick={() => setPortal(!portal)}
label="Portal"
checked={portal}
/>}
>
<BodyText size="small">Popover content.</BodyText>
<BodyText size="small">Second line of text.</BodyText>
</Popover>
)}
</State>
Best Practices
- Popovers should be placed next to the element it is related to.
- Popover information should not be critical to the page.
- Can be used to suggest or guide users through a page.
- To interrupt the user flow with content, use a modal, dialog, or takeover.
- For adding quick clarity to an element via hover, use a tooltip.
- For popover with actionable menu, use a action menu.
- For popover for input or dropdown, use a select.
Importing
import { Popover } from '@servicetitan/design-system';