Material UI popover not working with react class component - reactjs

I'm have not used material UI before and all its documentation is with reference with function components. I can not figure out why the popover is not working. Whenever I hover over the text the function is triggered but popover is showing up. I am able to access classes with this.props.classes. It would be great if anyone could help.
import React, { Component } from "react";
import Popover from "#material-ui/core/Popover";
import Typography from "#material-ui/core/Typography";
import { withStyles } from "#material-ui/core/styles";
const useStyles = (theme) => ({
root: {
backgroundColor: "red",
height: "30px",
},
lable: {
transform: "translate(5px, 2px) scale(1)",
pointerEvents: "none",
width: "100%",
height: "100%",
padding: "10px",
color: "red",
},
popover: {
pointerEvents: "none",
color:"pink"
},
paper: {
padding: theme.spacing(1),
},
etc:{
color: "red"
}
});
class SomeThing extends Component {
open;
constructor(props){
super(props)
this.handlePopoverClose = this.handlePopoverClose.bind(this);
this.handlePopoverOpen = this.handlePopoverOpen.bind(this);
this.state={
anchorEl: null,
}
}
componentDidMount(){
this.open = Boolean(this.state.anchorEl);
}
handlePopoverOpen = (event) => {
console.log("triggered!!!", event.currentTarget.innerText);
this.setState({ anchorEl: event.currentTarget.innerText });
};
handlePopoverClose = () => {
console.log("triggered again!!!", this.props);
this.setState({ anchorEl: null });
};
render() {
return (
<div>
<Typography
aria-owns={this.open ? "mouse-over-popover" : undefined}
aria-haspopup="true"
className={this.props.classes.etc}
onMouseEnter={this.handlePopoverOpen}
onMouseLeave={this.handlePopoverClose}
>
Hover with a Popover.
</Typography>
<Popover
id="mouse-over-popover"
className={this.props.classes.popover}
classes={{
paper: this.props.classes.paper
}}
open={this.open}
anchorEl={this.state.anchorEl}
anchorOrigin={{
vertical: "bottom",
horizontal: "left"
}}
transformOrigin={{
vertical: "top",
horizontal: "left"
}}
onClose={this.handlePopoverClose}
disableRestoreFocus
>
<Typography>I use Popover.</Typography>
</Popover>
</div>
);
}
}
export default withStyles(useStyles)(SomeThing);

You can define your open inside your render method; this way, open will always be a boolean and will not be set to undefined — which is the reason why it's not working.
render() {
const open = Boolean(this.state.anchorEl);
return (
<div>
<Typography
aria-owns={open ? "mouse-over-popover" : undefined}
aria-haspopup="true"
className={this.props.classes.etc}
onMouseEnter={this.handlePopoverOpen}
onMouseLeave={this.handlePopoverClose}
>
Hover with a Popover.
</Typography>
...
</div>
);
}

Related

Material UI Popover - how to open it without Hooks?

Because of an external library I have to use and I cannot avoid, a re-render of a Component I have is causing this error message:
Error: Rendered more hooks than during the previous render.
I know this is caused because I'm using hooks inside my component. See my component content:
import React, { useState } from 'react';
import Box from '#material-ui/core/Box';
import Popover from '#material-ui/core/Popover';
import ImageCell from 'App/components/DatabaseTable/Utils/ImageCell';
const Image = ({ defaultValueRow, valueRow }) => {
const [anchorEl, setAnchorEl] = useState(null);
const id = `popover-${defaultValueRow[valueRow]}`;
const handlePopoverOpen = (event) => setAnchorEl(event.currentTarget);
const handlePopoverClose = () => setAnchorEl(null);
return (
<>
<Box
aria-haspopup={'true'}
aria-owns={anchorEl ? id : undefined}
onMouseEnter={handlePopoverOpen}
onMouseLeave={handlePopoverClose}
>
<ImageCell src={valueRow[defaultValueRow]} />
</Box>
<Popover
id={id}
style={{ pointerEvents: 'none' }}
PaperProps={{
style: { padding: '8px' },
}}
open={anchorEl}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
>
<ImageCell
style={{ maxWidth: '125rem', maxHeight: '15rem' }}
src={valueRow[defaultValueRow]}
/>
</Popover>
</>
);
};
export default Image;
As I cannot avoid using this external library, I was wondering:
Is there any way to manage anchorEl to not be using the useState hook?

Styling of disabled and enabled button

