<html lang="en">
<head>
<meta charset="utf-8">
<title>Dialog Tutorial</title>
</head>
<body>
<article id="example-introduction">
<h1>Dialog Tutorial</h1>
<p>View the source of this page to see how a visually appealing, yet fully accessible dialog can be created. The rules for accessible dialogs are laid out in <a href="https://www.w3.org/WAI/PF/aria-practices/#dialog_modal">WAI-ARIA 1.0 Authoring Practices</a></p>
<p>There are two input fields surrounding the "open dialog" button to mimic interactive content outside of the dialog. They have no other purpose than serve as proof of focus being trapped within the dialog once it's shown.</p>
</article>
<div id="example-html">
<main>
<div>
<label for="before-button">Demo Text Input before Dialog Button</label> <input id="before-button">
</div>
<section>
<button type="button" id="open-dialog">Show Dialog</button>
</section>
<label for="after-button">Demo Text Input after Dialog Button</label> <input id="after-button">
</main>
<!--
If not using the <dialog> element of HTML5.1, it's best to use the <div> element for widgets
with semantic meaning that can only be defined via aria.
It is not uncommon to wrap the dialog's content in a <form>,
provide the confirmation button <button type="submit">OK</button>,
and wire up the submit event to close the dialog after processing
the user's input (while preventing the form submission itself).
This has the added benefit of <kbd>Enter</kbd> working out of the
box, without JavaScript having to listen for the key to be pressed.
Also touchscreen devices (such as modern mobile phones) have
built-in heuristics that change the appearance of OnScreenKeyboards
depending on <form> and <input> type.
When dealing with elements that contain focus, it is a good idea to make them focusable themselves
by adding tabindex="-1". This way a mouse click in the background of the dialog won't pass focus
to the document, but retain it in the dialog.
The aria attributes role, aria-labeledby and aria-describedby are necessary for the Accessibility Tree
to properly expose our dialog widget. Their specific meaning is explained here:
* https://www.w3.org/TR/wai-aria/roles#dialog
* https://www.w3.org/TR/wai-aria/states_and_properties#aria-labelledby
* https://www.w3.org/TR/wai-aria/states_and_properties#aria-describedby
-->
<div id="dialog" role="dialog" aria-labelledby="dialog-title" aria-describedby="dialog-description" tabindex="-1" hidden>
<form class="dialog-content">
<header>
<!--
The dialog's header explains what the user is being interrupted for.
While a label is considered mandatory, an extended description is optional.
-->
<h1 id="dialog-title">Name Entry</h1>
<p id="dialog-description">Please enter your full name.</p>
</header>
<section>
<!--
A dialog can do anything from displaying simple text, to hosting
complex user interaction. But since the latter is generally
considered bad practice, try keeping your dialogs as simple
as possible.
-->
<label for="within-dialog">Name</label> <input id="within-dialog">
</section>
<footer>
<!--
the footer of a dialog usually contains its control buttons
to close, confirm or do something else entirely.
-->
<button type="button" id="close-dialog">Close</button>
<button type="submit" id="save-dialog">Save</button>
</footer>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/ally.js/1.4.1/ally.min.js"></script>
</body>
</html>
// Grab the elements we need to interact with
var openButton = document.getElementById('open-dialog');
var dialog = document.getElementById('dialog');
var closeButton = document.getElementById('close-dialog');
// Data filled by openDialog() and later used by closeDialog()
var keyHandle;
var tabHandle;
var disabledHandle;
var hiddenHandle;
var focusedElementBeforeDialogOpened;
function openDialog() {
// Remember the focused element before we opened the dialog
// so we can return focus to it once we close the dialog.
focusedElementBeforeDialogOpened = document.activeElement;
// We're using a transition to reveal the dialog,
// so wait until the element is visible, before
// finding the first keyboard focusable element
// and passing focus to it, otherwise the browser
// might scroll the document to reveal the element
// receiving focus
ally.when.visibleArea({
context: dialog,
callback: function(context) {
// the dialog is visible on screen, so find the first
// keyboard focusable element (giving any element with
// autofocus attribute precendence). If the dialog does
// not contain any keyboard focusabe elements, focus will
// be given to the dialog itself.
var element = ally.query.firstTabbable({
context: context, // context === dialog
defaultToContext: true,
});
element.focus();
},
});
// Make sure that no element outside of the dialog
// can be interacted with while the dialog is visible.
// This means we don't have to handle Tab and Shift+Tab,
// but can defer that to the browser's internal handling.
disabledHandle = ally.maintain.disabled({
filter: dialog,
});
// Make sure that no element outside of the dialog
// is exposed via the Accessibility Tree, to prevent
// screen readers from navigating to content it shouldn't
// be seeing while the dialog is open. See example:
// https://marcysutton.com/slides/mobile-a11y-seattlejs/#/36
hiddenHandle = ally.maintain.hidden({
filter: dialog,
});
// Make sure that Tab key controlled focus is trapped within
// the tabsequence of the dialog and does not reach the
// browser's UI, e.g. the location bar.
tabHandle = ally.maintain.tabFocus({
context: dialog,
});
// React to enter and escape keys as mandated by ARIA Practices
keyHandle = ally.when.key({
escape: closeDialogByKey,
// Note: in non-interactive content you would also bind the enter
// key to close the dialog. In our example the form's submit event
// will cover that instead. The enter handler would be executed
// for *every* press of the enter key, even if the focused element
// is a button (which would invoke the default action). Try using
// a <form> for interactive content to get around that problem.
// enter: closeDialogByKey,
});
// Show the dialog
dialog.hidden = false;
}
function closeDialogByKey() {
// we need to let the keyboard event handlers finish,
// before actually closing the dialog. Otherwise the
// keydown of <kbd>enter</kbd> will close the dialog,
// focus is passed back to the open-dialog-button and
// the keyup of <kbd>enter</kbd> will open the dialog
// again.
setTimeout(closeDialog);
// alternatively we could've called event.preventDefault()
// and then run closeDialog() synchronously
}
function closeDialog() {
// undo listening to keyboard
keyHandle.disengage();
// undo trapping Tab key focus
tabHandle.disengage();
// undo hiding elements outside of the dialog
hiddenHandle.disengage();
// undo disabling elements outside of the dialog
disabledHandle.disengage();
// return focus to where it was before we opened the dialog
focusedElementBeforeDialogOpened.focus();
// hide or remove the dialog
dialog.hidden = true;
}
function saveDialog(event) {
// do not submit the form
event.preventDefault();
// do something with the entered data
var name = dialog.querySelector('input').value;
console.log('entered name', name);
closeDialog();
}
// wire up showing/hiding the dialog
openButton.addEventListener('click', openDialog, false);
closeButton.addEventListener('click', closeDialog, false);
dialog.addEventListener('submit', saveDialog, true);
Output
You can jump to the latest bin by adding /latest
to your URL
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |