diff options
Diffstat (limited to 'popperjs/package/lib/modifiers/preventOverflow.js.flow')
-rw-r--r-- | popperjs/package/lib/modifiers/preventOverflow.js.flow | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/popperjs/package/lib/modifiers/preventOverflow.js.flow b/popperjs/package/lib/modifiers/preventOverflow.js.flow new file mode 100644 index 0000000..d503cb9 --- /dev/null +++ b/popperjs/package/lib/modifiers/preventOverflow.js.flow @@ -0,0 +1,220 @@ +// @flow +import { top, left, right, bottom, start } from '../enums'; +import type { Placement, Boundary, RootBoundary } from '../enums'; +import type { Rect, ModifierArguments, Modifier, Padding } from '../types'; +import getBasePlacement from '../utils/getBasePlacement'; +import getMainAxisFromPlacement from '../utils/getMainAxisFromPlacement'; +import getAltAxis from '../utils/getAltAxis'; +import { within, withinMaxClamp } from '../utils/within'; +import getLayoutRect from '../dom-utils/getLayoutRect'; +import getOffsetParent from '../dom-utils/getOffsetParent'; +import detectOverflow from '../utils/detectOverflow'; +import getVariation from '../utils/getVariation'; +import getFreshSideObject from '../utils/getFreshSideObject'; +import { min as mathMin, max as mathMax } from '../utils/math'; + +type TetherOffset = + | (({ + popper: Rect, + reference: Rect, + placement: Placement, + }) => number | { mainAxis: number, altAxis: number }) + | number + | { mainAxis: number, altAxis: number }; + +// eslint-disable-next-line import/no-unused-modules +export type Options = { + /* Prevents boundaries overflow on the main axis */ + mainAxis: boolean, + /* Prevents boundaries overflow on the alternate axis */ + altAxis: boolean, + /* The area to check the popper is overflowing in */ + boundary: Boundary, + /* If the popper is not overflowing the main area, fallback to this one */ + rootBoundary: RootBoundary, + /* Use the reference's "clippingParents" boundary context */ + altBoundary: boolean, + /** + * Allows the popper to overflow from its boundaries to keep it near its + * reference element + */ + tether: boolean, + /* Offsets when the `tether` option should activate */ + tetherOffset: TetherOffset, + /* Sets a padding to the provided boundary */ + padding: Padding, +}; + +function preventOverflow({ state, options, name }: ModifierArguments<Options>) { + const { + mainAxis: checkMainAxis = true, + altAxis: checkAltAxis = false, + boundary, + rootBoundary, + altBoundary, + padding, + tether = true, + tetherOffset = 0, + } = options; + + const overflow = detectOverflow(state, { + boundary, + rootBoundary, + padding, + altBoundary, + }); + const basePlacement = getBasePlacement(state.placement); + const variation = getVariation(state.placement); + const isBasePlacement = !variation; + const mainAxis = getMainAxisFromPlacement(basePlacement); + const altAxis = getAltAxis(mainAxis); + const popperOffsets = state.modifiersData.popperOffsets; + const referenceRect = state.rects.reference; + const popperRect = state.rects.popper; + const tetherOffsetValue = + typeof tetherOffset === 'function' + ? tetherOffset({ + ...state.rects, + placement: state.placement, + }) + : tetherOffset; + const normalizedTetherOffsetValue = + typeof tetherOffsetValue === 'number' + ? { mainAxis: tetherOffsetValue, altAxis: tetherOffsetValue } + : { mainAxis: 0, altAxis: 0, ...tetherOffsetValue }; + const offsetModifierState = state.modifiersData.offset + ? state.modifiersData.offset[state.placement] + : null; + + const data = { x: 0, y: 0 }; + + if (!popperOffsets) { + return; + } + + if (checkMainAxis) { + const mainSide = mainAxis === 'y' ? top : left; + const altSide = mainAxis === 'y' ? bottom : right; + const len = mainAxis === 'y' ? 'height' : 'width'; + const offset = popperOffsets[mainAxis]; + + const min = offset + overflow[mainSide]; + const max = offset - overflow[altSide]; + + const additive = tether ? -popperRect[len] / 2 : 0; + + const minLen = variation === start ? referenceRect[len] : popperRect[len]; + const maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; + + // We need to include the arrow in the calculation so the arrow doesn't go + // outside the reference bounds + const arrowElement = state.elements.arrow; + const arrowRect = + tether && arrowElement + ? getLayoutRect(arrowElement) + : { width: 0, height: 0 }; + const arrowPaddingObject = state.modifiersData['arrow#persistent'] + ? state.modifiersData['arrow#persistent'].padding + : getFreshSideObject(); + const arrowPaddingMin = arrowPaddingObject[mainSide]; + const arrowPaddingMax = arrowPaddingObject[altSide]; + + // If the reference length is smaller than the arrow length, we don't want + // to include its full size in the calculation. If the reference is small + // and near the edge of a boundary, the popper can overflow even if the + // reference is not overflowing as well (e.g. virtual elements with no + // width or height) + const arrowLen = within(0, referenceRect[len], arrowRect[len]); + + const minOffset = isBasePlacement + ? referenceRect[len] / 2 - + additive - + arrowLen - + arrowPaddingMin - + normalizedTetherOffsetValue.mainAxis + : minLen - + arrowLen - + arrowPaddingMin - + normalizedTetherOffsetValue.mainAxis; + const maxOffset = isBasePlacement + ? -referenceRect[len] / 2 + + additive + + arrowLen + + arrowPaddingMax + + normalizedTetherOffsetValue.mainAxis + : maxLen + + arrowLen + + arrowPaddingMax + + normalizedTetherOffsetValue.mainAxis; + + const arrowOffsetParent = + state.elements.arrow && getOffsetParent(state.elements.arrow); + const clientOffset = arrowOffsetParent + ? mainAxis === 'y' + ? arrowOffsetParent.clientTop || 0 + : arrowOffsetParent.clientLeft || 0 + : 0; + + const offsetModifierValue = offsetModifierState?.[mainAxis] ?? 0; + const tetherMin = offset + minOffset - offsetModifierValue - clientOffset; + const tetherMax = offset + maxOffset - offsetModifierValue; + + const preventedOffset = within( + tether ? mathMin(min, tetherMin) : min, + offset, + tether ? mathMax(max, tetherMax) : max + ); + + popperOffsets[mainAxis] = preventedOffset; + data[mainAxis] = preventedOffset - offset; + } + + if (checkAltAxis) { + const mainSide = mainAxis === 'x' ? top : left; + const altSide = mainAxis === 'x' ? bottom : right; + const offset = popperOffsets[altAxis]; + + const len = altAxis === 'y' ? 'height' : 'width'; + + const min = offset + overflow[mainSide]; + const max = offset - overflow[altSide]; + + const isOriginSide = [top, left].indexOf(basePlacement) !== -1; + + const offsetModifierValue = offsetModifierState?.[altAxis] ?? 0; + const tetherMin = isOriginSide + ? min + : offset - + referenceRect[len] - + popperRect[len] - + offsetModifierValue + + normalizedTetherOffsetValue.altAxis; + const tetherMax = isOriginSide + ? offset + + referenceRect[len] + + popperRect[len] - + offsetModifierValue - + normalizedTetherOffsetValue.altAxis + : max; + + const preventedOffset = + tether && isOriginSide + ? withinMaxClamp(tetherMin, offset, tetherMax) + : within(tether ? tetherMin : min, offset, tether ? tetherMax : max); + + popperOffsets[altAxis] = preventedOffset; + data[altAxis] = preventedOffset - offset; + } + + state.modifiersData[name] = data; +} + +// eslint-disable-next-line import/no-unused-modules +export type PreventOverflowModifier = Modifier<'preventOverflow', Options>; +export default ({ + name: 'preventOverflow', + enabled: true, + phase: 'main', + fn: preventOverflow, + requiresIfExists: ['offset'], +}: PreventOverflowModifier); |