Dialog
Dialog
This page shows a simple modal pattern using the (role="dialog") ARIA attribute. A modal is a dialog box/popup window that is displayed on top of the current page and requires a user action to close it. The dialog is only available to users when the modal is active. When the modal is active, the rest of the page is unavailable by mouse, keyboard, touch, or screen reader.
Turn on a screen reader to experience this example in action.
Options | Show Dialog |
---|---|
Placing focus on the first focusable element within the dialog | |
Placing focus on the dialog <h1> |
Feedback Form
HTML Source Code
<div>
<div>
<table class="data">
<tr>
<th>Options</th>
<th>Show Dialog</th>
</tr>
<tr>
<td>Placing focus on the first focusable element within the dialog</td>
<td><button type="button" id="show-dialog" class="primary" >Show Dialog (Option 1)</button></td>
</tr>
<tr>
<td>Placing focus on the dialog <h1> </td>
<td><button type="button" id="show-dialog-2" class="primary" >Show Dialog (Option 2)</button></td>
</tr>
</table>
<!--button type="button" id="show-dialog" class="primary" >Show Dialog (Option 1)</button>
<button type="button" id="show-dialog-2" class="primary" >Show Dialog (Option 2)</button-->
</div>
</div>
<div class="dialog-container" id="dialog-1-container">
<div role="dialog" class="dialog" aria-labelledby="dialog-1-title" id="dialog-1" aria-modal="true" >
<h1 class="dialog-title" id="dialog-1-title">Feedback Form</h1>
<hr/>
<div class="dialog-form">
<div class="row">
<label for="first-name" class="label" >First Name </label><input type="text" id="first-name" class="extend" />
</div>
<div class="row">
<label for="last-name" class="label" >Last Name </label><input type="text" id="last-name" class="extend" />
</div>
<div class="row">
<label for="email-address" class="label" >Email Address </label><input type="text" id="email-address" class="extend" />
</div>
<div class="row">
<label for="comments" class="label" >Comments </label><textarea id="comments" class="extend" rows="5" ></textarea>
</div>
</div>
<hr/>
<div class="dialog-action">
<button type="button" id="dialog-btn-submit" class="primary" >Submit</button>
<button type="button" id="dialog-btn-cancel" class="cancel-button" >Cancel</button>
</div>
<button class="close-button" aria-label="close dialog" id="dialog-btn-close"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<!--div class="dialog-container" id="dialog-2-container">
<div role="dialog" class="dialog" aria-labelledby="dialog-2-title" id="dialog-2" aria-modal="true" >
<button class="close-button" aria-label="close dialog" id="dialog2-btn-close"><i class="fa-solid fa-xmark"></i></button>
<h1 class="dialog-title" id="dialog-2-title">Simple Message Dialog</h1>
<hr/>
<div class="dialog-form">
<p>This is a text message for this message dialog box.</p>
</div>
<hr/>
<div class="dialog-action">
<button type="button" id="dialog2-btn-ok" class="cancel-button" >Close</button>
</div>
</div>
</div-->
JavaScript Source Code
var dialog, dialog2;
window.addEventListener('load', function () {
var options = [];
dialog = new Dialog(document.getElementById('dialog-1-container'), options);
options = [];
options["type"] = USE_TABINDEX;
dialog2 = new Dialog(document.getElementById('dialog-1-container'), options);
document.getElementById('show-dialog').addEventListener('click', showDialog);
document.getElementById('show-dialog-2').addEventListener('click', showDialogWithLOption2);
document.getElementById('dialog-btn-submit').addEventListener('click', processSubmit);
});
function showDialog(event)
{
dialog.show(event.target);
}
function showDialogWithLOption2()
{
dialog2.show(event.target);
}
function processSubmit()
{
alert("Submitted data will be processed and the dialog will close.");
if (dialog.open) dialog.hide();
if (dialog2.open) dialog2.hide();
}
const USE_FIRST_ELEMENT_FOCUS = 1;
const USE_TABINDEX = 2;
class Dialog {
constructor(container, options )
{
var self=this;
this.type = USE_FIRST_ELEMENT_FOCUS;
this.triggeredBy = null;
this.open = false;
this.bodyHandler = null;
if (typeof options == "undefined") options = [];
if (!Array.isArray(options)) options = [];
if(typeof options["type"] === "number")
{
var val = options["type"];
if( (val == USE_FIRST_ELEMENT_FOCUS) || (val == USE_TABINDEX))
this.type = val;
}
if(!isDOMElement(container))
{
console.log("container parameter should be DOM HTML element object.");
return;
}
this.container = container;
container.addEventListener("keydown", function(event) { self.handleEscapeKey(event, self);});
var dialog = container.querySelector(".dialog");
if(!isDOMElement(dialog))
{
console.log("dialog not found inside the container provided.");
return;
}
this.dialog = dialog;
var btnCancel = container.querySelector(".cancel-button");
var btnClose = container.querySelector(".close-button");
if(btnCancel) btnCancel.addEventListener('click', function() { self.hide();} );
if(btnClose) btnClose.addEventListener('click', function() { self.hide();});
var focusElements = dialog.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');
if(focusElements.length > 0)
{
var firstFocusElement = focusElements[0];
var lastFocusElement = focusElements[focusElements.length-1];
self.firstFocusElement = firstFocusElement;
self.lastFocusElement = lastFocusElement;
lastFocusElement.addEventListener("keydown", function(event){
var isTabPressed = event.key === 'Tab' || event.keyCode === 9;
if(isTabPressed && !event.shiftKey)
{
event.preventDefault();
firstFocusElement.focus();
}
});
firstFocusElement.addEventListener("keydown", function(event){
var isTabPressed = event.key === 'Tab' || event.keyCode === 9;
if(isTabPressed && event.shiftKey )
{
event.preventDefault();
lastFocusElement.focus();
}
});
}
}
handleEscapeKey(event, dialog)
{
if(event.key == "Escape")
dialog.hide();
}
handleOutsideFocus(event, dialog )
{
var isTabPressed = event.key === 'Tab' || event.keyCode === 9;
if(isTabPressed && event.shiftKey && (event.target.tagName.toLowerCase() == "body") )
{
event.preventDefault();
dialog.lastFocusElement.focus();
}
if(event.key == "Escape")
dialog.hide();
}
show(element)
{
if(isDOMElement(element))
this.triggeredBy = element;
if(!this.container.classList.contains("show"))
this.container.classList.add("show");
if(this.type == USE_FIRST_ELEMENT_FOCUS)
this.firstFocusElement.focus();
if(this.type == USE_TABINDEX)
{
var title = this.dialog.querySelector(".dialog-title");
if(title)
{
title.setAttribute('tabindex', "-1");
setTimeout(function(){ title.focus(); },50);
}
else
console.log("title element not specified or does not have '.dialog-title' class added.");
}
var self = this;
var fn = function(event) { self.handleOutsideFocus(event, self); }
this.bodyHandler = fn;
document.body.addEventListener("keydown", fn );
this.open = true;
this.hideAllSiblings();
}
hide()
{
if(!this.open) return;
if(this.container.classList.contains("show"))
this.container.classList.remove("show");
if(this.triggeredBy )
this.triggeredBy.focus();
if(this.bodyHandler)
document.body.removeEventListener("keydown", this.bodyHandler );
this.open = false;
this.unhideAllSiblings();
}
hideAllSiblings()
{
if(!isDOMElement(this.container)) return;
var ancestor = null;
var parent = this.container;
var level = 0;
setTimeout(function(){
do
{
ancestor = parent;
parent = parent.parentNode;
level++;
var siblings = parent.childNodes;
siblings.forEach(function(child){
if( typeof child.tagName == "undefined") return;
var tagName = child.tagName.toLowerCase();
if( (tagName == "script") || (tagName == "style")) return;
if(!ancestor.isSameNode(child))
child.setAttribute("aria-hidden", "true");
});
}
while ( (level > 100) || (parent.tagName.toLowerCase() != "body"));
}, 100);
}
unhideAllSiblings()
{
if(!isDOMElement(this.container)) return;
var ancestor = null;
var parent = this.container;
var level = 0;
do
{
ancestor = parent;
parent = parent.parentNode;
level++;
var siblings = parent.childNodes;
siblings.forEach(function(child){
if( typeof child.tagName == "undefined") return;
var tagName = child.tagName.toLowerCase();
if( (tagName == "script") || (tagName == "style")) return;
if(!ancestor.isSameNode(child) && child.hasAttribute("aria-hidden") )
child.removeAttribute( "aria-hidden");
});
}
while ( (level > 100) || (parent.tagName.toLowerCase() != "body"));
}
}// of Dialog class
function isDOMElement(element)
{
if ( (typeof element === "undefined") || (typeof element !== "object"))
return false;
if( element instanceof HTMLElement)
return true;
if(element.nodeType === 1 && typeof element.nodeName==="string")
return true;
return false;
}
CSS Source Code
.dialog{
padding: 15px;
border: 2px solid gray;
width: 40%;
top: 2rem;
top: 5rem;
left: 25%;
position: absolute;
background-color: white;
}
.dialog-container{
position: fixed;
display: none;
overflow-y: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background: rgb(0 0 0 / 30%);
}
.show { display: block;}
.dialog-title {
text-align: left;
font-size: 24px;
width: calc(100% - 30px);
margin: -7px 0px 0px 3px;
}
.dialog-form { padding: 10px 20px;}
.dialog-action {
display: flex;
justify-content: center;
}
button {
padding: 7px 12px;
font-size: 15px;
margin: 4px;
}
button:hover, button:focus {
outline: 2px solid red;
}
.icon { font-size: 18px; }
.primary {
background-color: #0464b3;
color: white;
}
.row {display: flex; margin: 10px;}
.label {
width: 30%;
text-align: right;
margin-right: 15px;
}
.extend { width: 100%;}
.close-button {
position: absolute! important;
top: 5px;
right: 5px;
background-color: #0464b3;
color: white;
font-size: 20px;
padding: 2px 5px;
}
.cancel-button {
background-color: #840d11;
color: white;
}
@media screen and (max-width: 1000px) {
.dialog-form { padding: 5px;}
}
@media screen and (max-width: 720px) {
.row {display: flex; flex-direction: column; margin: 10px;}
.label
{ width: 100%;
text-align: left;
margin-right: 0px;
}
.dialog
{
width: 70%;
left: 10%;
}
}