overlay.tsx 5.9 KB

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