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>

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 &lt;h1&gt; </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%;
    }
}

Copy and Paste Full Page Example