import {forwardRef} from 'react'; import {PopperProps} from 'react-popper'; import {SerializedStyles} from '@emotion/react'; import styled from '@emotion/styled'; import {HTMLMotionProps, motion, MotionProps, MotionStyle} from 'framer-motion'; import OverlayArrow from 'sentry/components/overlayArrow'; import {defined} from 'sentry/utils'; import testableTransition from 'sentry/utils/testableTransition'; type OriginPoint = Partial<{x: number; y: number}>; interface OverlayProps extends HTMLMotionProps<'div'> { /** * Whether the overlay should animate in/out. If true, we'll also need * the `placement` and `originPoint` props. */ animated?: boolean; /** * Props to be passed into . If undefined, the overlay will * render with no arrow. */ arrowProps?: React.ComponentProps; children?: React.ReactNode; /** * The CSS styles for the "origin point" over the overlay. Typically this * would be the arrow (or tip). */ originPoint?: OriginPoint; /** * Additional style rules for the overlay content. */ overlayStyle?: React.CSSProperties | SerializedStyles; /** * Indicates where the overlay is placed. This is useful for the animation to * be animated 'towards' the placment origin, giving it a pleasing effect. */ placement?: PopperProps['placement']; } const overlayAnimation: MotionProps = { transition: {duration: 0.2}, initial: {opacity: 0}, animate: { opacity: 1, scale: 1, transition: testableTransition({ type: 'linear', ease: [0.5, 1, 0.89, 1], duration: 0.2, }), }, exit: { opacity: 0, scale: 0.95, transition: testableTransition({type: 'spring', delay: 0.1}), }, }; /** * Used to compute the transform origin to give the scale-down micro-animation * a pleasant feeling. Without this the animation can feel somewhat 'wrong' * since the direction of the scale isn't towards the reference element */ function computeOriginFromArrow( placement?: PopperProps['placement'], originPoint?: OriginPoint ): MotionStyle { const simplePlacement = placement?.split('-')[0]; const {y, x} = originPoint ?? {}; // XXX: Bottom means the arrow will be pointing up. switch (simplePlacement) { case 'top': return {originX: x ? `${x}px` : '50%', originY: '100%'}; case 'bottom': return {originX: x ? `${x}px` : '50%', originY: 0}; case 'left': return {originX: '100%', originY: y ? `${y}px` : '50%'}; case 'right': return {originX: 0, originY: y ? `${y}px` : '50%'}; default: return {originX: `50%`, originY: '50%'}; } } /** * A overlay component that has an optional nice ease in animation along with * a scale-down animation that animates towards an origin (think a tooltip * pointing at something). * * If animated (`animated` prop is true), should be used within a * ``. */ const Overlay = styled( forwardRef( ( { children, arrowProps, animated, placement, originPoint, style, overlayStyle: _overlayStyle, ...props }, ref ) => { const animationProps = animated ? { ...overlayAnimation, style: { ...style, ...computeOriginFromArrow(placement, originPoint), }, } : {style}; return ( {defined(arrowProps) && } {children} ); } ) )` position: relative; border-radius: ${p => p.theme.borderRadius}; background: ${p => p.theme.backgroundElevated}; box-shadow: 0 0 0 1px ${p => p.theme.translucentBorder}, ${p => p.theme.dropShadowHeavy}; font-size: ${p => p.theme.fontSizeMedium}; /* Override z-index from useOverlayPosition */ z-index: ${p => p.theme.zIndex.dropdown} !important; ${p => p.animated && `will-change: transform, opacity;`} ${p => p.overlayStyle as any} `; interface PositionWrapperProps extends React.HTMLAttributes { /** * Determines the zindex over the position wrapper */ zIndex: number; } /** * The PositionWrapper should be used when you're using the AnimatedOverlay as * part of dynamically positioned component (useOverlayPosition). Generally * this component will receive the `overlayProps`. * * This component ensures the wrapped AnimatedOverlay will not receive pointer * events while it is being animated out. Especially useful since the * `overlayProps` includes a onMouseEnter to allow the overlay to be hovered, * which we would not want while its fading away. */ const PositionWrapper = forwardRef( // XXX(epurkhiser): This is a motion.div NOT because it is animating, but // because we need the context of the animation starting for applying the // `pointerEvents: none`. ( { // XXX: Some of framer motions props are incompatible with // HTMLAttributes. Due to the way useOverlay uses this // component it must be compatible with that type. onAnimationStart: _onAnimationStart, onDragStart: _onDragStart, onDragEnd: _onDragEnd, onDrag: _onDrag, zIndex, style, ...props }, ref ) => ( ) ); export {Overlay, PositionWrapper};