-
Notifications
You must be signed in to change notification settings - Fork 20.6k
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
Conversation
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
Oh, one more thing -- this PR slightly changes things of returned data for hidden elements: Before: |
Once more, probably I've missed something, why do we need
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
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. |
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:
So I need to change that line of code to support IE8. Will do soon. |
This PR is reported against the |
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:
|
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 You may either remove the IE8 workarounds from this PR and leave it at that or you can report a separate PR to |
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 ) ) { |
There was a problem hiding this comment.
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.
Sure.
Yeah, I already commented this part in PR topic.
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 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 }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
* 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)
Correct comment
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 ) { |
There was a problem hiding this comment.
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
ornull
or{ top: 0, left: 0 }
- inaccurate for rare zero-size edge cases
- consider zero width or zero height "hidden"; return
undefined
ornull
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- If the element on which it was invoked does not have an associated layout box return an empty DOMRectList object and stop this algorithm.
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:
- 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.
Anyway, we can be here accurate as much as we want.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
👍
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Fixes upon comments in pull request * Add test for hidden (display: none) elements
Hi all, sorry for delay I had really busy days. I just made fixes as we discussed here. Also added test for hidden elements. |
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 }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto. +1 for throwing.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 }
.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tracking in #2114.
Also a behavior change for throwing on EDIT: But both behavior changes (exceptions from |
Test fix: insert temporary node into #qunit-fixture, instead of document.body
@scottgonzalez was this one of those spurious failures due to the Github API being too trigger-happy? |
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. |
Ping again, what stops this merging this now? |
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. |
@timmywil, 3 weeks later I want to ping you once more) |
@nazar-pc Don't worry. I haven't forgotten. Other things take precedence right now. |
Seriously, it s too long for already agreed change, almost 3 more weeks. |
@nazar-pc important thing that is will be released with 3.0, when it will be landed is not that relevant. |
This pull request fixes #1784 issue.
Quick summary:
element.contains()
andelement.compareDocumentPosition()
does not work with ShadowDOM and always treated as disconnected even if it in the treeelement.getBoundingClientRect()
work fine with elements in ShadowDOM, returns correct values when element is in the tree and zeroes when notBehavior of
getBoundingClientRect
:What was made:
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 disconnectedjQuery.contains()
has no effect with ShadowDOM (does not work as expected) I added check for existence of ShadowDOM before callingjQuery.contains()
so we do not rely on it when it does not work.Questions:
jQuery.supports.ShadowDOM
and then use it?.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
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 usejQuery.contains()
in situation when it does not work and, by the way, prevents IE8 to fail.