overlay.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import {forwardRef} from 'react';
  2. import {PopperProps} from 'react-popper';
  3. import {SerializedStyles} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {HTMLMotionProps, motion, MotionProps, MotionStyle} from 'framer-motion';
  6. import OverlayArrow from 'sentry/components/overlayArrow';
  7. import {defined} from 'sentry/utils';
  8. import testableTransition from 'sentry/utils/testableTransition';
  9. type OriginPoint = Partial<{x: number; y: number}>;
  10. interface OverlayProps extends HTMLMotionProps<'div'> {
  11. /**
  12. * Whether the overlay should animate in/out. If true, we'll also need
  13. * the `placement` and `originPoint` props.
  14. */
  15. animated?: boolean;
  16. /**
  17. * Props to be passed into <OverlayArrow />. If undefined, the overlay will
  18. * render with no arrow.
  19. */
  20. arrowProps?: React.ComponentProps<typeof OverlayArrow>;
  21. children?: React.ReactNode;
  22. /**
  23. * The CSS styles for the "origin point" over the overlay. Typically this
  24. * would be the arrow (or tip).
  25. */
  26. originPoint?: OriginPoint;
  27. /**
  28. * Additional style rules for the overlay content.
  29. */
  30. overlayStyle?: React.CSSProperties | SerializedStyles;
  31. /**
  32. * Indicates where the overlay is placed. This is useful for the animation to
  33. * be animated 'towards' the placment origin, giving it a pleasing effect.
  34. */
  35. placement?: PopperProps<any>['placement'];
  36. }
  37. const overlayAnimation: MotionProps = {
  38. transition: {duration: 0.2},
  39. initial: {opacity: 0},
  40. animate: {
  41. opacity: 1,
  42. scale: 1,
  43. transition: testableTransition({
  44. type: 'linear',
  45. ease: [0.5, 1, 0.89, 1],
  46. duration: 0.2,
  47. }),
  48. },
  49. exit: {
  50. opacity: 0,
  51. scale: 0.95,
  52. transition: testableTransition({type: 'spring', delay: 0.1}),
  53. },
  54. };
  55. /**
  56. * Used to compute the transform origin to give the scale-down micro-animation
  57. * a pleasant feeling. Without this the animation can feel somewhat 'wrong'
  58. * since the direction of the scale isn't towards the reference element
  59. */
  60. function computeOriginFromArrow(
  61. placement?: PopperProps<any>['placement'],
  62. originPoint?: OriginPoint
  63. ): MotionStyle {
  64. const simplePlacement = placement?.split('-')[0];
  65. const {y, x} = originPoint ?? {};
  66. // XXX: Bottom means the arrow will be pointing up.
  67. switch (simplePlacement) {
  68. case 'top':
  69. return {originX: x ? `${x}px` : '50%', originY: '100%'};
  70. case 'bottom':
  71. return {originX: x ? `${x}px` : '50%', originY: 0};
  72. case 'left':
  73. return {originX: '100%', originY: y ? `${y}px` : '50%'};
  74. case 'right':
  75. return {originX: 0, originY: y ? `${y}px` : '50%'};
  76. default:
  77. return {originX: `50%`, originY: '50%'};
  78. }
  79. }
  80. /**
  81. * A overlay component that has an optional nice ease in animation along with
  82. * a scale-down animation that animates towards an origin (think a tooltip
  83. * pointing at something).
  84. *
  85. * If animated (`animated` prop is true), should be used within a
  86. * `<AnimatePresence />`.
  87. */
  88. const Overlay = styled(
  89. ({
  90. children,
  91. arrowProps,
  92. animated,
  93. placement,
  94. originPoint,
  95. style,
  96. overlayStyle: _overlayStyle,
  97. ...props
  98. }: OverlayProps) => {
  99. const animationProps = animated
  100. ? {
  101. ...overlayAnimation,
  102. style: {
  103. ...style,
  104. ...computeOriginFromArrow(placement, originPoint),
  105. },
  106. }
  107. : {style};
  108. return (
  109. <motion.div {...props} {...animationProps}>
  110. {defined(arrowProps) && <OverlayArrow {...arrowProps} />}
  111. {children}
  112. </motion.div>
  113. );
  114. }
  115. )`
  116. position: relative;
  117. border-radius: ${p => p.theme.borderRadius};
  118. background: ${p => p.theme.backgroundElevated};
  119. box-shadow: 0 0 0 1px ${p => p.theme.translucentBorder}, ${p => p.theme.dropShadowHeavy};
  120. font-size: ${p => p.theme.fontSizeMedium};
  121. /* Override z-index from useOverlayPosition */
  122. z-index: ${p => p.theme.zIndex.dropdown} !important;
  123. ${p => p.animated && `will-change: transform, opacity;`}
  124. ${p => p.overlayStyle as any}
  125. `;
  126. interface PositionWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
  127. /**
  128. * Determines the zindex over the position wrapper
  129. */
  130. zIndex: number;
  131. }
  132. /**
  133. * The PositionWrapper should be used when you're using the AnimatedOverlay as
  134. * part of dynamically positioned component (useOverlayPosition). Generally
  135. * this component will receive the `overlayProps`.
  136. *
  137. * This component ensures the wrapped AnimatedOverlay will not receive pointer
  138. * events while it is being animated out. Especially useful since the
  139. * `overlayProps` includes a onMouseEnter to allow the overlay to be hovered,
  140. * which we would not want while its fading away.
  141. */
  142. const PositionWrapper = forwardRef<HTMLDivElement, PositionWrapperProps>(
  143. // XXX(epurkhiser): This is a motion.div NOT because it is animating, but
  144. // because we need the context of the animation starting for applying the
  145. // `pointerEvents: none`.
  146. (
  147. {
  148. // XXX: Some of framer motions props are incompatible with
  149. // HTMLAttributes<HTMLDivElement>. Due to the way useOverlay uses this
  150. // component it must be compatible with that type.
  151. onAnimationStart: _onAnimationStart,
  152. onDragStart: _onDragStart,
  153. onDragEnd: _onDragEnd,
  154. onDrag: _onDrag,
  155. zIndex,
  156. style,
  157. ...props
  158. },
  159. ref
  160. ) => (
  161. <motion.div
  162. {...props}
  163. ref={ref}
  164. style={{...style, zIndex}}
  165. initial={{pointerEvents: 'auto'}}
  166. exit={{pointerEvents: 'none'}}
  167. />
  168. )
  169. );
  170. export {Overlay, PositionWrapper};