-
Notifications
You must be signed in to change notification settings - Fork 58
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
v5.3.0 breaks literally all of our usage of tabbable
😅 when called on a node not attached to the document
#664
Comments
As some additional context, this broke out from under us, as a result of declaring our dependency on tabbable as I expect there are plenty of other projects which are also going to be broken in the same way -- next time they regenerate their lock files or upgrade a dependency which depends on |
Hey, thanks for reaching out! Just wondering, are you using focus-trap-react, or are you using tabbable on its own with the sole purpose of identifying focusable nodes before your dialog is rendered? Also, how is the node not in the tree yet? The definition of
But if you're not in control of this dialog (I assume, based on this, that it's a third-party library you're using, along with tabbable), so somehow it's still not attached to the DOM when That configuration would not rely on Does this dialog library have a hook so you can tell when it's been attached to the DOM?
I understand it feels like a bug, but technically, tabbable has never sought to support cases where it's operating on an orphaned root node. It assumes the node is in the DOM. I think it just happened to be that it conveniently used to use But that doesn't mean there's nothing to be done about the change to smooth things over. Maybe we can still improve on it without just reverting or adding yet another Right now, it feels like this dialog library is not React-based, or is but isn't playing by React rules, and tabbable is being asked to work around this, but maybe I'm missing something here. Please help me to understand if so! |
Citing this answer on SO:
This problem does not exist in modern react, where Another answer to the same SO question suggests a fairly clean solution to the problem, that is to take advantage of a setState callback. I am here sharing this information just because it pains me to know that my contribution broke something. Whether the problem should be addressed by tabbable is not my concern. |
@DaviDevMod That's not accurate. I would suggest let's not focus on the "React" part of this -- it's only incidental in that it's the thing causing the "node not attached to the DOM yet" timing, but that could occur outside React, too. |
@stefcameron We are using
While I understand totally what you're saying here (as the author of an internal support library myself) -- I would suggest that since this has worked this way for many years now across five major versions, and only suddenly broke two weeks ago, it is in fact a part of the API, even if it was never explicitly documented so in the past. I would be quite shocked if I'm the only one who hits this issue -- probably I'm just the first to hit it and take the time to identify the cause. I had to make an emergency patch to prevent dozens of dependent modules from suddenly failing at our company. So whether or not the tabbable API was intended to work this way previously -- 5.3.0 is a very-literally Breaking Change. At the very least I think you should document it as such, revert this change in a 5.3.2 release, and re-publish as 6.0.0, to prevent anyone else from stumbling into this.
This is a very common misunderstanding of what My perspective is: you were absolutely right to fear breaking clients by making this change, and I hope that you'll consider reverting it quickly and working out the details we're discussing now for a future release.
I'm the author of the UI library which provides the Dialog component -- but I'm not in control of when the rendered dialog gets attached to the document. |
And although again I feel this should be first reverted and then improved upon, what did you think of my idea of running the "fast" version after a |
As an aside, any library of a single utility function that is nearing 1M+ downloads per week has a pretty strong argument to make about something missing in the browser. Perhaps we should work on a proposal to W3C to add |
That's a very good point.
Thank you! This is indeed an easily overlooked detail about React I had never considered before, but makes a lot of sense.
Well, you know what they say about hindsight... 😉 Performance, especially in this I like your suggestion of a Trying this out locally... |
Thank you Stefan! Just to clarify, my comment about fearing breakage was not meant as a critique! :) Just camaraderie for the spidey sense about unexpected breakages, and how they usually come out of the most innocuous-seeming changes! 😆 |
🤜 🤛 |
…ment Fixes #664 There are legitimate cases where tabbable's APIs are expected to work on container nodes that are not actually attached to the document. The result is that the `getClientRects()` API never returns any rects for visible nodes, resulting in tabbable thinking ALL nodes in the container are hidden.
Using the old loop is bad as it causes layout thrashing, but if it's the only option you have, it's better than nothing. In any case, I feel your angriness, seeing your code breaking apart just by updating dependencies can easily be the worst nightmare of a developer. |
Thanks for your input @DaviDevMod @craigkovatch So I opened a draft #665 which falls back to the old traversal if the node isn't in the document at the time the check is being performed. However, this is what I'm seeing: It doesn't work when the node is not in the document. If I roll back to 5.2.1, it actually works, whether the node is in or out of the document but I have no idea why, at least not right now. I don't see what could have happened that would now cause different behavior with But then it seemed to work for you. But my manual test in the browser (latest Chrome on macOS) shows it doesn't work either. So how in the world did this ever work? Even if I rolled back to the original loop without the This comment on a somewhat related issue back in 2015 is in line with my manual test in the browser: "I don't see how the cloned node can have a computed style, as it isn't in the document." But I acknowledge this is not the exact same issue, though the cloned node isn't attached, so seems similar... |
To be clear, when I say it worked under 5.2.1 code, I mean it worked in tests that weee jsdom-based which is questionable. We never had tests that checked that tabbable worked on detached containers, AFAICT. |
The |
I got it: I worked because it never worked in the first place. |
@DaviDevMod no angriness here :) I have caused this kind of problem just as often as I've been a victim of it. Any urgency in my words is in the interest of preventing someone else from hitting this who may be less able to figure out the cause. We're all in this absurd software life together! |
So the real fix to this is to check if the node is detached, and then assume the node is visible -- basically, @craigkovatch has been running in |
With that perspective, I can see why you wouldn't want to make that assumption in the library, though... Option 1) un-break the library for any other existing consumers which may have a secret, bad dependency on this undocumented and weird behavior Option 2) leave the new, correct behavior in 5.3.x, but break lots of consumers.... This bad set of alternatives is why I think reverting to 5.2.1 as 5.3.2, and then re-publishing 5.3.1 as 6.0.0, is the best option. Marking the "breaking" (i.e. fixing, lol) change to the API as a major version prevents any accidents. |
Yes, you're right, that is probably the best option. I am loathe to move even more code into a |
If it can make your code work right now, why no? But anyway, if I've understood correctly, the bug consisted in a simple missed check for whether an element is attached to the document or not, so it can be fixed easily. Edit: oh yeah, there is still the problem to decide what |
tabbable
😅tabbable
😅 when called on a node not attached to the document
It can be both :) I am concerned about others who may hit this. (It also makes testing much more difficult, as the logic that throws an error if there's nothing tabbable is now asynchronous, which is considerably more difficult to catch in an async jasmine+React+enzyme world.) |
@craigkovatch this is the only open issue on tabbable 😆 eventually they will arrive here and try some workaround waiting for the fix. Anyway the bug doesn't exist for those using react with function components and probably the same is true for those using frameworks other than react, so hopefully there are not so many broken projects out there 😅 |
Love this discussion! 😆 Glad we can laugh a bit along the way, even with memes. Yes, what an adventure we are on in software land... So I think we have solution, which is to assume that a detached node is visible. That should restore the old behavior, while letting us keep using the new optimization with I will update my PR tomorrow accordingly and will get a patch out. |
@DaviDevMod This is simply not correct, please read my reply above #664 (comment). (And even if it was, it's not an excuse.) React never guarantees a particular component is attached to the document. Using a hook-based lifecycle method in a functional component has no bearing on that. |
@craigkovatch It's true that you can reproduce the bug in function components using gaearon suggested a workaround in the thread you linked because at the time there were no hooks around. https://www.google.com/search?q=react+16.8+release+date In any case the fix is coming! 🎉 |
@DaviDevMod sorry, none of that is true, and I don't want to argue about it anymore. It's not that simple. (e.g. his workaround is only relevant if you own the entire app your code is running within. I don't.)
It took me nearly two hours to determine the source of the build breakage in the ~25 downstream modules that suddenly broke as a result of this change. Again, I'm not angry about it, but in large and complex software projects, it's often non-trivial to determine what broke, let alone how it broke. I feel you're being a bit dismissive at this point, and I ask that you consider that there's lots of context on literally a million different dependents of this library that none of the three of us understand. It's important to try to Do The Right Thing for them, even if the diagnosis here is now clear. If we can prevent other people from spending a day on this, that's a better path than making it easy to fix only after they are broken and eventually find their way here. |
@craigkovatch I didn't mean to be dismissive, I apologize if I'm giving this impression. I was just relieved and optimistic because the fix is already done and will land soon. I don't have a broken codebase to deal with in the meanwhile though. I can understand that you are not going to feel relieved till the fix has been released. If you want to help those that will look for the issue, maybe you can add a line to your original post pointing to some helpful comment (maybe you could write such comment for them). |
I updated the title in a meager effort toward helping more. I would love to add more, but I think the failure mode will be highly context-dependent and so it's hard to come up with more "search keywords". In my case they just looked like a bunch of jasmine tests that timed out. I went binary searching through changes in modules between early January and now to eventually narrow it down to a change that updated ~12 dependencies, and then took a wild guess that it was |
For posterity -- throwing everything behind a delay works fine at runtime, but still breaks all the downstream tests, because there's now asynchronous behavior that a test I don't own needs to be aware of. That's specific to my context, of course, but wanted to fill out why sometimes it's not so simple :( |
…ment Fixes #664 There are legitimate cases where tabbable's APIs are expected to work on container nodes that are not actually attached to the document. The result is that the `getClientRects()` API never returns any rects for visible nodes, resulting in tabbable thinking ALL nodes in the container are hidden.
OK, well, that was not as easy as it looked given the added complexity of shadow DOM support, but I think I've got this fixed. Hopefully the patch will be good. #665 |
…ment (#665) Fixes #664 There are legitimate cases where tabbable's APIs are expected to work on container nodes that are not actually attached to the document. The result is that the `getClientRects()` API never returns any rects for visible nodes, resulting in tabbable thinking ALL nodes in the container are hidden. __NOTE:__ Assuming the node is visible under the default/legacy "full" mode when the node is __not__ in the document is actually the legacy behavior. When a node is detached, `getComputedStyle(node).display` is always `undefined` and our checks were explicitly verifying if `getComputedStyle(node).display === 'none'` to conclude a node was hidden, which means this check would fail and we could conclude the node was __visible__ even if it was not.
…ocument (#665) Fixes #664 There are legitimate cases where tabbable's APIs are expected to work on container nodes that are not actually attached to the document. The result is that the `getClientRects()` API never returns any rects for visible nodes, resulting in tabbable thinking ALL nodes in the container are hidden. __NOTE:__ Assuming the node is visible under the default/legacy "full" mode when the node is __not__ in the document is actually the legacy behavior. When a node is detached, `getComputedStyle(node).display` is always `undefined` and our checks were explicitly verifying if `getComputedStyle(node).display === 'none'` to conclude a node was hidden, which means this check would fail and we could conclude the node was __visible__ even if it was not.
@stefcameron yesterday I just trusted that you found a solution, without thinking too much about it. But now that I think about it, restoring the old behaviour means that when a node is not attached to the DOM it is automatically considered tabbable as soon as it enters the The possibilities I can see here are:
The last two options are not necessarily a breaking change, since the old behaviour for detached nodes wasn't even intended, it just happened to be somehow exploitable. Regarding the last option, I think that the only way would be introducing async code in tabbable, because it's not possible to asses whether a detached node is focusable and/or tabbable. So the only way is to wait until the node is attached. I wrote some code to see what the async logic would look liike. Here is the added logic for const isFocusable = function (node, options, tries) {
if (!document.body.includes(node)) {
if (tries > 9) throw new Error(node + 'is not attached to the DOM.');
return async () => isFocusable(node, options, tries + 1);
}
...rest of the function...
} The value of I abstain from giving opinions on what should be done, I just wanted to share my reasoning on the issue. |
Thanks for thinking about it. I don't see how this is the case, however. If the node is attached, we'll do the That is the old behavior, because prior to But no one was the wiser, probably because they didn't have this special case where they had hidden nodes in a detached container they were expecting to be determined hidden, so not tabbable/focusable, and were getting what they thought may have been bad results from tabbable. Or they did get unexpected nodes back from tabbable and just learned to live with it or figured something else out, and they've been living with this for potentially years. So best to keep that expectation at this point. |
Fix is available in tabbable 5.3.2 now. |
I agree that the current behavior should be preserved as there are probably many that depends on it, but would change next major version to swap the default behavior to reflect the fact that detached nodes are not actually tabbable/focusable, with an option to ignore |
Sorry you are right, restoring the old behaviour only means to consider detached nodes visible, which is indeed a smaller problem. |
I appreciate everyone's input/discussion on this! The issue is patched in v5.3.2, and I've created an issue to fix the default behavior in the next major (though no plans to publish a major ATM). #666 |
@all-contributors add @craigkovatch for bug |
I've put up a pull request to add @craigkovatch! 🎉 |
Thank you @stefcameron thats very nice. I could not be more pleased that the fix for this “beast” of a bug is #666 😂😂😈 |
…fixes BNF IE11 .contains issue in react-focus-trap with sub-dep tabbable versions > 5.2.1 (see focus-trap/tabbable#664)
* GN-186 Remove non-fix for dropdown closing issue from BNF launch day * GN-186 npm shrinkwrap, ensuring same deps are installed everywhere - fixes BNF IE11 .contains issue in react-focus-trap with sub-dep tabbable versions > 5.2.1 (see focus-trap/tabbable#664) * GN-186 Fix issue with links not wrapping due to IE11 flexbox shortcomings when width isn't specified * attempt to increase specificity of selector * GN-186 Move IE specific style up a level to isolate change to menuWrapper li only * GN-186 Add margin bottom to back to top link to fix issue with link being hidden by footer in IE11 * GN-186 Move ie11 specific style to navlinks * GN-186 Apply width fix for IE11 flexbox shortcomings
This is truly OSS at its finest, you don't see many respectful threads like these days 👏 👴 Just want to leave a thank you note for this, I've finally got some background on why all my tests were failing when migrated to tabbable@6 (via focus-trap@7) while everything seemed to be actually working on the live app (vue). The test setup is For the moment, I'm using the EDIT: I don't need to suspect, it is the reason, I just need to read the actual documentation chapters. 🤦 |
The change to use
getClientRects().length
completely breaks this library for us: #604We use
tabbable
when initializing dialogs, to find candidate element(s) to auto-focus when the dialog appears. These dialogs are React components and the call totabbable()
occurs during thecomponentDidMount
lifecycle method. At that moment, the DOM elements we want to traverse usingtabbable
are fully formed, but they are not yet attached to the document. SogetClientRects().length
always returns 0, and thus every single one of our calls totabbable
fails. (At this library level, we have no way to control when the dialog is actually attached to the document.)Could this change be reverted, or can we scope it to only be used when
document.body.contains(node)
?The text was updated successfully, but these errors were encountered: