Click-Away Listener
The Click-Away Listener component detects when a click event happens outside of its child element.
Introduction
Click-Away Listener is a utility component that listens for click events outside of its child. (Note that it only accepts one child element.) This is useful for components like the Popper which should close when the user clicks anywhere else in the document. Click-Away Listener also supports the Portal component.
The demo below shows how to hide a menu dropdown when users click
anywhere else on the page. The open state is kept in the URL with reactRouter: the
button toggles between #/click-away and #/,
and onClickAway navigates back to #/.
dropdownStyles <- list(position = "absolute", top = 28, right = 0, left = 0, zIndex = 1, border = "1px solid", p = 1, bgcolor = "background.paper")
clickAway <- function(route, open, mouseEvent = NULL, touchEvent = NULL, portal = FALSE, styles = dropdownStyles) {
dropdown <- Box(sx = styles, "Click me, I will stay visible until you click outside.")
do.call(ClickAwayListener, c(
list(onClickAway = JS(sprintf(
"() => { if (window.location.hash === '#/%s') window.location.hash = '/'; }", route
))),
if (!is.null(mouseEvent)) list(mouseEvent = mouseEvent, touchEvent = touchEvent),
list(
Box(
sx = list(position = "relative"),
Button(variant = "outlined", size = "small", href = if (open) "#/" else paste0("#/", route), "Open menu dropdown"),
if (open) (if (portal) Portal(dropdown) else dropdown)
)
)
))
}
clickAwayDemo <- function(route, ...) {
RouterProvider(
router = createHashRouter(
Route(path = route, element = clickAway(route, TRUE, ...)),
Route(path = "*", element = clickAway(route, FALSE, ...))
)
)
}
muiMaterialPage(CssBaseline(), clickAwayDemo("click-away"))JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import ClickAwayListener from '@mui/material/ClickAwayListener';
export default function ClickAway() {
const [open, setOpen] = React.useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
const handleClickAway = () => {
setOpen(false);
};
const styles = {
position: 'absolute',
top: 28,
right: 0,
left: 0,
zIndex: 1,
border: '1px solid',
p: 1,
bgcolor: 'background.paper',
};
return (
<ClickAwayListener onClickAway={handleClickAway}>
<Box sx={{ position: 'relative' }}>
<button type="button" onClick={handleClick}>
Open menu dropdown
</button>
{open ? (
<Box sx={styles}>
Click me, I will stay visible until you click outside.
</Box>
) : null}
</Box>
</ClickAwayListener>
);
}Customization
Use with Portal
The following demo uses the Portal() component to render
the dropdown into a new subtree outside of the current DOM
hierarchy:
muiMaterialPage(
CssBaseline(),
clickAwayDemo(
"portal-click-away",
portal = TRUE,
styles = list(
position = "fixed", width = 200, top = "50%", left = "50%", transform = "translate(-50%, -50%)",
border = "1px solid", p = 1, bgcolor = "background.paper"
)
)
)JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Portal from '@mui/material/Portal';
export default function PortalClickAway() {
const [open, setOpen] = React.useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
const handleClickAway = () => {
setOpen(false);
};
const styles = {
position: 'fixed',
width: 200,
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
border: '1px solid',
p: 1,
bgcolor: 'background.paper',
};
return (
<ClickAwayListener onClickAway={handleClickAway}>
<div>
<button type="button" onClick={handleClick}>
Open menu dropdown
</button>
{open ? (
<Portal>
<Box sx={styles}>
Click me, I will stay visible until you click outside.
</Box>
</Portal>
) : null}
</div>
</ClickAwayListener>
);
}Listening for leading events
By default, the Click-Away Listener component responds to trailing events: the end of a click or touch.
You can set the component to listen for leading
events (the start of a click or touch) using the
mouseEvent and touchEvent props, as shown in
the following demo:
When the component is set to listen for leading events, interactions with the scrollbar are ignored.
muiMaterialPage(CssBaseline(), clickAwayDemo("leading-click-away", mouseEvent = "onMouseDown", touchEvent = "onTouchStart"))JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import ClickAwayListener from '@mui/material/ClickAwayListener';
export default function LeadingClickAway() {
const [open, setOpen] = React.useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
const handleClickAway = () => {
setOpen(false);
};
const styles = {
position: 'absolute',
top: 28,
right: 0,
left: 0,
zIndex: 1,
border: '1px solid',
p: 1,
bgcolor: 'background.paper',
};
return (
<ClickAwayListener
mouseEvent="onMouseDown"
touchEvent="onTouchStart"
onClickAway={handleClickAway}
>
<Box sx={{ position: 'relative' }}>
<button type="button" onClick={handleClick}>
Open menu dropdown
</button>
{open ? (
<Box sx={styles}>
Click me, I will stay visible until you click outside.
</Box>
) : null}
</Box>
</ClickAwayListener>
);
}Accessibility
By default, Click-Away Listener adds an onClick handler
to its child. This can result in screen readers announcing that the
child is clickable, even though this onClick handler has no
effect on the child itself.
To prevent this behavior, add role = "presentation" to
the child element:
ClickAwayListener(
onClickAway = JS("() => {}"),
Box(role = "presentation", tags$h1("non-interactive heading"))
)This is also required to fix a known issue in NVDA when using Firefox that prevents the announcement of alert messages. See this GitHub issue.
Click-away in Shiny apps
Send the click-away to the server with triggerEvent(),
for example to close a panel rendered with renderUI():
ClickAwayListener(onClickAway = triggerEvent("clicked_away"), Box(uiOutput("panel")))