Dialog (Simple Dialog)

Dialog (Simple 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.

HTML Source Code

<div>
    <div>
        <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"  >
        <h2 class="dialog-title"  id="dialog-1-title">Feedback Form</h2>
        <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" >
        <button  class="close-button" aria-label="close dialog" id="dialog2-btn-close"><i class="fa-solid fa-xmark"></i></button>
        <h2 class="dialog-title"  id="dialog-2-title">Simple Message Dialog</h2>
        <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-2-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.");
    dialog.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: 50%;
    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: center;
    font-size: 24px;
}
.dialog-form { padding: 10px 60px;}
.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%;
    }
}

Copy and Paste Full Page Example