Material-ui: open menu by event hover - reactjs

Currently the menuItem only opens your children after a click. Is there an attribute that I agree to open via Hover?
<MenuItem key={index}
menuItems={menuitems}
**onHover={true}**
>
menuItem
</MenuItem>

There is not specific attribute available through the material-ui library. However, you could create this yourself pretty easily using the onMouseOver event.
I've adapted the Simple Menu example from the material-ui site to show you how this can be done:
import React from 'react';
import Button from 'material-ui/Button';
import Menu, { MenuItem } from 'material-ui/Menu';
class SimpleMenu extends React.Component {
state = {
anchorEl: null,
open: false,
};
handleClick = event => {
this.setState({ open: true, anchorEl: event.currentTarget });
};
handleRequestClose = () => {
this.setState({ open: false });
};
render() {
return (
<div>
<Button
aria-owns={this.state.open ? 'simple-menu' : null}
aria-haspopup="true"
onClick={this.handleClick}
{ // The following line makes the menu open whenever the mouse passes over the menu }
onMouseOver={this.handleClick}
>
Open Menu
</Button>
<Menu
id="simple-menu"
anchorEl={this.state.anchorEl}
open={this.state.open}
onRequestClose={this.handleRequestClose}
>
<MenuItem onClick={this.handleRequestClose}>Profile</MenuItem>
<MenuItem onClick={this.handleRequestClose}>My account</MenuItem>
<MenuItem onClick={this.handleRequestClose}>Logout</MenuItem>
</Menu>
</div>
);
}
}
export default SimpleMenu;

I got it to work by upping the z-index of the button. Otherwise, the mouse technically goes out of the button when the modal appears on top of the button. Then the menu will close since the user is no longer hovering.
If you add onMouseLeave to Menu then onMouseLeave will only trigger if you go out of the browser. So instead, I added onMouseLeave to MuiList which doesn't take up the whole page.
I also added need some extra conditionals in the handleOpen to account for if the mouse leaves the button but enters the menu.
import React, { useState } from "react";
import Button from "#material-ui/core/Button";
import Menu from "#material-ui/core/Menu";
import { createMuiTheme, ThemeProvider } from "#material-ui/core/styles";
const theme = createMuiTheme({});
const MyMenu = () => {
const [anchorEl, setAnchorEl] = useState(null);
const [open, setOpen] = useState(false);
const handleOpen = (event) => {
setAnchorEl(event.currentTarget);
setOpen(true);
};
const handleClose = (e) => {
if (e.currentTarget.localName !== "ul") {
const menu = document.getElementById("simple-menu").children[2];
const menuBoundary = {
left: menu.offsetLeft,
top: e.currentTarget.offsetTop + e.currentTarget.offsetHeight,
right: menu.offsetLeft + menu.offsetWidth,
bottom: menu.offsetTop + menu.offsetHeight
};
if (
e.clientX >= menuBoundary.left &&
e.clientX <= menuBoundary.right &&
e.clientY <= menuBoundary.bottom &&
e.clientY >= menuBoundary.top
) {
return;
}
}
setOpen(false);
};
theme.props = {
MuiList: {
onMouseLeave: (e) => {
handleClose(e);
}
}
};
return (
<div>
<ThemeProvider theme={theme}>
<Button
id="menubutton1"
aria-owns={open ? "simple-menu" : null}
aria-haspopup="true"
onMouseOver={handleOpen}
onMouseLeave={handleClose}
style={{ zIndex: 1301 }}
>
Open Menu
</Button>
<Menu
id="simple-menu"
anchorEl={anchorEl}
open={open}
anchorOrigin={{
vertical: "bottom",
horizontal: "center"
}}
transformOrigin={{
vertical: "top",
horizontal: "center"
}}
>
Menu
<br />
Items
</Menu>
</ThemeProvider>
</div>
);
};
export default MyMenu;
CodeSandbox

I have added mouseLeave listener on container div to close the menu, and mouseOver listener on menu button to open menu. This worked for me...
<div onMouseLeave={closeMenu}>
<button onMouseOver=(openMenu) />
<Menu />
<MenuItems />
</div>

This is how I did it:
https://codesandbox.io/s/mui-menu-hover-to-show-dropdown-iguukw?file=/src/TopMenu.tsx
I used on onMouseLeave and onMouseEnter events to control when to show and hide the dropdown menus.
I also used a string state to determine which dropdown menu should show. Only one dropdown menu should show at one moment of time.

Related

Dismiss Material UI Popper after a few seconds automatically

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);
};

