@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.
246 lines (241 loc) • 9.2 kB
JavaScript
;
'use client';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useAnchorPositioning = useAnchorPositioning;
var React = _interopRequireWildcard(require("react"));
var _react2 = require("@floating-ui/react");
var _utils = require("@floating-ui/utils");
var _useEnhancedEffect = require("./useEnhancedEffect");
var _DirectionContext = require("../direction-provider/DirectionContext");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
/**
* Provides standardized anchor positioning behavior for floating elements. Wraps Floating UI's
* `useFloating` hook.
* @ignore - internal hook.
*/
function useAnchorPositioning(params) {
const {
anchor,
floatingRootContext,
positionMethod = 'absolute',
side: sideParam = 'top',
sideOffset = 0,
align = 'center',
alignOffset = 0,
collisionBoundary,
collisionPadding = 5,
fallbackAxisSideDirection = 'none',
sticky = false,
keepMounted = false,
arrowPadding = 5,
mounted,
trackAnchor = true,
allowAxisFlip = true,
nodeId
} = params;
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
// tooltip. If this assumption ends up being false, we can switch to state to manage the arrow's
// presence.
const arrowRef = React.useRef(null);
const middleware = [(0, _react2.offset)({
mainAxis: sideOffset,
crossAxis: alignOffset,
alignmentAxis: alignOffset
})];
const flipMiddleware = (0, _react2.flip)({
...commonCollisionProps,
fallbackAxisSideDirection: allowAxisFlip ? fallbackAxisSideDirection : 'none'
});
const shiftMiddleware = (0, _react2.shift)({
...commonCollisionProps,
crossAxis: sticky,
limiter: sticky ? undefined : (0, _react2.limitShift)(() => {
if (!arrowRef.current) {
return {};
}
const {
height
} = arrowRef.current.getBoundingClientRect();
return {
offset: height / 2 + (typeof collisionPadding === 'number' ? collisionPadding : 0)
};
})
});
// https://floating-ui.com/docs/flip#combining-with-shift
if (align !== 'center') {
middleware.push(flipMiddleware, shiftMiddleware);
} else {
middleware.push(shiftMiddleware, flipMiddleware);
}
middleware.push((0, _react2.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, _react2.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
}), [arrowPadding]), (0, _react2.hide)(), {
name: 'transformOrigin',
fn({
elements,
middlewareData,
placement: renderedPlacement
}) {
const currentRenderedSide = (0, _utils.getSide)(renderedPlacement);
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 transformOrigin = {
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];
elements.floating.style.setProperty('--transform-origin', transformOrigin);
return {};
}
});
// 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(() => ({
// Keep `ancestorResize` for window resizing. TODO: determine the best configuration, or
// if we need to allow options.
ancestorScroll: trackAnchor,
elementResize: trackAnchor && typeof ResizeObserver !== 'undefined',
layoutShift: trackAnchor && typeof IntersectionObserver !== 'undefined'
}), [trackAnchor]);
const {
refs,
elements,
floatingStyles,
middlewareData,
update,
placement: renderedPlacement,
context: positionerContext,
isPositioned
} = (0, _react2.useFloating)({
rootContext,
placement,
middleware,
strategy: positionMethod,
whileElementsMounted: keepMounted ? undefined : (...args) => (0, _react2.autoUpdate)(...args, autoUpdateOptions),
nodeId
});
const registeredPositionReferenceRef = React.useRef(null);
(0, _useEnhancedEffect.useEnhancedEffect)(() => {
if (!mounted) {
return;
}
const resolvedAnchor = typeof anchor === 'function' ? anchor() : anchor;
if (resolvedAnchor) {
const unwrappedElement = isRef(resolvedAnchor) ? resolvedAnchor.current : resolvedAnchor;
refs.setPositionReference(unwrappedElement);
registeredPositionReferenceRef.current = unwrappedElement;
}
}, [mounted, refs, anchor]);
React.useEffect(() => {
if (!mounted) {
return;
}
// 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 anchor === 'function') {
return;
}
if (isRef(anchor) && anchor.current !== registeredPositionReferenceRef.current) {
refs.setPositionReference(anchor.current);
registeredPositionReferenceRef.current = anchor.current;
}
}, [mounted, refs, anchor]);
React.useEffect(() => {
if (keepMounted && mounted && elements.domReference && elements.floating) {
return (0, _react2.autoUpdate)(elements.domReference, elements.floating, update, autoUpdateOptions);
}
return undefined;
}, [keepMounted, mounted, elements, update, autoUpdateOptions]);
const renderedSide = (0, _utils.getSide)(renderedPlacement);
const isLogicalSideParam = sideParam === 'inline-start' || sideParam === 'inline-end';
const logicalRight = isRtl ? 'inline-start' : 'inline-end';
const logicalLeft = isRtl ? 'inline-end' : 'inline-start';
const logicalRenderedSide = {
top: 'top',
right: isLogicalSideParam ? logicalRight : 'right',
bottom: 'bottom',
left: isLogicalSideParam ? logicalLeft : 'left'
}[renderedSide];
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,
renderedSide: logicalRenderedSide,
renderedAlign,
anchorHidden,
refs,
positionerContext,
isPositioned
}), [floatingStyles, arrowStyles, arrowRef, arrowUncentered, logicalRenderedSide, renderedAlign, anchorHidden, refs, positionerContext, isPositioned]);
}
function isRef(param) {
return param != null && 'current' in param;
}