elementsFromPoint() returns duplicate HTML tag

RESOLVED INVALID

Status

()

RESOLVED INVALID
2 years ago
2 years ago

People

(Reporter: Jip-Hop, Unassigned)

Tracking

({testcase})

48 Branch
testcase
Points:
---

Firefox Tracking Flags

(Not tracked)

Details

(Reporter)

Description

2 years ago
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/601.5.17 (KHTML, like Gecko) Version/9.1 Safari/601.5.17

Steps to reproduce:

I assigned the body and html tags position:relative;
html, body{
  position:relative;
}
Then I made a position absolute div with z-index:-1;

When I call document.elementsFromPoint() with the coordinates of the div, the returned list contains the HTML tag twice. Once before the div and once as the last element of the list.

Here is a demo: https://jsfiddle.net/0v7xsd7y/


Actual results:

With z-index: -1;

The actual result is: H2 BODY HTML DIV HTML


Expected results:

The expected result is: H2 BODY HTML DIV

The length of the returned list is too long. There aren't 5 elements at these coordinates, instead there are 4. The html tag is listed twice.

I think this might be because the specification mentions that the document (html tag) is always returned as last element in the list. At least I remember something like that.

However this leads to strange behavior in this edge case.

I think it could be solved here: https://dxr.mozilla.org/mozilla-central/source/dom/base/nsDocument.cpp#3428

Something like this could work:

    // Used to filter out repeated elements in sequence.
    // Used to check if last added element (HTML tag) has already been added
    dom::Element* lastAdded = nullptr;
    
    for (uint32_t i = 0; i < outFrames.Length(); i++) {
        nsIContent* node = GetContentInThisDocument(outFrames[i]);
        
        if (node && node->IsElement()){
            dom::Element* element = node->AsElement();
            if (element != lastAdded) {
                aElements.AppendElement(element);
                lastAdded = element;
            }
        }
    }
    
    int lastElementIndex = aElements.IndexOf(lastAdded);
    int realLastElementIndex = aElements.Length() - 1;
    
    if(lastElementIndex < realLastElementIndex){
        // Remove last element, it's a duplicate entry of the HTML tag
        aElements.RemoveElementAt(realLastElementIndex);
    }

In my example code I removed the parts that are needed to get elementFromPoint to work with this code as well. So that should be put back in.

Updated

2 years ago
Component: Untriaged → Untriaged
Keywords: testcase
Product: Firefox → Core
(Reporter)

Comment 1

2 years ago
On second thought, an when testing this scenario on Google Chrome I came to the conclusion the expected outcome is:

H2 BODY DIV HTML

This is also what Chrome outputs in this case.

I've updated the jsfiddle to show the order of elements more clearly: https://jsfiddle.net/0v7xsd7y/1/

With z-index: -1, the div is positioned between the html and body tag. The html tag has a solid green background. If the div would be drawn behind the html tag, it would be hidden behind the green background. Clearly this is not the case as the div with red background is still visible. It is drawn behind the body tag though, because the striped background of the body is in front of the div.

I'm not sure why Firefox puts the duplicate HTML tag in there, but the one at the end is correct. The one in the middle shouldn't be there.
I think the result in Firefox is the correct one per the spec:
https://drafts.csswg.org/cssom-view/#dom-document-elementsfrompoint
In particular, the last "HTML" comes from:
"If the document has a root element, and the last item in sequence
is not the root element, append the root element to sequence."
Component: Untriaged → DOM
Fwiw, I'm guessing that step is there to represent the "canvas":
https://www.w3.org/TR/CSS22/zindex.html#painting-order
(Reporter)

Comment 4

2 years ago
Yes I agree the HTML element should be the last item in the list. That's in correspondence with the way it works in Chrome too. But the second HTML node (between BODY and DIV) seems to be an error.
No, because that's where the "layout box" (using the elementsFromPoint spec term) for
the HTML element is located in the stack.  The DIV layout box is behind it.
Perhaps Chrome simply doesn't implement negative z-index correctly?
(Reporter)

Comment 6

2 years ago
But just using common sense I'd say a single DOM node can't be in two places at the same time. So where is the HTML node in this case? Below or above the DIV with negative z-index? According to the visual evidence I'd say the HTML node is below the DIV, because the green background from the HTML tag doesn't cover the DIV.

