Material UI Popover - how to open it without Hooks? - reactjs

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?

Related

Factoring material UI popover into a higher order component and keep the popover state in the `hoc`

I am following the react material ui doc here: https://mui.com/components/popover/, and I'm attempting to factor all the popover view and state logic into a higher order component. The one from the doc is here:
function BasicPopoverFromDoc(props) {
const [anchorEl, setAnchorEl] = React.useState(null);
const handlePopoverOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handlePopoverClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
return (
<div>
<Typography
aria-owns={open ? 'mouse-over-popover' : undefined}
aria-haspopup="true"
onMouseEnter={handlePopoverOpen}
onMouseLeave={handlePopoverClose}
style={{color:'white'}}
>
Hover with a Popover.
</Typography>
<Popover
id="mouse-over-popover"
sx={{
pointerEvents: 'none',
}}
open={open}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={handlePopoverClose}
disableRestoreFocus
>
<Typography sx={{ p: 1 }}>I use Popover.</Typography>
</Popover>
</div>
);
}
I lifted all the state logic into a hoc as follows, taking <Child/> as a param:
function WithPopOver(props){
const [anchorEl, setAnchorEl] = React.useState(null);
const handlePopoverOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handlePopoverClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const { Child } = props;
console.log('with Popover: ', open)
return (
<div>
<Child
{...props}
aria-owns={open ? 'mouse-over-popover' : undefined}
aria-haspopup="true"
onMouseEnter={handlePopoverOpen}
onMouseLeave={handlePopoverClose}
/>
<Popover
id="mouse-over-popover"
sx={{
pointerEvents: 'none',
}}
open={open}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={handlePopoverClose}
disableRestoreFocus
>
<Typography sx={{ p: 1 }}>I use Popover.</Typography>
</Popover>
</div>
);
}
but when i try to use it this way:
function BasicPopover(props){
return (
<WithPopOver
{...props}
Child={() => <Typography style={{color:'white'}}> wrapped in hoc </Typography>}
/>
)
}
It seems like the popover does not display. It refuses to display even when I change open to true by force. What am i missing here?
You haven't passed Child props to it's children, so onMouseEnter and onMouseLeave never fires, try on this:
<WithPopOver
{...props}
Child={(props) => <Typography style={{color:'white'}} {...props}> wrapped in hoc </Typography>}
/>

How to store a component and render conditionally

I'm using material-ui popover component wrapping a formik form. it's obvious every time it is closed, the form re renders and all values get cleaned.
I thought about storing values in parent, but it's more complicated and I'm using dynamic component, then it's not a solution here.
I am looking for a way to get a copy of children and just show it inside popover.
Here is my code:
export default function ButtonPopover({ icon: Icon, children }) {
const [anchorEl, setAnchorEl] = useState(null);
const open = (event) => {
setAnchorEl(event.currentTarget);
};
const close = () => {
setAnchorEl(null);
};
return (
<>
<IconButton onClick={open}>
<Icon />
</IconButton>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={close}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
>
// Here is what I need to be saved.
{children}
</Popover>
</>
);
}
I'm not sure what you're trying to do but this is how you render conditionally:
export default function ButtonPopover({ icon: Icon, children }) {
const [anchorEl, setAnchorEl] = useState(null);
const open = (event) => {
setAnchorEl(event.currentTarget);
};
const close = () => {
setAnchorEl(null);
};
const renderChildren = () => {
if (myCondition == true) {
return (
<>
<Text>render when condition equals true</Text>
</>
)
} else {
return (
<>
<Text>render when condition doesn't equal true</Text>
</>
)
}
}
return (
<>
<IconButton onClick={open}>
<Icon />
</IconButton>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={close}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
>
{renderChildren()}
</Popover>
</>
);
}

Material-ui.Why Popover set Modal's backdrop invisible

