overlay.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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. forwardRef<HTMLDivElement, OverlayProps>(
  90. (
  91. {
  92. children,
  93. arrowProps,
  94. animated,
  95. placement,
  96. originPoint,
  97. style,
  98. overlayStyle: _overlayStyle,
  99. ...props
  100. },
  101. ref
  102. ) => {
  103. const animationProps = animated
  104. ? {
  105. ...overlayAnimation,
  106. style: {
  107. ...style,
  108. ...computeOriginFromArrow(placement, originPoint),
  109. },
  110. }
  111. : {style};
  112. return (
  113. <motion.div {...props} {...animationProps} ref={ref}>
  114. {defined(arrowProps) && <OverlayArrow {...arrowProps} />}
  115. {children}
  116. </motion.div>
  117. );
  118. }
  119. )
  120. )`
  121. position: relative;
  122. border-radius: ${p => p.theme.borderRadius};
  123. background: ${p => p.theme.backgroundElevated};
  124. box-shadow: 0 0 0 1px ${p => p.theme.translucentBorder}, ${p => p.theme.dropShadowHeavy};
  125. font-size: ${p => p.theme.fontSizeMedium};
  126. /* Override z-index from useOverlayPosition */
  127. z-index: ${p => p.theme.zIndex.dropdown} !important;
  128. ${p => p.animated && `will-change: transform, opacity;`}
  129. ${p => p.overlayStyle as any}
  130. `;
  131. interface PositionWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
  132. /**
  133. * Determines the zindex over the position wrapper
  134. */
  135. zIndex: number;
  136. }
  137. /**
  138. * The PositionWrapper should be used when you're using the AnimatedOverlay as
  139. * part of dynamically positioned component (useOverlayPosition). Generally
  140. * this component will receive the `overlayProps`.
  141. *
  142. * This component ensures the wrapped AnimatedOverlay will not receive pointer
  143. * events while it is being animated out. Especially useful since the
  144. * `overlayProps` includes a onMouseEnter to allow the overlay to be hovered,
  145. * which we would not want while its fading away.
  146. */
  147. const PositionWrapper = forwardRef<HTMLDivElement, PositionWrapperProps>(
  148. // XXX(epurkhiser): This is a motion.div NOT because it is animating, but
  149. // because we need the context of the animation starting for applying the
  150. // `pointerEvents: none`.
  151. (
  152. {
  153. // XXX: Some of framer motions props are incompatible with
  154. // HTMLAttributes<HTMLDivElement>. Due to the way useOverlay uses this
  155. // component it must be compatible with that type.
  156. onAnimationStart: _onAnimationStart,
  157. onDragStart: _onDragStart,
  158. onDragEnd: _onDragEnd,
  159. onDrag: _onDrag,
  160. zIndex,
  161. style,
  162. ...props
  163. },
  164. ref
  165. ) => (
  166. <motion.div
  167. {...props}
  168. ref={ref}
  169. style={{...style, zIndex}}
  170. initial={{pointerEvents: 'auto'}}
  171. exit={{pointerEvents: 'none'}}
  172. />
  173. )
  174. );
  175. export {Overlay, PositionWrapper};