@wordpress/components
Version:
UI components for WordPress.
145 lines (136 loc) • 5.84 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.NULL_ELEMENT_OFFSET_RECT = void 0;
exports.getElementOffsetRect = getElementOffsetRect;
exports.useTrackElementOffsetRect = useTrackElementOffsetRect;
var _element = require("@wordpress/element");
var _compose = require("@wordpress/compose");
/* eslint-disable jsdoc/require-param */
/**
* WordPress dependencies
*/
/**
* The position and dimensions of an element, relative to its offset parent.
*/
/**
* An `ElementOffsetRect` object with all values set to zero.
*/
const NULL_ELEMENT_OFFSET_RECT = exports.NULL_ELEMENT_OFFSET_RECT = {
element: undefined,
top: 0,
right: 0,
bottom: 0,
left: 0,
width: 0,
height: 0
};
/**
* Returns the position and dimensions of an element, relative to its offset
* parent, with subpixel precision. Values reflect the real measures before any
* potential scaling distortions along the X and Y axes.
*
* Useful in contexts where plain `getBoundingClientRect` calls or `ResizeObserver`
* entries are not suitable, such as when the element is transformed, and when
* `element.offset<Top|Left|Width|Height>` methods are not precise enough.
*
* **Note:** in some contexts, like when the scale is 0, this method will fail
* because it's impossible to calculate a scaling ratio. When that happens, it
* will return `undefined`.
*/
function getElementOffsetRect(element) {
var _offsetParent$getBoun, _offsetParent$scrollL, _offsetParent$scrollT;
// Position and dimension values computed with `getBoundingClientRect` have
// subpixel precision, but are affected by distortions since they represent
// the "real" measures, or in other words, the actual final values as rendered
// by the browser.
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return;
}
const offsetParent = element.offsetParent;
const offsetParentRect = (_offsetParent$getBoun = offsetParent?.getBoundingClientRect()) !== null && _offsetParent$getBoun !== void 0 ? _offsetParent$getBoun : NULL_ELEMENT_OFFSET_RECT;
const offsetParentScrollX = (_offsetParent$scrollL = offsetParent?.scrollLeft) !== null && _offsetParent$scrollL !== void 0 ? _offsetParent$scrollL : 0;
const offsetParentScrollY = (_offsetParent$scrollT = offsetParent?.scrollTop) !== null && _offsetParent$scrollT !== void 0 ? _offsetParent$scrollT : 0;
// Computed widths and heights have subpixel precision, and are not affected
// by distortions.
const computedWidth = parseFloat(getComputedStyle(element).width);
const computedHeight = parseFloat(getComputedStyle(element).height);
// We can obtain the current scale factor for the element by comparing "computed"
// dimensions with the "real" ones.
const scaleX = computedWidth / rect.width;
const scaleY = computedHeight / rect.height;
return {
element,
// To obtain the adjusted values for the position:
// 1. Compute the element's position relative to the offset parent.
// 2. Correct for the scale factor.
// 3. Adjust for the scroll position of the offset parent.
top: (rect.top - offsetParentRect?.top) * scaleY + offsetParentScrollY,
right: (offsetParentRect?.right - rect.right) * scaleX - offsetParentScrollX,
bottom: (offsetParentRect?.bottom - rect.bottom) * scaleY - offsetParentScrollY,
left: (rect.left - offsetParentRect?.left) * scaleX + offsetParentScrollX,
// Computed dimensions don't need any adjustments.
width: computedWidth,
height: computedHeight
};
}
const POLL_RATE = 100;
/**
* Tracks the position and dimensions of an element, relative to its offset
* parent. The element can be changed dynamically.
*
* When no element is provided (`null` or `undefined`), the hook will return
* a "null" rect, in which all values are `0` and `element` is `undefined`.
*
* **Note:** sometimes, the measurement will fail (see `getElementOffsetRect`'s
* documentation for more details). When that happens, this hook will attempt
* to measure again after a frame, and if that fails, it will poll every 100
* milliseconds until it succeeds.
*/
function useTrackElementOffsetRect(targetElement, deps = []) {
const [indicatorPosition, setIndicatorPosition] = (0, _element.useState)(NULL_ELEMENT_OFFSET_RECT);
const intervalRef = (0, _element.useRef)();
const measure = (0, _compose.useEvent)(() => {
// Check that the targetElement is still attached to the DOM, in case
// it was removed since the last `measure` call.
if (targetElement && targetElement.isConnected) {
const elementOffsetRect = getElementOffsetRect(targetElement);
if (elementOffsetRect) {
setIndicatorPosition(elementOffsetRect);
clearInterval(intervalRef.current);
return true;
}
} else {
clearInterval(intervalRef.current);
}
return false;
});
const setElement = (0, _compose.useResizeObserver)(() => {
if (!measure()) {
requestAnimationFrame(() => {
if (!measure()) {
intervalRef.current = setInterval(measure, POLL_RATE);
}
});
}
});
(0, _element.useLayoutEffect)(() => {
setElement(targetElement);
if (!targetElement) {
setIndicatorPosition(NULL_ELEMENT_OFFSET_RECT);
}
}, [setElement, targetElement]);
// Escape hatch to force a remeasurement when something else changes rather
// than the target elements' ref or size (for example, the target element
// can change its position within the tablist).
(0, _element.useLayoutEffect)(() => {
measure();
// `measure` is a stable function, so it's safe to omit it from the deps array.
// deps can't be statically analyzed by ESLint
}, deps);
return indicatorPosition;
}
/* eslint-enable jsdoc/require-param */
//# sourceMappingURL=element-rect.js.map