133 lines
5.6 KiB
JavaScript
133 lines
5.6 KiB
JavaScript
import { computePosition } from '@floating-ui/dom';
|
|
import { isHTMLElement } from '@fluentui/react-utilities';
|
|
import { debounce, writeArrowUpdates, writeContainerUpdates } from './utils';
|
|
import { listScrollParents } from './utils/listScrollParents';
|
|
import { POSITIONING_END_EVENT } from './constants';
|
|
import { createResizeObserver } from './utils/createResizeObserver';
|
|
/**
|
|
* @internal
|
|
* @returns manager that handles positioning out of the react lifecycle
|
|
*/ export function createPositionManager(options) {
|
|
let isDestroyed = false;
|
|
const { container, target, arrow, strategy, middleware, placement, useTransform = true, disableUpdateOnResize = false } = options;
|
|
const targetWindow = container.ownerDocument.defaultView;
|
|
if (!target || !container || !targetWindow) {
|
|
return {
|
|
updatePosition: ()=>undefined,
|
|
dispose: ()=>undefined
|
|
};
|
|
}
|
|
// When the dimensions of the target or the container change - trigger a position update
|
|
const resizeObserver = disableUpdateOnResize ? null : createResizeObserver(targetWindow, (entries)=>{
|
|
// If content rect dimensions to go 0 -> very likely that `display: none` is being used to hide the element
|
|
// In this case don't update and let users update imperatively
|
|
const shouldUpdateOnResize = entries.every((entry)=>{
|
|
return entry.contentRect.width > 0 && entry.contentRect.height > 0;
|
|
});
|
|
if (shouldUpdateOnResize) {
|
|
updatePosition();
|
|
}
|
|
});
|
|
let isFirstUpdate = true;
|
|
const scrollParents = new Set();
|
|
// When the container is first resolved, set position `fixed` to avoid scroll jumps.
|
|
// Without this scroll jumps can occur when the element is rendered initially and receives focus
|
|
Object.assign(container.style, {
|
|
position: 'fixed',
|
|
left: 0,
|
|
top: 0,
|
|
margin: 0
|
|
});
|
|
const forceUpdate = ()=>{
|
|
// debounced update can still occur afterwards
|
|
// early return to avoid memory leaks
|
|
if (isDestroyed) {
|
|
return;
|
|
}
|
|
if (isFirstUpdate) {
|
|
listScrollParents(container).forEach((scrollParent)=>scrollParents.add(scrollParent));
|
|
if (isHTMLElement(target)) {
|
|
listScrollParents(target).forEach((scrollParent)=>scrollParents.add(scrollParent));
|
|
}
|
|
scrollParents.forEach((scrollParent)=>{
|
|
scrollParent.addEventListener('scroll', updatePosition, {
|
|
passive: true
|
|
});
|
|
});
|
|
resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.observe(container);
|
|
if (isHTMLElement(target)) {
|
|
resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.observe(target);
|
|
}
|
|
isFirstUpdate = false;
|
|
}
|
|
Object.assign(container.style, {
|
|
position: strategy
|
|
});
|
|
computePosition(target, container, {
|
|
placement,
|
|
middleware,
|
|
strategy
|
|
}).then(({ x, y, middlewareData, placement: computedPlacement })=>{
|
|
// Promise can still resolve after destruction
|
|
// early return to avoid applying outdated position
|
|
if (isDestroyed) {
|
|
return;
|
|
}
|
|
writeArrowUpdates({
|
|
arrow,
|
|
middlewareData
|
|
});
|
|
writeContainerUpdates({
|
|
container,
|
|
middlewareData,
|
|
placement: computedPlacement,
|
|
coordinates: {
|
|
x,
|
|
y
|
|
},
|
|
lowPPI: ((targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.devicePixelRatio) || 1) <= 1,
|
|
strategy,
|
|
useTransform
|
|
});
|
|
container.dispatchEvent(new CustomEvent(POSITIONING_END_EVENT));
|
|
}).catch((err)=>{
|
|
// https://github.com/floating-ui/floating-ui/issues/1845
|
|
// FIXME for node > 14
|
|
// node 15 introduces promise rejection which means that any components
|
|
// tests need to be `it('', async () => {})` otherwise there can be race conditions with
|
|
// JSDOM being torn down before this promise is resolved so globals like `window` and `document` don't exist
|
|
// Unless all tests that ever use `usePositioning` are turned into async tests, any logging during testing
|
|
// will actually be counter productive
|
|
if (process.env.NODE_ENV === 'development') {
|
|
// eslint-disable-next-line no-console
|
|
console.error('[usePositioning]: Failed to calculate position', err);
|
|
}
|
|
});
|
|
};
|
|
const updatePosition = debounce(()=>forceUpdate());
|
|
const dispose = ()=>{
|
|
isDestroyed = true;
|
|
if (targetWindow) {
|
|
targetWindow.removeEventListener('scroll', updatePosition);
|
|
targetWindow.removeEventListener('resize', updatePosition);
|
|
}
|
|
scrollParents.forEach((scrollParent)=>{
|
|
scrollParent.removeEventListener('scroll', updatePosition);
|
|
});
|
|
scrollParents.clear();
|
|
resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect();
|
|
};
|
|
if (targetWindow) {
|
|
targetWindow.addEventListener('scroll', updatePosition, {
|
|
passive: true
|
|
});
|
|
targetWindow.addEventListener('resize', updatePosition);
|
|
}
|
|
// Update the position on initialization
|
|
updatePosition();
|
|
return {
|
|
updatePosition,
|
|
dispose
|
|
};
|
|
}
|