"For each layout box in the viewport, in paint order..." What does the HTML tag in the middle represent? Not the green background surely (I'm referring to the second demo: https://jsfiddle.net/0v7xsd7y/1/). As far as I can see there's nothing to be painted between the BODY and DIV.

It's not a bug if this is the way it's supposed to be. But I'd still like to point out the difference in behavior with Chrome and the fact that I don't see why this is correct behavior.

I'd very much like to understand why this is correct.
> But just using common sense I'd say a single DOM node can't be in two places at the same time.

I told you on the list: a DOM node can have multiple parts with different z-orders that stack differently relative to other parts of other nodes.   In other words, it can be present multiple times in a z-ordered list at a given point.

> So where is the HTML node in this case?

Its layout box is above the div with negative z-index.  You can see this if you put borders on both and see which border paints on top.

> Not the green background surely

Indeed, because the background gets propagated to the canvas, which is behind the <div>.

>  As far as I can see there's nothing to be painted between the BODY and DIV.

Try the border thing I suggested.
https://jsfiddle.net/0v7xsd7y/3/ shows what I mean: The div's background is clearly painted behind the border of the <html> element.

And yes, if you look at Chrome it does get it wrong in terms of the actual painting, not just theelementsFromPoint results.  I believe this is a known bug in Chrome, actually.  In any case, elementsFromPoint is just reporting what the layout engine is actually doing here.
Status: UNCONFIRMED → RESOLVED
Last Resolved: 2 years ago
Resolution: --- → INVALID
(Reporter)

Comment 9

2 years ago
Thanks for updating the demo. Now I see how it works in Firefox. I updated it again so that elementsFromPoint() still gets results: https://jsfiddle.net/0v7xsd7y/4/.

I see the yellow border overlaps the DIV so the result from elementsFromPoint() is indeed correct. The DIV is indeed between the borders of the HTML tag and the background of the HTML tag.

That this behavior is possible is totally new to me, so sorry for opening up this bug related to elementsFromPoint(). Since I can only set a single z-index for the entire DOM node, I expected this to be applied to all parts of the element.

Opening up your demo in Chrome gives me different results, where the yellow border and green background are rendered on the same z-index. All behind the DIV.

When I manually apply z-index:0; to the HTML tag (which is not the default value, I know) the yellow border and background collapse into a single element painted behind the DIV. This, it seems, is what Chrome does. At least the visual result is the same.

> I told you on the list: a DOM node can have multiple parts with different z-orders that stack differently relative to other parts of other nodes. In other words, it can be present multiple times in a z-ordered list at a given point.

Are there other examples where a node can be sandwiched between multiple parts of another node? Or is this only possible with the root node? I'm curious for another case where elementsFromPoint() would return duplicate results (other than HTML). If this is a general case I really need to change how I intend to use this function. 

At least now I know where this duplicate is coming from. Thanks!
> Are there other examples where a node can be sandwiched between multiple parts of another node?

For the case of a _node_, I'm not sure; this might be the only case.

And in terms of what elementsFromPoint returns, I _think_ it can't.  But in terms of what's actually going on, consider this testcase:

  <div style="background: green; padding: 10px; color: white">
    <div style="background: purple; height: 20px; margin-bottom: -20px"></div>
    Some text
  </div>

The text of the outer div is on top of the inner div's text (if there were any), which is on top of the purple background which is on top of the green background.  elementsFromPoint shows this as "inner, outer, body, html", but that's slightly silly; if there were a <span> wrapping "some text" it would be claimed to be on top of the inner <div> for elementsFromPoint purposes...
And it actually gets worse: https://jsfiddle.net/b3156rc2/1/ shows that elementsFromPoint will happily ignore the text, while actually clicking at those coordinates will not and will instead have the outer div as target....
We do return the outer div if you use elementFromPoint instead though:
http://hg.mozilla.org/mozilla-central/annotate/ab8a76ac7b34/dom/base/nsDocument.cpp#l3433

I wonder why they didn't spec node[s]FromPoint instead, they seem more useful.
Hmm, should the methods use same kind of targeting as mouse events: if text node is found by hit testing, use its parent element as the target.
(In reply to Mats Palmgren (:mats) from comment #12)
> We do return the outer div if you use elementFromPoint instead though:
Looks like Gecko and Blink behave differently there.
Blink returns body.



FWIW, I filed https://www.w3.org/Bugs/Public/show_bug.cgi?id=29591
(In reply to Olli Pettay [:smaug] from comment #13)
> Hmm, should the methods use same kind of targeting as mouse events: if text
> node is found by hit testing, use its parent element as the target.

Isn't that what elementFromPoint does?  (see link in comment 12 above)
(but elementsFromPoint (plural) is intentionally different for some reason)
Kyle might know why there's a difference since he wrote this code.
Flags: needinfo?(khuey)
Different Kyle ...
Flags: needinfo?(khuey) → needinfo?(kyle)
Ok, I /think/ https://bugzilla.mozilla.org/show_bug.cgi?id=1164427#c17 answers what you're looking for? If not, ni? me again.
Depends on: 1164427
Flags: needinfo?(kyle)
No, not really, but I guess there is no answer since we're just implementing a legacy
Microsoft API, so they are the only ones who can answer my question I suppose.
You need to log in before you can comment on or make changes to this bug.