overlay.tsx 5.8 KB

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