123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- 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 <OverlayArrow />. If undefined, the overlay will
- * render with no arrow.
- */
- arrowProps?: React.ComponentProps<typeof OverlayArrow>;
- 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<any>['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<any>['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
- * `<AnimatePresence />`.
- */
- const Overlay = styled(
- forwardRef<HTMLDivElement, OverlayProps>(
- (
- {
- children,
- arrowProps,
- animated,
- placement,
- originPoint,
- style,
- overlayStyle: _overlayStyle,
- ...props
- },
- ref
- ) => {
- const animationProps = animated
- ? {
- ...overlayAnimation,
- style: {
- ...style,
- ...computeOriginFromArrow(placement, originPoint),
- },
- }
- : {style};
- return (
- <motion.div {...props} {...animationProps} ref={ref}>
- {defined(arrowProps) && <OverlayArrow {...arrowProps} />}
- {children}
- </motion.div>
- );
- }
- )
- )`
- 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<HTMLDivElement> {
- /**
- * 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<HTMLDivElement, PositionWrapperProps>(
- // 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<HTMLDivElement>. 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
- ) => (
- <motion.div
- {...props}
- ref={ref}
- style={{...style, zIndex}}
- initial={{pointerEvents: 'auto'}}
- exit={{pointerEvents: 'none'}}
- />
- )
- );
- export {Overlay, PositionWrapper};
|