I have one button (from material ui) which is greyed out if the date is not set. If you set a date it should be clickable. I want to style the button for those cases.
This is the button:
<Button style={{
marginTop: 10
}} disabled={this.props.date ? false : true} onClick={this.sendRequest} variant="contained" color="primary">Send Request</Button>
Those are my button-classes for styling:
'.enabledButton': {
background: '#ffb303!important',
},
'.defaultButton': {
background: '#cfcfcf!important',
},
I tried to apply it in the false / true check. If its true it should apply the .enabledButton and for the false case it should apply the .defaultButton.
Can someone help me with it?
Thank you very much!
You can override css which is injected by material ui
you can use rule name
Both options are covered in the working demo here
Code snippet
const useStyles = makeStyles(theme => ({
root: {
"& > *": {
margin: theme.spacing(1)
},
// using classname
"& .Mui-disabled": {
background: "#ffb303"
}
}
}));
const useStyles2 = makeStyles(theme => ({
root: {
"& > *": {
margin: theme.spacing(1)
},
"&$disabled": {
background: "rgba(0, 0, 0, 0.12)",
color: "red",
boxShadow: "none"
}
},
disabled: {}
}));
export default function ContainedButtons(props) {
const classes = useStyles();
const classes2 = useStyles2();
return (
<>
<div className={classes.root}>
<Button
variant="contained"
color="primary"
disabled={props.date ? false : true}
>
Button (using css name)
</Button>
</div>
<div>
<Button
classes={{ root: classes2.root, disabled: classes2.disabled }}
variant="contained"
color="primary"
disabled={props.date ? false : true}
>
Button (using rule name)
</Button>
</div>
</>
);
}
In your case you can use classes atribute by material-ui. I made you a full example using a functional component:
import React from 'react'
import Button from '#material-ui/core/Button'
import { makeStyles } from '#material-ui/core/styles'
const useStyles = makeStyles(theme => ({
button: {
backgroundColor: '#ffb303',
},
disabledButton: {
backgroundColor: '#cfcfcf',
}
}))
export default () => {
const [disabled, setDisabled] = React.useState(false)
const classes = useStyles()
const toggleDisabled = () => setDisabled(prev => !prev)
return (
<>
<Button
disabled={disabled}
onClick={toggleDisabled}
classes={{
root: classes.button,
disabled: classes.disabled
}}
variant="contained"
>
Toggle
</Button>
<Button
disabled={!disabled}
onClick={toggleDisabled}
classes={{
root: classes.button,
disabled: classes.disabled
}}
variant="contained"
>
Toggle
</Button>
</>
)
}
const useStyles = makeStyles({
enabledButton: {
background: '#ffb303!important',
'&:disabled': {
background: '#cfcfcf!important',
}
},
});
function Componenet() {
const classes = useStyles();
...
...
return (
<Button
className={classes.enabledButton}
disabled={!this.props.date}
onClick={this.sendRequest}
variant="contained"
color="primary"
>
Send Request
</Button>
);
}
You can try it in 2 ways:
1st Method:
You can add the styles directly and do the validation like this (but its not preferrable to inject styles directly)
<div className={classes.root}>
<Button variant="contained">Default</Button>
<Button style={{
marginTop: 10,
backgroundColor: `${this.props.date ? '#ffb303':'#cfcfcf'}`
}} disabled={this.props.date ? false : true}
variant="contained" color="primary">Send Request</Button>
2nd Method:
You can use styles and do the validation in your code.
const useStyles = makeStyles((theme) => ({
enabledButton: {
backgroundColor: '#ffb303',
},
defaultButton: {
backgroundColor: '#cfcfcf',
},
}));
const classes = useStyles();
<div className={classes.root}>
<Button variant="contained">Default</Button>
<Button style={{
marginTop: 10,
}} disabled={this.props.date ? false : true}
className={this.props.date ? classes.enabledButton : classes.defaultButton}
variant="contained" color="primary">Send Request</Button>
simple and easy to use my snippet:
<TextField
fullWidth={fullWidth}
disabled={disabled}
onChange={onChange}
InputProps={{
classes: {
underline: classes.underline,
disabled: disabled ? classes.disabled : {},
},
}}
{...rest}
/>
)
classes
const useStyles = makeStyles((theme) => ({
label: {
paddingRight: theme.spacing(1),
fontFamily: 'Montserrat Light',
fontSize: `0.875rem`,
},
underline: {
marginTop: 0,
marginBottom: 0,
fontFamily: 'Montserrat Light',
color: `${$white}`,
fontSize: `0.875rem`,
'&::after': {
borderBottom: `1px solid ${$white}`,
},
'&::before': {
borderBottom: `1px solid rgba(255, 255, 255, 0.36)`,
},
'&:hover&::before': {
borderBottom: `1px solid ${$white}`,
},
},
disabled: {
'&:hover&::before': {
borderBottom: `1px dotted rgba(255, 255, 255, 0.36)`,
},
},
}))

