resize-observer-polyfill icon indicating copy to clipboard operation
resize-observer-polyfill copied to clipboard

Wrong behavior for elements with dimensions less than 0.5px

Open que-etc opened this issue 8 years ago • 12 comments

Elements with dimensions less than 0.5 are considered to be empty. This happens because of the approach used to detect certain types of elements: basically we check whether clientWidth and clientHeight properties are equal to zero and return an empty rectangle if it's so. This way we can discard non-replaced inline, hidden, detached and empty elements.

But the thing is that client dimensions are being rounded and thus a rounded value < 0.5 will be 0. Unfortunately there is not much that can be done about it. If not for this approach it would be necessary to implement separate methods for all of the above cases and it's barely possible to perform a precise and performance effective test for hidden elements (display: none case). E.g. even jQuery gives incorrect results in its' ':visible' filter considering that an element with dimensions less than 0.5 is hidden.

This issue is not planed to be fixed.

que-etc avatar Sep 19 '16 12:09 que-etc

basically we check whether clientWidth and clientHeight properties are equal to zero

I noticed that clientWidth and clientHeight seems to round values to the nearest pixel integer (0.49 -> 0), and doesn't represent what is actually rendered on screen (I think these might be "aliased" values). I chose to use getComputedStyle because it returns a float.

Is ResizeObserver supposed to give us float values or integer values?

trusktr avatar Mar 12 '17 20:03 trusktr

Unfortunately there is not much that can be done about it.

Why not use getComputedStyle? Is it heavier?

trusktr avatar Mar 12 '17 20:03 trusktr

Fiddle: https://jsfiddle.net/611a1fk6/1/

trusktr avatar Mar 12 '17 20:03 trusktr

Here's a performance test: https://jsfiddle.net/611a1fk6/2/

The client* method is much faster (5 seconds vs 40 seconds, the test blocks while running).

Is there no other API like clientWidth/clientHeight, but that returns floats? If not, maybe a spec change is needed because if we can't get the actual size of what we see rendered on screen, there's something wrong (well, we can, with getComputedStyle, but it is slow)...

trusktr avatar Mar 12 '17 20:03 trusktr

@trusktr, thanks for reviewing this issue!

I noticed that clientWidth and clientHeight seems to round values to the nearest pixel integer (0.49 -> 0)...

Exactly for this reason client properties are used mostly to filter out hidden/empty/detached and non-replaced inline elements. To compute actual dimensions of a content rectangle this library uses, just as you've suggested it, computed CSS values of width and height properties.

The main reason why we can't rely on computed styles exclusively, is that they give incorrect values when the display mode of an element is none: https://jsfiddle.net/que_etc/yLbogv2t/

Here, take a look at the code responsible for retrieving content dimensions of an HTMElement.

Is ResizeObserver supposed to give us float values or integer values?

Yeap, it's supposed to give non-rounded values.

Is there no other API like clientWidth/clientHeight, but that returns floats?

Well, there is a draft of the getBoxQuads method which can give dimensions of the margin|border|padding|content box-models, but for the time being, it's been implemented only in Firefox Nightly. And even if it was available in the rest of browsers, we wouldn't be able to use it as it would give rectangle after applying CSS transformations (just as the getBoundingClientRect does), which is not compliant with the ResizeObsrever spec.

P.S. I'll give a more detailed answer to your question regarding performance and transitions a bit later. It's quite late here in Ukraine :)

Though the short answer is that this polyfill detects each frame only of those transitions that don't have a delay and fires just one notification (with the final state of an element) for the rest of them. Basically, it repeats the size check insofar it detects changes in dimensions of observed elements and uses subscription for the transitionend event to catch postponed changes.

que-etc avatar Mar 12 '17 23:03 que-etc

The main reason why we can't rely on computed styles exclusively, is that they give incorrect values when the display mode of an element is none: https://jsfiddle.net/que_etc/yLbogv2t/

Yeah, but that's subjective: it may be easy to say that a width of 200x200 is expected, just that the element is not rendered. That actually makes more sense to me, because the width isn't 0, it is 200, an intrinsic property that is separate from whether or not something is rendered.

Are there cases where it is impossible to tell if an element is not visible without using client properties? I mean, maybe we can't rely just on computed values, but maybe we can rely on computed values + styles?

It may be that I just don't know what you know. Can you give an example of where we can't, for example, check both computed style, not just size? (f.e. Why not check display, visibility, etc, and computed style instead of clientWidth/clientHeight?)

And even if it was available in the rest of browsers, we wouldn't be able to use it as it would give rectangle after applying CSS transformations (just as the getBoundingClientRect does), which is not compliant with the ResizeObsrever spec.

Are you sure? The example in the "Using bounds" section shows that the DOMQuad returned has float DOMPoint values. We can take p4 and p1 in that example and calculate the hypotenuse between them, which gives us the floating-point height of the element. (but it would be nice if they simply gave us properties as easy to access as the clientWidth/clientHeight)

Looking forward to a small brief on how the transition stuff works! :}

