Skip to content

Fix .offset() to work in ShadowDOM #2043

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from

Conversation

NekR
Copy link
Contributor

@NekR NekR commented Jan 29, 2015

This pull request fixes #1784 issue.

Quick summary:

  • element.contains() and element.compareDocumentPosition() does not work with ShadowDOM and always treated as disconnected even if it in the tree
  • element.getBoundingClientRect() work fine with elements in ShadowDOM, returns correct values when element is in the tree and zeroes when not

Behavior of getBoundingClientRect:

  • For hidden elements returns all zeros
  • For disconnected elements return all zeros
  • In IE8 throws error when uses with disconnected element

What was made:

  • Added guard which returns dummy block when rect has only zeroes (disconnected or hidden)
  • jQuery.contains() check was not removed since without it IE8 will throw error and hence we need to return early for IE8 if element is disconnected
  • But because jQuery.contains() has no effect with ShadowDOM (does not work as expected) I added check for existence of ShadowDOM before calling jQuery.contains() so we do not rely on it when it does not work.

Questions:

  • Maybe it's better to move check of ShadowDOM existence to jQuery.supports.ShadowDOM and then use it?
  • @nazar-pc mentioned in Issues using ShadowDom and ShadowRoot elements #1784 what he already made tests for .offset() in ShadowDOM. I can made my own test or we can use his since they are already made. If you decide to use his test, then probable it's better what he will commint them by himself.

Also @gibson042 said in #1784

I don't like this conflation of unrelated behaviors.

