@primer/react
Version:
An implementation of GitHub's Primer Design System using React
94 lines (87 loc) • 5.7 kB
JavaScript
'use strict';
var React = require('react');
var behaviors = require('@primer/behaviors');
var useProvidedRefOrCreate = require('./useProvidedRefOrCreate.js');
var useResizeObserver = require('./useResizeObserver.js');
var useIsomorphicLayoutEffect = require('../utils/useIsomorphicLayoutEffect.js');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var React__default = /*#__PURE__*/_interopDefault(React);
/**
* Calculates the top and left values for an absolutely-positioned floating element
* to be anchored to some anchor element. Returns refs for the floating element
* and the anchor element, along with the position.
* @param settings Settings for calculating the anchored position.
* @param dependencies Dependencies to determine when to re-calculate the position.
* @returns An object of {top: number, left: number} to absolutely-position the
* floating element.
*/
function useAnchoredPosition(settings, dependencies = []) {
const floatingElementRef = useProvidedRefOrCreate.useProvidedRefOrCreate(settings === null || settings === void 0 ? void 0 : settings.floatingElementRef);
const anchorElementRef = useProvidedRefOrCreate.useProvidedRefOrCreate(settings === null || settings === void 0 ? void 0 : settings.anchorElementRef);
const savedOnPositionChange = React__default.default.useRef(settings === null || settings === void 0 ? void 0 : settings.onPositionChange);
const [position, setPosition] = React__default.default.useState(undefined);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setPrevHeight] = React__default.default.useState(undefined);
const topPositionChanged = (prevPosition, newPosition) => {
return prevPosition && ['outside-top', 'inside-top'].includes(prevPosition.anchorSide) && (
// either the anchor changed or the element is trying to shrink in height
prevPosition.anchorSide !== newPosition.anchorSide || prevPosition.top < newPosition.top);
};
const updateElementHeight = () => {
let heightUpdated = false;
setPrevHeight(prevHeight => {
var _floatingElementRef$c, _floatingElementRef$c2;
// if the element is trying to shrink in height, restore to old height to prevent it from jumping
if (prevHeight && prevHeight > ((_floatingElementRef$c = (_floatingElementRef$c2 = floatingElementRef.current) === null || _floatingElementRef$c2 === void 0 ? void 0 : _floatingElementRef$c2.clientHeight) !== null && _floatingElementRef$c !== void 0 ? _floatingElementRef$c : 0)) {
requestAnimationFrame(() => {
floatingElementRef.current.style.height = `${prevHeight}px`;
});
heightUpdated = true;
}
return prevHeight;
});
return heightUpdated;
};
const updatePosition = React__default.default.useCallback(() => {
var _floatingElementRef$c5;
if (floatingElementRef.current instanceof Element && anchorElementRef.current instanceof Element) {
const newPosition = behaviors.getAnchoredPosition(floatingElementRef.current, anchorElementRef.current, settings);
setPosition(prev => {
if (settings !== null && settings !== void 0 && settings.pinPosition && topPositionChanged(prev, newPosition)) {
var _anchorElementRef$cur, _anchorElementRef$cur2, _floatingElementRef$c3, _floatingElementRef$c4;
const anchorTop = (_anchorElementRef$cur = (_anchorElementRef$cur2 = anchorElementRef.current) === null || _anchorElementRef$cur2 === void 0 ? void 0 : _anchorElementRef$cur2.getBoundingClientRect().top) !== null && _anchorElementRef$cur !== void 0 ? _anchorElementRef$cur : 0;
const elementStillFitsOnTop = anchorTop > ((_floatingElementRef$c3 = (_floatingElementRef$c4 = floatingElementRef.current) === null || _floatingElementRef$c4 === void 0 ? void 0 : _floatingElementRef$c4.clientHeight) !== null && _floatingElementRef$c3 !== void 0 ? _floatingElementRef$c3 : 0);
if (elementStillFitsOnTop && updateElementHeight()) {
return prev;
}
}
if (prev && prev.anchorSide === newPosition.anchorSide) {
var _savedOnPositionChang;
// if the position hasn't changed, don't update
(_savedOnPositionChang = savedOnPositionChange.current) === null || _savedOnPositionChang === void 0 ? void 0 : _savedOnPositionChang.call(savedOnPositionChange, newPosition);
}
return newPosition;
});
} else {
var _savedOnPositionChang2;
setPosition(undefined);
(_savedOnPositionChang2 = savedOnPositionChange.current) === null || _savedOnPositionChang2 === void 0 ? void 0 : _savedOnPositionChang2.call(savedOnPositionChange, undefined);
}
setPrevHeight((_floatingElementRef$c5 = floatingElementRef.current) === null || _floatingElementRef$c5 === void 0 ? void 0 : _floatingElementRef$c5.clientHeight);
},
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
[floatingElementRef, anchorElementRef, ...dependencies]);
useIsomorphicLayoutEffect(() => {
savedOnPositionChange.current = settings === null || settings === void 0 ? void 0 : settings.onPositionChange;
}, [settings === null || settings === void 0 ? void 0 : settings.onPositionChange]);
useIsomorphicLayoutEffect(updatePosition, [updatePosition]);
useResizeObserver.useResizeObserver(updatePosition); // watches for changes in window size
useResizeObserver.useResizeObserver(updatePosition, floatingElementRef); // watches for changes in floating element size
return {
floatingElementRef,
anchorElementRef,
position
};
}
exports.useAnchoredPosition = useAnchoredPosition;