import React, { useMemo, useState, type HTMLAttributes } from 'react' import classnames from 'classnames' import { usePopper } from 'react-popper' import { Heading } from '~components/Heading' import { Icon } from '~components/Icon' import { Text } from '~components/Text' import { type OverrideClassName } from '~components/types/OverrideClassName' import { type Placement, type PopoverSize } from './types' import { mapLineVariant, mapSizeToClass } from './utils/classMappers' import styles from './Popover.module.scss' export type PopoverProps = { children: React.ReactNode placement?: Placement size?: PopoverSize heading?: string dismissible?: boolean onClose?: (event: React.MouseEvent) => void singleLine?: boolean referenceElement: HTMLElement | null } & OverrideClassName> // Sync with styles.scss const arrowWidth = 14 const arrowHeight = 7 /** * {@link https://cultureamp.design/components/popover/ Guidance} | * {@link https://cultureamp.design/storybook/?path=/docs/components-popover--default-kaizen-site-demo Storybook} */ export const Popover = ({ children, placement = 'top', size = 'small', heading, dismissible = false, onClose, singleLine = false, referenceElement, classNameOverride, ...restProps }: PopoverProps): JSX.Element => { const [popperElement, setPopperElement] = useState(null) const [arrowElement, setArrowElement] = useState(null) const { styles: popperStyles, attributes } = usePopper(referenceElement, popperElement, { modifiers: [ { name: 'arrow', options: { element: arrowElement, // Ensures that the arrow doesn't go too far to the left or right // of the tooltip. padding: arrowWidth / 2 + 10, }, }, { name: 'offset', options: { offset: [0, arrowHeight + 6], }, }, { name: 'preventOverflow', options: { // Makes sure that the popover isn't flush up against the end of the // viewport padding: 8, altAxis: true, altBoundary: true, tetherOffset: 50, }, }, { name: 'flip', options: { padding: 8, altBoundary: true, fallbackPlacements: ['left', 'top', 'bottom', 'right'], }, }, ], placement, }) return (
{heading && (
{heading} {dismissible && ( )}
)} {children}
) } Popover.displayName = 'Popover' type PopoverPropsWithoutRef = Omit /** * How to use: * * const [referenceElementRef, Popover] = usePopover() * * return (<> * * Hello world * ) * * The purpose of this hook is to abstract away some of the awkwardness with the * requirement of passing in refs with popper. We need to use `useState` instead * of `useRef`, which may not be immediately intuitive. * * The popper documentation to help provide more context: * https://popper.js.org/react-popper/v2/hook/ */ export const usePopover = (): [ (element: HTMLElement | null) => void, (props: PopoverPropsWithoutRef) => JSX.Element | null, ] => { const [referenceElement, setReferenceElement] = useState(null) // I guess the problem with this pattern, is that every time referenceElement // changes, a brand new component is generated, which would be bad for memoization. // In this situation however, the value is rarely going to change, and // popovers aren't going to include content with expensive render times. const PopoverWithRef = useMemo( // eslint-disable-next-line react/display-name () => (props: PopoverPropsWithoutRef) => referenceElement ? : null, [referenceElement], ) return [setReferenceElement, PopoverWithRef] }