Closed Jacob-Allen closed 6 years ago
Hi @Jacob-Allen, before the modal is open, it will put in a queue the document.activeElement
, so it can try to return the focus after the modal is closed. So, if this element doesn't exist in the next route this warn will be presented.
Hey @diasbruno, It is often that the modal is never opened on certain pages. Is there anything I can do to drop it from the queue before I dismount the component so that I can circumvent this warning?
There is an option we added recently - shouldReturnFocusAfterClose={bool}
. If false, it will be no-op when pop out of the queue.
It should work fine in case where you can jump to other pages from the modal, but if it's not the case, it may cause some accessibility issues.
I'll keep this open, because this feature is not documented.
@diasbruno, I am also getting this warning more than a dozen times. It does not happen when I run my app in the browser, but when I run my tests. I see that the logic for displaying this warning has existed for some time now, but I only started seeing it when I upgrade to v3.1.7.
I don't fully understand why this warning is being shown to me. It occurs on components that have modals in them, but they are closed by default, so I'm curious why would they ever be attempting to "return focus" when they shouldn't have been muddling with the focus in the first place (since they are all closed).
before the modal is open, it will put in a queue the document.activeElement, so it can try to return the focus after the modal is closed.
Doh, I missed this my first read through! So the modals are adding to the queue before they are open, i.e. when they are rendered.
I can remove the error by not manually unmounting my component (removing ReactDOM.unmountComponentAtNode(node)
from my smoke test), which come to think of it seems fine.
I suppose I hadn't considered this when I first started writing my tests, but there is usually no reason to manually unmount (only caveat is when testing things like <ReactModal>
open/close directly, since exiting a test with a modal open will pollute document.body.classList
, which could affect other tests). I think it would be safe to remove the manual unmount call for my smoke tests, which also resolves my issue of seeing this warning so many times when running tests.
Thanks!
I'm getting this warning, too. Tracing into it, the following code in focusManager.js
is the issue:
function returnFocus() { var toFocus = null; try { toFocus = focusLaterElements.pop(); toFocus.focus(); return; } catch (e) { console.warn(["You tried to return focus to", toFocus, "but it is not in the DOM anymore"].join(" ")); } }
For whatever reason, toFocus
is undefined when the line toFocus.focus()
is called. Why not include a test to ensure that toFocus
has a valid value before executing that?
In my test case, I haven't actually displayed a modal yet, and still get the warning (i.e. my render( )
method does include a <Modal>
element, but isOpen=false
so it is not actually visible).
Hi @royrwood ,
@diasbruno mentioned that there is an undocumented prop to bypass the focusmanager. By adding shouldReturnFocusAfterClose={false}
to the Modal it will opt out of the focus handler. I have the parent component set this value to false in the componentWillUnmount and it has caused the warning to go away.
This is actually documented on the https://reactcommunity.org/react-modal/. (sorry)
The problem with this is that I don't want to opt out of the focus manager. I just want to be able to render components in my test without every component that has a modal attached to it throwing this warning.
This check in returnFocus
as it stands now is making a huge assumption in that it never expects a component to be manually unmounted, which is unrealistic.
This will warn on any test that unmounts—including Enzyme unmounts—and will even show the warning with the default CRA setup.
@indiesquidge I definitely agree that this should not be the default behavior. I agree that this opt out should not be required.
I opened this issue because I think the part of
making a huge assumption
is in fact an undesirable feature in the current implementation of this package.
I have only referenced this workaround as a way to silence the warning, not as justification of a permanent intended fix.
I know the warning could simply be removed, but ideally the contributors will expand on this logic and auto opt out of the focus manager before a dismount.
I would love to help improve the sophistication of the focus manager to better work with unmounted components.
isMounted
flagWithout a deep dive into this codebase, the simplest solution I can think of is simply to use a flag that tracks whether or not the component is mounted, could be as simple as
// in <ModalPortal>
componentDidMount () {
this._isMounted = true
}
// later …
componentWillUnmount () {
this._isMounted = false
}
Then when we call returnFocus
on the afterClose
instance method, we could check this flag
afterClose = () => {
if (this.props.shouldReturnFocusAfterClose && this._isMounted) {
// where returnFocus is called; only runs if the component is still mounted
}
}
I tested this out in a small CRA build and it works as expected (i.e. warnings are gone, all tests still pass, returnFocus
still works in browser).
focusLaterElements
arrayAnother option would be to simple check if there are any elements in the focusLaterElements
array in returnFocus
and if there aren't simply return
// in focusManager.js
returnFocus () {
if (focusLaterElements.length === 0) {
return
}
// …
}
And have a public function on the focus manager to reset focusLaterElements
to an empty array when the component unmounts
// in focusManager.js
export function clearFocusLaterElements () {
focusLaterElements.length = 0
}
// in <ModalPortal>
componentWillUnmount () {
focusManager.clearFocusLaterElements()
}
Both of these solutions seem to solve the issue, but I'm willing to bet I'm not handling some edge case perhaps. @diasbruno, anything come to mind on an edge case I might be missing? Any preference for either solution? Would it be worth my time to get something like this up in a PR?
@indiesquidge Checking for the length of focusLaterElements
looks nice, but this may hide a bug if the focus manager has a real bug (failed/miss to queue the element, for example). We can temporarily apply this patch though. :+1:
Using the mounted flag on afterClose
looks a bit weird because the only way it can/should be called is if the ModalPortal
is actually mounted. This is something we can investigate (afterClose been called when it shouldn't).
@royrwood pointed out that it's possible to get this warning even if the modal is not open. (Can you get a reproducible example for this?).
Since this is a warning, I think we can look for bugs on the focus manager and the paths that uses this API.
@diasbruno
possible to get this warning even if the modal is not open
My implementation of react-modal is only rarely opened. It mounts closed and dismounts closed 99% of the time and I always get this warning without the opt out.
It is often that the modal is never opened on certain pages.
@Jacob-Allen I miss this part. :)
@Jacob-Allen @indiesquidge @royrwood
Here is a place to dig in Modal.js#L144. It's possible that on componentWillUnmount
of the modal is scheduling the close action event when already closed.
@diasbruno, I think I may need a bit more context to understand the scope of this issue if I am to help find an adequate solution.
Using the mounted flag on
afterClose
looks a bit weird because the only way it can/should be called is if theModalPortal
is actually mounted.
Could you explain why you think afterClose
is being called when it shouldn't be? From what I understand (and from what other people are reporting here), ModalPortal
is mounted when the component using the modal is mounted, not when the modal itself is opened. (This can be verified in the examples by putting console statements in componentWillUnmount
and observing that the function is never called save on the React Router example.)
One could say that afterClose
is perhaps poorly named (or at the very least does not make sense to be invoked in componentWillUnmount
with all of its current behavior), but I don't see why it shouldn't be called.
Here is a place to dig in Modal.js#L144. It's possible that on componentWillUnmount of the modal is scheduling the close action event when already closed.
@diasbruno, I think I am confused by your recommendation here. <Modal>
does not seem to make any reference to the focusManager
—don't we want to be looking for a fix in <ModalPortal>
, or am I misunderstanding something?
I'm referring to a possible path that can trigger afterClose
.
Checking for the length of
focusLaterElements
looks nice, but this may hide a bug if the focus manager has a real bug (failed/miss to queue the element, for example).
So the way I am understanding it, the way that elements are added to the queue is via the focusManager.markForFocusLater()
function, which is currently only ever used once in the open
instance method of the <ModalPortal>
. That open
instance method is only ever called twice (in componentDidMount
and componentWillReceiveProps
), and both times it is called it is behind a predicate of this.props.isOpen
.
Reconciling this with when focusManager.returnFocus()
is called (the function that is the source of this warning issue), which is only ever used in the afterClose
instance method of <ModalPortal>
. That afterClose
instance method is only ever called twice: once in componentWillUnmount
, another invocation in the instance method closeWithoutTimeout
as the callback to setState()
.
Now, running the tests with the codebase as it stands results in 12 focus warnings by my count. If we simply mimic the predicate on which open
is invoked (i.e. this.props.isOpen
must be true) for afterClose
, we see the number of focus warnings in the test drop down 75% (to only 3 warnings).
componentWillUnmount() {
if (this.props.isOpen) {
this.afterClose();
}
clearTimeout(this.closeTimer);
}
So I've been playing around with the source code for a while now, and despite the above snippet removing 75% of the test case warnings, there is an overarching issue at hand here: we are often calling focusLaterElements.pop()
on an empty array.
There is an even simpler solution that gets rid of 100% of the test warnings
let toFocus = null;
try {
+ if (focusLaterElements.length !== 0) {
toFocus = focusLaterElements.pop();
toFocus.focus();
+ }
return;
} catch (e) {
// …
}
That's it. Just don't try and call pop()
on an empty array, otherwise we will always find ourselves in the catch block.
Now to address your concern mentioned above
Checking for the length of
focusLaterElements
looks nice, but this may hide a bug if the focus manager has a real bug (failed/miss to queue the element, for example).
I'm not entirely sure I see a way in which an element misses the queue. As I laid out at the top of this comment, an element can only ever be added if the open
instance method is invoked, which is always behind a this.props.isOpen
flag. Appending to the focusLaterElements
array is not async in any way, so (AFAICT) there is not any case in which an element can miss the queue if the modal is opened.
The only theoretical situation I can come up with is if the element to return the focus to (i.e. whichever element opened the modal in the first place I would think), somehow asynchronously disappeared after the modal was already opened. I can't possibly see a use case for such an event, and even if there was it seems like it break WebAIM guidelines.
If there is some other situation in which the element to return the focus to will not exist after the modal is opened, please let me know.
As it stands though, I think that the simple predicate check that focusLaterElements
is not an empty array solves 99% of these warning issues.
@indiesquidge looks good!
I have recently upgraded react-modal from 1.7.1 to 3.1.6. I am now getting a warning in the console whenever react-modal unmounts.
Steps to reproduce:
Expected behavior:
Additional notes:
The modals are behaving normally, I am just unsure why the focus manager is giving me this warning.