UNPKG

@base-ui-components/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

328 lines (322 loc) 12.8 kB
"use strict"; 'use client'; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.useAnchorPositioning = useAnchorPositioning; var React = _interopRequireWildcard(require("react")); var _utils = require("@floating-ui/utils"); var _owner = require("@base-ui-components/utils/owner"); var _useIsoLayoutEffect = require("@base-ui-components/utils/useIsoLayoutEffect"); var _useLatestRef = require("@base-ui-components/utils/useLatestRef"); var _useEventCallback = require("@base-ui-components/utils/useEventCallback"); var _index = require("../floating-ui-react/index"); var _DirectionContext = require("../direction-provider/DirectionContext"); var _arrow = require("../floating-ui-react/middleware/arrow"); function getLogicalSide(sideParam, renderedSide, isRtl) { const isLogicalSideParam = sideParam === 'inline-start' || sideParam === 'inline-end'; const logicalRight = isRtl ? 'inline-start' : 'inline-end'; const logicalLeft = isRtl ? 'inline-end' : 'inline-start'; return { top: 'top', right: isLogicalSideParam ? logicalRight : 'right', bottom: 'bottom', left: isLogicalSideParam ? logicalLeft : 'left' }[renderedSide]; } function getOffsetData(state, sideParam, isRtl) { const { rects, placement } = state; const data = { side: getLogicalSide(sideParam, (0, _utils.getSide)(placement), isRtl), align: (0, _utils.getAlignment)(placement) || 'center', anchor: { width: rects.reference.width, height: rects.reference.height }, positioner: { width: rects.floating.width, height: rects.floating.height } }; return data; } /** * Provides standardized anchor positioning behavior for floating elements. Wraps Floating UI's * `useFloating` hook. */ function useAnchorPositioning(params) { const { // Public parameters anchor, positionMethod = 'absolute', side: sideParam = 'bottom', sideOffset = 0, align = 'center', alignOffset = 0, collisionBoundary, collisionPadding = 5, sticky = false, arrowPadding = 5, trackAnchor = true, // Private parameters keepMounted = false, floatingRootContext, mounted, collisionAvoidance, shiftCrossAxis = false, nodeId, adaptiveOrigin } = params; const collisionAvoidanceSide = collisionAvoidance.side || 'flip'; const collisionAvoidanceAlign = collisionAvoidance.align || 'flip'; const collisionAvoidanceFallbackAxisSide = collisionAvoidance.fallbackAxisSide || 'end'; const anchorFn = typeof anchor === 'function' ? anchor : undefined; const anchorFnCallback = (0, _useEventCallback.useEventCallback)(anchorFn); const anchorDep = anchorFn ? anchorFnCallback : anchor; const anchorValueRef = (0, _useLatestRef.useLatestRef)(anchor); const direction = (0, _DirectionContext.useDirection)(); const isRtl = direction === 'rtl'; const side = { top: 'top', right: 'right', bottom: 'bottom', left: 'left', 'inline-end': isRtl ? 'left' : 'right', 'inline-start': isRtl ? 'right' : 'left' }[sideParam]; const placement = align === 'center' ? side : `${side}-${align}`; const commonCollisionProps = { boundary: collisionBoundary === 'clipping-ancestors' ? 'clippingAncestors' : collisionBoundary, padding: collisionPadding }; // Using a ref assumes that the arrow element is always present in the DOM for the lifetime of the // popup. If this assumption ends up being false, we can switch to state to manage the arrow's // presence. const arrowRef = React.useRef(null); // Keep these reactive if they're not functions const sideOffsetRef = (0, _useLatestRef.useLatestRef)(sideOffset); const alignOffsetRef = (0, _useLatestRef.useLatestRef)(alignOffset); const sideOffsetDep = typeof sideOffset !== 'function' ? sideOffset : 0; const alignOffsetDep = typeof alignOffset !== 'function' ? alignOffset : 0; const middleware = [(0, _index.offset)(state => { const data = getOffsetData(state, sideParam, isRtl); const sideAxis = typeof sideOffsetRef.current === 'function' ? sideOffsetRef.current(data) : sideOffsetRef.current; const alignAxis = typeof alignOffsetRef.current === 'function' ? alignOffsetRef.current(data) : alignOffsetRef.current; return { mainAxis: sideAxis, crossAxis: alignAxis, alignmentAxis: alignAxis }; }, [sideOffsetDep, alignOffsetDep, isRtl, sideParam])]; const shiftDisabled = collisionAvoidanceAlign === 'none' && collisionAvoidanceSide !== 'shift'; const crossAxisShiftEnabled = !shiftDisabled && (sticky || shiftCrossAxis || collisionAvoidanceSide === 'shift'); const flipMiddleware = collisionAvoidanceSide === 'none' ? null : (0, _index.flip)({ ...commonCollisionProps, mainAxis: !shiftCrossAxis && collisionAvoidanceSide === 'flip', crossAxis: collisionAvoidanceAlign === 'flip' ? 'alignment' : false, fallbackAxisSideDirection: collisionAvoidanceFallbackAxisSide }); const shiftMiddleware = shiftDisabled ? null : (0, _index.shift)(data => { const html = (0, _owner.ownerDocument)(data.elements.floating).documentElement; return { ...commonCollisionProps, // Use the Layout Viewport to avoid shifting around when pinch-zooming // for context menus. rootBoundary: shiftCrossAxis ? { x: 0, y: 0, width: html.clientWidth, height: html.clientHeight } : undefined, mainAxis: collisionAvoidanceAlign !== 'none', crossAxis: crossAxisShiftEnabled, limiter: sticky || shiftCrossAxis ? undefined : (0, _index.limitShift)(() => { if (!arrowRef.current) { return {}; } const { height } = arrowRef.current.getBoundingClientRect(); return { offset: height / 2 + (typeof collisionPadding === 'number' ? collisionPadding : 0) }; }) }; }, [commonCollisionProps, sticky, shiftCrossAxis, collisionPadding, collisionAvoidanceAlign]); // https://floating-ui.com/docs/flip#combining-with-shift if (collisionAvoidanceSide === 'shift' || collisionAvoidanceAlign === 'shift' || align === 'center') { middleware.push(shiftMiddleware, flipMiddleware); } else { middleware.push(flipMiddleware, shiftMiddleware); } middleware.push((0, _index.size)({ ...commonCollisionProps, apply({ elements: { floating }, rects: { reference }, availableWidth, availableHeight }) { Object.entries({ '--available-width': `${availableWidth}px`, '--available-height': `${availableHeight}px`, '--anchor-width': `${reference.width}px`, '--anchor-height': `${reference.height}px` }).forEach(([key, value]) => { floating.style.setProperty(key, value); }); } }), (0, _arrow.arrow)(() => ({ // `transform-origin` calculations rely on an element existing. If the arrow hasn't been set, // we'll create a fake element. element: arrowRef.current || document.createElement('div'), padding: arrowPadding, offsetParent: 'floating' }), [arrowPadding]), (0, _index.hide)(), { name: 'transformOrigin', fn(state) { const { elements, middlewareData, placement: renderedPlacement, rects, y } = state; const currentRenderedSide = (0, _utils.getSide)(renderedPlacement); const currentRenderedAxis = (0, _utils.getSideAxis)(currentRenderedSide); const arrowEl = arrowRef.current; const arrowX = middlewareData.arrow?.x || 0; const arrowY = middlewareData.arrow?.y || 0; const arrowWidth = arrowEl?.clientWidth || 0; const arrowHeight = arrowEl?.clientHeight || 0; const transformX = arrowX + arrowWidth / 2; const transformY = arrowY + arrowHeight / 2; const shiftY = Math.abs(middlewareData.shift?.y || 0); const halfAnchorHeight = rects.reference.height / 2; const isOverlappingAnchor = shiftY > (typeof sideOffset === 'function' ? sideOffset(getOffsetData(state, sideParam, isRtl)) : sideOffset); const adjacentTransformOrigin = { top: `${transformX}px calc(100% + ${sideOffset}px)`, bottom: `${transformX}px ${-sideOffset}px`, left: `calc(100% + ${sideOffset}px) ${transformY}px`, right: `${-sideOffset}px ${transformY}px` }[currentRenderedSide]; const overlapTransformOrigin = `${transformX}px ${rects.reference.y + halfAnchorHeight - y}px`; elements.floating.style.setProperty('--transform-origin', crossAxisShiftEnabled && currentRenderedAxis === 'y' && isOverlappingAnchor ? overlapTransformOrigin : adjacentTransformOrigin); return {}; } }, adaptiveOrigin); // Ensure positioning doesn't run initially for `keepMounted` elements that // aren't initially open. let rootContext = floatingRootContext; if (!mounted && floatingRootContext) { rootContext = { ...floatingRootContext, elements: { reference: null, floating: null, domReference: null } }; } const autoUpdateOptions = React.useMemo(() => ({ elementResize: trackAnchor && typeof ResizeObserver !== 'undefined', layoutShift: trackAnchor && typeof IntersectionObserver !== 'undefined' }), [trackAnchor]); const { refs, elements, x, y, middlewareData, update, placement: renderedPlacement, context, isPositioned, floatingStyles: originalFloatingStyles } = (0, _index.useFloating)({ rootContext, placement, middleware, strategy: positionMethod, whileElementsMounted: keepMounted ? undefined : (...args) => (0, _index.autoUpdate)(...args, autoUpdateOptions), nodeId }); const { sideX, sideY } = middlewareData.adaptiveOrigin || {}; const floatingStyles = React.useMemo(() => adaptiveOrigin ? { position: positionMethod, [sideX]: `${x}px`, [sideY]: `${y}px` } : originalFloatingStyles, [adaptiveOrigin, sideX, sideY, positionMethod, x, y, originalFloatingStyles]); const registeredPositionReferenceRef = React.useRef(null); (0, _useIsoLayoutEffect.useIsoLayoutEffect)(() => { if (!mounted) { return; } const anchorValue = anchorValueRef.current; const resolvedAnchor = typeof anchorValue === 'function' ? anchorValue() : anchorValue; const unwrappedElement = (isRef(resolvedAnchor) ? resolvedAnchor.current : resolvedAnchor) || null; const finalAnchor = unwrappedElement || null; if (finalAnchor !== registeredPositionReferenceRef.current) { refs.setPositionReference(finalAnchor); registeredPositionReferenceRef.current = finalAnchor; } }, [mounted, refs, anchorDep, anchorValueRef]); React.useEffect(() => { if (!mounted) { return; } const anchorValue = anchorValueRef.current; // Refs from parent components are set after useLayoutEffect runs and are available in useEffect. // Therefore, if the anchor is a ref, we need to update the position reference in useEffect. if (typeof anchorValue === 'function') { return; } if (isRef(anchorValue) && anchorValue.current !== registeredPositionReferenceRef.current) { refs.setPositionReference(anchorValue.current); registeredPositionReferenceRef.current = anchorValue.current; } }, [mounted, refs, anchorDep, anchorValueRef]); React.useEffect(() => { if (keepMounted && mounted && elements.domReference && elements.floating) { return (0, _index.autoUpdate)(elements.domReference, elements.floating, update, autoUpdateOptions); } return undefined; }, [keepMounted, mounted, elements, update, autoUpdateOptions]); const renderedSide = (0, _utils.getSide)(renderedPlacement); const logicalRenderedSide = getLogicalSide(sideParam, renderedSide, isRtl); const renderedAlign = (0, _utils.getAlignment)(renderedPlacement) || 'center'; const anchorHidden = Boolean(middlewareData.hide?.referenceHidden); const arrowStyles = React.useMemo(() => ({ position: 'absolute', top: middlewareData.arrow?.y, left: middlewareData.arrow?.x }), [middlewareData.arrow]); const arrowUncentered = middlewareData.arrow?.centerOffset !== 0; return React.useMemo(() => ({ positionerStyles: floatingStyles, arrowStyles, arrowRef, arrowUncentered, side: logicalRenderedSide, align: renderedAlign, anchorHidden, refs, context, isPositioned, update }), [floatingStyles, arrowStyles, arrowRef, arrowUncentered, logicalRenderedSide, renderedAlign, anchorHidden, refs, context, isPositioned, update]); } function isRef(param) { return param != null && 'current' in param; }