i'm using two components from MUI, accordion and Modal. I have the Modal on the accordion summary, but when i open it, everytime i click inside the modal, the accordion also receives the interaction, thus it expands. I passed the open state of Modal to the Accordion, and tried changing things based on the open state. Like
pointerEvents:`${open == true? 'none': 'auto'}`
Tried something like this with disabled, but it remains the same. I have a sample of the code here
https://stackblitz.com/edit/react-ts-zer8p7?file=accordion.js
Thanks in advance
The issue here is that your click event is bubbling up the component tree until it is consumed by the accordion summary. You can stop this anywhere in the hierarchy by using the event stopPropagation function. You could change your BaseModal component to something like so:
export default function BasicModal() {
const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const onModalClick = (e) => {
e.stopPropagation();
};
return (
<div onClick={onModalClick}>
<Button onClick={handleOpen}>Open modal</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Typography id="modal-modal-title" variant="h6" component="h2">
Text in a modal
</Typography>
<Typography id="modal-modal-description" sx={{ mt: 2 }}>
<TemporaryDrawer />
</Typography>
</Box>
</Modal>
</div>
);
}
The onModalClick receives the click event and stops it from propagating up the component tree to the accordion summary.
Related
I am using Material-UI component, useState hook and NextJS framework.
I am mapping someData in my render, the structure of a someData element is :
someData[i] : {
"id": number,
"name": string,
"surname": string
}
My problem is to pass mapped data to Modal (specific to the mapped data).
{someData.map((data) => (
<Grid item key={data.id}>
<Card>
<CardContent>
<Typography>
{data.surname} {data.name}'s card.
</Typography>
</CardContent>
<Button
onClick={() => {
setModalData(data);
setOpen(true);
}}
>
Follow
</Button>
<Modal
open={open}
onClose={setOpen(false)}
>
<Box>
<Typography>
Follow {modalData.surname} ?
</Typography>
<Button onClick={() => handleFollowSubmit(modalData)}>
/*Function declared not important here*/
Yes
</Button>
</Box>
</Modal>
</Card>
</Grid>
))}
And the state used here are:
const [open, setOpen] = useState(false); // to handle the Modal opening
const [modalData, setModalData] = useState(null); // to pass Data to the modal
The idea is that you can't pass mapped data to a modal using the mapping, you have to use a State Hook to do so: When you open a modal, you pass the corresponding data through the State Hook.
But when I render the webpage I get this error :
TypeError: Cannot read properties of null (reading 'surname')
Pointing at modalData.surname
Any help would be appreciated!
Just add !!modalData to its open prop?
<Modal
open={!!modalData && open}
onClose={setOpen(false)}
/>
Update:
Initializing modalData like this:
const [modalData, setModalData] = useState({"id": number, "name": string, "surname": string})
solves the problem. I think initializing it with null created an error as the components are rendered before the data is fetched.
I have an Antd popover, that by clicking a button inside its content, opens a modal.
I want to close the popover when the modal opens.
When I tried just passing the popover visibility state setter down to the modal as a prop, there was a problem. There was some kind of "collision" between the state of the modal and the passed down prop state of the popover:
Collision CodeSandbox example
I did find a workaround - creating the modal state variables in the parent component (the popover) and passing them down to the modal using props:
Working CodeSandbox example
First of all, you can notice that the modal isn't closing at it supposed to - there's no nice smooth animation minimizing it, it just suddenly disappears. For reference, you can look here to see how it should look like when closing.
So my question is - why did this collision happen? Is there a better way to solve it?
Thanks!
This collision happens because in show modal handler you set visibility of popover to false and hide it and ant-popover-hidden class add to it's div element so anything inside it would not display like Modal however you show modal but because of its parent it couldn't visible, so I think You must separate modal from the popover content and place it somewhere beside them like this:
const Test = () => {
const [isSharePopoverVisible, setIsSharePopoverVisible] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const handlePopoverVisibleChange = () => {
setIsSharePopoverVisible(!isSharePopoverVisible);
};
const handleOk = () => {
setIsModalVisible(false);
};
const handleCancel = () => {
setIsModalVisible(false);
};
const showModal = () => {
setIsModalVisible(true);
setIsSharePopoverVisible(false);
};
return (
<>
<Popover
trigger="click"
title="Test"
visible={isSharePopoverVisible}
onVisibleChange={handlePopoverVisibleChange}
content={
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
}
>
<Button>Test</Button>
</Popover>
<Modal
title="Basic Modal"
visible={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
>
<p>Some contents...</p>
</Modal>
</>
);
};
Let's say I have a button that opens a Dialog component. The button has custom theming/styling to specify various states, one of them being the :focus state:
const useStyles = makeStyles({
root: {
"&:focus": {
backgroundColor: "#3A7DA9"
}
}
});
export default function App() {
const [open, setOpen] = useState(false);
const classes = useStyles();
return (
<div className="App">
<Button
id="button-that-opens-modal"
className={classes.root}
onClick={() => setOpen(true)}
>
Open the modal
</Button>
<Dialog open={open}>
<h3>This is the modal</h3>
<Button onClick={() => setOpen(false)}>
Close
</Button>
</Dialog>
</div>
);
}
What I've noticed is that every time I have this pattern, (where a button opens a dialog modal), when the modal is closed, the #button-that-opens-modal is left with a :focus state, which looks bad in terms of styling. Here's a quick gif:
Codesandbox demonstrating the issue
Is this a known issue? I don't see why the :focus should be automatically applied to the button when the modal closes. How can I stop this?
I tried this:
I can add a ref to the button, and make sure to manually unfocus the button in various places. Adding it in the onExited method of the Dialog works, but flashes the focus state for a second:
export default function App() {
const [open, setOpen] = useState(false);
const buttonRef = useRef();
const classes = useStyles();
return (
<div className="App">
<Button
ref={buttonRef}
className={classes.root}
onClick={() => setOpen(true)}
>
Open the modal
</Button>
<Dialog
open={open}
TransitionProps={{
onExited: () => {
buttonRef.current?.blur(); // helps but creates a flash
}
}}
>
<h3>This is the modal</h3>
<Button onClick={() => {setOpen(false)}}>
Close
</Button>
</Dialog>
</div>
);
}
sandbox showing this very imperfect solution
And even if I found exactly the right event handler to blur the button such the styling looks correct, this is not something I want to do for every Dialog in an app that has many Button - Dialog pairs. Is there a Material-UI prop I can use to disable this 'auto-focus' back on the button, rather than having to create a ref and manually .blur it for every Dialog?
This is for accessibilty purpose. You can disable it by adding prop disableRestoreFocus on your Dialog :)
I have 2 Dialogs, one on outer scope (parent) and one as a child.
export default function App() {
const [parentDialogOpen, setParentDialogOpen] = useState(false);
const [childDialogOpen, setChildDialogOpen] = useState(false);
const handleParentClick = () => {
setParentDialogOpen(true);
};
const handleChildClick = (e) => {
e.stopPropagation();
setChildDialogOpen(true);
};
return (
<div className="App">
<Dialog
open={parentDialogOpen}
onClose={() => setParentDialogOpen(false)}
>
<Box p={4}>Parent Dialog</Box>
</Dialog>
<Box bgcolor="red" p={2} onClick={handleParentClick}>
<Dialog
open={childDialogOpen}
onClose={(e) => setChildDialogOpen(false)}
>
<Box p={2}>Child Dialog</Box>
</Dialog>
<button onClick={handleChildClick}>Child</button>
</Box>
</div>
);
}
When I click on the button, it opens a dialog (Child Dialog), but when I close it, the Parent Dialog pops up. My expected behavior is that clicking on the child button should not fire onClick event on the parent (as I've added e.stopPropagation() to Child's onClick handler)
Adding e.stopPropagation() on handleChildClick does stop the event to propagate to Parent's onClick, but when I close the Dialog, Parent's onClick still fires.
I have also tried adding e.stopPropagation() to the Child Dialog's onClose, but it does not help.
Although moving the Child Dialog to the same level as Parent Dialog makes it work as expected, I cannot do that because in my (real) code I have to dynamically render the child component which the child component may have it's own Dialog.
Is there a way to solve this problem?
[UPDATE]: Seems like Parent Dialog's onClick event also fire when clicking ANYWHERE when Child Dialog is open. For example, clicking INSIDE child dialog will fire Parent Dialog's onClick (without closing Child Dialog)
Here is a Code Sandbox: https://codesandbox.io/embed/eloquent-hypatia-nuksu?fontsize=14&hidenavigation=1&theme=dark
Just stop the propagation in the onClick event of the Dialog (see below)
import "./styles.css";
import { Box, Dialog } from "#material-ui/core";
import { useState } from "react";
export default function App() {
const [parentDialogOpen, setParentDialogOpen] = useState(false);
const [childDialogOpen, setChildDialogOpen] = useState(false);
const handleParentClick = () => {
setParentDialogOpen(true);
};
const handleChildClick = (e) => {
e.stopPropagation();
setChildDialogOpen(true);
};
return (
<div className="App">
<Dialog
open={parentDialogOpen}
onClose={() => setParentDialogOpen(false)}
>
<Box p={4}>Parent Dialog</Box>
</Dialog>
<Box bgcolor="red" p={2} onClick={handleParentClick}>
<Dialog
open={childDialogOpen}
onClick={(e) => e.stopPropagation()}
onClose={(e) => setChildDialogOpen(false)}
>
<Box p={2}>Child Dialog</Box>
</Dialog>
<button onClick={handleChildClick}>Child</button>
</Box>
</div>
);
}
if your aim is to trigger parent dialog open when child is closed you could go with the following;
...
...
<Dialog
open={childDialogOpen}
onClose={(e) => {
setChildDialogOpen(false);
setParentDialogOpen(true);
}}
>
Then get rid of handleParentClick for good
I am developing a React functional component for a model CRUD operations, this component will render save and delete buttons on the model form, and I am trying to show a waiting indicator when the user clicks the save or delete button and hide the indicator when the process completes.
I am using the material-ui React components library, and for the waiting indicator I am using the Backdrop component.
the component props are the save and delete callbacks and set by the parent component.
I added a boolean state to show/hide this backdrop, but the waiting indicator is not showing as the setState in react is asynchronous. so how can I achieve this?
here is my component:
export default function ModelControls({onSave, onDelete}) {
const [wait, setWait] = useState(false);
const saveClick = () => {
setWait(true);
const retId = onSave();
setWait(false);
...
};
return (
<Container maxWidth={"xl"}>
<Grid container spacing={2}>
<Grid item xs={6}>
<Box display="flex" justifyContent="flex-end">
<Box component="span">
<Button size="small" color="primary" onClick={saveClick}>
<SaveIcon />
</Button>
</Box>
</Box>
</Grid>
</Grid>
<Backdrop open={wait}>
<CircularProgress color="primary" />
</Backdrop>
</Container>
);
}
Just make the function async and add await in front of the save function.
const saveClick = async () => {
setWait(true);
const retId = await onSave();
setWait(false);
};
Thanks #Dipansh, you inspired me to the following solution.
now the onSave callback from parent must return a promise object
const saveClick = () => {
setWait(true);
onSave().then((retId) => {
...
setWait(false);
});
};
this way it is working as needed.