React Display Separate Popover Component on Menu Click - reactjs

I have the below menu:
So, ideally you'd click on those 3 dots which would open up another box next to it, I decided to use a popover(https://material-ui.com/components/popover/).. The problem is that when you click the 3 dots nothing happens. I assume this is because onClick function returns a functional popover component but that doesn't get displayed. I put in debuggers and alerts inside the functional component no hit at all.
This is those 3 dots
<IconButton
aria-describedby="simple-popover"
variant="contained"
onClick={e => this.moreClick(e, props.children)}
>
<More />
</IconButton>
This is moreClick function
moreClick = (e, officeAccount) => {
return (
<AccountFavoritesPopover element={e} officeAccount={officeAccount} />
);
};
This is the whole popover
import React from "react";
import Popover from "#material-ui/core/Popover";
export default function AccountFavoritesPopover(element, officeAccount) {
const anchorEl = element;
const open = Boolean(anchorEl);
const id = open ? "simple-popover" : undefined;
return (
<div>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
//onClose={alert}
anchorOrigin={{
vertical: "top",
horizontal: "right"
}}
transformOrigin={{
vertical: "top",
horizontal: "left"
}}
>
<div>{officeAccount}</div>
</Popover>
</div>
);
}
Does popover have to be inside the main file? Currently the 3 dots is in the main file and AccountFavoritesPopover is a whole separate file.
I attempted to put "AccountFavoritesPopover" code inside the main file but I cannot utilize useState in the main file because it's a class. Also, I cannot convert it to actual state and use setState because once setState kicks in, the menu will disappear...
Edit:
Below is the Menu
<Creatable
id="Select"
menuIsOpen={this.state.Focused}
components={{
Option: this.Option
}}
/>
Below is the Options inside menu
<div style={{ position: "relative" }}>
<components.Option {...props} />
<div id="MoreBox">
<IconButton
aria-describedby="simple-popover"
variant="contained"
onClick={e => this.moreClick(e, props.children)}
>
<More />
</IconButton>
</div>
</div>

Try this, this should work(Not tested)
Main.js
export default class Main extends Component {
constructor(props) {
this.state = {
selectedIndex: 0,
selectedId: 0,
anchorEl: null
};
}
moreClick = (anchorEl, selectedId, selectedIndex) => {
this.setState({
selectedId,
selectedIndex,
anchorEl,
open: true,
});
}
handleClose = () => {
this.setState({
open: false
});
}
render() {
const menu = [
{
id: 1,
text: '002',
more: 'more 003'
},
{
id: 2,
text: '003',
more: 'more 003'
},
{
id: 3,
text: '004',
more: 'more 003'
}
]
const menuDom = menu.map((m, index) => {
return (
<IconButton
key={m.id}
aria-describedby="simple-popover"
variant="contained"
onClick={e => this.moreClick(e.currentTarget, index, m.id)}>
{m.text}
</IconButton>
)
})
const more = (<More>{menu[this.state.selectedIndex].text}</More>)
return (
<div>
{menuDom}
<AccountFavoritesPopover open={this.state.open} anchorEl={this.state.anchorEl} onClose={this.handleClose}>
{more}
</AccountFavoritesPopover>
</div>
)
}
}
AccountFavoritesPopover.js
export default function AccountFavoritesPopover(open, anchorEl, onClose) {
const id = open ? "simple-popover" : undefined;
return (
<div>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={{
vertical: "top",
horizontal: "right"
}}
transformOrigin={{
vertical: "top",
horizontal: "left"
}}
>
<div>{this.props.children}</div>
</Popover>
</div>
);
}

Related

Open a Single Dropdown Menu (Instead of all of them)

I followed the documentation to create a dropdown menu using Material UI. However, none of the docs have a good example for handling multiple dropdowns in the same menu.
I got it mostly working - however, when I open a dropdown, they ALL open. I'm assuming this is because open = Boolean(anchorEl) opens the menu whenever an anchorEl is set. So how can I adjust this so it only opens the specific menu that is clicked?
const NavBarMainMenu = () => {
const [anchorEl, setAnchorEl] = useState(null)
const open = Boolean(anchorEl)
const handleClick = event => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
return (
<>
<Box sx={{ flexGrow: 1, display: { xs: "none", lg: "flex" } }}>
{pages.map(page => {
return (
<>
<Button
key={page.title}
id={page.title + "-button"}
onClick={handleClick}
aria-controls={open ? page.title : undefined}
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
>
{page.title}
</Button>
<Menu
anchorEl={anchorEl}
id={page.title}
open={open}
onClose={handleClose}
onClick={handleClose}
transformOrigin={{ horizontal: "right", vertical: "top" }}
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
>
{page.children.map(child => {
return <MenuItem key={child.title}>{child.title}</MenuItem>
})}
</Menu>
</>
)
})}
</Box>
</>
)
}
You can try something like this:
const NavBarMainMenu = () => {
const [anchorEls, setAnchorEls] = useState({}) // <-- Here use a object
const isOpen = (id) => Boolean(anchorEls[id]) // <-- Here you need a function to get if open is true
const handleClick = (event, id) => {
setAnchorEsl({ ...anchorEls, [id]: event.currentTarget }) // <-- Here you set anchor value using an id
}
const handleClose = (id) => {
setAnchorEls({ ...anchorEls, [id]: null }) // <-- Here you delete anchor value by ID
}
return (
<>
<Box sx={{ flexGrow: 1, display: { xs: "none", lg: "flex" } }}>
{pages.map(page => {
return (
<>
<Button
key={page.title}
id={page.title + "-button"}
onClick={(event) => handleClick(event, page.title)}
aria-controls={isOpen(page.title) ? page.title : undefined}
aria-haspopup="true"
aria-expanded={isOpen(page.title) ? "true" : undefined}
>
{page.title}
</Button>
<Menu
anchorEl={anchorEls[page.title]}
id={page.title}
open={isOpen(page.title)}
onClose={() => handleClose(page.title)}
onClick={() => handleClose(page.title)}
transformOrigin={{ horizontal: "right", vertical: "top" }}
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
>
{page.children.map(child => {
return <MenuItem key={child.title}>{child.title}</MenuItem>
})}
</Menu>
</>
)
})}
</Box>
</>
)
}

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

