Skip to content

Commit

Permalink
feat: observe target move on the page and update position (#7427)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored May 20, 2024
1 parent dd6c5b0 commit fb1b726
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 0 deletions.
12 changes: 12 additions & 0 deletions packages/overlay/src/vaadin-overlay-position-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { getAncestorRootNodes } from '@vaadin/component-base/src/dom-utils.js';
import { observeMove } from './vaadin-overlay-utils.js';

const PROP_NAMES_VERTICAL = {
start: 'top',
Expand Down Expand Up @@ -153,6 +154,12 @@ export const PositionMixin = (superClass) =>
this.__positionTargetAncestorRootNodes.forEach((node) => {
node.addEventListener('scroll', this.__onScroll, true);
});

if (this.positionTarget) {
this.__observePositionTargetMove = observeMove(this.positionTarget, () => {
this._updatePosition();
});
}
}

/** @private */
Expand All @@ -166,6 +173,11 @@ export const PositionMixin = (superClass) =>
});
this.__positionTargetAncestorRootNodes = null;
}

if (this.__observePositionTargetMove) {
this.__observePositionTargetMove();
this.__observePositionTargetMove = null;
}
}

/** @private */
Expand Down
13 changes: 13 additions & 0 deletions packages/overlay/src/vaadin-overlay-utils.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @license
* Copyright (c) 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/

/**
* Observe moving an element around on a page.
*
* Based on the idea from https://samthor.au/2021/observing-dom/ as implemented in Floating UI
* https://github.com/floating-ui/floating-ui/blob/58ed169/packages/dom/src/autoUpdate.ts#L45
*/
export function observeMove(element: HTMLElement, callback: () => void): () => void;
89 changes: 89 additions & 0 deletions packages/overlay/src/vaadin-overlay-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* @license
* Copyright (c) 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/

/**
* Observe moving an element around on a page.
*
* Based on the idea from https://samthor.au/2021/observing-dom/ as implemented in Floating UI
* https://github.com/floating-ui/floating-ui/blob/58ed169/packages/dom/src/autoUpdate.ts#L45
*
* @param {HTMLElement} element
* @param {Function} callback
* @return {Function}
*/
export function observeMove(element, callback) {
let io = null;

const root = document.documentElement;

function cleanup() {
io && io.disconnect();
io = null;
}

function refresh(skip = false, threshold = 1) {
cleanup();

const { left, top, width, height } = element.getBoundingClientRect();

if (!skip) {
callback();
}

if (!width || !height) {
return;
}

const insetTop = Math.floor(top);
const insetRight = Math.floor(root.clientWidth - (left + width));
const insetBottom = Math.floor(root.clientHeight - (top + height));
const insetLeft = Math.floor(left);

const rootMargin = `${-insetTop}px ${-insetRight}px ${-insetBottom}px ${-insetLeft}px`;

const options = {
rootMargin,
threshold: Math.max(0, Math.min(1, threshold)) || 1,
};

let isFirstUpdate = true;

function handleObserve(entries) {
let ratio = entries[0].intersectionRatio;

if (ratio !== threshold) {
if (!isFirstUpdate) {
return refresh();
}

// It's possible for the watched element to not be at perfect 1.0 visibility when we create
// the IntersectionObserver. This has a couple of causes:
// - elements being on partial pixels
// - elements being hidden offscreen (e.g., <html> has `overflow: hidden`)
// - delays: if your DOM change occurs due to e.g., page resize, you can see elements
// behind their actual position
//
// In all of these cases, refresh but with this lower ratio of threshold. When the element
// moves beneath _that_ new value, the user will get notified.
if (ratio === 0.0) {
ratio = 0.0000001; // Just needs to be non-zero
}

refresh(false, ratio);
}

isFirstUpdate = false;
}

io = new IntersectionObserver(handleObserve, options);

io.observe(element);
}

refresh(true);

return cleanup;
}
26 changes: 26 additions & 0 deletions packages/overlay/test/position-mixin-listeners.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,32 @@ describe('position mixin listeners', () => {
expect(updatePositionSpy.called).to.be.false;
});

it('should update position on target move by changing style', async () => {
target.style.position = 'static';
await nextFrame();
updatePositionSpy.resetHistory();

target.marginTop = '20px';
// Wait for intersection observer
await nextFrame();
await nextFrame();

expect(updatePositionSpy.called).to.be.true;
});

it('should not update position on target move when closed', async () => {
target.style.position = 'static';
await nextFrame();
updatePositionSpy.resetHistory();

overlay.opened = false;

target.marginTop = '20px';
await nextFrame();
await nextFrame();
expect(updatePositionSpy.called).to.be.false;
});

['document', 'visual viewport', 'ancestor'].forEach((name) => {
describe(name, () => {
let scrollableNode;
Expand Down

0 comments on commit fb1b726

Please sign in to comment.