admin管理员组文章数量:1146251
I use an HTML <dialog> element. I want to be able to close the dialog when clicking outside of it. Using "blur" or "focusout" event does not work.
I want the same thing as Material Design dialog, where it closes the dialog when you click outside of it:
.html
How can I achieve that?
Thanks in advance.
I use an HTML <dialog> element. I want to be able to close the dialog when clicking outside of it. Using "blur" or "focusout" event does not work.
I want the same thing as Material Design dialog, where it closes the dialog when you click outside of it:
https://material-components-web.appspot.com/dialog.html
How can I achieve that?
Thanks in advance.
Share Improve this question edited Jul 28, 2022 at 8:28 djvg 14.2k7 gold badges83 silver badges113 bronze badges asked Apr 26, 2018 at 7:54 UltimatePlayTheGameUltimatePlayTheGame 3611 gold badge3 silver badges4 bronze badges 1- 1 Does this answer your question? How to close the new html <dialog> tag by clicking on its ::backdrop – Adrian Commented Mar 14, 2024 at 18:09
8 Answers
Reset to default 28When a dialog is opened in modal mode, a click anywhere on the viewport will be recorded as a click on that dialog.
The showModal() method of the HTMLDialogElement interface displays the dialog as a modal, over the top of any other dialogs that might be present. It displays into the top layer, along with a ::backdrop pseudo-element. Interaction outside the dialog is blocked and the content outside it is rendered inert. Source: HTMLDialogElement.showModal()
One way to solve the question is to:
- Nest a
div
inside your dialog and, using CSS, make sure it covers the same area as the dialog (note that browsers apply default styles to dialogs such as padding) - Add an event listener to close the dialog when a user clicks on the dialog element (anywhere on the viewport)
- Add an event listener to prevent propagation of clicks on the
div
nested inside the dialog (so that the dialog does not get closed if a user clicks on it)
You can test this with the code snippet below.
const myButton = document.getElementById('myButton');
myButton.addEventListener('click', () => myDialog.showModal());
const myDialog = document.getElementById('myDialog');
myDialog.addEventListener('click', () => myDialog.close());
const myDiv = document.getElementById('myDiv');
myDiv.addEventListener('click', (event) => event.stopPropagation());
#myDialog {
width: 200px;
height: 100px;
padding: 0;
}
#myDiv {
width: 100%;
height: 100%;
padding: 1rem;
}
<button id="myButton">Open dialog</button>
<dialog id="myDialog">
<div id="myDiv">
Click me and I'll stay...
</div>
</dialog>
This is how I did it:
function dialogClickHandler(e) {
if (e.target.tagName !== 'DIALOG') //This prevents issues with forms
return;
const rect = e.target.getBoundingClientRect();
const clickedInDialog = (
rect.top <= e.clientY &&
e.clientY <= rect.top + rect.height &&
rect.left <= e.clientX &&
e.clientX <= rect.left + rect.width
);
if (clickedInDialog === false)
e.target.close();
}
Modal
To close a modal dialog (i.e. a dialog opened with showModal
) by clicking on the backdrop, you could do as follows:
const button = document.getElementById('my-button');
const dialog = document.getElementById('my-dialog');
button.addEventListener('click', () => {dialog.showModal();});
// here's the closing part:
dialog.addEventListener('click', (event) => {
if (event.target.id !== 'my-div') {
dialog.close();
}
});
#my-dialog {padding: 0;}
#my-div {padding: 16px;}
<button id="my-button">open dialog</button>
<dialog id="my-dialog">
<div id="my-div">click outside to close</div>
</dialog>
This places the dialog content in a <div>
, which is then used to detect whether the click was outside the dialog, as suggested here. The padding and margins in the example are adjusted to make sure the <dialog>
border and <div>
border coincide.
Note that the modal dialog's "background" can be selected in CSS using ::backdrop
.
Non-modal
For a non-modal dialog (opened using show), you could add the event listener to the window
element instead of the dialog
, e.g.:
window.addEventListener('click', (event) => {
if (!['my-button', 'my-div'].includes(event.target.id)) {
dialog.close();
}
});
In this case we also need to filter out button clicks, otherwise the dialog is immediately closed after clicking the "open dialog" button.
This is 5 year-old question but I thought I'd provide a straightforward solution for 2024. By that I mean not adding any new elements to the screen or measuring anything to achieve it. Here's a codepen if you want to jump straight to the code:
https://codepen.io/dengel29/pen/vYPVMXE
The idea here is you can just target the id of your dialog to close it, so if your HTML looks like this:
<button id='opener'>Open Dialog</button>
<button id='closer'>Close Dialog</button>
<dialog id='modal'> // <-- here's the id to target
<div class="dialog-inner">
<form>
<button id="cancel" formmethod="dialog" >x</button>
<h1>A title for your modal?</h1>
<div class="content">
<p>Anything else you'd like to add here</p>
</div>
</form>
</div>
</dialog>
Your js logic for opening and closing programmatically can simply be:
// save a reference to the modal, selected by the id used in markup
const modal = document.querySelector('#modal')
// provide a function to 'click outside to close' event listener
function clickOutsideToClose(e) {
if (e.target.id === 'modal') closeModalHandler()
}
// only add the event when the modal is open. you can add this function to fire on any click event listener elsewhere on your page
function openModal() {
modal.showModal();
modal.addEventListener('click', clickOutsideToClose)
}
// this programmatically closes the dialog, and cleans up the event listener
function closeModalHandler() {
modal.removeEventListener('click', clickOutsideToClose)
modal.close();
}
It works, but not quite perfect – you'll realize if you simply implement the HTML and JS that sometimes your clicks inside the modal / pop-up will still close it. This is because some of the user-agent padding
bleeds into the inside of dialog. In other words, the border of the actual modal is not the actual edge of the HTML element, so we simply reset the padding with this CSS:
dialog {
padding: 0 0 0 0;
}
I'm sure there are plenty of accessibility improvements that could be made, but I believe this gets the job done with the fewest
The below code will automatically apply the desired functionality to all dialog
elements on the page.
HTMLDialogElement.prototype.triggerShow = HTMLDialogElement.prototype.showModal;
HTMLDialogElement.prototype.showModal = function() {
this.triggerShow();
this.onclick = event => {
let rect = this.getBoundingClientRect();
if(event.clientY < rect.top || event.clientY > rect.bottom) return this.close();
if(event.clientX < rect.left || event.clientX > rect.right) return this.close();
}
}
A summary of the provided answers
- Add a click event listener in the dialog to close it, but wrap its contents in a div with a click event listener that calls to
event.stopPropagation())
. Problem: You have to alter the html structure and potentially the CSS rules you might have. Link - Get the coordinates of the dialog with
e.target.getBoundingClientRect()
and compare them with the coordinates of the click event. Problem: You have to also check that you, in fact, clicked in a dialog element withe.target.tagName
to prevent issues with forms. Link - Wrap the contents of the dialog in a div with an id and make sure that
event.target.id !== 'my-div-id'
before closing. Problem: It won't work if you have anything nested in that div unless everything within also has the same id. Link - Add an id to the dialog element itself and make sure that
event.target.id === 'my-dialog-id'
. This way you don't have to wrap its content in a redundant div. Problem: You have to add an id to the dialog just to make this work. Link
My answer
The solution I liked the most is the last one I mentioned above, but you don't really have to add an id attribute because you might as well check that the element instance is exactly the same, like so:
myDialog.addEventListener('click', event => {
if(event.target === myDialog) {
myDialog.close();
}
});
Advantages:
- No need to add more html elements or attributes anywhere
- The only one element that adds a listener is the dialog itself
- Its also the simplest solution
Here is a full example with two dialog elements, one purely information, and the other including an dialog form.
const initializeDialog = function(dialogElement) {
// enhance opened standard HTML dialog element by closing it when clicking outside of it
dialogElement.addEventListener('click', function(event) {
const eventTarget = event.target;
if (dialogElement === eventTarget) {
console.log("click on dialog element's content, padding, border, or margin");
const dialogElementRect = dialogElement.getBoundingClientRect();
console.log("dialogElementRect.width", dialogElementRect.width);
console.log("dialogElementRect.height", dialogElementRect.height);
console.log("dialogElementRect.top", dialogElementRect.top);
console.log("dialogElementRect.left", dialogElementRect.left);
console.log("event.offsetX", event.offsetX);
console.log("event.clientX", event.clientX);
console.log("event.offsetY", event.offsetY);
console.log("event.clientY", event.clientY);
if (
(dialogElementRect.top > event.clientY) ||
(event.clientY > (dialogElementRect.top + dialogElementRect.height)) ||
(dialogElementRect.left > event.clientX) ||
(event.clientX > (dialogElementRect.left + dialogElementRect.width))
) {
console.log("click on dialog element's margin. closing dialog element");
dialogElement.close();
}
else {
console.log("click on dialog element's content, padding, or border");
}
}
else {
console.log("click on an element WITHIN dialog element");
}
});
const maybeDialogFormElement = dialogElement.querySelector('form[method="dialog"]');
if (! maybeDialogFormElement) {
// this dialog element does NOT contain a "<form method="dialog">".
// Hence, any contained buttons intended for closing the dialog will
// NOT be automatically set up for closing the dialog
// (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#usage_notes ).
// Therefore, programmatically set up close buttons
const closeButtons = dialogElement.querySelectorAll('button[data-action-close], button[data-action-cancel]');
closeButtons.forEach(closeButton => {
closeButton.addEventListener('click', () => dialogElement.close() );
});
}
return dialogElement;
};
const initializeFormDialog = function(formDialog, formCloseHandler) {
const submitButton = formDialog.querySelector('button[type="submit"]');
const inputElement = formDialog.querySelector('input');
formDialog.originalShowModal = formDialog.showModal;
formDialog.showModal = function() {
// populate input element with initial or latest submit value
inputElement.value = submitButton.value;
formDialog.dataset.initialInputElementValue = inputElement.value;
formDialog.originalShowModal();
}
// allow confirm-input-by-pressing-Enter-within-input-element
inputElement.addEventListener('keydown', event => {
if (event.key === 'Enter') {
//prevent default action, which in dialog-form case would effectively cancel, not confirm the dialog
event.preventDefault();
submitButton.click();
}
});
submitButton.addEventListener('click', () => {
submitButton.value = inputElement.value;
// add dialog-was-confirmed marker
formDialog.dataset.confirmed = "true";
});
formDialog.addEventListener('close', event => {
if (formCloseHandler) {
const returnValue = formDialog.returnValue;
const dialogWasConfirmed = (formDialog.dataset.confirmed === "true");
let inputElementValueHasChanged;
if (dialogWasConfirmed) {
inputElementValueHasChanged = (returnValue === formDialog.dataset.initialInputElementValue) ? false : true;
}
else {
inputElementValueHasChanged = false;
}
formCloseHandler(returnValue, dialogWasConfirmed, inputElementValueHasChanged);
}
// remove dialog-was-confirmed marker
delete formDialog.dataset.confirmed;
});
};
const myFormDialogCloseHandler = function(returnValue, dialogWasConfirmed, inputElementValueHasChanged) {
const resultDebugOutput = document.getElementById('output-result');
const resultDebugEntryString = `<pre>dialog confirmed? ${dialogWasConfirmed}
input value changed? ${inputElementValueHasChanged}
returnValue: "${returnValue}"</pre>`;
resultDebugOutput.insertAdjacentHTML('beforeend', resultDebugEntryString);
};
const informationalDialog = document.getElementById('dialog-informational');
initializeDialog(informationalDialog);
const showDialogInformationalButton = document.getElementById('button-show-dialog-informational');
showDialogInformationalButton.addEventListener('click', () => informationalDialog.showModal());
const formDialog = document.getElementById('dialog-form');
initializeDialog(formDialog);
initializeFormDialog(formDialog, myFormDialogCloseHandler);
const showDialogFormButton = document.getElementById('button-show-dialog-form');
showDialogFormButton.addEventListener('click', () => {
formDialog.showModal();
});
dialog {
/* for demonstrational purposes, provide different styles for content, padding, and border */
background-color: LightSkyBlue;
border: 2rem solid black;
/* give padding a color different from content; see https://stackoverflow.com/a/35252091/923560 */
padding: 1rem;
box-shadow: inset 0 0 0 1rem LightGreen;
}
dialog header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
}
dialog header button[data-action-close]::before,
dialog header button[data-action-cancel]::before {
content: "✕";
}
dialog footer {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
<button id="button-show-dialog-informational" type="button">Show informational dialog</button>
<button id="button-show-dialog-form" type="button">Show dialog with form</button>
<dialog id="dialog-informational">
<header>
<strong>Informational dialog header</strong>
<button aria-labelledby="dialog-close" data-action-close="true"></button>
</header>
<div>
<p>This is the dialog content.</p>
</div>
<footer>
<button id="dialog-close" data-action-close="true">Close dialog</button>
</footer>
</dialog>
<dialog id="dialog-form">
<form method="dialog">
<header>
<strong>Dialog with form</strong>
<button aria-labelledby="dialog-form-cancel" data-action-cancel="true" value="cancel-header"></button>
</header>
<div>
<p>This is the dialog content.</p>
<label for="free-text-input">Text input</label>
<input type="text" id="free-text-input" name="free-text-input" />
</div>
<footer>
<button id="dialog-form-cancel" value="cancel-footer">Cancel</button>
<button type="submit" id="dialog-form-confirm" value="initial value">Confirm</button>
</footer>
</form>
</dialog>
<div id="output-result"></div>
You can add the attribute popover
to the dialog element. This enables you to use the popover API, which has native support for detecting clicks outside. You can also use the popover API with any HTML element too!
Try out the popover here!
本文标签: How to close a native HTML dialog when clicking outside with JavaScriptStack Overflow
版权声明:本文标题:How to close a native HTML dialog when clicking outside with JavaScript? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1737184735a1966250.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论