Creating a modal
When creating the modal for TaskFlow, we want to be aware of how it should be done. If we would insert the modal in any random component, it could end up being misplaced or cut-off of by an overflow: hidden;
property on a parent element. There's also some accessibility rules we want to cover.
This is where the dialog
element comes in handy. Specifically designed for modal dialog boxes, it offers a seamless interruption of user interaction with the rest of the page while maintaining its position atop the DOM hierarchy. Leveraging this element, we can use our modal component in any part of our application. The dialog
element also offers additional conveniences such as built-in methods like showModal()
and close()
, eliminating the need for custom state management solutions like useState
hooks.
To dynamically control the opening and closing of our modal, we need the useRef
hook to directly reference it in the real DOM. For instance, suppose we embed the modal component in our homepage. In that case, we can define a function to toggle its visibility:
import styles from './Modal.module.css';
const modalRef = useRef<HTMLDialogElement>(null);
const toggleModal = () => {
if (!modalRef.current) {
return;
}
if (modalRef.current.hasAttribute("open")) {
modalRef.current.close();
return;
}
modalRef.current.showModal();
};
return (
<main>
<button onClick={toggleModal}>Open modal</button>
<dialog className={styles.modal} ref={modalRef} toggleModal={toggleModal}>
<p>Hi viewers 👋</p>
</dialog>
</main>
);
In our styled modal class, we can apply some styling to our dialog wrapper as well as a backdrop which is selectable via the pseudo selector ::backdrop
. The styling we apply ensures that the dialog element renders in the middle of the screen.
.dialog {
height: max-content;
margin: auto;
width: max-content;
}
.dialog::backdrop {
background-color: "#4B5464CC"; /* Semi-transparent black color */
}
Bubbling​
We've added the onClick
callback to the dialog
to close the modal when clicking on the backdrop. However, the modal will also close when clicking within the modal itself. This is not at all what we want!
This is caused by a process called 'bubbling': the event
is propagated upwards in the DOM and captured by the onClick
callback. You can think of propagation as electricity running through a wire, until it reaches its destination. The event passes through every element on the DOM until it reaches the end, or is forcibly stopped. To stop this from happening, we can simply add a event.stopPropagation()
as a callback to our contents click handler.
return (
<main>
<button onClick={toggleModal}>Open modal</button>
<dialog className={styles.modal} ref={modalRef} toggleModal={toggleModal}>
<div onClick={(event) => event.stopPropagation()}>
<p>Hi viewers 👋</p>
</div>
</dialog>
</main>
);
A perhaps more "clean" way to achieve the same goal, is to use the target
and nodeName
properties. The target
property of the event interface refers to the element that triggered the event, in our case, it's an onClick
event. By examining the nodeName
property of our target, we can selectively invoke our toggle function.
const handleBackdropClick = (event: React.MouseEvent<HTMLDialogElement>) => {
const target = event.target as HTMLDialogElement;
if (target.nodeName === "DIALOG") {
toggleModal();
}
};
return (
<main>
<button onClick={toggleModal}>Open modal</button>
<dialog className={styles.modal} ref={modalRef} toggleModal={handleBackdropClick}>
<div>
<p>Hi viewers 👋</p>
</div>
</dialog>
</main>
);
};
In this implementation, when a click event occurs within the dialog, we examine the target element. If the target's nodeName is "DIALOG", indicating that the click happened on the dialog itself rather than its children, we trigger the toggleModal function. This approach offers a more concise and readable solution to handle clicks within the modal.