I am trying to understand the best way of conditionally showing a Tooltip, based on if a sibling component Popper is open or not. I want to to show it by default on hover of its child the ButtonBase. I want the tooltip to never be open if the Popper is open. The tooltip title is acting as a summary of what's selected in the options list in the Popper when its closed, having it open with the Popper open is not ideal and cluttered. I am new to hooks so trying to understand how I can incorporate a hook to set the tooltipOpen state correctly with the conditional need.
export default function TooltipWithPopper() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = React.useState(null);
const [value, setValue] = React.useState([options[1], options[11]]);
const [pendingValue, setPendingValue] = React.useState([]);
const [tooltipOpen, setTooltipOpen] = React.useState(false);
const handleClick = (event) => {
setPendingValue(value);
setAnchorEl(event.currentTarget);
setTooltipOpen(false);
};
const handleClose = (event, reason) => {
if (reason === "toggleInput") {
return;
}
setValue(pendingValue);
if (anchorEl) {
anchorEl.focus();
}
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? "github-label" : undefined;
return (
<React.Fragment>
<div className={classes.root}>
<Tooltip title={value.map((i) => i.title).join(", ")}>
<ButtonBase
disableRipple
className={classes.button}
aria-describedby={id}
onClick={handleClick}
>
<span>Label</span>
{value.length}/{options.length}
</ButtonBase>
</Tooltip>
</div>
<Popper
id={id}
open={open}
anchorEl={anchorEl}
placement="bottom-start"
className={classes.popper}
>
<Autocomplete
open
onClose={handleClose}
multiple
classes={{
paper: classes.paper,
option: classes.option,
popperDisablePortal: classes.popperDisablePortal
}}
value={pendingValue}
onChange={(event, newValue) => {
setPendingValue(newValue);
}}
disableCloseOnSelect
disablePortal
renderTags={() => null}
noOptionsText="No labels"
.....
/>
</Popper>
</React.Fragment>
);
}
Here is a demo of the tooltip being applied to the trigger element. How can I set it to only be open depending on another components' state? I've tried adding a setTooltipOpen(false) call when the handleClick is called when opens the Popper.
Demo: https://codesandbox.io/s/material-demo-forked-0wgyh?file=/demo.js:0-6181
You can control the Tooltip open prop value with your tooltipOpen state (implementation is up to you) and provide conditions that if the Popper is open, then automatically, the Tooltip open prop value computation will disregard the tooltipOpen state and assign false.
In my example below, I control the tooltipOpen state via onMouseEnter && onMouseLeave events
<Tooltip
open={open === true ? false : tooltipOpen}
title={value.map((i) => i.title).join(", ")}
>
<ButtonBase
disableRipple
className={classes.button}
aria-describedby={id}
onClick={handleClick}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
>
<span>Label</span>
{value.length}/{options.length}
</ButtonBase>
</Tooltip>
Related
I want this popper to show when the "copy link" button is clicked to let the user know that it has been copied, but then disappear on its own after a second or two. Here is the code for the popper
import * as React from 'react';
import Box from '#mui/material/Box';
import Popper from '#mui/material/Popper';
export default function SimplePopper() {
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const open = Boolean(anchorEl);
const id = open ? 'simple-popper' : undefined;
return (
<div>
<button aria-describedby={id} type="button" onClick={handleClick}>
Copy Link
</button>
<Popper id={id} open={open} anchorEl={anchorEl}>
<Box sx={{ border: 1, p: 1, bgcolor: 'background.paper' }}>
Link Copied
</Box>
</Popper>
</div>
);
}
You might be able to do something with setTimeout in handleClick.
Try modifying handleClick like so:
const handleClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
setTimeout(() => setAnchorEl(null), 3000);
};
The first button:
const [open, setOpen] = useState(false);
return (
<>
<SideCart open={open} />
<CartButton open={open} onClick={() => setOpen(!open)}>
)
</>
The sidebar:
const [hide, setHide] = useState(open ? true : true);
return (
<SidecartContainer open={open} hide={hide}>
// Thats the second button
// I want to close the sidebar component with this button
<ExitButton hide={hide} onClick={() => setHide(!hide)}>
</SidecartContainer>
const SidecartContainer = styled.div`
transform: ${({ open, hide }) =>
open && hide ? "translateX(0)" : "translateX(100%)"};
`;
I have one button triggering the Open state of sidebar and when it opens I have an x button to close the sidebar.
It only works once.
What shall I use so whenever I click the open button to open and then when I click on close to hide?
It's made with styled-components.
I think your problem is that this line const [hide, setHide] = useState(open ? true : true); only runs once on component mount. What you need is a useEffect in the sidebar to listen for changes to open and apply them to the hide state like so:
const [hide, setHide] = useState(open ? true : true);
useEffect(() => {
setHide(!open);
}, [open]);
return (
<SidecartContainer open={open} hide={hide}>
--------Thats the second button
--------I want to close the sidebar component with this button
<ExitButton hide={hide} onClick={() => setHide(!hide)}>
</SidecartContainer>
I finally found what's the right way to do it and it works.
First I declared outside of the sidebar the useStates logic:
const [open, setOpen] = useState("");
const hide = () => setOpen("translateX(100%)");
const show = () => setOpen("translateX(0)");
then I passed the props to Sidecart.js:
const SideCart = (props) => {
return (
<SidecartContainer transform={props.transform} open={props.open}>
<ExitButton open={props.open} onClick={props.onClick}>
<CloseIcon />
</ExitButton>
<ProductCards>
<CartProductCard />
</ProductCards>
<Total></Total>
</SidecartContainer>
);
};
also this is important, in the styled component css I declared the prop value:
const SidecartContainer = styled.div`
transform: ${(props) => props.transform};
`;
Finally I changed the onClick functions accordingly:
<SideCart transform={open} open={open} onClick={hide} />
<CartButton open={open} onClick={show}>
I am implementing two text fields next to each other and they have end adornments as a buttons. Those buttons toggle popper visibility. Also each popper has clickawaylistener so the popper is closed when mouse clicks outside popper. If first popper is opened it should be closed when I click button of second text field adornment. Issue is that end adornments have event propagation stopped. I do that to prevent clickaway event when clicking adornment, so to prevent instant closing of popper when it is opened by toggle handler.
I was thinking about wrapping TextField into ClickAwayListener but it didn't work.
P.S. Both TextField will be rendered from separate components and I don't want to share any props between them as they should be independent.
https://codesandbox.io/s/basictextfields-material-demo-forked-rykrx
const [firstPopperVisible, setFirstPopperVisible] = React.useState(false);
const [secondPopperVisible, setSecondPopperVisible] = React.useState(false);
const firstTextFieldRef = React.useRef();
const secondTextFieldRef = React.useRef();
const toggleFirstPopperVisible = (e) => {
e.stopPropagation();
setFirstPopperVisible((prev) => !prev);
};
const handleFirstPopperClickAway = (e) => {
setFirstPopperVisible(false);
};
const toggleSecondPopperVisible = (e) => {
e.stopPropagation();
setSecondPopperVisible((prev) => !prev);
};
const handleSecondPoppertClickAway = (e) => {
setSecondPopperVisible(false);
};
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<div>
<TextField
label="Outlined"
variant="outlined"
inputRef={firstTextFieldRef}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
edge="end"
onClick={toggleFirstPopperVisible}
>
<Visibility />
</IconButton>
</InputAdornment>
)
}}
/>
<ClickAwayListener onClickAway={handleFirstPopperClickAway}>
<Popper
open={firstPopperVisible}
anchorEl={firstTextFieldRef.current}
placement="bottom-start"
>
Content
</Popper>
</ClickAwayListener>
</div>
<div>
<TextField
label="Outlined"
variant="outlined"
inputRef={secondTextFieldRef}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
edge="end"
onClick={toggleSecondPopperVisible}
>
<Visibility />
</IconButton>
</InputAdornment>
)
}}
/>
<ClickAwayListener onClickAway={handleSecondPoppertClickAway}>
<Popper
open={secondPopperVisible}
anchorEl={secondTextFieldRef.current}
placement="bottom-start"
>
Content
</Popper>
</ClickAwayListener>
</div>
</div>
);
}
EDIT: Found a temporary solution by wrapping TextField into div and then wrapping tat ClickawayListener. Also prevented propagation on popper itself where needed. This is not ideal, but for my case it worked.
<ClickAwayListener onClickAway={handleFirstPopperClickAway}>
<div style={{display: inline-box}>
<TextField>
.....
</TextField>
</div>
</ClickawayListener>
<Popper>
....
</Popper>
UPDATED
Wrap the ClickAwayListener in a conditional statement:
{firstPopperVisible && (
<ClickAwayListener onClickAway={handleFirstPopperClickAway}>
<Popper open={firstPopperVisible} anchorEl={firstTextFieldRef.current} placement="bottom-start">
Content
</Popper>
</ClickAwayListener>
)}
...
{secondPopperVisible && (
<ClickAwayListener onClickAway={handleSecondPoppertClickAway}>
<Popper open={secondPopperVisible} anchorEl={secondTextFieldRef.current} placement="bottom-start">
Content
</Popper>
</ClickAwayListener>
)}
Codesandbox Demo
PREVIOUS
I recommend you look at Portals for this. Instead of having multiple elements in the dom, you have one that gets added where needed, as needed.
Portals provide a first-class way to render children into a DOM node
that exists outside the DOM hierarchy of the parent component.
Your single Portal component:
import ReactDOM from 'react-dom';
import React, { useEffect, useState } from 'react';
const Component = ({ content, handleCloseClick }) => {
return <div onClick={handleCloseClick}>{content}</div>;
};
interface PortalProps {
isShowing: boolean;
content: any;
location: any;
handleCloseClick: () => void;
}
const Portal = ({ isShowing, content, handleCloseClick, location }: PortalProps) => (isShowing ? ReactDOM.createPortal(<Component handleCloseClick={handleCloseClick} content={content} />, location.current) : null);
export default Portal;
Which is then used once in your main component:
import React, { useState, useRef } from 'react';
import Widget from './Widget';
import './styles.css';
export default function App() {
const [isShowing, setIsShowing] = useState<boolean>(false);
const [content, setContent] = useState<string>();
const buttonRef = useRef(null);
const handleClick = (e) => {
const { target } = e;
buttonRef.current = e.target.parentNode;
setContent(target.dataset.content);
setIsShowing(true);
};
const handleCloseClick = () => {
buttonRef.current = null;
setContent('');
setIsShowing(false);
};
return (
<div className="App">
<div>
<button onClick={handleClick} data-content={`content for one`}>
One
</button>
</div>
<div>
<button onClick={handleClick} data-content={`content for two`}>
Two
</button>
</div>
<Widget isShowing={isShowing} location={buttonRef} content={content} handleCloseClick={handleCloseClick} />
</div>
);
}
Codesandbox Demo
I'm using Material UI's nested/select ItemList component to dynamically generates any number of dropdown menu items based on how many items belong in this header as you can maybe tell from the mapping function. On another file 1 layer above this one, I am again mapping and generating multiple of these DropDownMenus, is it possible for these components to communicate to each other?
This is the file in question
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
maxWidth: 330,
backgroundColor: theme.palette.background.paper,
},
nested: {
paddingLeft: theme.spacing(4),
}
}));
export default function DropDownMenu(props) {
const classes = useStyles();
const [open, setOpen] = React.useState(true);
let unitName = props.unit[0];
let chapterList = props.unit.slice(1);
const [selectedIndex, setSelectedIndex] = React.useState(1);
const handleListItemClick = (index) => {
console.log("ItemClicked");
console.log(index);
setSelectedIndex(index);
};
const handleClick = () => {
setOpen(!open);
};
const selectMenuItem = (chapter, index) => {
props.chooseChapter(chapter)
handleListItemClick(index)
}
let dropDownUnit = chapterList.map((chapter, index) => {
return (
<ListItem button
className={classes.selected}
selected={selectedIndex === index}
onClick={() => selectMenuItem(chapter, index)}
key={index}>
<ListItemText primary={chapter} />
</ListItem>
)
})
return (
<List
component="nav"
aria-labelledby="nested-list-subheader"
subheader={
<ListSubheader component="div" id="nested-list-subheader">
</ListSubheader>
}
className={classes.root}
>
<ListItem button onClick={handleClick}>
<ListItemText primary={unitName} />
{!open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={!open} timeout="auto" unmountOnExit>
<List component="div" disablePadding className={classes.selected}>
{dropDownUnit}
</List>
</Collapse>
</List>
);
}
Psudo Style - What I'm trying to accomplish
<DropDownMenu>
<MenuItem> // Suppose this is selected
<MenuItem>
<DropDownMenu>
<MenuItem> // onClick --> Select this and deselect all other selected buttons
You can have a parent of these components such that the parent will keep the state of who is active. That way you can pass that state & the state setter as props so that everyone will know who is active
export default function App() {
const [selectedItem, setSelectedItem] = React.useState();
return (
<>
<DropDownMenu
selectedItem={selectedItem} // pass down as props
setSelectedItem={setSelectedItem} // pass down as props
unit={...}
chooseChapter={function () {}}
/>
...
On the children, simply refactor the Call To Action (in this case onClick) to set the state using the passed down props. Pay attention to the selected prop of ListItem, we now use the state we have passed down from the parent
let dropDownUnit = chapterList.map((chapter, index) => {
return (
<ListItem
button
className={classes.selected}
selected={props.selectedItem === chapter}
onClick={() => props.setSelectedItem(chapter)}
key={index}
>
<ListItemText primary={chapter} />
</ListItem>
);
});
I have a nav menu built with material-ui/core in Navbar.
I use useRef to track the position of clicked button on toggle menu close.
anchorRef.current.contains(event.target)
And I am getting 'Uncaught TypeError: anchorRef.current.contains is not a function' .
I tried 'Object.values(anchorRef.current).includes(event.target)' instead, it always returns false.
-- update --
anchorRef.current.props Object.
withStyles {
props:{
aria-haspopup: "true"
aria-owns: undefined
children: "계정"
className: "nav-menu--btn"
onClic: f onClick()
get ref: f()
isReactWarning: true
arguments: (...)
caller: (...)
length: 0
name: "warnAboutAccessingRef"
...
}, context{...}, refs{...}, ...}
ToggleMenuList
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<Button
ref={anchorRef}
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
ToggleMenuItems
const ToggleMenuItems = ({
listId,
activeId,
handleClose,
anchorRef,
items,
}) => {
const isOpen = activeId === listId;
const leftSideMenu = activeId === 3 || activeId === 4 ? 'leftSideMenu' : '';
return (
<Popper
open={isOpen}
anchorEl={anchorRef.current}
keepMounted
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom',
}}
className={`toggle-menu ${leftSideMenu}`}
>
<Paper id="menu-list-grow">
<ClickAwayListener
onClickAway={handleClose}
>
<MenuList className="toggle-menu--list">
{items.map(e => (
<MenuItem
key={e.id}
className="toggle-menu--item"
onClick={handleClose}
>
<Link
to={e.to}
className="anchor td-none c-text1 toggle-menu--link"
>
{e.name}
</Link>
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
);
};
export default ToggleMenuItems;
react: ^16.8.6
react-dom: ^16.8.6
react-router-dom: ^4.3.1
#material-ui/core: ^3.1.2
I assume your ToggleMenuItems sets up global(document-level?) event listener on click to collapse Menu on clicking somewhere outside.
And you have a sibling button element. Clicking on that you want to keep menu expanded, right? So that was the point to use .contains in onClick to check if we are clicked outside of ToggleMenuItems but in scope of specific Button. The reason why it does not work: <Button> is custom class-based React component so it returns React component instance in ref. And it does not have any DOM-specific methods like .contains
You can rework you current approach: just stop bubbling event in case Button has been clicked. It would stop global event handler set by ToggleMenuItems to react.
const stopPropagation = (event) => event.stopPropagation();
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<div onClick={stopPropagation}>
<Button
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
</div>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
I've put stopPropagation handler outside since it does not depend on any internal variable.