Reapply removal of findDOMNode from Modal and Popover, remove autofocus from Modal#46429
Merged
Reapply removal of findDOMNode from Modal and Popover, remove autofocus from Modal#46429
Conversation
gzdunek
approved these changes
Sep 10, 2024
bl-nero
approved these changes
Sep 10, 2024
2548d3f to
440eeae
Compare
ryanclark
approved these changes
Sep 11, 2024
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
In order to remove
findDOMNodeusage from our project, I refactored the Modal and Popover components in #46123. But I had to quickly revert it in #46398 because certain modals would go into an infinite loop, which broke the enterprise tests.What was wrong
The new code would go into an infinite loop calling
focuson the child of<Modal>. Grzegorz and Bartosz helped me pin down the issue to a situation where at least two modals are shown on the screen. We attributed this todocument.addEventListener('focus', this.enforceFocus, true)and modals fighting for focus.But upon looking at the old Modal implementation, I came to the conclusion that in theory the logic was the same. The JSDocs for relevant props said this:
If you consider those comments at face value, you'll realize that modals in our app cannot work this way. There are some places in the UI which show two modals at once. For example, the failing NotificationRoutingRules story shows a list of rules, which is a modal in itself. After you click "View" on a rule and then the trash icon in the top right, it shows another modal for confirmation. If both are shown at the same time, which modal is supposed to keep focus? How do you make sure that one modal doesn't steal the focus of another? FWIW, we've run into a similar problem in the past, see
pauseUserInteraction.The new code would go into an infinite loop whenever you clicked on the trash icon. This made me suspect that the old code never worked properly when it came to enforcing focus. I added some console logs and I could see that it does call
.focus()on the modal child, but it actually didn't causedocument.activeElementto change.The actual broken code
It turns out that indeed the autofocus behavior and focus enforcement behavior never worked properly. Calling
.focus()on an element doesn't do anything if the element itself isn't focusable. To work around this, Modal'sautoFocusmethod would add thetabIndexattribute to the modal child if it didn't already have it. The problem is, this method would never get called.autoFocusis called fromhandleOpened, which is called fromhandleOpenonly ifthis.dialogRefis present.handleOpenis called fromcomponentDidMountandcomponentDidUpdateif theopenprop is true. After adding console logs to those methods, I realized thatthis.dialogRefis never ever present during the invocation of those lifecycle methods. See the following video where I interact with various modals andthis.dialogRefis never present, hencehandleOpenedand thusautoFocusis never called.handleOpen-early-return.mov
The old code used a hand-crafted callback ref through a hand rolled
RootRefcomponent which setdialogRefduring RootRef'scomponentDidMountandcomponentDidUpdatemethods.teleport/web/packages/design/src/Modal/Modal.jsx
Lines 198 to 211 in db689c7
The new code changed it to a normal ref that's set on the div above the modal's child. This made it so that the ref was present during Modal's
componentDidMountandcomponentDidUpdatemethods, soautoFocuswas executed and it set the tabIndex attribute on the modal's child. This later caused two modals to fight for focus because notthis.dialogEl().focus()actually changed focus,this.dialogEl()became a focusable element.The solution
I decided to completely remove the broken code. The general behavior it was supposed to establish was actually desirable. I remember that in Connect we often add
autoFocusto fields in modals, because otherwise after opening the modal you'd have to click on the field to begin typing. It also indeed shouldn't be possible to let focus escape the modal.However, if we want to be able to show multiple modals at the same time, as we already do, then the implementation must take that into account. Taking care of this would require more time than I have at the moment, which is why I'm deleting the broken implementation instead of replacing it.
The only situation where the autofocus implementation would actually work is if the child passed to
<Modal>was a focusable element, either a div with tabindex or an inherently focusable element like a button for example. But I looked through the code place and there doesn't appear to be a single place where we do this.