How to use anchorEl with styled() in MUI? - reactjs

I'm trying to anchor a popover component to a button component. The problem is that this doesn't seem to work if the button is styled using styled() (I'm using emotion).
This code causes the following warning: MUI: The `anchorEl` prop provided to the component is invalid.
Because anchorEl is invalid the popover will simply postion itself on the top left corner of the screen.
import { useState } from "react";
import { styled } from "#mui/material/styles";
import Popover from "#mui/material/Popover";
import Button from "#mui/material/Button";
export default function BasicPopover() {
const [anchorEl, setAnchorEl] = useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const StyledButton = styled((props) => <Button {...props} />)(
({ theme }) => ({
//some styling
})
);
return (
<div>
<StyledButton
variant="contained"
onClick={handleClick}
>
Open Popover
</StyledButton>
<Popover open={open} anchorEl={anchorEl} onClose={handleClose}>
The content of the Popover.
</Popover>
</div>
);
}
I found a slightly different approach using refs here, but I couldn't figure out how to make it work with styled() either.
I'm still rather new to react so please be gentle.

I don't know why, but if you move the styled() out of the main component it works.
const StyledButton = styled((props) => <Button {...props} />)(
({ theme }) => ({
//some styling
})
);
export default function BasicPopover() {
//[...]
}

Related

How to test Material-UI Popover close implementation

I would like to make sure that my implementation of a Popover element combined with a trigger button works as expected.
I was unable to have a working test for asserting that the Popover gets closed after the user presses esc. I was able to make this test work with Modal, but I must use a Popover in my current project.
Component code:
import {
Button, Popover,
} from '#mui/material';
import React from 'react';
export default function SimpleModal() {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? 'user-menu' : undefined;
return (
<>
<Button
aria-controls={id}
aria-haspopup="true"
onClick={handleClick}
>
Open Modal
</Button>
<Popover open={open} onClose={handleClose}>
<h1>Text in Modal</h1>
</Popover>
</>
);
}
Test file code:
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import SimpleModal from './SimpleModal';
test('should close when esc key is pressed', async () => {
render(<SimpleModal />);
userEvent.click(screen.getByText('Open Modal'));
expect(screen.getByText('Text in Modal')).toBeInTheDocument();
userEvent.keyboard('{esc}');
await expect(screen.queryByText('Text in Modal')).not.toBeInTheDocument();
});
As suggested by #juliomalves, wrapping the last expect in a waitFor corrected the test:
await waitFor(() => expect(...));

How to prevent re-render when using react material ui dialogs

I'm using modals from the react-material-ui library for a project, and I noticed a side effect when trying to open/close the dialog component. Try this code (code sandbox):
import { useState } from "react";
import moment from "moment";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import DialogTitle from "#material-ui/core/DialogTitle";
import DialogActions from "#material-ui/core/DialogActions";
import "./styles.css";
export default function App() {
const [open, setOpen] = useState(false);
const timeStamp = moment().format("HH:mm:ss SS");
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div className="App">
<h1>Mui Dialog</h1>
<h2>Rendered on {timeStamp}</h2>
<Button variant="outlined" color="primary" onClick={handleOpen}>
Open simple dialog
</Button>
<Dialog open={open}>
<DialogTitle>This is a simple dialog</DialogTitle>
<DialogActions>
<Button onClick={handleClose} color="primary" autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
</div>
);
}
You'll see that clicking on the button open will cause a re-render and closing the dialog will have the same effect, it's normal because state changed after calling hooks. This is often undesirable, I don't want to re-render the page when opening or after closing.
So I tried to use the following solution, based of refs (code sandbox):
import { useState, useRef, forwardRef, useImperativeHandle } from "react";
import moment from "moment";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import DialogTitle from "#material-ui/core/DialogTitle";
import DialogActions from "#material-ui/core/DialogActions";
import "./styles.css";
const SimpleDialog = forwardRef(({ title }, ref) => {
const [open, setOpen] = useState(false);
const innerRef = useRef();
const handleClose = () => {
setOpen(false);
};
useImperativeHandle(ref, () => ({
openDialog: () => setOpen(true),
closeDialog: () => setOpen(false)
}));
return (
<Dialog open={open} ref={innerRef}>
<DialogTitle>{title}</DialogTitle>
<DialogActions>
<Button onClick={handleClose} color="primary" autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
);
});
export default function App() {
const timeStamp = moment().format("HH:mm:ss SS");
const dialogRef = useRef();
const handleOpen = () => {
dialogRef.current.openDialog();
};
return (
<div className="App">
<h1>Mui Dialog</h1>
<h2>Rendered on {timeStamp}</h2>
<Button variant="outlined" color="primary" onClick={handleOpen}>
Open simple dialog
</Button>
<SimpleDialog ref={dialogRef} title="This is a simple dialog" />
</div>
);
}
My question is: is this a correct approach to solve the problem of unwanted re-renders in the case of react-material-ui modals ?
Regards.

Material UI Rotate Menu Icons Individually

