mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 18:22:39 +00:00
89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
import { Placement } from '@popperjs/core';
|
|
import { PropsWithChildren, ReactNode, useEffect, useRef, useState } from 'react';
|
|
import { usePopper } from 'react-popper';
|
|
|
|
type RequiredProps = {
|
|
/** The content to display in the popper */
|
|
content: ReactNode;
|
|
/** Whether the popper is open */
|
|
isOpen: boolean;
|
|
};
|
|
|
|
type OptionalProps = Partial<{
|
|
/** Called when the user clicks outside the popper */
|
|
onClickOutside: () => void;
|
|
/** Where to place the popper relative to the reference element */
|
|
placement: Placement;
|
|
}>;
|
|
|
|
type Props = RequiredProps & OptionalProps;
|
|
|
|
/**
|
|
* ## Popper
|
|
* Popper lets you position elements so that they don't go out of the bounds of the window.
|
|
* @url https://popper.js.org/react-popper/ for more information.
|
|
*/
|
|
export function Popper(props: PropsWithChildren<Props>) {
|
|
const { children, content, isOpen, onClickOutside, placement } = props;
|
|
|
|
const [referenceElement, setReferenceElement] =
|
|
useState<HTMLDivElement | null>(null);
|
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
|
null
|
|
);
|
|
|
|
// One would imagine we could just use useref here, but it's against react-popper documentation and causes a positioning bug
|
|
// We still need them to call focus and clickoutside events :(
|
|
const popperRef = useRef<HTMLDivElement | null>(null);
|
|
const parentRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
|
placement,
|
|
});
|
|
|
|
/** Close the popper when the user clicks outside */
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (
|
|
!popperRef.current?.contains(event.target as Node) &&
|
|
!parentRef.current?.contains(event.target as Node)
|
|
) {
|
|
onClickOutside?.();
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
} else {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [isOpen]);
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
ref={(node) => {
|
|
setReferenceElement(node);
|
|
parentRef.current = node;
|
|
}}>
|
|
{children}
|
|
</div>
|
|
{isOpen && (
|
|
<div
|
|
ref={(node) => {
|
|
setPopperElement(node);
|
|
popperRef.current = node;
|
|
}}
|
|
style={{ ...styles.popper, zIndex: 5 }}
|
|
{...attributes.popper}>
|
|
{content}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|