1

I need a function that can calculate the visible area of ​​an element that is currently visible on the screen without the part hidden by overflow: scroll, position: absolute etc.

That is, the result of this function getVisiblePart(el) will be Visible Rect is: {x: 10, y: 20, height: 50, width: 700}

Background of the question: The need for such a function is due to a feature of the W3C specification for working with webdriver: https://w3c.github.io/webdriver/webdriver-spec.html#element-interactability

An element’s in-view centre point is the centre point of the area of the first DOM client rectangle that is inside the viewport.

Frameworks like selenoid/selenide for e2e tests use the w3c principle to calculate the center of the visible element to move the cursor to it, while allowing you to specify an offset. The main problem is to find out the actual size of the visible area of ​​the element at the moment, in order to calculate the correct offsets, for example, to calculate the upper left border.

The solution to this problem for selenoid/selenide would be:

Selenide.actions().moveToElement(el, getVisiblePart(el).width / -2, getVisiblePart(el).height / -2)

I have read a lot of similar topics, for example:

All answers either give a boolean whether the element is visible, or fail to take into account that the element may be partially hidden overflow: scroll

Real Example with scrolls (I need to find visible blue rect position with any scroll position):

.a {
  height: 250px;
  overflow: scroll;
  padding-top: 100px;
  background: yellow;
}
.b {
  height: 500px;
  overflow: scroll;
}
.c {
  height: 1000px;
  background: blue;
}
#target {
  border: 2px dashed red;
  position: absolute;
  pointer-events: none;
  transform: translate(-1px,-1px); /*because of border*/
}
<div class="a">
  <div class="b">
    <div class="c" />
  </div>
</div>
<div id="target" />

In answer to this question, I have already partially solved this problem, achieved the result I needed using Intersection Observer API, but I do not consider this solution to be good, at least because it is not synchronous with when the function is called and on the issue of cross-browser compatibility.

mixalbl4
  • 3,507
  • 1
  • 30
  • 44

2 Answers2

1

My workaround based on Intersection Observer API.

I do not consider this solution to be good, at least because it is not synchronous with when calling the function and on the issue of cross-browser compatibility.

This example perfectly demonstrates the final version that I want to get.

var options = {
    // root: document.querySelector('#scrollArea'),
    // rootMargin: '0px',
    threshold: 1
}
var callback = function(entries, observer) {
    const r = entries[0].intersectionRect;
    document.getElementById('target').setAttribute('style', `top: ${r.top}px;left: ${r.left}px;height: ${r.height}px;width: ${r.width}px;`);
    observer.unobserve(c);
};

const c = document.querySelector('.c');
setInterval(() => {
  var observer = new IntersectionObserver(callback, options);
  observer.observe(c);
}, 200);
.a {
  height: 250px;
  overflow: scroll;
  padding-top: 100px;
  background: yellow;
}
.b {
  height: 500px;
  overflow: scroll;
}
.c {
  height: 1000px;
  background: blue;
}
#target {
  border: 2px dashed red;
  position: absolute;
  pointer-events: none;
  transform: translate(-1px,-1px); /*because of border*/
}
<div class="a">
  <div class="b">
    <div class="c" />
  </div>
</div>
<div id="target" />

Explanation: every 200ms we calculate the visible area of ​​the blue element and highlight it.

An example of how I use this for Selenide:

    val containerSelector = "div.testEl"
    val rect = executeAsyncJavaScript<String>(
        "var callback = arguments[arguments.length - 1];" +
                "var el = document.querySelector('" + containerSelector + "');" +
                "new IntersectionObserver(" +
                "    (entries, observer) => {" +
                "        observer.unobserve(el);" +
                "        var r = entries[0].intersectionRect;" +
                "        callback([r.x, r.y, r.height, r.width].map(v => Math.ceil(v)).join(','));" +
                "    }," +
                "    { threshold: 1 }" +
                ").observe(el);"
    )!!.split(',').toTypedArray();

    LEFT_TOP_X = rect[3].toInt() / -2 + 1 // +1 for Float tolerance
    LEFT_TOP_Y = rect[2].toInt() / -2 + 1 // +1 for Float tolerance

    // Move cursor to TOP LEFT
    Selenide.actions().moveToElement(el, LEFT_TOP_X, LEFT_TOP_Y)
mixalbl4
  • 3,507
  • 1
  • 30
  • 44
0

v2 may help but the asynch still stinks

https://github.com/szager-chromium/IntersectionObserver/blob/v2/explainer.md

https://w3c.github.io/IntersectionObserver/v2/

https://chromestatus.com/feature/5878481493688320

https://szager-chromium.github.io/IntersectionObserver/demo/cashbomb/hidden/

JoePythonKing
  • 1,080
  • 1
  • 9
  • 18
  • v2 just return boolean like "full visible" or no, but doesnt return current visible rect. Async... I have tried some experements with official w3c IntersectionObserverPolyfill, it works sync but looks like kludge. `const rect = IntersectionObserver.prototype._computeTargetAndRootIntersection(el, getBoundingClientRect(el))` Demo: https://codepen.io/mixal_bl4/pen/abVdZYB – mixalbl4 Feb 03 '22 at 06:33