Hide popper of one field when opening popper of another field

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

GatsbyJS Burger Menu close menu on AnchorLink click

I have created a working burger menu in my Gatsby application using react-burger-menu. I am trying to get it so that when an AnchorLink in the burger is clicked, the Burger menu closes. I have tried doing <Menu isOpen={ false }> as suggested in the docs but not working. Any help?
This is my Burger menu code:
import React from "react"
import { slide as Menu } from "react-burger-menu"
import { AnchorLink } from "gatsby-plugin-anchor-links"
const Burger = () => {
return (
<div className="burger-menu">
<Menu right isOpen={ false }>
<AnchorLink to="/#about">About Me</AnchorLink>
<AnchorLink to="/#experience">Experience</AnchorLink>
<AnchorLink to="/#projects">Projects</AnchorLink>
<AnchorLink to="/#contact">Contact</AnchorLink>
</Menu>
</div>
)
}
export default Burger
You can provide event handlers for Menu and AnchorLink and add a state menuOpen to close/open sidebar.
const Burger = () => {
const [menuOpen, setMenuOpen] = React.useState(false)
// This keeps your state in sync with the opening/closing of the menu
// via the default means, e.g. clicking the X, pressing the ESC key etc.
const handleStateChange = state => {
setMenuOpen(state.isOpen)
}
// This can be used to close the menu, e.g. when a user clicks a menu item
const closeMenu = () => {
setMenuOpen(false)
}
return (
<div className="burger-menu">
<Menu
right
isOpen={menuOpen}
onStateChange={state => handleStateChange(state)}
>
<AnchorLink
to="/#about"
onClick={() => {
closeMenu()
}}
>
About Me
</AnchorLink>
<AnchorLink
to="/#experience"
onClick={() => {
closeMenu()
}}
>
Experience
</AnchorLink>
<AnchorLink
to="/#projects"
onClick={() => {
closeMenu()
}}
>
Projects
</AnchorLink>
<AnchorLink
to="/#contact"
onClick={() => {
closeMenu()
}}
>
Contact
</AnchorLink>
</Menu>
</div>
)
}

How can I put a button into collapse?

I want to put a button into a collapse, I am using the collapse of antd, that new button doesn't should open or close the collapse, I want to give her other functionality?
const { Collapse, Button } = antd;
const { Panel } = Collapse;
function callback(key) {
console.log(key);
}
const text = ` hi `;
ReactDOM.render(
<Collapse
defaultActiveKey={['1']} onChange={callback}>
<Panel header={<Button type="primary">Primary Button</Button>} key="1" >
<Button type="link">My button</Button> >
<p>{text}</p>
</Panel>
</Collapse>,
mountNode,
);
Why does the COLLAPSE open when I click the button? I don't want the COLLAPSE to open
I guess the button you want to not open the collapse is your Primary Button in header of Panel, To do that you have to control activeKey manually, and check that when user click on the Panel header are they click on the Primary Button or outside of it.
Try this:
import React, { useState, useRef } from "react";
import * as antd from "antd";
const { Collapse, Button } = antd;
const { Panel } = Collapse;
const text = ` hi `;
export const App = () => {
const [activeKey, setActiveKey] = useState(0);
const isButtonClicked = useRef(false);
const callback = key => {
console.log(key, isButtonClicked.current);
// Check if use click on the button don not update activekey
if (
isButtonClicked &&
isButtonClicked.current &&
isButtonClicked.current === true
) {
isButtonClicked.current = false;
return;
}
if (!activeKey) {
setActiveKey(key);
} else {
setActiveKey(0);
}
};
return (
<Collapse activeKey={activeKey} onChange={callback}>
<Panel
header={
<Button
onClick={() => {
// set the isButtonClicked ref to tru when user click on Button
isButtonClicked.current = true;
// Do other functionality you want for this button here
}}
type="primary"
>
Primary Button
</Button>
}
key="1"
>
<Button type="link">My button</Button> ><p>{text}</p>
</Panel>
</Collapse>
);
};
I create a codesandbox to demonstrate it
try this:
const { Collapse, Button } = antd;
const { Panel } = Collapse;
function callback(key) {
console.log(key);
}
const text = ` hi `;
ReactDOM.render(
<Collapse defaultActiveKey={['1']} onChange={callback}>
<Panel header={<Button type="primary">Primary Button</Button>} key="1" >
<Button type="link">My button</Button>
<p>{text}</p>
</Panel>
</Collapse>,
mountNode,
);
If you want a button inside a panel, the button code should be inside the panel, not inside its tag.. instead of:
<Panel header="This is panel header 1" key="1"
<Button type="link">My button</Button> >
<p>{text}</p>
</Panel>
this should be
<Panel header="This is panel header 1" key="1">
<Button type="link">My button</Button>
<p>{text}</p>
</Panel>

