-
Notifications
You must be signed in to change notification settings - Fork 120
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
Fix clickaway bugs #9331
Fix clickaway bugs #9331
Conversation
</LWClickAwayListener> | ||
</LWPopper> |
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.
Note that the onClickAway
function above doesn't actually close the popper unless the click is on the button to do so. This seems like a roundabout way of doing it, but it's how it was before
// and then the LWClickAwayListener was triggered immediately, closing the dialog. | ||
// I don't understand exactly why this fixes it | ||
setTimeout(() => setAnchorEl(null), 0); | ||
setAnchorEl(null); |
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 was to fix a bug with choosing a custom date range in the author analytics page (which would close the modal as soon as it was opened). I'm not sure exactly what fixed that but it has now gone away
@@ -17,7 +17,7 @@ const NewModeratorActionDialog = ({classes, onClose, userId}: { | |||
const { WrappedSmartForm, LWDialog } = Components; | |||
|
|||
return ( | |||
<LWDialog open={true}> | |||
<LWDialog open={true} onClose={onClose}> |
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 was the one case where removing the external clickaway required changing something in a dialog component. All the others already had both the external LWClickAwayCancel
and this onClose
which would independently close the dialog
</LWClickAwayListener> | ||
</LWPopper> |
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 one I didn't test, I'm fairly confident it will work based on all the others
@@ -35,14 +35,12 @@ import React, { | |||
FunctionComponent | |||
} from 'react'; | |||
|
|||
type FocusEvents = 'focusin' | 'focusout'; |
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.
When Sarah and I were testing we ran into some cases where focus events would wrongly trigger the clickaway. I'm not sure if these would still happen with the other bug fixes, but I don't think we benefit from having clickaways triggered on focus events anywhere so I decided to remove them to avoid future problems
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.
Nice - LGTM!
This is the specific bug this is fixing (before), there were a couple of other bugs like this which have already been worked around:
Screen.Recording.2024-05-08.at.09.05.12.mp4
The fix in this PR is essentially this line:
There are also some changes related to how
LWClickAwayListener
interacts with portals (where an element is created at the root level of the document rather than in the place it is instantiated) to hopefully make it a bit more robust.Details of the
bubbledEventTarget
fixThis is the bit of code that handles document-level click events (i.e. events anywhere on the page), to decide whether they count as a clickaway for the particular element that is wrapped in
LWClickAwayListener
:isContainedByNode
is checking whether the clicked element is a child of theLWClickAwayListener
wrapped element in the DOM tree (i.e. in the actual html),isBubbledEventTarget
is supposed to check whether the clicked element is a child of the wrapped element in the React tree.In the case where portals (Dialogs, Poppers) are involved as the wrapped element, these can be different, because a portal creates the element at the root level of the document, so it won't be a DOM-child of the
LWClickAwayListener
. It will be a React-child however, because portals are designed to maintain the logical tree structure by bubbling events up through nested portals even if they end up separated in the DOM.The bug was that clicks on various MUI components generate a synthetic event that doesn't have a proper
target
field, sobubbledEventTarget.current
wouldn't be set properly andisBubbledEventTarget
would wrongly befalse
. Hence this change to use the "nativeEvent" in that case:Details of the other changes related to portals
Given the fix above, the case where
isContainedByNode
is false, butisBubbledEventTarget
is true (sufficient to correctly not trigger the clickaway) would be in situations like this where there is aLWClickAwayListener
outside a portal-generating component (in this caseLWPopper
):The
LWPopper
will be portal-ed away somewhere else so won't be a DOM child of theLWClickAwayListener
. I've changed all cases like this to instead have theLWClickAwayListener
inside the portal-generating component like so:This way it's always the case that the clickaway-able component is both a DOM child and a React child of the clickaway listener. This should make things easier to think about, and also potentially prevent bugs in future where there is some issue with the click event being bubbled all the way up to
LWClickAwayListener
, because it will then just fall back to theisContainedByNode
in most cases (except when there are also nested portals involved). This would have independently prevented the specific bug in the video above....
Separately, there are also various MUI components that create portals (e.g.
Dialog
,Select
), and these have their own way of handling clickaways for themselves[1]. In the case ofuseDialog
we were adding aLWClickAwayListener
(controlled bynoClickawayCancel
) on top of the clickaway already provided by the MUIDialog
. I thought it was better to leave it to the default MUI behaviour in that case, so I have removed thenoClickawayCancel
option and made it so the dialog is responsible for disabling the clickaway itself (by not passing anonClose
prop to the dialog).[1] The logic in this case appears to be that only the topmost MUI portal element will ever be considered for a clickaway. So if there is a
Select
open over aDialog
the first clickaway will only close theSelect
even if it's outside theDialog
bounds also.┆Issue is synchronized with this Asana task by Unito