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 :)
Related
I have a problem with a dialog not closing properly.
Dialog open is set to a state variable intialized as true, and immediately changed to false.
The dialog doesn't close properly, and the app does not respond.
I have demonstrated this in the following sandbox where the button can not be clicked, though it should be available.
EDIT:
To make clear. The dialog is not supposed to ever open. But the button outside the dialog (with the text "a button") should be clickable.
https://codesandbox.io/s/adoring-benji-300mcm
This code will work:
export default function App() {
const [open, setOpen] = useState(false);
console.log(open);
return (
<div>
<Dialog open={open} height="100px" width="100px" id="111">
<button onClick={() => setOpen(false)}>close</button>
</Dialog>
<button onClick={() => console.log("click")}>a button</button>
</div>
);
}
You don't need the useEffect to change state immediately, you could just pass false as a desired initial value to the state. Now the button "a button" is working.
EDIT:
To make the code working without removing useEffect, add conditional rendering:
export default function App() {
const [open, setOpen] = useState(true);
useEffect(() => setOpen(false), []);
console.log(open);
return (
<div>
{open && (
<Dialog open={open} height="100px" width="100px" id="111">
<button onClick={() => setOpen(false)}>close</button>
</Dialog>
)}
<button onClick={() => console.log("click")}>a button</button>
</div>
);
}
My best guess is that in your code the Dialogue gets rendered anyways, even though it is not displayed, because at first "open" was true. So in order for it not to be rendered, or to be rendered conditionally, you should check state.
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.
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>
</>
);
};
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 have a component which has a button within it, like so -
<Button variant="primary" disabled={checkAccepted} onClick={openModal}>Send</Button>
I would like this button to, when it is active, to open up a modal when clicked. I am unsure how to do this and have been messing around with props but can't seem to figure it out. I also want the modal to be reusable so that any content can be passed in the modal body.I am thinking how do I open up the modal from within my openModal function?
I tried returning it like so -
const openModal = () => {
return (
<Modal>
<ModalBody>*Pass in swappable content here*</ModalBody>
</Modal>
)
}
But that doesn't seem to work. I am sure I am missing something.
You can't return components from an event handler. The way to handle events in react is almost always to alter the state of your application which triggers a re-render. In your case you need to keep track of the open state of your modal.
This can be done either in a controlled way (you keep track of the open state yourself and pass it to your <Modal> component as a prop) or in an uncontrolled way (the <Modal> component manages the open state itself). The second approach requires that you provide e.g. an element to render to your Modal component that acts as a trigger:
const MyModal = ({ children, trigger }) => {
const [modal, setModal] = useState(false);
const toggle = () => setModal(!modal);
return (
<div>
{React.cloneElement(trigger, { onClick: toggle })}
<Modal isOpen={modal} toggle={toggle}>
<ModalBody>{children}</ModalBody>
</Modal>
</div>
);
};
Then you can use it like that:
<MyModal trigger={<Button variant="primary">Send</Button>}>
<p>This is the content.</p>
</MyModal>
Or you can implement it in a controlled way. This is more flexible as it allows you to render the triggering element anywhere:
const MyModal = ({ children, isOpen, toggle }) => (
<div>
<Modal isOpen={isOpen} toggle={toggle}>
<ModalBody>{children}</ModalBody>
</Modal>
</div>
);
Usage Example:
function App() {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
return (
<div className="App">
<Button variant="primary" onClick={toggle}>
Send
</Button>
<MyModal isOpen={isOpen} toggle={toggle}>
<p>This is the content.</p>
</MyModal>
</div>
);
}
You should pass the function which triggers the modal to your <Button /> component as prop. Then, in your component, you want to add the onClick event. You can't set an onClick event to the <Button />. It will think of onClick as a prop being passed to <Button />. Within <Button /> you can set the onClick event to an actual <button> element, and use the function which was passed in as a prop on that event.
You can use state to keep track of when the modal button is clicked. Your function can look like: (I am using class based components here, but you can do the same thing with functional components)
buttonClickedHandler = () => {
this.setState({isModalButtonClicked: !this.state.isModalButtonClicked});
}
Then, you can set the Modal component,
<Modal isShow={this.state.isModalButtonClicked} modalButton={this.buttonClickedHandler}>
<div> ...set contents of modal</div>
</Modal>
<button onClick={this.buttonClickedHandler}>Show Modal</button>
So, within the Modal component, you can have something like this:
<React.Fragment>
<Backdrop showModal={this.props.isShow} clicked={this.props.modalButton}/>
{this.props.children}
</React.Fragment>
Backdrop is basically the greyed out background. You can also set an onClick event to listen to when the backdrop is clicked.