When I click on one button in a navigation bar, I want to only flip one icon instead of all of the icons in the navigation bar. Right now when I click on one button, all of the icons flip instead of just the one I clicked.
Here is a demo: https://codesandbox.io/s/festive-frost-53nny
Any help is greatly appreciated.
You are using one open state for all items. Create an array of open items like your anchor Array.
import "./styles.css";
import React from "react";
import { makeStyles } from "#material-ui/core/styles";
import clsx from "clsx";
import Button from "#material-ui/core/Button";
import { KeyboardArrowUp } from "#material-ui/icons";
import AppBar from "#material-ui/core/AppBar";
import Toolbar from "#material-ui/core/Toolbar";
import { menuItems } from "./menuItems";
const useStyles = makeStyles((theme) => ({
openX: {
transform: "scaleX(1)"
},
closeX: {
transform: "scaleX(-1)"
},
openY: {
transform: "scaleY(1)"
},
closeY: {
transform: "scaleY(-1)"
}
}));
export default function App() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (index, event) => {
setAnchorEl({ [index]: event.currentTarget });
setOpen((prevState) => {
const newState = [...prevState];
newState[index] = !prevState[index];
return newState;
});
};
const [open, setOpen] = React.useState(
new Array(Object.keys(menuItems).length).fill(false)
);
return (
<AppBar>
<Toolbar>
<div>
{Object.keys(menuItems).map((item, index) => (
<div key={index}>
<React.Fragment>
<Button onClick={(e) => handleClick(index, e)}>
{item}
<KeyboardArrowUp
className={clsx(
!open[index] && classes.closeY,
open[index] && classes.openY
)}
/>
</Button>
</React.Fragment>
</div>
))}
</div>
</Toolbar>
</AppBar>
);
}

Show MaterialUi snackbar by method

I want to show message in material.ui by only call method not ading component to parent component (like toastify.js). So, I wrote example like below. But I couldn't call showSnack() method. How can I achieve this?
Note: I don't want add component to demo js like < SnackbarHelper />. I only want show snackbar calling by function.
CODESANDBOX LINK
Demo.js
import React from "react";
import Button from "#material-ui/core/Button";
import SnackHelper from "./snackHelper";
export default function PositionedSnackbar() {
function showMessage() {
console.log("I want call snackHelper.showSnack");
// snackHelper.showSnack();
}
return (
<div>
<Button variant="contained" onClick={() => showMessage()}>
SHOW MESSAGE
</Button>
</div>
);
}
snackbarHelper.js
import React from "react";
import Snackbar from "#material-ui/core/Snackbar";
export default function SnackHelper() {
const [state, setState] = React.useState({
open: false
});
const { vertical, horizontal, open } = state;
const showSnack = (newState) => () => {
setState({ open: true, ...newState });
};
const handleClose = () => {
setState({ ...state, open: false });
};
return (
<div>
<Snackbar
anchorOrigin={{ vertical, horizontal }}
open={open}
onClose={handleClose}
message=""
key={vertical + horizontal}
/>
</div>
);
}
I found solution in this article for same thing what I was looking. Only difference is, this is for confirmation dialog and written by typescript. But, it can be easily changed to toast message by javascript. https://dev.to/dmtrkovalenko/the-neatest-way-to-handle-alert-dialogs-in-react-1aoe
You can get working example code https://codesandbox.io/s/neat-dialogs-3h5ou?from-embed=&file=/src/ConfirmationService.tsx

Material UI Dialog turned to Hook flickering

I'm trying to turn Material UI's dialog into a "useDialog" hook so it keeps track of it's own open state.
Unfortunately I've encountered a problem that whenever I update a state further up the hierarchy, the dialog flickers and I'm not exactly sure why and how to circumvent it. I feel like a useRef is needed there somewhere, but I'm not sure. Here's a reproduced minimal example: https://codesandbox.io/s/flickering-dialog-minimal-example-ehruf?file=/src/App.js
And the code in question:
import React, { useState } from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle
} from "#material-ui/core";
export default function App() {
const [openDialog, Dialog] = useDialog();
const [counter, setCounter] = useState(0);
return (
<div>
<Dialog title="Hello">
<div>{counter}</div>
<button onClick={() => setCounter(counter => counter + 1)}>
Increase
</button>
</Dialog>
<button onClick={openDialog}>Open dialog</button>
</div>
);
}
const useDialog = () => {
const [open, setOpen] = useState(false);
const handleClose = () => {
setOpen(false);
};
const someDialog = ({ title, children }) => {
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
);
};
return [
() => {
setOpen(true);
},
someDialog
];
};
The reason the dialog flickers is that a new Dialog component is created on every render(as a result of state change) in App. The old Dialog is unmounted and replaced by the new Dialog.
A rule of thumb is you should never define components while rendering.
That's why I suggest you separate your custom dialog component from useDialog hook:
const MyDialog = ({ open, handleClose, title, children }) => {
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
);
};
You can, however, keep some of the logic inside useDialog and reuse them:
const useDialog = () => {
const [open, setOpen] = useState(false);
const openDialog = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const props = {
open,
handleClose
};
return [openDialog, props];
};
More about why returning components from hook can be a bad idea.
Custom hooks are not made for returning a component, instead they are used to create a common logic which will be shared by different components.
In your case I would suggest you to create a common component for your dialog. And use this component wherever you want. Like this:
<CustomDialog open={open}>
// Your jsx here
</CustomDialog>
const CustomDialog = ({children}) => {
return <Dialog open={open} onClose={handleClose}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
}
For more information about custom hooks:
https://reactjs.org/docs/hooks-custom.html
To whomever finds this issue because of my mistake. I was using a styled Dialog and defined the styled Dialog inside the functional component. Just put the custom styling outside of any component.
// This needed to be outstide of the BetterDialog component
const CustomDialog = styled(Dialog)({
"& .MuiDialog-paper": {
zIndex: 90
}
})
...
function BetterDialog(props: BetterDialogProps) {
...

Resources