MUI Popover not anchoring properly (AnchorEl, React, material-table, MUI)

I've looked through many questions regarding popover anchoring, but I haven't seen any when using a MaterialTable element from the material-table library: https://github.com/mbrn/material-table .
While debugging it looks like the anchorEl properly holds the button reference, but it seems to rerender a second time and loses the reference. From what I can tell this is from the button being remounted. So the final rendering puts the popover in the top left corner of the screen by default. I'm wondering if anyone has found a way to either prevent this remount or some other workaround.
export class UsersList extends Component {
constructor(props) {
super(props);
this.state = {
anchorEl: null,
anchorReference: "anchorEl"
};
}
render() {
const { classes } = this.props;
var { anchorEl } = this.state;
const open = Boolean(anchorEl);
return (
<MaterialTable
isLoading={this.state.isLoading}
columns={[
{ title: "Username", field: "username" },
...more columns
]}
data={this.state.users}
onRowClick={(evt, selectedRow) => this.setState({ selectedRow })}
//where I update my anchorEl on a click on the "edit" icon
actions={[
{
icon: "edit",
tooltip: "Edit",
onClick: (event, rowData) => {
this.setState({ anchorEl: event.currentTarget });
}
},
},
]}
components={{
Body: props => (
<React.Fragment>
<MTableBody {...props} />
<Popover
//a breakpoint here is hit twice. First time with valid ref, second time without ref
getContentAnchorEl={null}
id="myId"
open={open}
onClose={this.handlePopoverClose.bind(this)}
anchorEl={anchorEl}
getContentAnchorEl={null}
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
transformOrigin={{ vertical: "top", horizontal: "center" }}
open={open}
>
<Typography>The content of the Popover.</Typography>
</Popover>
EDIT** codesandbox running example: If you click on the edit column row item a popup shows up in the top left hand of the screen instead of next to the rowitem: https://codesandbox.io/s/loving-tdd-8r910?file=/src/App.js
Move popover to a separate component so that anchorEl would be in the same component as Popover. Example with menu:
function MenuCell() {
const [anchorEl, setAnchorEl] = useState(null);
return (
<div>
<IconButton
aria-label="more"
aria-haspopup="true"
onClick={(event) => setAnchorEl(event.currentTarget)}
>
<MoreVertIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
<MenuItem selected={false} onClick={() => setAnchorEl(null)}>
Item 1
</MenuItem>
<MenuItem selected={false} onClick={() => setAnchorEl(null)}>
Item 2
</MenuItem>
<MenuItem selected={false} onClick={() => setAnchorEl(null)}>
Item 3
</MenuItem>
</Menu>
</div>
)
}
And use in Column cell definition:
function CustomTable() {
const menuColumn = {
id: 'menus',
Header: '',
Cell: ({ row }) => <MenuCell row={row} />,
};
// ...
}

Material UI popover fails to open on anchor

I am trying to have a popover appear over an icon when hovering. I tried to adapt the code from here https://material-ui.com/components/popover/#mouse-over-interaction. My solution is below. I tried to remove some of the complexities of the project setup but had to include some extra stuff for clarity.
export interface Props {
popoverAnchor?: HTMLElement;
setPopoverAnchor: (anchor?: HTMLElement) => void;
}
render()
const {
popoverAnchor,
setPopoverAnchor
} = this.props;
const open = Boolean(popoverAnchor);
return(
...
<div className={classes.flex}>
<HelpOutlineIcon
aria-owns={open ? 'mouse-over-popover' : undefined}
aria-haspopup="true"
onMouseEnter={(event: any) => {
setPopoverAnchor(event.currentTarget)
}}
onMouseLeave={() => setPopoverAnchor()}
/>
<Popover
id="mouse-over-popover"
className={(classes.popover)}
classes={{
paper: classes.paper,
}}
open={open}
anchorEl={popoverAnchor}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={() => setPopoverAnchor()}
disableRestoreFocus
>
<Typography>I use Popover.</Typography>
</Popover>
</div>
constants.ts
export const SET_POPUP_ANCHOR = 'SET_POPUP_ANCHOR';
export type SET_POPUP_ANCHOR= typeof SET_POPUP_ANCHOR;
store.ts
export interface State {
anchor?: HTMLElement;
}
actions.ts
export interface SetPopupAnchor {
type: constants.SET_POPUP_ANCHOR;
anchor?: HTMLElement;
}
export type Action = SetPopupAnchor;
export function handlePopoverAnchor(anchor?: HTMLElement): SetPopupAnchor {
return {
type: constants.SET_POPUP_ANCHOR,
anchor: anchor
}
}
DemoContainer.ts
export function mapDispatchToProps(dispatch: Dispatch<actions.AnonymizationAction>) {
return {
setPopoverAnchor: (anchor?: HTMLElement) => dispatch(actions.handlePopoverAnchor(anchor))
}
}
This fails to create any popup at all and no error appears on the console. after debugging i saw that the element object successfully makes it all the way to the the handlePopoverAnchor action meaning the state should have been changed.

Resources