How can I create a button to a dialog box inside the PopperComponent of Material UI Labs Autocomplete

I have a Material UI Autocomplete which renders a list of Chips. I have added a button at the bottom of the PopperComponent which when clicked should open a dialog box.
But the Autocomplete doesn't allow the DialogBox to be opened. But the weird part is if I add 'open' to Autocomplete it works.
I have tried adding the onMouseDown event instead of onClick. Also, tried the event.preventDefault(). None of them works. However onMouseDown definitely called my listener for the Dialog box and changed its open state to true, but the dialog box did not appear.
This is the link to the sandbox.
Sandbox to the code
This is the component that implements the Dialog Box.
import React, { useState } from "react";
import { withStyles } from "#material-ui/core/styles";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import MuiDialogTitle from "#material-ui/core/DialogTitle";
import MuiDialogContent from "#material-ui/core/DialogContent";
import MuiDialogActions from "#material-ui/core/DialogActions";
import IconButton from "#material-ui/core/IconButton";
import CloseIcon from "#material-ui/icons/Close";
import Typography from "#material-ui/core/Typography";
import { orange } from "#material-ui/core/colors";
const styles = theme => ({
form: {
display: "flex",
flexDirection: "column",
margin: "auto",
width: "fit-content"
},
formControl: {
marginTop: theme.spacing(2),
minWidth: 120
},
formControlLabel: {
marginTop: theme.spacing(1)
},
closeButton: {
position: "absolute",
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500]
},
selectEmpty: {
marginTop: theme.spacing(2)
},
floatingLabelFocusStyle: {
color: "green"
},
separator: {
marginTop: theme.spacing(1)
},
menuStyle: {
border: "1px solid black",
borderRadius: "5%",
backgroundColor: "lightgrey"
}
});
const DialogTitle = withStyles(styles)(props => {
const { children, classes, onClose, ...other } = props;
return (
<MuiDialogTitle disableTypography className={classes.root} {...other}>
<Typography variant="h6">{children}</Typography>
{onClose ? (
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={onClose}
>
<CloseIcon />
</IconButton>
) : null}{" "}
</MuiDialogTitle>
);
});
const DialogContent = withStyles(theme => ({
root: {
padding: theme.spacing(2)
}
}))(MuiDialogContent);
const DialogActions = withStyles(theme => ({
root: {
margin: 0,
padding: theme.spacing(1)
}
}))(MuiDialogActions);
const ActionButton = withStyles(theme => ({
root: {
color: "#E87424",
backgroundColor: "white",
"&:hover": {
backgroundColor: orange[100]
}
}
}))(Button);
const ManageTagButton = withStyles(theme => ({
root: {
color: "#E87424"
}
}))(Button);
const TagContainer = props => {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
console.log("Dialog box clicked");
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<ManageTagButton
onMouseDown={event => {
event.preventDefault();
handleClickOpen();
}}
size="small"
>
MANAGE TAGS
</ManageTagButton>
<Dialog
fullWidth
maxWidth={"sm"}
onClose={handleClose}
aria-labelledby="customized-dialog-title"
open={open}
>
<DialogTitle id="customized-dialog-title">Manage Tags</DialogTitle>
<DialogContent dividers>
<h1>
This is the component of the dialog box. In reality I neeed to
display a data table with CRUD operations to add more tags.
</h1>
</DialogContent>
<DialogActions>
<ActionButton autoFocus onClick={handleClose} color="secondary">
CLOSE
</ActionButton>
</DialogActions>
</Dialog>
</div>
);
};
export default TagContainer;
This is the component that implements the Autocomplete.
import React, { Fragment } from "react";
import Chip from "#material-ui/core/Chip";
import Autocomplete from "#material-ui/lab/Autocomplete";
import { withStyles } from "#material-ui/core/styles";
import { makeStyles } from "#material-ui/core/styles";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
import List from "#material-ui/core/List";
import ListItem from "#material-ui/core/ListItem";
import ListItemText from "#material-ui/core/ListItemText";
import ListItemSecondaryAction from "#material-ui/core/ListItemSecondaryAction";
import Paper from "#material-ui/core/Paper";
import TagContainer from "./TagContainer";
const ListItemCustom = withStyles(theme => ({
gutters: {
paddingLeft: 0,
paddingRight: 0
},
secondaryAction: {
paddingRight: 0
}
}))(ListItem);
const AutocompleteCustom = withStyles(theme => ({
endAdornment: {
display: "none"
}
}))(Autocomplete);
const CreateButton = withStyles(theme => ({
root: {
color: "#E87424"
}
}))(Button);
const MuiFilledInputCustom = makeStyles(
{
underline: {
"&&&:before": {
borderBottom: "none"
},
"&&:after": {
borderBottom: "none"
}
}
},
{ name: "MuiFilledInput" }
);
const loadCustomStyles = () => {
MuiFilledInputCustom();
};
export default function AddTagToThread() {
loadCustomStyles();
const handleSubmit = () => {
console.log("Add tags to thread");
};
const useStyles = makeStyles({
root: {
minWidth: 300,
width: 300,
height: 250,
minHeight: 250,
zIndex: 1
},
buttons: {
display: "flex",
justifyContent: "flex-end"
}
});
const PaperComponentCustom = options => {
const classes = useStyles();
const { containerProps, children } = options;
return (
<Paper className={classes.root} {...containerProps} square>
{children}
<div className={classes.buttons}>
<TagContainer />
</div>
</Paper>
);
};
return (
<List dense={false}>
<ListItemCustom>
<ListItemText>
<AutocompleteCustom
multiple
id="size-small-filled-multi"
size="medium"
options={tagList}
noOptionsText="No options"
freeSolo
filterSelectedOptions
PaperComponent={PaperComponentCustom}
getOptionLabel={option => option.name}
onChange={(event, value) => {
console.log(value);
}}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
variant="default"
style={{
backgroundColor: option.color
}}
label={option.name}
size="medium"
{...getTagProps({ index })}
/>
))
}
renderOption={option => (
<Fragment>
<Chip
variant="default"
style={{
backgroundColor: option.color,
padding: "15px",
marginLeft: "12px"
}}
label={option.name}
size="medium"
/>
</Fragment>
)}
renderInput={params => (
<TextField
{...params}
variant="filled"
label="Filter By Tag"
placeholder="Select Tag"
/>
)}
/>
</ListItemText>
<ListItemSecondaryAction>
<CreateButton onClick={handleSubmit}>ADD TAG</CreateButton>
</ListItemSecondaryAction>
</ListItemCustom>
</List>
);
}
const tagList = [
{ name: "Follow Up", tagId: 1, color: "#FFC107" },
{ name: "Important", tagId: 2, color: "#46B978" },
{ name: "Idea", tagId: 3, color: "#EEA5F6" },
{ name: "Non Issue", tagId: 4, color: "#2EACE2" }
];
I have been stuck at this for last couple of days. Any help is greatly appreciated.
The issue with your code is that the <Dialog/> component is in the PaperComponentCustom component which gets unmounted after the option is selected.
<Paper className={classes.root} {...containerProps} square>
{children}
<ManageTagButton onMouseDown={handleClickOpen} fullWidth>
MANAGE TAGS
</ManageTagButton>
</Paper>
The solution to keep only the <ManageTagButton/> component in the PaperComponentCustom and move the <Dialog/> component one level up. I imagine that even if you have 10 elements in the <List/> you would still have only one <Dialog>, you cannot have 10 dialog components opened at once.
So therefore your <AddTagToThread/> component should render the dialog directly and the state of the dialog open and the handlers handleOpen and handleClose should be moved in the <AddTagToThread/> component also
Working codesandbox HERE, code below
Autocomplete component
import React, { Fragment, useState } from "react";
import Chip from "#material-ui/core/Chip";
import Autocomplete from "#material-ui/lab/Autocomplete";
import { withStyles } from "#material-ui/core/styles";
import { makeStyles } from "#material-ui/core/styles";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
import List from "#material-ui/core/List";
import ListItem from "#material-ui/core/ListItem";
import ListItemText from "#material-ui/core/ListItemText";
import ListItemSecondaryAction from "#material-ui/core/ListItemSecondaryAction";
import Paper from "#material-ui/core/Paper";
import TagContainer from "./TagContainer";
const ListItemCustom = withStyles(theme => ({
gutters: {
paddingLeft: 0,
paddingRight: 0
},
secondaryAction: {
paddingRight: 0
}
}))(ListItem);
const AutocompleteCustom = withStyles(theme => ({
endAdornment: {
display: "none"
}
}))(Autocomplete);
const CreateButton = withStyles(theme => ({
root: {
color: "#E87424"
}
}))(Button);
const MuiFilledInputCustom = makeStyles(
{
underline: {
"&&&:before": {
borderBottom: "none"
},
"&&:after": {
borderBottom: "none"
}
}
},
{ name: "MuiFilledInput" }
);
const loadCustomStyles = () => {
MuiFilledInputCustom();
};
const ManageTagButton = withStyles(theme => ({
root: {
color: "#E87424"
}
}))(Button);
export default function AddTagToThread() {
loadCustomStyles();
const handleSubmit = () => {
console.log("Add tags to thread");
};
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
console.log("Dialog box clicked");
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const useStyles = makeStyles({
root: {
minWidth: 300,
width: 300,
height: 250,
minHeight: 250,
zIndex: 1
},
buttons: {
display: "flex",
justifyContent: "flex-end"
}
});
const PaperComponentCustom = options => {
const classes = useStyles();
const { containerProps, children } = options;
return (
<Paper className={classes.root} {...containerProps} square>
{children}
<ManageTagButton onMouseDown={handleClickOpen} fullWidth>
MANAGE TAGS
</ManageTagButton>
</Paper>
);
};
return (
<>
<TagContainer open={open} handleClose={handleClose} />
<List dense={false}>
<ListItemCustom>
<ListItemText>
<AutocompleteCustom
multiple
id="size-small-filled-multi"
size="medium"
options={tagList}
noOptionsText="No options"
freeSolo
filterSelectedOptions
PaperComponent={PaperComponentCustom}
getOptionLabel={option => option.name}
onChange={(event, value) => {
console.log(value);
}}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
variant="default"
style={{
backgroundColor: option.color
}}
label={option.name}
size="medium"
{...getTagProps({ index })}
/>
))
}
renderOption={option => (
<Fragment>
<Chip
variant="default"
style={{
backgroundColor: option.color,
padding: "15px",
marginLeft: "12px"
}}
label={option.name}
size="medium"
/>
</Fragment>
)}
renderInput={params => (
<TextField
{...params}
variant="filled"
label="Filter By Tag"
placeholder="Select Tag"
/>
)}
/>
</ListItemText>
<ListItemSecondaryAction>
<CreateButton onClick={handleSubmit}>ADD TAG</CreateButton>
</ListItemSecondaryAction>
</ListItemCustom>
</List>
</>
);
}
const tagList = [
{ name: "Follow Up", tagId: 1, color: "#FFC107" },
{ name: "Important", tagId: 2, color: "#46B978" },
{ name: "Idea", tagId: 3, color: "#EEA5F6" },
{ name: "Non Issue", tagId: 4, color: "#2EACE2" }
];
Dialog component
import React, { useState } from "react";
import { withStyles } from "#material-ui/core/styles";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import MuiDialogTitle from "#material-ui/core/DialogTitle";
import MuiDialogContent from "#material-ui/core/DialogContent";
import MuiDialogActions from "#material-ui/core/DialogActions";
import IconButton from "#material-ui/core/IconButton";
import CloseIcon from "#material-ui/icons/Close";
import Typography from "#material-ui/core/Typography";
import { orange } from "#material-ui/core/colors";
const styles = theme => ({
form: {
display: "flex",
flexDirection: "column",
margin: "auto",
width: "fit-content"
},
formControl: {
marginTop: theme.spacing(2),
minWidth: 120
},
formControlLabel: {
marginTop: theme.spacing(1)
},
closeButton: {
position: "absolute",
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500]
},
selectEmpty: {
marginTop: theme.spacing(2)
},
floatingLabelFocusStyle: {
color: "green"
},
separator: {
marginTop: theme.spacing(1)
},
menuStyle: {
border: "1px solid black",
borderRadius: "5%",
backgroundColor: "lightgrey"
}
});
const DialogTitle = withStyles(styles)(props => {
const { children, classes, onClose, ...other } = props;
return (
<MuiDialogTitle disableTypography className={classes.root} {...other}>
<Typography variant="h6">{children}</Typography>
{onClose ? (
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={onClose}
>
<CloseIcon />
</IconButton>
) : null}{" "}
</MuiDialogTitle>
);
});
const DialogContent = withStyles(theme => ({
root: {
padding: theme.spacing(2)
}
}))(MuiDialogContent);
const DialogActions = withStyles(theme => ({
root: {
margin: 0,
padding: theme.spacing(1)
}
}))(MuiDialogActions);
const ActionButton = withStyles(theme => ({
root: {
color: "#E87424",
backgroundColor: "white",
"&:hover": {
backgroundColor: orange[100]
}
}
}))(Button);
const TagContainer = ({ open, handleClose }) => {
return (
<Dialog
fullWidth
maxWidth={"sm"}
onClose={handleClose}
aria-labelledby="customized-dialog-title"
open={open}
>
<DialogTitle id="customized-dialog-title">Manage Tags</DialogTitle>
<DialogContent dividers>
<h1>
This is the component of the dialog box. In reality I neeed to display
a data table with CRUD operations to add more tags.
</h1>
</DialogContent>
<DialogActions>
<ActionButton autoFocus onClick={handleClose} color="secondary">
CLOSE
</ActionButton>
</DialogActions>
</Dialog>
);
};
export default TagContainer;

