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.

220 lines
7.3 KiB

2 months ago
  1. // @flow
  2. import { top, left, right, bottom, start } from '../enums';
  3. import type { Placement, Boundary, RootBoundary } from '../enums';
  4. import type { Rect, ModifierArguments, Modifier, Padding } from '../types';
  5. import getBasePlacement from '../utils/getBasePlacement';
  6. import getMainAxisFromPlacement from '../utils/getMainAxisFromPlacement';
  7. import getAltAxis from '../utils/getAltAxis';
  8. import { within, withinMaxClamp } from '../utils/within';
  9. import getLayoutRect from '../dom-utils/getLayoutRect';
  10. import getOffsetParent from '../dom-utils/getOffsetParent';
  11. import detectOverflow from '../utils/detectOverflow';
  12. import getVariation from '../utils/getVariation';
  13. import getFreshSideObject from '../utils/getFreshSideObject';
  14. import { min as mathMin, max as mathMax } from '../utils/math';
  15. type TetherOffset =
  16. | (({
  17. popper: Rect,
  18. reference: Rect,
  19. placement: Placement,
  20. }) => number | { mainAxis: number, altAxis: number })
  21. | number
  22. | { mainAxis: number, altAxis: number };
  23. // eslint-disable-next-line import/no-unused-modules
  24. export type Options = {
  25. /* Prevents boundaries overflow on the main axis */
  26. mainAxis: boolean,
  27. /* Prevents boundaries overflow on the alternate axis */
  28. altAxis: boolean,
  29. /* The area to check the popper is overflowing in */
  30. boundary: Boundary,
  31. /* If the popper is not overflowing the main area, fallback to this one */
  32. rootBoundary: RootBoundary,
  33. /* Use the reference's "clippingParents" boundary context */
  34. altBoundary: boolean,
  35. /**
  36. * Allows the popper to overflow from its boundaries to keep it near its
  37. * reference element
  38. */
  39. tether: boolean,
  40. /* Offsets when the `tether` option should activate */
  41. tetherOffset: TetherOffset,
  42. /* Sets a padding to the provided boundary */
  43. padding: Padding,
  44. };
  45. function preventOverflow({ state, options, name }: ModifierArguments<Options>) {
  46. const {
  47. mainAxis: checkMainAxis = true,
  48. altAxis: checkAltAxis = false,
  49. boundary,
  50. rootBoundary,
  51. altBoundary,
  52. padding,
  53. tether = true,
  54. tetherOffset = 0,
  55. } = options;
  56. const overflow = detectOverflow(state, {
  57. boundary,
  58. rootBoundary,
  59. padding,
  60. altBoundary,
  61. });
  62. const basePlacement = getBasePlacement(state.placement);
  63. const variation = getVariation(state.placement);
  64. const isBasePlacement = !variation;
  65. const mainAxis = getMainAxisFromPlacement(basePlacement);
  66. const altAxis = getAltAxis(mainAxis);
  67. const popperOffsets = state.modifiersData.popperOffsets;
  68. const referenceRect = state.rects.reference;
  69. const popperRect = state.rects.popper;
  70. const tetherOffsetValue =
  71. typeof tetherOffset === 'function'
  72. ? tetherOffset({
  73. ...state.rects,
  74. placement: state.placement,
  75. })
  76. : tetherOffset;
  77. const normalizedTetherOffsetValue =
  78. typeof tetherOffsetValue === 'number'
  79. ? { mainAxis: tetherOffsetValue, altAxis: tetherOffsetValue }
  80. : { mainAxis: 0, altAxis: 0, ...tetherOffsetValue };
  81. const offsetModifierState = state.modifiersData.offset
  82. ? state.modifiersData.offset[state.placement]
  83. : null;
  84. const data = { x: 0, y: 0 };
  85. if (!popperOffsets) {
  86. return;
  87. }
  88. if (checkMainAxis) {
  89. const mainSide = mainAxis === 'y' ? top : left;
  90. const altSide = mainAxis === 'y' ? bottom : right;
  91. const len = mainAxis === 'y' ? 'height' : 'width';
  92. const offset = popperOffsets[mainAxis];
  93. const min = offset + overflow[mainSide];
  94. const max = offset - overflow[altSide];
  95. const additive = tether ? -popperRect[len] / 2 : 0;
  96. const minLen = variation === start ? referenceRect[len] : popperRect[len];
  97. const maxLen = variation === start ? -popperRect[len] : -referenceRect[len];
  98. // We need to include the arrow in the calculation so the arrow doesn't go
  99. // outside the reference bounds
  100. const arrowElement = state.elements.arrow;
  101. const arrowRect =
  102. tether && arrowElement
  103. ? getLayoutRect(arrowElement)
  104. : { width: 0, height: 0 };
  105. const arrowPaddingObject = state.modifiersData['arrow#persistent']
  106. ? state.modifiersData['arrow#persistent'].padding
  107. : getFreshSideObject();
  108. const arrowPaddingMin = arrowPaddingObject[mainSide];
  109. const arrowPaddingMax = arrowPaddingObject[altSide];
  110. // If the reference length is smaller than the arrow length, we don't want
  111. // to include its full size in the calculation. If the reference is small
  112. // and near the edge of a boundary, the popper can overflow even if the
  113. // reference is not overflowing as well (e.g. virtual elements with no
  114. // width or height)
  115. const arrowLen = within(0, referenceRect[len], arrowRect[len]);
  116. const minOffset = isBasePlacement
  117. ? referenceRect[len] / 2 -
  118. additive -
  119. arrowLen -
  120. arrowPaddingMin -
  121. normalizedTetherOffsetValue.mainAxis
  122. : minLen -
  123. arrowLen -
  124. arrowPaddingMin -
  125. normalizedTetherOffsetValue.mainAxis;
  126. const maxOffset = isBasePlacement
  127. ? -referenceRect[len] / 2 +
  128. additive +
  129. arrowLen +
  130. arrowPaddingMax +
  131. normalizedTetherOffsetValue.mainAxis
  132. : maxLen +
  133. arrowLen +
  134. arrowPaddingMax +
  135. normalizedTetherOffsetValue.mainAxis;
  136. const arrowOffsetParent =
  137. state.elements.arrow && getOffsetParent(state.elements.arrow);
  138. const clientOffset = arrowOffsetParent
  139. ? mainAxis === 'y'
  140. ? arrowOffsetParent.clientTop || 0
  141. : arrowOffsetParent.clientLeft || 0
  142. : 0;
  143. const offsetModifierValue = offsetModifierState?.[mainAxis] ?? 0;
  144. const tetherMin = offset + minOffset - offsetModifierValue - clientOffset;
  145. const tetherMax = offset + maxOffset - offsetModifierValue;
  146. const preventedOffset = within(
  147. tether ? mathMin(min, tetherMin) : min,
  148. offset,
  149. tether ? mathMax(max, tetherMax) : max
  150. );
  151. popperOffsets[mainAxis] = preventedOffset;
  152. data[mainAxis] = preventedOffset - offset;
  153. }
  154. if (checkAltAxis) {
  155. const mainSide = mainAxis === 'x' ? top : left;
  156. const altSide = mainAxis === 'x' ? bottom : right;
  157. const offset = popperOffsets[altAxis];
  158. const len = altAxis === 'y' ? 'height' : 'width';
  159. const min = offset + overflow[mainSide];
  160. const max = offset - overflow[altSide];
  161. const isOriginSide = [top, left].indexOf(basePlacement) !== -1;
  162. const offsetModifierValue = offsetModifierState?.[altAxis] ?? 0;
  163. const tetherMin = isOriginSide
  164. ? min
  165. : offset -
  166. referenceRect[len] -
  167. popperRect[len] -
  168. offsetModifierValue +
  169. normalizedTetherOffsetValue.altAxis;
  170. const tetherMax = isOriginSide
  171. ? offset +
  172. referenceRect[len] +
  173. popperRect[len] -
  174. offsetModifierValue -
  175. normalizedTetherOffsetValue.altAxis
  176. : max;
  177. const preventedOffset =
  178. tether && isOriginSide
  179. ? withinMaxClamp(tetherMin, offset, tetherMax)
  180. : within(tether ? tetherMin : min, offset, tether ? tetherMax : max);
  181. popperOffsets[altAxis] = preventedOffset;
  182. data[altAxis] = preventedOffset - offset;
  183. }
  184. state.modifiersData[name] = data;
  185. }
  186. // eslint-disable-next-line import/no-unused-modules
  187. export type PreventOverflowModifier = Modifier<'preventOverflow', Options>;
  188. export default ({
  189. name: 'preventOverflow',
  190. enabled: true,
  191. phase: 'main',
  192. fn: preventOverflow,
  193. requiresIfExists: ['offset'],
  194. }: PreventOverflowModifier);