overlay.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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 {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 = 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:
  129. 0 0 0 1px ${p => p.theme.translucentBorder},
  130. ${p => p.theme.dropShadowHeavy};
  131. font-size: ${p => p.theme.fontSizeMedium};
  132. /* Override z-index from useOverlayPosition */
  133. z-index: ${p => p.theme.zIndex.dropdown} !important;
  134. ${p => p.animated && `will-change: transform, opacity;`}
  135. /* Specificity hack to allow override styles to have higher specificity than
  136. * styles provided in any styled components which extend Overlay */
  137. :where(*) {
  138. ${p => p.overlayStyle as any}
  139. }
  140. `;
  141. interface PositionWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
  142. /**
  143. * Determines the zindex over the position wrapper
  144. */
  145. zIndex: number;
  146. }
  147. /**
  148. * The PositionWrapper should be used when you're using the AnimatedOverlay as
  149. * part of dynamically positioned component (useOverlayPosition). Generally
  150. * this component will receive the `overlayProps`.
  151. *
  152. * This component ensures the wrapped AnimatedOverlay will not receive pointer
  153. * events while it is being animated out. Especially useful since the
  154. * `overlayProps` includes a onMouseEnter to allow the overlay to be hovered,
  155. * which we would not want while its fading away.
  156. */
  157. const PositionWrapper = forwardRef<HTMLDivElement, PositionWrapperProps>(
  158. // XXX(epurkhiser): This is a motion.div NOT because it is animating, but
  159. // because we need the context of the animation starting for applying the
  160. // `pointerEvents: none`.
  161. (
  162. {
  163. // XXX: Some of framer motions props are incompatible with
  164. // HTMLAttributes<HTMLDivElement>. Due to the way useOverlay uses this
  165. // component it must be compatible with that type.
  166. onAnimationStart: _onAnimationStart,
  167. onDragStart: _onDragStart,
  168. onDragEnd: _onDragEnd,
  169. onDrag: _onDrag,
  170. zIndex,
  171. style,
  172. ...props
  173. },
  174. ref
  175. ) => (
  176. <motion.div
  177. {...props}
  178. ref={ref}
  179. style={{...style, zIndex}}
  180. initial={{pointerEvents: 'auto'}}
  181. exit={{pointerEvents: 'none'}}
  182. />
  183. )
  184. );
  185. export {Overlay, PositionWrapper};