Open menu on mouseover and Close menu on mouseleave in react

I just started plating around with react. I am currently working on my navBar using material-ui and react. When I hover over the menu, the drop-down appears. But in order to close the drop-down, I have to click on the outside of the drop-down. I want to be able to close the dropdown when I hover out of the drop-down or move to the different menu option (in which case a different drop-down should appear). Something like this one: https://www.palantir.com/
I looked around but I didn't find the solution. This was the closest I got: Material-ui: open menu by event hover
I tried using the same technique and added this to my code but to no avail. Any suggestions? Thanks!
Edits: I recreated my problem here: https://react-xmaiyw.stackblitz.io
The problem can be seen when clicked on 'Why us'.
handleClick = (event) => {
event.preventDefault();
this.setState({
open: true,
anchorEl: event.currentTarget,
});
};
handleRequestClose = () => {
this.setState({
open: false,
});
};
render() {
return (
<FlatButton
onClick={this.handleClick}
onMouseOver={this.handleClick}
onMouseLeave={this.handleRequestClose} //When I add this line of
//code, it keeps flickering very fast almost as if drop-down
//doesn't open
label="Why Us?"
/>
)}
The flickering is caused by the opening of the menu underneath your mouse. When the menu opens, the mouse is no longer over the button, so it prompts a mouseleave event, closing the menu, so that your mouse is now above the button again, prompting a mouseenter event, which opens the menu...and so on and so forth.
You can accomplish what you'd like with some additional logic to track where the mouse is, and a timeout to ensure that the user has time to transition the mouse between the button and the menu.
import React from 'react';
import Button from 'material-ui/Button';
import Menu, { MenuItem } from 'material-ui/Menu';
const timeoutLength = 300;
class SimpleMenu extends React.Component {
state = {
anchorEl: null,
// Keep track of whether the mouse is over the button or menu
mouseOverButton: false,
mouseOverMenu: false,
};
handleClick = event => {
this.setState({ open: true, anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ mouseOverButton: false, mouseOverMenu: false });
};
enterButton = () => {
this.setState({ mouseOverButton: true });
}
leaveButton = () => {
// Set a timeout so that the menu doesn't close before the user has time to
// move their mouse over it
setTimeout(() => {
this.setState({ mouseOverButton: false });
}, timeoutLength);
}
enterMenu = () => {
this.setState({ mouseOverMenu: true });
}
leaveMenu = () => {
setTimeout(() => {
this.setState({ mouseOverMenu: false });
}, timeoutLength);
}
render() {
// Calculate open state based on mouse location
const open = this.state.mouseOverButton || this.state.mouseOverMenu;
return (
<div>
<Button
aria-owns={this.state.open ? 'simple-menu' : null}
aria-haspopup="true"
onClick={this.handleClick}
onMouseEnter={this.enterButton}
onMouseLeave={this.leaveButton}
>
Open Menu
</Button>
<Menu
id="simple-menu"
anchorEl={this.state.anchorEl}
open={open}
onClose={this.handleClose}
MenuListProps={{
onMouseEnter: this.enterMenu,
onMouseLeave: this.leaveMenu,
}}
>
<MenuItem onClick={this.handleClose}>Profile</MenuItem>
<MenuItem onClick={this.handleClose}>My account</MenuItem>
<MenuItem onClick={this.handleClose}>Logout</MenuItem>
</Menu>
</div>
);
}
}
export default SimpleMenu;
I used the MenuListProps to set the mouseEnter and mouseLeave events directly on the MenuList itself because the Menu component includes a bunch of invisible (disply: none) transition elements that have weird effects on mouse events. The MenuList is the element that's actually displayed so it makes sense to set the mouse events directly on it.
You'll probably need to play around with the timeoutLength and transitions to get everything looking smooth.
I faced same problems.
I solved the issues like this. I gaved LeaveMenu event to total component and menu component seperately, after it, it worked perfectly
import React from 'react';
import {
Menu,
MenuItem as MuiMenuItem,
Avatar,
Divider,
Typography,
Switch,
Fade,
} from '#mui/material';
import { useHistory } from 'react-router-dom';
import { styled } from '#mui/styles';
import { DarkMode as DarkModeIcon } from '#mui/icons-material';
/******************** Styled Components ********************/
const UserAvatarButton = styled('div')(({ active, theme }) => ({
height: 72,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '0px 20px',
cursor: 'pointer',
borderBottom: active ? `3px solid ${theme.palette.primary.main}` : 'none',
borderRadius: 0,
}));
const ProfileMenuNavigation = styled(Menu)(() => ({
'& .MuiList-root': {
paddingTop: 0,
paddingBottom: 0,
minWidth: 220,
maxWidth: 350,
},
}));
const MenuItem = styled(MuiMenuItem)(({ theme }) => ({
padding: 16,
width: '100%',
'&:hover': {
backgroundColor: theme.palette.background.main,
boxShadow: '5px 0px 5px 0px #888888',
transition: 'box-shadow 0.3s ease-in-out',
},
}));
const ProfileMenuText = styled(Typography)(() => ({
fontFamily: 'Poppins',
marginLeft: 16,
marginRight: 16,
fontSize: 16,
fontWeight: 600,
}));
/******************** Main Component ********************/
const ProfileMenu = ({ menus, active }) => {
const history = useHistory();
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
if (anchorEl) {
setAnchorEl(null);
} else {
setAnchorEl(event.currentTarget);
}
};
const handleClose = () => {
setAnchorEl(null);
};
const goPath = (path) => {
setAnchorEl(null);
history.push(path);
};
const leaveMenu = () => {
setTimeout(() => {
setAnchorEl(null);
}, 300);
};
return (
<div onMouseLeave={leaveMenu}>
<UserAvatarButton
id="account-button"
active={active}
aria-controls={open ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
onMouseOver={(event) => setAnchorEl(event.currentTarget)}
>
<Avatar
sx={{
width: 38,
height: 38,
}}
alt="Avatar"
src="https://i.pravatar.cc/300"
/>
</UserAvatarButton>
<ProfileMenuNavigation
id="account-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'account-button',
onMouseLeave: leaveMenu,
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
TransitionComponent={Fade}
>
{menus.map((menu, index) => (
<div key={index}>
<MenuItem onClick={() => goPath(menu.path)}>
{menu?.icon}
<ProfileMenuText>{menu.text}</ProfileMenuText>
</MenuItem>
<Divider style={{ margin: 0 }} />
</div>
))}
<MenuItem onClick={() => {}}>
<DarkModeIcon />
<ProfileMenuText>Night Mode</ProfileMenuText>
<div style={{ marginLeft: 16 }}>
<Switch />
</div>
</MenuItem>
</ProfileMenuNavigation>
</div>
);
};
export default ProfileMenu;

