tabbable: v5.3.0 breaks literally all of our usage of `tabbable` š when called on a node not attached to the document
The change to use getClientRects().length completely breaks this library for us: https://github.com/focus-trap/tabbable/pull/604
We 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 to tabbable() occurs during the componentDidMount lifecycle method. At that moment, the DOM elements we want to traverse using tabbable are fully formed, but they are not yet attached to the document. So getClientRects().length always returns 0, and thus every single one of our calls to tabbable 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)?
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 40 (24 by maintainers)
Commits related to this issue
- [#664] Re-introduce fallback to displayCheck=full if node not in document Fixes #664 There are legitimate cases where tabbable's APIs are expected to work on container nodes that are not actually at... — committed to focus-trap/tabbable by stefcameron 2 years ago
- [#664] Re-introduce fallback to displayCheck=full if node not in document Fixes #664 There are legitimate cases where tabbable's APIs are expected to work on container nodes that are not actually at... — committed to focus-trap/tabbable by stefcameron 2 years ago
- [#664] Re-introduce fallback to displayCheck=full if node not in document (#665) Fixes #664 There are legitimate cases where tabbable's APIs are expected to work on container nodes that are not a... — committed to focus-trap/tabbable by stefcameron 2 years ago
- [#664] Assume node is visible when displayCheck=full if node not in document (#665) Fixes #664 There are legitimate cases where tabbable's APIs are expected to work on container nodes that are not a... — committed to focus-trap/tabbable by stefcameron 2 years ago
- 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 https://github.com/focus-trap/tabbab... — committed to nice-digital/global-nav by johndavey72 2 years ago
- GN-186 IE11 fixes (#194) * 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 issu... — committed to nice-digital/global-nav by johndavey72 2 years ago
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
vue + @vue/test-utils + jest + jsdom, so I suspect the issue is related to the fact thatjsdomreturns anemptyarray forgetClientRects()event when attached. <= mocking this to return something else and combining it withdelayInitialFocus: falseprop that I learned from the focus-trap-react repo should make things back on track. (Will update this comment if confirmed)For the moment, Iām using the
legacy-fullāescape hatchā for tests š āļøEDIT: I donāt need to suspect, it is the reason, I just need to read the actual documentation chapters. š¤¦
Thank you @stefcameron thats very nice. I could not be more pleased that the fix for this ābeastā of a bug is #666 ššš
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
isHiddenon detached nodes.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
getClientRects()when itās attached.I will update my PR tomorrow accordingly and will get a patch out.
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.
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
displayCheck: noneall this time because the node was detached. Because when detached,getComputedStyle()returnsundefined, but we were always checking=== 'none'so we would conclude the node was visible.I got it: I worked because it never worked in the first place.
@craigkovatch
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
isHidden()function in tabbable, has been a point of contention in the past (hence thedisplayModeoption thatās relatively new for the lifespan of tabbable) and I was excited this might be a good solution, and seemed safe enough.I like your suggestion of a
document.body.contains(node)check and falling back to the old loop up the parent chain.Trying this out locallyā¦
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
Sorry you are right, restoring the old behaviour only means to consider detached nodes visible, which is indeed a smaller problem.
Fix is available in tabbable 5.3.2 now.
Thanks for thinking about it. I donāt see how this is the case, however. If the node is attached, weāll do the
getClientRects()check. If the node is detached, weāll assume itās visible ā once we get to the place inisHidden()where we would normally do thegetClientRects()check.That is the old behavior, because prior to
getClientRects(), we were doinggetComputedStyle(node).display === 'none' -> node IS hiddenand when the node was detached, we would fail this check (because thedisplayproperty would haveundefinedas a value instead of the computed CSS style) and determine the node was NOT hidden.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.
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 š¦
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
tabbable.@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).
@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 š
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
displayChecklogic to use in case the element is detached.@DaviDevMod
Yes, youāre right, that is probably the best option. I am loathe to move even more code into a
setTimeout(or equivalent), but itās kind of my fault for assuming that unattached nodes have any sensible definition of ātabbableā in the first place, I suppose š I will try this out as a workaround for my current state of the world. I think still thereās some decisions to make for this library, though.@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!
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
:tabbableand:focusableas real selectors in the DOM/CSS spec(s). This is information the browser already has, at all times, and it seems liketabbablewill always be doomed to playing catch-up, trying to reconstruct this knowledge from the āoutsideā.