glass-app-manager
Version:
Informatica's Glass Framework CLI for bootstrapping
150 lines (128 loc) • 4.33 kB
JavaScript
// @flow
import * as React from "react";
import Positioner from "react-positioner";
export type PopoverProps = {
/**
* Specify how you want the user to interact with the trigger.
*/
on?: "click" | "focus" | "hover",
/**
* Set `true` to open the popup by default. Use this when
* you want to _control_ when the popup is open or not.
*/
open?: boolean,
/**
* Specify a custom trigger. This makes the popup _uncontrolled_
* and state will be handled internally.
*/
trigger?: React.ElementType,
children: React.Node,
/**
* Specify the content of the popup.
*/
content: React.Node,
defaultOpen?: boolean,
onOpen?: Event => void,
onClose?: Event => void,
bodyOffset?: number,
position?: "top" | "top-left" | "top-right" | "bottom" | "bottom-left" | "bottom-right" | "left" | "right",
targetOffset?: number,
};
export default function Popover({
bodyOffset = 6,
children,
content,
defaultOpen = false,
open,
on = "",
onOpen,
onClose,
position = "bottom",
targetOffset = 8,
trigger,
...rest
}: PopoverProps) {
const [internalIsOpen, setInternalIsOpen] = React.useState(defaultOpen);
const [focus, setFocus] = React.useState(false);
// Handle close event
const handleClose = React.useCallback(
e => {
onClose && onClose(e); // Trigger user's custom onClose event
setInternalIsOpen(false);
},
[onClose, setInternalIsOpen]
);
// Handle clicks on content component
const handleContentClick = React.useCallback(
e => {
if (content.props && content.props.onClick) {
content.props.onClick(e); // Trigger any onClick events inside content
}
// Prevent document's click listener from closing content when clicked
e.nativeEvent.stopImmediatePropagation();
},
[content.props]
);
// Handle open event
const handleOpen = React.useCallback(
e => {
onOpen && onOpen(e); // Trigger
setInternalIsOpen(true);
},
[onOpen]
);
// Handle clicks on trigger component
const handleToggle = React.useCallback(
e => {
if (open || (open === undefined && internalIsOpen)) {
handleClose(e);
} else {
handleOpen(e);
}
},
[open, internalIsOpen, handleClose, handleOpen]
);
const getEventProps = React.useMemo(
() => ({
...(on.indexOf("hover") !== -1
? {
onMouseEnter: handleOpen,
onMouseLeave: handleClose,
}
: {}),
...(on.indexOf("focus") !== -1
? {
onFocus: e => {
handleOpen(e);
setFocus(true);
},
// TODO: Fix the race condition with document click listener
onBlur: () => setFocus(false),
}
: {}),
...(on.indexOf("click") !== -1
? {
onClick: handleToggle,
}
: {}),
}),
[on, handleClose, handleOpen, handleToggle, setFocus]
);
// Click listener for clicks outside of the trigger component
React.useEffect(() => {
if ((open || (open === undefined && internalIsOpen)) && !focus) {
document.addEventListener("click", handleClose);
}
return () => document.removeEventListener("click", handleClose);
}, [internalIsOpen, open, focus, handleClose]);
return (
<Positioner
content={React.cloneElement(content, { onClick: handleContentClick })}
isShown={open !== undefined ? open : internalIsOpen}
position={position}
bodyOffset={bodyOffset}
targetOffset={targetOffset}>
{React.cloneElement(trigger || children, { ...getEventProps })}
</Positioner>
);
}