Can't understand why Popover set backdrop invisible by default, and get no way to change it.
Did I miss something important in Material Design?Or can I just create an issue for it?
<Modal
container={container}
open={open}
ref={ref}
BackdropProps={{ invisible: true }}
className={clsx(classes.root, className)}
{...other}
>
https://github.com/mui-org/material-ui/blob/next/packages/material-ui/src/Popover/Popover.js#L386
You can change it with BackdropProps={{ invisible: false }}. In the code snippet you included from Popover, if BackdropProps has been specified on the Popover it will be part of {...other} and will win over the earlier BackdropProps={{ invisible: true }}.
Here's a working example based on one of the demos:
import React from "react";
import { makeStyles } from "#material-ui/core/styles";
import Popover from "#material-ui/core/Popover";
import Typography from "#material-ui/core/Typography";
import Button from "#material-ui/core/Button";
const useStyles = makeStyles((theme) => ({
typography: {
padding: theme.spacing(2)
}
}));
export default function SimplePopover() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? "simple-popover" : undefined;
return (
<div>
<Button
aria-describedby={id}
variant="contained"
color="primary"
onClick={handleClick}
>
Open Popover
</Button>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center"
}}
transformOrigin={{
vertical: "top",
horizontal: "center"
}}
BackdropProps={{ invisible: false }}
>
<Typography className={classes.typography}>
The content of the Popover.
</Typography>
</Popover>
</div>
);
}

Material UI popover not working with react class component

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

How to set Anchor to Popover in Material-UI

I have a Popover in Material-UI from which I want to permanently set the anchor to a button. Not only on click with event.currentTarget.
How can I do this with typescript?
Unfortunately, the current examples in Material-UI use event.currentTarget and with a reference it is not working.
import React,{useRef} from 'react';
import { makeStyles, createStyles, Theme } from '#material-ui/core/styles';
import Popover from '#material-ui/core/Popover';
import Typography from '#material-ui/core/Typography';
import Button from '#material-ui/core/Button';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
typography: {
padding: theme.spacing(2),
},
}),
);
export default function SimplePopover() {
const classes = useStyles();
const ref = useRef(null)
return (
<div>
<Button ref={ref} variant="contained" color="primary">
Open Popover
</Button>
<Popover
open
anchorEl={ref}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
<Typography className={classes.typography}>The content of the Popover.</Typography>
</Popover>
</div>
);
}
Here a codesandbox for it.
You must have missed some detail. I followed the Simple Popover example in official docs and it still works.
If you want to use useRef, make sure to refer to buttonRef.current when setting anchorRef
Below is the forked codesandbox:
2 things
you need to store the state of opened popup
open it from the outside
close it from the inside
relevant JS:
import React, { useRef, useState } from "react";
import { makeStyles, createStyles, Theme } from "#material-ui/core/styles";
import Popover from "#material-ui/core/Popover";
import Typography from "#material-ui/core/Typography";
import Button from "#material-ui/core/Button";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
typography: {
padding: theme.spacing(2)
}
})
);
export default function SimplePopover() {
const classes = useStyles();
const ref = useRef(null);
const [popOverVisible, setPopOverVisible] = useState(false);
const togglePopOver = () => {
popOverVisible === false
? setPopOverVisible(true)
: setPopOverVisible(false);
};
return (
<div>
<Button
ref={ref}
variant="contained"
color="primary"
onClick={togglePopOver}
>
Open Popover
</Button>
Status: {popOverVisible.toString()}
<Popover
open={popOverVisible}
anchorEl={ref}
anchorOrigin={{
vertical: "bottom",
horizontal: "center"
}}
transformOrigin={{
vertical: "top",
horizontal: "center"
}}
>
<Button
ref={ref}
variant="contained"
color="primary"
onClick={togglePopOver}
>
CLOSE Popover
</Button>
<Typography className={classes.typography}>
The content of the Popover.
</Typography>
</Popover>
</div>
);
}
EDIT: upon the user's comment below:
<Popover
open={popOverVisible}
anchorEl={ref}
anchorReference="anchorPosition"
anchorPosition={{ top: 50, left: 140 }}
anchorOrigin={{
vertical: "bottom",
horizontal: "left"
}}
transformOrigin={{
vertical: "top",
horizontal: "left"
}}
>
forked sandbox here

Resources