diff --git a/packages/react-components/src/Popper/popper.tsx b/packages/react-components/src/Popper/popper.tsx index 5dd9d774..269c4a94 100644 --- a/packages/react-components/src/Popper/popper.tsx +++ b/packages/react-components/src/Popper/popper.tsx @@ -10,7 +10,7 @@ type BaseProps = { /** * Popper render node. */ - children: React.ReactNode; + children: React.ReactNode | ((style: React.CSSProperties) => React.ReactNode); /** * It's used to set the position of the popper. */ @@ -27,9 +27,14 @@ type BaseProps = { * Make your popper the same width as the reference. */ allowUseSameWidth?: boolean; + /** + * Popper.js is based on a "plugin-like" architecture, + * most of its features are fully encapsulated "modifiers". + */ + modifiers?: Modifier[]; }; -type PopperProps = BaseProps & React.HTMLAttributes; +type PopperProps = BaseProps & Omit, 'children'>; export const Popper: React.FC = (props) => { const { @@ -39,33 +44,42 @@ export const Popper: React.FC = (props) => { open, disablePortal, allowUseSameWidth, + modifiers, ...other } = props; const [popperElement, setPopperElement] = React.useState(null); - const sameWidthModifier: Modifier<'sameWidth'> = React.useMemo( - () => ({ - name: 'sameWidth', - enabled: allowUseSameWidth, - phase: 'beforeWrite', - requires: ['computeStyles'], - fn: ({ state }) => { - // eslint-disable-next-line no-param-reassign - state.styles.popper.width = `${state.rects.reference.width}px`; + const popperModifiers: Modifier[] = React.useMemo(() => { + let baseModifiers: Modifier[] = [ + { + name: 'sameWidth', + enabled: allowUseSameWidth, + phase: 'beforeWrite', + requires: ['computeStyles'], + fn: ({ state }) => { + // eslint-disable-next-line no-param-reassign + state.styles.popper.width = `${state.rects.reference.width}px`; + }, + effect: ({ state }) => { + // @ts-ignore + // eslint-disable-next-line no-param-reassign + state.elements.popper.style.width = `${state.elements.reference.offsetWidth}px`; + }, }, - effect: ({ state }) => { - // @ts-ignore - // eslint-disable-next-line no-param-reassign - state.elements.popper.style.width = `${state.elements.reference.offsetWidth}px`; - }, - }), - [], - ); + ]; + + if (modifiers && modifiers.length) { + baseModifiers = baseModifiers.concat(modifiers); + } + + return baseModifiers; + }, [allowUseSameWidth, modifiers]); + const { styles, attributes } = usePopper( anchorEl, popperElement, { placement, - modifiers: [sameWidthModifier], + modifiers: popperModifiers, }, ); @@ -77,7 +91,7 @@ export const Popper: React.FC = (props) => { role="tooltip" {...attributes.popper} > - {children} + {typeof children === 'function' ? children(styles.arrow) : children} ); diff --git a/packages/react-components/src/Tooltip/tooltip.tsx b/packages/react-components/src/Tooltip/tooltip.tsx index f7f7cbbc..9ea5a866 100644 --- a/packages/react-components/src/Tooltip/tooltip.tsx +++ b/packages/react-components/src/Tooltip/tooltip.tsx @@ -66,6 +66,10 @@ export type TooltipBaseProps = { * Add delay in hiding the tooltip. */ leaveDelay?: number; + /** + * If `true`, adds an arrow to the tooltip. + */ + arrow?: boolean; }; export type TooltipProps = TooltipBaseProps & Omit, 'title' | 'children'>; @@ -85,40 +89,72 @@ const stylesKeyframeOpacity = keyframes` } `; -const stylesTooltip = () => css({ +const stylesTooltip = (props: TooltipBaseProps) => css({ label: 'Tooltip', boxShadow: 'var(--pv-shadow-light-low)', maxWidth: '300px', wordWrap: 'break-word', fontSize: 0, animation: `${stylesKeyframeOpacity} 225ms`, + position: 'relative', + ...(props.size === 'small' && { + padding: '5px 8px', + }), + ...(props.size === 'large' && { + padding: '8px 10px', + }), }); -const stylesSizeSmall = () => css({ - label: 'small', - padding: '5px 8px', -}); - -const stylesSizeLarge = () => css({ - label: 'large', - padding: '8px 10px', -}); - -const stylesPopper = (interactive?: boolean) => css({ +const stylesPopper = (props: TooltipBaseProps) => css({ label: 'Popper', - pointerEvents: interactive ? 'auto' : 'none', + pointerEvents: props.interactive ? 'auto' : 'none', zIndex: 1500, '&[data-popper-placement^="bottom"]': { padding: 'var(--pv-size-base-3) 0px', + '[data-popper-arrow]': { + top: 0, + marginTop: '-4px', + }, }, '&[data-popper-placement^="top"]': { padding: 'var(--pv-size-base-3) 0px', + '[data-popper-arrow]': { + bottom: 0, + marginBottom: '-4px', + }, }, '&[data-popper-placement^="right"]': { padding: '0px var(--pv-size-base-3)', + '[data-popper-arrow]': { + left: 0, + marginLeft: '-4px', + }, }, '&[data-popper-placement^="left"]': { padding: '0px var(--pv-size-base-3)', + '[data-popper-arrow]': { + right: 0, + marginRight: '-4px', + }, + }, +}); + +const stylesArrow = (props: TooltipBaseProps) => css({ + label: 'arrow', + width: '8px', + height: '8px', + background: 'transparent', + position: 'absolute', + display: 'block', + color: props.color === 'white' ? 'var(--pv-color-white)' : 'var(--pv-color-gray-10)', + '&::before': { + content: '""', + margin: 'auto', + display: 'block', + width: '100%', + height: '100%', + backgroundColor: 'currentColor', + transform: 'rotate(45deg)', }, }); /** @@ -141,6 +177,7 @@ export const Tooltip: React.FC = (props) => { disablePortal, enterDelay, leaveDelay, + arrow, ...other } = props; const [open, setOpen] = useControllableState({ @@ -149,6 +186,7 @@ export const Tooltip: React.FC = (props) => { }); const nodeRef = React.useRef(null); const multiRef = useMergedRef((children as any).ref, nodeRef); + const [arrowRef, setArrowRef] = React.useState(null); const enterTimer = React.useRef(); const leaveTimer = React.useRef(); @@ -218,31 +256,51 @@ export const Tooltip: React.FC = (props) => { anchorEl={nodeRef.current} open={title && open} placement={placement} - className={cx({ - [stylesPopper(interactive)]: true, - })} + className={stylesPopper(props)} disablePortal={disablePortal} + modifiers={[ + { + name: 'arrow', + enabled: Boolean(arrowRef), + options: { + element: arrowRef, + padding: 8, + }, + }, + ]} {...popperProps} > - - ( + - {title} - - + + {title} + + { + arrow + ? ( + + ) + : null + } + + )} ); @@ -257,4 +315,5 @@ Tooltip.defaultProps = { disablePortal: true, enterDelay: 100, leaveDelay: 0, + arrow: false, };