About this line of code: if ( !elem.createShadowRoot && !jQuery.contains( docElem, elem ) ) {
I agree with it, but this check is more performant than tree traversal or try..catch block. Also it's not so much unrelated, it's used to prevent use jQuery.contains() in situation when it does not work and, by the way, prevents IE8 to fail.

NekR added 3 commits January 28, 2015 03:01
Add guard for ```createShadowRoot``` existence and return early if
user-agent does not support ShadowDOM and elem is disconnected

Fixes jquery#1784
Restore correct possition of ```docElem``` in the code
@NekR
Copy link
Contributor Author

NekR commented Jan 29, 2015

Oh, one more thing -- this PR slightly changes things of returned data for hidden elements:

Before: { top: 0 + pageYOffset - clientTop, left: 0 + pageYOffset - clientLeft }
After: always { top: 0, left: 0 }

@nazar-pc
Copy link
Contributor

Once more, probably I've missed something, why do we need if ( !rect.width && !rect.height ) { if documentation says:

While it is possible to get the coordinates of elements with visibility:hidden set, display:none is excluded from the rendering tree and thus has a position that is undefined.

We can save few bytes by removing it without breaking behavior defined before.

Also

if ( !doc ) {
    return;
}

can likely be removed (#1784 (comment)), it makes no sense at all to call $(document).offset() because of:

Get the current coordinates of the first element, or set the coordinates of every element, in the set of matched elements, relative to the document.

It is just ridiculous to do that (also, it does not even returns zeros), thus we can save few more bytes.

If mentioned changes will be applied I like it otherwise.

@NekR
Copy link
Contributor Author

NekR commented Jan 29, 2015

Once more, probably I've missed something, why do we need if ( !rect.width && !rect.height ) { if documentation says:
We can save few bytes by removing it without breaking behavior defined before.

getBoundingClientRect() always returns ClientRect and for hidden elements (display: none; for example) it returns: ClientRect {height: 0, width: 0, left: 0, bottom: 0, right: 0}

can likely be removed (#1784 (comment)), it makes no sense at all to call $(document).offset() because of:

I do not decide such things and even more, that lane of code is not related to ShadowDOM issue which I want to fix with this PR. So, probably, if you want to change that behavior of jQuery then I think you can create separate PR which resolves that.

Anyway, thank you, because your comment helped me remember this thing about IE8:

[1] In IE8 and below, the TextRectangle object returned by getBoundingClientRect() lacks height and width properties. Also, additional properties (including height and width) cannot be added onto these TextRectangle objects.

So I need to change that line of code to support IE8. Will do soon.

@mgol
Copy link
Member

mgol commented Jan 29, 2015

So I need to change that line of code to support IE8. Will do soon.

This PR is reported against the master branch where we support IE9+ only. The compat branch supports IE8+.

@NekR
Copy link
Contributor Author

NekR commented Jan 29, 2015

This PR is reported against the master branch where we support IE9+ only. The compat branch supports IE8+.

I understand it. But in this comment #1784 (comment) @gibson042 said that for this case it would be enough to have one PR for both versions (If I correctly understod him).

So:

  • ShadowDOM problem should be fixed if both versions, I think.
  • If you want, I may remove all IE8 tricks from this code (and do not add more) and will make separate PR for compat version.
  • If you want same code in both versions, then I will add fix for IE8 here.

@mgol
Copy link
Member

mgol commented Jan 29, 2015

I understand it. But in this comment #1784 (comment) @gibson042 said that for this case it would be enough to have one PR for both versions (If I correctly understod him).

If the patch needed for both branches is similar, we usually just do the cherry-picking with those minor necessary modifications ourselves. But the PR against the master branch should not have fixes for IE8.

You may either remove the IE8 workarounds from this PR and leave it at that or you can report a separate PR to compat that would contain the IE8 quirks.

if ( !jQuery.contains( docElem, elem ) ) {
// Make sure it's not a disconnected DOM node,
// which is enough only if user-agent does not support ShadowDOM
if ( !elem.createShadowRoot && !jQuery.contains( docElem, elem ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conflation of unrelated behaviors is Shadow DOM support and applicability of getBoundingClientRect to disconnected elements. As I mentioned in #1784, I'd like to see this entire block (and all detection or special treatment of disconnection) removed.

@NekR
Copy link
Contributor Author

NekR commented Jan 29, 2015

If the patch needed for both branches is similar, we usually just do the cherry-picking with those minor necessary modifications ourselves. But the PR against the master branch should not have fixes for IE8.

Sure.

The conflation of unrelated behaviors is Shadow DOM support and applicability of getBoundingClientRect to disconnected elements.

Yeah, I already commented this part in PR topic.

As I mentioned in #1784, I'd like to see this entire block (and all detection or special treatment of disconnection) removed.

Ok, sure. I just was not clear about it in your previous comment. This would be better "We do not need IE8 support in master so please remove IE8 related code here and we will figure out about other way for IE8 in compat branch by ourselves".
I understand why it's unrelated to master -- because of no support of IE8. In all other cases it's very related unless you plan to use there ``try..catch`.

Will remove that block soon.

Remove IE8 unrelated code: ShadowDOM checks and getBoundingClientRect quirks

Fixes jquery#1784
return box;
// Make sure element is not hidden or disconnected
if ( !rect.width && !rect.height ) {
return { top: 0, left: 0 };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather just return; here... { top: 0, left: 0 } looks like a real value, when we know it cannot be.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds resonable. Will change it in next commit.

NekR added 3 commits January 30, 2015 02:55
* Make more precise detection of hidden elements,
  match only those which are disconnected or ```display: none;```
  (i.e. those which does not participate in browser layout).
* Return ```undefined``` for hidden/disconnected elements instead of
  zeros (also changed corresponding test and ```.setOffset()``` method)
return box;
// Make sure element is not hidden (display: none) or disconnected
if ( !rect.width && !rect.height && !rect.left &&
!rect.top && !rect.right && !rect.bottom ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is overkill; the only properties that matter for detecting visibility are width and height—and note that there is literally no way to differentiate no-content inline elements that happen to be at viewport origin from disconnected or display:none elements with gBCR, since they all have zero width/height/top/left. All are arguably "hidden" in the parlance of our API documentation, so our handling comes down to a matter of taste (which is responsible for the churn here; sorry about that @NekR):

  • assume any data is valid and always apply scroll offset
    • inaccurate for disconnected/display:none
  • assume zero width and zero height indicate disconnected/display:none; return undefined or null or { top: 0, left: 0 }
    • inaccurate for rare zero-size edge cases
  • consider zero width or zero height "hidden"; return undefined or null or { top: 0, left: 0 }
    • inaccurate for slightly less rare zero-size edge cases

All the "inaccurate" results are allowed by our documentation, but are probably not equally useful to consumers (and in fact, the undefined or null results may be beneficial). Option 2 is only relevant if we want to give normal treatment to single-dimension elements (e.g., explicitly-sized elements or no-content display:block), so what's more valuable: accurate position for zero-size elements (first option), or distinct output for probably-unpositionable input (third or second option, especially with non-rect return)?

@dmethvin, @mzgol, @timmywil: care to weigh in?

Edit: I personally lean towards the second option and null/undefined, which tries hard to give a position, but not so hard that one is mistakenly applied to truly unpositioned elements.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my personal opinion, I do not think what visually hidden element is always hidden by perspective of developer or even browser. If element participates in browser layout -- then that element absolutely not hidden. Consider following situation:

Someone uses empty <div> like a placeholder for future content (ajax loading for example). By some situation -- user scroll page so that position <div> becomes in view (or other same situation). And then developer needs to load content in that <div>.
So, to determine this situation developer absolutely need to know position of that <div>, even if height of that <div> is 0.

Try to run this example line by line in console ($0 === some block in this page):

var a = document.createElement('div');
$0.appendChild(a);
a.getBoundingClientRect();
// ClientRect {height: 0, width: 653.6363525390625, left: 124.90056610107422, bottom: 405.553955078125, right: 778.5369262695312…}
a.style.margin = '20px';
a.getBoundingClientRect();
// ClientRect {height: 0, width: 613.6363525390625, left: 144.9005584716797, bottom: 425.553955078125, right: 758.5369262695312…}

As you can see, practically that element is not hidden (even if it's not visible by eye on a page). Even more, it still participates in layout and when you will add margin to element, it still will be hidden, but will take 40px height on a screen.

If this is "rare" case, then okay, I will simplify that condition. But for now I will wait for other responses.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you, which is why I'm advocating a test like !rect.width && !rect.height that will not trip up on single-dimension elements such as your no-content div. But at the same time, we must recognize that dimensionless (e.g., no-content inline) can be disconnected/display:none false positives (or vice versa)... I don't think the extra top/left checks are worth it, given that even they are imperfect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the extra top/left checks are worth it

Do you talk here about performance?

There is only one possible false-positive situation when element is placed at top-left corner and screen is not scrolled (or simply position: fixed).
If you really care about this situation we can use getClientRects() which returns 0 length list for elements which does not participate in layout, according to spec:

  1. If the element on which it was invoked does not have an associated layout box return an empty DOMRectList object and stop this algorithm.

http://www.w3.org/TR/cssom-view/#dom-element-getclientrects

So this might be the case:

if ( !element.getClientRects().length ) {
  // We are not interested
}

Also, if you do not like getClientRects() method, we can use something like .offsetParent which returns null almost in same situations (but getClientRects() is better of course because .offsetParent will require a lot of other checks):

The offsetParent attribute must return the result of running these steps:

  1. If any of the following holds true return null and terminate this algorithm:
  • The element does not have an associated CSS layout box.
  • The element is the root element.
  • The element is the HTML body element.
  • The element’s computed value of the position property is fixed.

http://dev.w3.org/csswg/cssom-view/#dom-htmlelement-offsetparent

Anyway, we can be here accurate as much as we want.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lean towards 2 as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the extra top/left checks are worth it

Do you talk here about performance?

No; code size.

If you really care about this situation we can use getClientRects()

Oh perfect, I forgot that was exposed directly!

// Return a non-empty value only if it the element has layout
if ( rect.width || rect.height || elem.getClientRects().length ) {
    return {
        top: rect.top + win.pageYOffset - docElem.clientTop,
        left: rect.left + win.pageXOffset - docElem.clientLeft
    };
}

And just fall out of the function with an implicit undefined otherwise.

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gibson042 we do not need first check about rect width/height if we going to use getClientRects() because getBoundingClientRect() is backed by it, so in your example check for rect width/height is overkill because in all cases there they will match, getClientRects() will also match too.

So, you guys want to match only truly hidden/disconnected elements (with CSS layout) then it's enought to check for getClientRects() length. If you plan to support "Option 2" where we treat zeros of width & height as hidden element -- then check only for those properties is enough.

But I also see reason in your check, @gibson042. In perform world calling of getClientRects() and then getBoundingClientRect() should trigget only one layout (or zero), but I am not really sure about all browsers. So in this case your check seems resonable -- call getClientRects() only when it's required.

Anyway, I am ready to use each of those options, so please, pick one and I will be happy to update my PR (yes I saw what @dmethvin and @timmywil prefer "Option 2", but it's still not clear to me which we are really gonna use).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason our API users seem to strongly prefer silent nonsense results without errors for edge cases, so returning an object there might result in fewer bug reports.

We have three internal uses of .offset() as well; one in the offset setter (see above) and two in .position(). I'm willing to let the latter throw on no-layout input, but I'm not sure about the former—is there a case for essentially letting the offset setter proxy .css({ top: y, left: x }) in the absence of a layout box? If not, the code changes there go away and that throws as well. And having typed it, I'm thinking that throwing there too is reasonable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

match only truly hidden/disconnected elements (with CSS layout)

call getClientRects() only when it's required

Yes to both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes to both.

Got it now, very good idea @gibson042.

Will make all these changes soon.

* Fixes upon comments in pull request
* Add test for hidden (display: none) elements
@NekR
Copy link
Contributor Author

NekR commented Feb 19, 2015

Hi all, sorry for delay I had really busy days. I just made fixes as we discussed here. Also added test for hidden elements.
So if I understood all right -- we do not need special tests for ShadowDOM since we do not use special checks for it and just assume what it works for any visible and attached element in the tree, right?

@nazar-pc
Copy link
Contributor

Any updates here? Would be nice to finally close this issue.

@@ -32,7 +32,7 @@ jQuery.offset = {
elem.style.position = "relative";
}

curOffset = curElem.offset();
curOffset = curElem.offset() || { top: 0, left: 0 };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relates to #2043 (comment) ... do we want to suppress exceptions when setting document-relative offset on no-layout elements? I'd like some other opinions here, but personally am inclined to drop this change and let the call throw.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto. +1 for throwing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would need to mention it in the release notes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relates to #2043 (comment) ... do we want to suppress exceptions when setting document-relative offset on no-layout elements? I'd like some other opinions here, but personally am inclined to drop this change and let the call throw.

Do you suggest to just remove || { top: 0, left: 0 } or guard for curOffset and throw manually custom error?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per meeting, remove || { top: 0, left: 0 }.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, after further discussion, we'll leave as is and make this change separately. Sorry to confuse! Nothing further for you to do here @NekR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking in #2114.

@gibson042
Copy link
Member

Also a behavior change for throwing on $disconnected.position(): #2043 (comment)

EDIT: But both behavior changes (exceptions from $disconnected.position() and exceptions or undefined from $disconnected.offset()) are "safe" in that they only manifest with documented-invalid input (e.g., disconnected elements).

Test fix: insert temporary node into #qunit-fixture,
instead of document.body
@NekR
Copy link
Contributor Author

NekR commented Mar 2, 2015

Hmm, CLA test says:

Unfortunately, there was an error checking the CLA signatures.

Error: Command failed: fatal: Invalid revision range 4fae911..2e7e1b1

What is wrong with it, should I rebase to latest jquery master?

@dmethvin
Copy link
Member

dmethvin commented Mar 3, 2015

@scottgonzalez was this one of those spurious failures due to the Github API being too trigger-happy?

@scottgonzalez
Copy link
Member

Could be. The missing revision was the new one, so it would make sense that it's the result of the PR branch not being updated in time. Unfortunately that means that the retry logic won't fix this.

@nazar-pc
Copy link
Contributor

Ping again, what stops this merging this now?
As I see removing || { top: 0, left: 0 } is the only thing here.

@NekR
Copy link
Contributor Author

NekR commented Mar 19, 2015

@nazar-pc

They said:

Actually, after further discussion, we'll leave as is and make this change separately. Sorry to confuse! Nothing further for you to do here @NekR

So actually nothing should prevent it (at least known).

@timmywil
Copy link
Member

This is ready to be merged. I was about to, but noticed that I'll need to spend a little time figuring out what is needed to cherry-pick this to the compat branch. I plan on getting to it this weekend.

@nazar-pc
Copy link
Contributor

nazar-pc commented Apr 9, 2015

@timmywil, 3 weeks later I want to ping you once more)

@timmywil
Copy link
Member

timmywil commented Apr 9, 2015

@nazar-pc Don't worry. I haven't forgotten. Other things take precedence right now.

@nazar-pc
Copy link
Contributor

Seriously, it s too long for already agreed change, almost 3 more weeks.

@markelog
Copy link
Member

@nazar-pc important thing that is will be released with 3.0, when it will be landed is not that relevant.

@timmywil timmywil closed this in 1617479 May 5, 2015
timmywil pushed a commit that referenced this pull request May 5, 2015
@NekR NekR deleted the 1784-fix-offset-in-shadow-dom branch May 6, 2015 18:33
@markelog markelog mentioned this pull request Nov 16, 2015
@markelog markelog mentioned this pull request Dec 22, 2015
@lock lock bot locked as resolved and limited conversation to collaborators Jan 19, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

Successfully merging this pull request may close these issues.

Issues using ShadowDom and ShadowRoot elements
10 participants