You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

263 lines
6.8 KiB

  1. // @flow
  2. import type {
  3. PositioningStrategy,
  4. Offsets,
  5. Modifier,
  6. ModifierArguments,
  7. Rect,
  8. Window,
  9. } from '../types';
  10. import {
  11. type BasePlacement,
  12. type Variation,
  13. top,
  14. left,
  15. right,
  16. bottom,
  17. end,
  18. } from '../enums';
  19. import getOffsetParent from '../dom-utils/getOffsetParent';
  20. import getWindow from '../dom-utils/getWindow';
  21. import getDocumentElement from '../dom-utils/getDocumentElement';
  22. import getComputedStyle from '../dom-utils/getComputedStyle';
  23. import getBasePlacement from '../utils/getBasePlacement';
  24. import getVariation from '../utils/getVariation';
  25. import { round } from '../utils/math';
  26. // eslint-disable-next-line import/no-unused-modules
  27. export type RoundOffsets = (
  28. offsets: $Shape<{ x: number, y: number, centerOffset: number }>
  29. ) => Offsets;
  30. // eslint-disable-next-line import/no-unused-modules
  31. export type Options = {
  32. gpuAcceleration: boolean,
  33. adaptive: boolean,
  34. roundOffsets?: boolean | RoundOffsets,
  35. };
  36. const unsetSides = {
  37. top: 'auto',
  38. right: 'auto',
  39. bottom: 'auto',
  40. left: 'auto',
  41. };
  42. // Round the offsets to the nearest suitable subpixel based on the DPR.
  43. // Zooming can change the DPR, but it seems to report a value that will
  44. // cleanly divide the values into the appropriate subpixels.
  45. function roundOffsetsByDPR({ x, y }): Offsets {
  46. const win: Window = window;
  47. const dpr = win.devicePixelRatio || 1;
  48. return {
  49. x: round(x * dpr) / dpr || 0,
  50. y: round(y * dpr) / dpr || 0,
  51. };
  52. }
  53. export function mapToStyles({
  54. popper,
  55. popperRect,
  56. placement,
  57. variation,
  58. offsets,
  59. position,
  60. gpuAcceleration,
  61. adaptive,
  62. roundOffsets,
  63. isFixed,
  64. }: {
  65. popper: HTMLElement,
  66. popperRect: Rect,
  67. placement: BasePlacement,
  68. variation: ?Variation,
  69. offsets: $Shape<{ x: number, y: number, centerOffset: number }>,
  70. position: PositioningStrategy,
  71. gpuAcceleration: boolean,
  72. adaptive: boolean,
  73. roundOffsets: boolean | RoundOffsets,
  74. isFixed: boolean,
  75. }) {
  76. let { x = 0, y = 0 } = offsets;
  77. ({ x, y } =
  78. typeof roundOffsets === 'function'
  79. ? roundOffsets({ x, y })
  80. : { x, y });
  81. const hasX = offsets.hasOwnProperty('x');
  82. const hasY = offsets.hasOwnProperty('y');
  83. let sideX: string = left;
  84. let sideY: string = top;
  85. const win: Window = window;
  86. if (adaptive) {
  87. let offsetParent = getOffsetParent(popper);
  88. let heightProp = 'clientHeight';
  89. let widthProp = 'clientWidth';
  90. if (offsetParent === getWindow(popper)) {
  91. offsetParent = getDocumentElement(popper);
  92. if (
  93. getComputedStyle(offsetParent).position !== 'static' &&
  94. position === 'absolute'
  95. ) {
  96. heightProp = 'scrollHeight';
  97. widthProp = 'scrollWidth';
  98. }
  99. }
  100. // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it
  101. offsetParent = (offsetParent: Element);
  102. if (
  103. placement === top ||
  104. ((placement === left || placement === right) && variation === end)
  105. ) {
  106. sideY = bottom;
  107. const offsetY =
  108. isFixed && offsetParent === win && win.visualViewport
  109. ? win.visualViewport.height
  110. : // $FlowFixMe[prop-missing]
  111. offsetParent[heightProp];
  112. y -= offsetY - popperRect.height;
  113. y *= gpuAcceleration ? 1 : -1;
  114. }
  115. if (
  116. placement === left ||
  117. ((placement === top || placement === bottom) && variation === end)
  118. ) {
  119. sideX = right;
  120. const offsetX =
  121. isFixed && offsetParent === win && win.visualViewport
  122. ? win.visualViewport.width
  123. : // $FlowFixMe[prop-missing]
  124. offsetParent[widthProp];
  125. x -= offsetX - popperRect.width;
  126. x *= gpuAcceleration ? 1 : -1;
  127. }
  128. }
  129. const commonStyles = {
  130. position,
  131. ...(adaptive && unsetSides),
  132. };
  133. ({ x, y } =
  134. roundOffsets === true
  135. ? roundOffsetsByDPR({ x, y })
  136. : { x, y });
  137. if (gpuAcceleration) {
  138. return {
  139. ...commonStyles,
  140. [sideY]: hasY ? '0' : '',
  141. [sideX]: hasX ? '0' : '',
  142. // Layer acceleration can disable subpixel rendering which causes slightly
  143. // blurry text on low PPI displays, so we want to use 2D transforms
  144. // instead
  145. transform:
  146. (win.devicePixelRatio || 1) <= 1
  147. ? `translate(${x}px, ${y}px)`
  148. : `translate3d(${x}px, ${y}px, 0)`,
  149. };
  150. }
  151. return {
  152. ...commonStyles,
  153. [sideY]: hasY ? `${y}px` : '',
  154. [sideX]: hasX ? `${x}px` : '',
  155. transform: '',
  156. };
  157. }
  158. function computeStyles({ state, options }: ModifierArguments<Options>) {
  159. const {
  160. gpuAcceleration = true,
  161. adaptive = true,
  162. // defaults to use builtin `roundOffsetsByDPR`
  163. roundOffsets = true,
  164. } = options;
  165. if (false) {
  166. const transitionProperty =
  167. getComputedStyle(state.elements.popper).transitionProperty || '';
  168. if (
  169. adaptive &&
  170. ['transform', 'top', 'right', 'bottom', 'left'].some(
  171. (property) => transitionProperty.indexOf(property) >= 0
  172. )
  173. ) {
  174. console.warn(
  175. [
  176. 'Popper: Detected CSS transitions on at least one of the following',
  177. 'CSS properties: "transform", "top", "right", "bottom", "left".',
  178. '\n\n',
  179. 'Disable the "computeStyles" modifier\'s `adaptive` option to allow',
  180. 'for smooth transitions, or remove these properties from the CSS',
  181. 'transition declaration on the popper element if only transitioning',
  182. 'opacity or background-color for example.',
  183. '\n\n',
  184. 'We recommend using the popper element as a wrapper around an inner',
  185. 'element that can have any CSS property transitioned for animations.',
  186. ].join(' ')
  187. );
  188. }
  189. }
  190. const commonStyles = {
  191. placement: getBasePlacement(state.placement),
  192. variation: getVariation(state.placement),
  193. popper: state.elements.popper,
  194. popperRect: state.rects.popper,
  195. gpuAcceleration,
  196. isFixed: state.options.strategy === 'fixed',
  197. };
  198. if (state.modifiersData.popperOffsets != null) {
  199. state.styles.popper = {
  200. ...state.styles.popper,
  201. ...mapToStyles({
  202. ...commonStyles,
  203. offsets: state.modifiersData.popperOffsets,
  204. position: state.options.strategy,
  205. adaptive,
  206. roundOffsets,
  207. }),
  208. };
  209. }
  210. if (state.modifiersData.arrow != null) {
  211. state.styles.arrow = {
  212. ...state.styles.arrow,
  213. ...mapToStyles({
  214. ...commonStyles,
  215. offsets: state.modifiersData.arrow,
  216. position: 'absolute',
  217. adaptive: false,
  218. roundOffsets,
  219. }),
  220. };
  221. }
  222. state.attributes.popper = {
  223. ...state.attributes.popper,
  224. 'data-popper-placement': state.placement,
  225. };
  226. }
  227. // eslint-disable-next-line import/no-unused-modules
  228. export type ComputeStylesModifier = Modifier<'computeStyles', Options>;
  229. export default ({
  230. name: 'computeStyles',
  231. enabled: true,
  232. phase: 'beforeWrite',
  233. fn: computeStyles,
  234. data: {},
  235. }: ComputeStylesModifier);