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.

233 lines
5.8 KiB

2 months ago
  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 }, win: Window): Offsets {
  46. const dpr = win.devicePixelRatio || 1;
  47. return {
  48. x: round(x * dpr) / dpr || 0,
  49. y: round(y * dpr) / dpr || 0,
  50. };
  51. }
  52. export function mapToStyles({
  53. popper,
  54. popperRect,
  55. placement,
  56. variation,
  57. offsets,
  58. position,
  59. gpuAcceleration,
  60. adaptive,
  61. roundOffsets,
  62. isFixed,
  63. }: {
  64. popper: HTMLElement,
  65. popperRect: Rect,
  66. placement: BasePlacement,
  67. variation: ?Variation,
  68. offsets: $Shape<{ x: number, y: number, centerOffset: number }>,
  69. position: PositioningStrategy,
  70. gpuAcceleration: boolean,
  71. adaptive: boolean,
  72. roundOffsets: boolean | RoundOffsets,
  73. isFixed: boolean,
  74. }) {
  75. let { x = 0, y = 0 } = offsets;
  76. ({ x, y } =
  77. typeof roundOffsets === 'function' ? roundOffsets({ x, y }) : { x, y });
  78. const hasX = offsets.hasOwnProperty('x');
  79. const hasY = offsets.hasOwnProperty('y');
  80. let sideX: string = left;
  81. let sideY: string = top;
  82. const win: Window = window;
  83. if (adaptive) {
  84. let offsetParent = getOffsetParent(popper);
  85. let heightProp = 'clientHeight';
  86. let widthProp = 'clientWidth';
  87. if (offsetParent === getWindow(popper)) {
  88. offsetParent = getDocumentElement(popper);
  89. if (
  90. getComputedStyle(offsetParent).position !== 'static' &&
  91. position === 'absolute'
  92. ) {
  93. heightProp = 'scrollHeight';
  94. widthProp = 'scrollWidth';
  95. }
  96. }
  97. // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it
  98. offsetParent = (offsetParent: Element);
  99. if (
  100. placement === top ||
  101. ((placement === left || placement === right) && variation === end)
  102. ) {
  103. sideY = bottom;
  104. const offsetY =
  105. isFixed && offsetParent === win && win.visualViewport
  106. ? win.visualViewport.height
  107. : // $FlowFixMe[prop-missing]
  108. offsetParent[heightProp];
  109. y -= offsetY - popperRect.height;
  110. y *= gpuAcceleration ? 1 : -1;
  111. }
  112. if (
  113. placement === left ||
  114. ((placement === top || placement === bottom) && variation === end)
  115. ) {
  116. sideX = right;
  117. const offsetX =
  118. isFixed && offsetParent === win && win.visualViewport
  119. ? win.visualViewport.width
  120. : // $FlowFixMe[prop-missing]
  121. offsetParent[widthProp];
  122. x -= offsetX - popperRect.width;
  123. x *= gpuAcceleration ? 1 : -1;
  124. }
  125. }
  126. const commonStyles = {
  127. position,
  128. ...(adaptive && unsetSides),
  129. };
  130. ({ x, y } =
  131. roundOffsets === true
  132. ? roundOffsetsByDPR({ x, y }, getWindow(popper))
  133. : { x, y });
  134. if (gpuAcceleration) {
  135. return {
  136. ...commonStyles,
  137. [sideY]: hasY ? '0' : '',
  138. [sideX]: hasX ? '0' : '',
  139. // Layer acceleration can disable subpixel rendering which causes slightly
  140. // blurry text on low PPI displays, so we want to use 2D transforms
  141. // instead
  142. transform:
  143. (win.devicePixelRatio || 1) <= 1
  144. ? `translate(${x}px, ${y}px)`
  145. : `translate3d(${x}px, ${y}px, 0)`,
  146. };
  147. }
  148. return {
  149. ...commonStyles,
  150. [sideY]: hasY ? `${y}px` : '',
  151. [sideX]: hasX ? `${x}px` : '',
  152. transform: '',
  153. };
  154. }
  155. function computeStyles({ state, options }: ModifierArguments<Options>) {
  156. const {
  157. gpuAcceleration = true,
  158. adaptive = true,
  159. // defaults to use builtin `roundOffsetsByDPR`
  160. roundOffsets = true,
  161. } = options;
  162. const commonStyles = {
  163. placement: getBasePlacement(state.placement),
  164. variation: getVariation(state.placement),
  165. popper: state.elements.popper,
  166. popperRect: state.rects.popper,
  167. gpuAcceleration,
  168. isFixed: state.options.strategy === 'fixed',
  169. };
  170. if (state.modifiersData.popperOffsets != null) {
  171. state.styles.popper = {
  172. ...state.styles.popper,
  173. ...mapToStyles({
  174. ...commonStyles,
  175. offsets: state.modifiersData.popperOffsets,
  176. position: state.options.strategy,
  177. adaptive,
  178. roundOffsets,
  179. }),
  180. };
  181. }
  182. if (state.modifiersData.arrow != null) {
  183. state.styles.arrow = {
  184. ...state.styles.arrow,
  185. ...mapToStyles({
  186. ...commonStyles,
  187. offsets: state.modifiersData.arrow,
  188. position: 'absolute',
  189. adaptive: false,
  190. roundOffsets,
  191. }),
  192. };
  193. }
  194. state.attributes.popper = {
  195. ...state.attributes.popper,
  196. 'data-popper-placement': state.placement,
  197. };
  198. }
  199. // eslint-disable-next-line import/no-unused-modules
  200. export type ComputeStylesModifier = Modifier<'computeStyles', Options>;
  201. export default ({
  202. name: 'computeStyles',
  203. enabled: true,
  204. phase: 'beforeWrite',
  205. fn: computeStyles,
  206. data: {},
  207. }: ComputeStylesModifier);