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]
}