React PDF viewer component rerenders constantly

I am using a React PDF viewer in my project. I have a react mui dialog component that I use with react draggable to drag it around.
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import makeStyles from "#material-ui/core/styles/makeStyles";
import DialogContent from "#material-ui/core/DialogContent";
import IconButton from "#material-ui/core/IconButton";
import ClearIcon from "#material-ui/icons/Clear";
import Draggable from "react-draggable";
import Paper from "#material-ui/core/Paper";
import Dialog from "#material-ui/core/Dialog";
import PDFViewer from "./PDFViewer";
function PaperComponent({...props}) {
return (
<Draggable
>
<Paper {...props} />
</Draggable>
);
}
const StyledDialog = withStyles({
root: {
pointerEvents: "none"
},
paper: {
pointerEvents: "auto"
},
scrollPaper: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
marginRight: 20
}
})(props => <Dialog hideBackdrop {...props} />);
const useStyles = makeStyles({
dialog: {
cursor: 'move'
},
dialogContent: {
'&:first-child': {
padding: 10,
background: 'white'
}
},
clearIcon: {
position: 'absolute',
top: -20,
right: -20,
background: 'white',
zIndex: 1,
'&:hover': {
background: 'white'
}
},
paper: {
overflowY: 'visible',
maxWidth: 'none',
maxHeight: 'none',
width: 550,
height: 730
}
});
const PDFModal = (props) => {
const classes = useStyles();
const {open, onClose, pdfURL} = props;
return (
<StyledDialog
open={open}
classes={{root: classes.dialog, paper: classes.paper}}
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog"
>
<DialogContent classes={{root: classes.dialogContent}} id="draggable-dialog">
<IconButton className={classes.clearIcon} aria-label="Clear" onClick={onClose}>
<ClearIcon/>
</IconButton>
<PDFViewer
url={pdfURL}
/>
</DialogContent>
</StyledDialog>
);
};
export default PDFModal;
And this is the PDFViewer component:
import React from 'react';
import { Viewer, SpecialZoomLevel, Worker } from '#react-pdf-viewer/core';
import { defaultLayoutPlugin } from '#react-pdf-viewer/default-layout';
import '#react-pdf-viewer/core/lib/styles/index.css';
import '#react-pdf-viewer/default-layout/lib/styles/index.css';
import ArrowForward from "#material-ui/icons/ArrowForward";
import ArrowBack from "#material-ui/icons/ArrowBack";
import Button from "#material-ui/core/Button";
import IconButton from "#material-ui/core/IconButton";
import RemoveCircleOutlineIcon from '#material-ui/icons/RemoveCircleOutline';
import AddCircleOutlineIcon from '#material-ui/icons/AddCircleOutline';
import './PDFViewer.css';
const PDFViewer = ({url}) => {
const renderToolbar = (Toolbar) => (
<Toolbar>
{
(slots) => {
const {
CurrentPageLabel, CurrentScale, GoToNextPage, GoToPreviousPage, ZoomIn, ZoomOut,
} = slots;
return (
<div
style={{
alignItems: 'center',
display: 'flex',
}}
>
<div style={{ padding: '0px 2px' }}>
<ZoomOut>
{
(props) => (
<IconButton aria-label="delete" onClick={props.onClick}>
<RemoveCircleOutlineIcon />
</IconButton>
)
}
</ZoomOut>
</div>
<div style={{ padding: '0px 2px' }}>
<CurrentScale>
{
(props) => (
<span>{`${Math.round(props.scale * 100)}%`}</span>
)
}
</CurrentScale>
</div>
<div style={{ padding: '0px 2px' }}>
<ZoomIn>
{
(props) => (
<IconButton aria-label="delete" onClick={props.onClick}>
<AddCircleOutlineIcon />
</IconButton>
)
}
</ZoomIn>
</div>
<div style={{ padding: '0px 2px', marginLeft: 'auto' }}>
<GoToPreviousPage>
{
(props) => (
<Button
style={{
cursor: props.isDisabled ? 'not-allowed' : 'pointer',
height: '30px',
width: '30px'
}}
disabled={props.isDisabled}
disableElevation
disableFocusRipple
onClick={props.onClick}
variant="outlined">
<ArrowBack fontSize="small"/>
</Button>
)
}
</GoToPreviousPage>
</div>
<div style={{ padding: '0px 2px' }}>
<CurrentPageLabel>
{
(props) => (
<span>{`${props.currentPage + 1} av ${props.numberOfPages}`}</span>
)
}
</CurrentPageLabel>
</div>
<div style={{ padding: '0px 2px' }}>
<GoToNextPage>
{
(props) => (
<Button
style={{
cursor: props.isDisabled ? 'not-allowed' : 'pointer',
height: '30px',
width: '30px'
}}
disabled={props.isDisabled}
disableElevation
disableFocusRipple
onClick={props.onClick}
variant="outlined">
<ArrowForward fontSize="small"/>
</Button>
)
}
</GoToNextPage>
</div>
</div>
)
}
}
</Toolbar>
);
const defaultLayoutPluginInstance = defaultLayoutPlugin({
renderToolbar,
sidebarTabs: defaultTabs => [defaultTabs[1]]
});
// constantly called
console.log('entered')
return (
<div
style={{
height: '100%',
}}
>
<Worker workerUrl="https://unpkg.com/pdfjs-dist#2.5.207/build/pdf.worker.min.js">
<Viewer
fileUrl={url}
defaultScale={SpecialZoomLevel.PageFit}
plugins={[
defaultLayoutPluginInstance
]}
/>
</Worker>
</div>
);
};
export default PDFViewer;
I can see in the console that PDFViewer is being constantly called. I am not sure what is causing this rerenders the whole time?
Isn't it make sense to re-render when you have a new fileUrl passed to PDFModal? The following sequence should be how the app is executed.
PDFModal, PDFViewer and other related components init
When a file is dragged into the PaperComponent context, the upper level component handles it and passing pdfURL as props
const PDFModal = (props) => {
const { ......., pdfURL } = props;
//...skipped code
return (
<StyledDialog
PaperComponent={PaperComponent}
>
//...skipped code
<PDFViewer
url={pdfURL}
/>
</StyledDialog>
);
};
PDFViewer updated because there is a new prop.
const PDFViewer = ({ url }) => {
//...skipped code
return (
//...skipped code
<Viewer
fileUrl={url}
/>
);
}
I agree what #LindaPaiste said, putting Toolbar maybe an option since it doesn't use the url props passed in. For the re-render problem, I suggest that useCallback can be used to wrap the whole PDFViewer component. Only update the component when the url has changed.
This link provide some insights on when to use useCallback which can be a reference.
const PDFViewer = useCallback(
({ url }) => {
//...skipped code
return (
//...skipped code
<Viewer
fileUrl={url}
/>
)
}, [url])

Resources