On a sidenote, this is particularly interesting. I will need to understand these minute details because I'm intending to add WebGL to my project, and wish to coordinate DOM and WebGL in the same 3D space. By knowing these types of quirks I can make sure that a WebGL plane (for example) can overlay perfectly on a DOM element (to apply for example a glossy effect to a DOM element). The concept will be similar to Famous' now-abandoned "mixed mode", or like in this example but you can see in the example that the WebGL isn't clipped perfectly. Looks like you're well-versed on these sizing details since you've had to deal with them in your polyfill. :}

trusktr avatar Mar 14 '17 04:03 trusktr

After doing some testing, making just a simple requestAnimationFrame loop like the following uses about 7% CPU in Chrome (on my MacBook Pro) even though the loop does absolutely nothing:

requestAnimationFrame(function loop() {
  requestAnimationFrame(loop)
})

If your polyfill doesn't make a polling loop (in supported browsers), I think I might want to switch to it just for this reason alone!

trusktr avatar Mar 18 '17 21:03 trusktr

Quick question: when do the event handlers fire? Do they fire in an animation frame (in browsers that support that)?

trusktr avatar Mar 18 '17 21:03 trusktr

Yeah, but that's subjective

I probably shouldn't have said that its values were incorrect. They just don't result in the behavior described in the Resize Observer spec which suggests that the display: none gives an empty rectangle:

  • observation will fire when watched Element display gets set to none.
  • observation will fire when observation starts if Element has display, and Element’s size is not 0,0.

So it wouldn't be possible to adhere to those conditions if we used computed styles as is.

Are there cases where it is impossible to tell if an element is not visible without using client properties? I mean, maybe we can't rely just on computed values, but maybe we can rely on computed values + styles?

Indeed, there are cases when using computed styles solely is not enough.

<div style="display: none;">
    <div>
        <div id="target"></div>
    </div>
</div>

To tell if the target element is hidden (using computed styles only) you would need to go up the tree checking every parent node for the display value (with the getComputedStyle) because the display state of target itself is block.

There is also the offsetParent property value of which is null when an element or its parent nodes are hidden. But, unfortunately, it will be null as well in case when an element (or its parent nodes) has a fixed positioning (position: fixed). So, we end up in a similar loop checking for position instead of display.

That is why I think that the client/offset dimensions is the most effective approach in this case.

On top of that, with the clientWidth && clientHeight condition we also achieve this behavior:

  • observation will fire when watched Element is inserted/removed from DOM.
  • observations do not fire for non-replaced inline Elements.

the DOMQuad returned has float DOMPoint values

You are right, but I was talking about transformations like scaling that affect the data returned by getBoxQuads.

<div style="width: 200.5px; height: 200.5px; transform: scale(0.5);"></div>
<!--
    getBoxQuads({
        box: 'content'
    }) => [{
        bounds: {
            ...
            width: 100.25,
            height: 100.25
        }
    }]

    ResizeObserver() => [{
        contentRect: {
            width: 200.5,
            height: 200.5
        }
    }]
-->

That is actually the exact reason why this method can't be fully polyfiiled.

we need floating point values that represent what is actually rendered on screen. clientWidth and clientHeigh tare, for all intents and purposes, values aliased to CSS pixels

I'd also like to clarify things regarding your comment on the spec repository.

ResizeObserver, as well as this polyfill, observes content dimensions with floating points instead of client[width/height] properties.

que-etc avatar Mar 19 '17 01:03 que-etc

That is why I think that the client/offset dimensions is the most effective approach in this case.

FYI element.getClientRects().length should be zero only when element is detached or not rendered. It's probably the most robust way to detect those two cases.

rjgotten avatar Aug 29 '17 08:08 rjgotten

bug found. debugging from https://github.com/react-component/menu/issues/444 this is minimal code to reproduce

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ResizeObserver IE11 bug</title>
    <style>
        .hidden {
            height: 0;
            overflow: hidden;
        }
    </style>
    <script src="dist/ResizeObserver.global.js"></script>
</head>
<body>
<div id="parent">
    <div class="hidden">label 1</div>
    <div class="hidden">label 2</div>
    <div class="hidden">label 3</div>
</div>
<script>

    var p = document.querySelector('#parent');
    var children = document.querySelectorAll('.hidden');

    function pResize() {
        console.log('resize in Parent');
    }

    function cResize(entries) {
        var i;
        for (i = 0; i < entries.length; i++) {
            console.log('resize in Child', entries[i].target.innerText);
        }
    }
    var obp = new ResizeObserver(pResize);
    var obc = new ResizeObserver(cResize);
    obp.observe(p);
    var i, el;
    for (i = 0; i < children.length; i++) {
        el = children[i];
        obc.observe(el);
    }
</script>
</body>
</html>

for a hidden element above, clientWidth is 0 on IE11 while on chromium or firefox is non-zero, which will make different behaviors. Shall we use getClientRects instead?

jonirrings avatar May 11 '22 06:05 jonirrings

@que-etc

ikonan avatar May 27 '22 02:05 ikonan