Material-UI - Tooltip on Select/MenuItem component stays displayed after selection

I'm using Material-UI's Select component with a Tooltip surrounding it, like so:
<Tooltip title="Tooltip Test">
<Select value={this.state.age} onChange={this.handleChange}
inputProps={{ name: "age", id: "age-simple" }}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>...</MenuItem>
</Select>
</Tooltip>
My problem is that when I click the Select component, the Tooltip stays displayed, during the use of the Select, and even after an item was selected.
I want it to disappear as soon as the Select is clicked so that it doesn't stay over the MenuItems (Changing the zIndex is not the solution I want) and also still is not displayed even after selecting an item in the menu.
I made a codesandbox with the simple issue I have: https://codesandbox.io/s/yvloqr5qoj
But I am using typescript and this is the actual code I'm working with:
ControlledTooltip
import * as React from 'react';
import PropTypes from 'prop-types';
import withStyles, { WithStyles } from 'material-ui/styles/withStyles';
import { Tooltip } from 'material-ui';
const styles = {
}
type State = {
open: boolean,
};
type Props = {
id: string,
msg: string,
children: PropTypes.node,
};
class ControlledTooltip extends React.PureComponent<Props & WithStyles<keyof typeof styles>, State> {
constructor(props) {
super(props);
this.state = {
open: false,
};
}
private handleTooltipClose(): void {
this.setState({ open: false });
}
private handleTooltipOpen(): void {
this.setState({ open: true });
}
render() {
const { id, msg, children } = this.props;
const { open } = this.state;
return(
<div>
<Tooltip id={id}
title={msg}
open={open}
onClose={this.handleTooltipClose}
onOpen={this.handleTooltipOpen}
>
{children ? children : null}
</Tooltip>
</div>
);
}
}
export default withStyles(styles)(ControlledTooltip);
Component using the Tooltip
<ControlledTooltip msg={'Filter'}>
<Select value={this.state.type} onChange={this.handleChange.bind(this, Filter.Type)}>
{this.docTypeFilters.map(item => {
return (<MenuItem key={item} value={item}>{item}</MenuItem>);
})}
</Select>
</ControlledTooltip>
TL;DR: You have to make your tooltip controlled.
UPDATE:
Based on your actual code, replace the return of your render() with this:
<Tooltip id={id}
title={msg}
open={open}
>
<div onMouseEnter={this.handleTooltipOpen}
onMouseLeave={this.handleTooltipClose}
onClick={this.handleTooltipClose}
>
{children ? children : null}
</div>
</Tooltip>
The problem was that you were using onClose/onOpen from the tooltip, which works as is uncontrolled. Now the div containing the select(or any children) has the control over the tooltip.
ANSWER FOR THE PASTEBINS
You will need to handle the open prop of the Tooltip:
class SimpleSelect ...
constructor(...
state = {..., open: false}; // the variable to control the tooltip
Alter your change handling:
handleChange = event => {
this.setState({ [event.target.name]: event.target.value, open: false });// keeps the tooltip hidding on the select changes
};
handleOpen = (open) => {
this.setState({ open }); // will show/hide tooltip when called
}
And reflect it in your render():
const { open } = this.state; // obtains the current value for the tooltip prop
...
<Tooltip title="Tooltip Test" open={open}>
...
<Select ... onMouseEnter={() => this.handleOpen(true)}
onMouseLeave={() => this.handleOpen(false)}
onClick={() => this.handleOpen(false)} >
The event handlers(onMouseEnter, onMouseLeave, onClick) in Select now control the tooltip show/hide behavior.
Same as David's answer, but for functional components with React hooks.
In your component before the return:
const [tooltipOpen, setTooltipOpen] = useState(false)
const handleTooltip = bool => {
setTooltipOpen(bool)
}
Then in your return:
<Tooltip ... open={tooltipOpen}>
<Select
onMouseEnter={() => {handleTooltip(true)}}
onMouseLeave={() => {handleTooltip(false)}}
onMouseClick={() => {handleTooltip(false)}}
...
>

Resources