Skip to content

Instantly share code, notes, and snippets.

@arihantverma
Created February 15, 2024 14:22
Show Gist options
  • Save arihantverma/af03123bcd2ac641f82ac8ddb1b018a9 to your computer and use it in GitHub Desktop.
Save arihantverma/af03123bcd2ac641f82ac8ddb1b018a9 to your computer and use it in GitHub Desktop.
<html>
<head>
<title>Woohooo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script defer src="./index.js"></script>
<style>
.hidden {
display: none;
}
[role="dialog"] {
box-sizing: border-box;
padding: 15px;
border: 1px solid #000;
background-color: #fff;
min-height: 100vh;
}
@media screen and (min-width: 640px) {
[role="dialog"] {
position: absolute;
top: 2rem;
left: 50vw; /* move to the middle of the screen (assumes relative parent is the body/viewport) */
transform: translateX(
-50%
); /* move backwards 50% of this element's width */
min-width: calc(
640px - (15px * 2)
); /* == breakpoint - left+right margin */
min-height: auto;
box-shadow: 0 19px 38px rgb(0 0 0 / 12%), 0 15px 12px rgb(0 0 0 / 22%);
}
}
.dialog_label {
text-align: center;
}
.dialog_form {
margin: 15px;
}
.dialog_form .label_text {
box-sizing: border-box;
padding-right: 0.5em;
display: inline-block;
font-size: 16px;
font-weight: bold;
width: 30%;
text-align: right;
}
.dialog_form .label_info {
box-sizing: border-box;
padding-right: 0.5em;
font-size: 12px;
width: 30%;
text-align: right;
display: inline-block;
}
.dialog_form_item {
margin: 10px 0;
font-size: 0;
}
.dialog_form_item .wide_input {
box-sizing: border-box;
max-width: 70%;
width: 27em;
}
.dialog_form_item .city_input {
box-sizing: border-box;
max-width: 70%;
width: 17em;
}
.dialog_form_item .state_input {
box-sizing: border-box;
max-width: 70%;
width: 15em;
}
.dialog_form_item .zip_input {
box-sizing: border-box;
max-width: 70%;
width: 9em;
}
.dialog_form_actions {
text-align: right;
padding: 0 20px 20px;
}
.dialog_close_button {
float: right;
position: absolute;
top: 10px;
left: 92%;
height: 25px;
}
.dialog_close_button img {
border: 0;
}
.dialog_desc {
padding: 10px 20px;
}
/* native <dialog> element uses the ::backdrop pseudo-element */
/* dialog::backdrop, */
.dialog-backdrop {
display: none;
position: fixed;
overflow-y: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
}
@media screen and (min-width: 640px) {
.dialog-backdrop {
background: rgb(0 0 0 / 30%);
}
}
.dialog-backdrop.active {
display: block;
}
.no-scroll {
overflow-y: auto !important;
}
/* this is added to the body when a dialog is open */
.has-dialog {
overflow: hidden;
}
</style>
</head>
<body>
<button type="button" onclick="openDialog('dialog1', this)">
Add Delivery Address
</button>
<div id="dialog_layer" class="dialogs">
<div
role="dialog"
id="dialog1"
aria-labelledby="dialog1_label"
aria-modal="true"
class="hidden"
>
<h2 id="dialog1_label" class="dialog_label">Add Delivery Address</h2>
<div class="dialog_form">
<div class="dialog_form_item">
<label>
<span class="label_text">Street:</span>
<input type="text" class="wide_input" />
</label>
</div>
<div class="dialog_form_item">
<label>
<span class="label_text">City:</span>
<input type="text" class="city_input" />
</label>
</div>
<div class="dialog_form_item">
<label>
<span class="label_text">State:</span>
<input type="text" class="state_input" />
</label>
</div>
<div class="dialog_form_item">
<label>
<span class="label_text">Zip:</span>
<input type="text" class="zip_input" />
</label>
</div>
<div class="dialog_form_item">
<label for="special_instructions">
<span class="label_text">Special instructions:</span>
</label>
<input
id="special_instructions"
type="text"
aria-describedby="special_instructions_desc"
class="wide_input"
/>
<div class="label_info" id="special_instructions_desc">
For example, gate code or other information to help the driver
find you
</div>
</div>
</div>
<div class="dialog_form_actions">
<button
type="button"
onclick="openDialog('dialog2', this, 'dialog2_para1')"
>
Verify Address
</button>
<button
type="button"
onclick="replaceDialog('dialog3', undefined, 'dialog3_close_btn')"
>
Add
</button>
<button type="button" onclick="closeDialog(this)">Cancel</button>
</div>
</div>
<div
id="dialog2"
role="dialog"
aria-labelledby="dialog2_label"
aria-describedby="dialog2_desc"
aria-modal="true"
class="hidden"
>
<h2 id="dialog2_label" class="dialog_label">Verification Result</h2>
<div id="dialog2_desc" class="dialog_desc">
<p tabindex="-1" id="dialog2_para1">
This is just a demonstration. If it were a real application, it
would provide a message telling whether the entered address is
valid.
</p>
<p>
For demonstration purposes, this dialog has a lot of text. It
demonstrates a scenario where:
</p>
<ul>
<li>
The first interactive element, the help link, is at the bottom of
the dialog.
</li>
<li>
If focus is placed on the first interactive element when the
dialog opens, the validation message may not be visible.
</li>
<li>
If the validation message is visible and the focus is on the help
link, then the focus may not be visible.
</li>
<li>
When the dialog opens, it is important that both:
<ul>
<li>
The beginning of the text is visible so users do not have to
scroll back to start reading.
</li>
<li>The keyboard focus always remains visible.</li>
</ul>
</li>
</ul>
<p>There are several ways to resolve this issue:</p>
<ul>
<li>
Place an interactive element at the top of the dialog, e.g., a
button or link.
</li>
<li>
Make a static element focusable, e.g., the dialog title or the
first block of text.
</li>
</ul>
<p>
Please <em>DO NOT </em> make the element with role dialog focusable!
</p>
<ul>
<li>
The larger a focusable element is, the more difficult it is to
visually identify the location of focus, especially for users with
a narrow field of view.
</li>
<li>
The dialog has a visual border, so creating a clear visual
indicator of focus when the entire dialog has focus is not very
feasible.
</li>
<li>
Screen readers read the label and content of focusable elements.
The dialog contains its label and a lot of content! If a dialog
like this one has focus, the actual focus is difficult to
comprehend.
</li>
</ul>
<p>
In this dialog, the first paragraph has
<code>tabindex=<q>-1</q></code
>. The first paragraph is also contained inside the element that
provides the dialog description, i.e., the element that is
referenced by <code>aria-describedby</code>. With some screen
readers, this may have one negative but relatively insignificant
side effect when the dialog opens -- the first paragraph may be
announced twice. Nonetheless, making the first paragraph focusable
and setting the initial focus on it is the most broadly accessible
option.
</p>
</div>
<div class="dialog_form_actions">
<a href="#" onclick="openDialog('dialog4', this)">link to help</a>
<button type="button" onclick="openDialog('dialog4', this)">
accepting an alternative form
</button>
<button type="button" onclick="closeDialog(this)">Close</button>
</div>
</div>
<div
id="dialog3"
role="dialog"
aria-labelledby="dialog3_label"
aria-describedby="dialog3_desc"
aria-modal="true"
class="hidden"
>
<h2 id="dialog3_label" class="dialog_label">Address Added</h2>
<p id="dialog3_desc" class="dialog_desc">
The address you provided has been added to your list of delivery
addresses. It is ready for immediate use. If you wish to remove it,
you can do so from
<a href="#" onclick="openDialog('dialog4', this)">your profile.</a>
</p>
<div class="dialog_form_actions">
<button
type="button"
id="dialog3_close_btn"
onclick="closeDialog(this)"
>
OK
</button>
</div>
</div>
<div
id="dialog4"
role="dialog"
aria-labelledby="dialog4_label"
aria-describedby="dialog4_desc"
class="hidden"
aria-modal="true"
>
<h2 id="dialog4_label" class="dialog_label">End of the Road!</h2>
<p id="dialog4_desc" class="dialog_desc">
You activated a fake link or button that goes nowhere! The link or
button is present for demonstration purposes only.
</p>
<div class="dialog_form_actions">
<button
type="button"
id="dialog4_close_btn"
onclick="closeDialog(this)"
>
Close
</button>
</div>
</div>
</div>
</body>
</html>
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*/
"use strict";
var aria = aria || {};
aria.Utils = aria.Utils || {};
(function () {
/*
* When util functions move focus around, set this true so the focus listener
* can ignore the events.
*/
// ✅
aria.Utils.IgnoreUtilFocusChanges = false;
// ✅
aria.Utils.dialogOpenClass = "has-dialog";
/**
* @description Set focus on descendant nodes until the first focusable element is
* found.
* @param element
* DOM node for which to find the first focusable descendant.
* @returns {boolean}
* true if a focusable element is found and focus is set.
*/
// ✅
aria.Utils.focusFirstDescendant = function (element) {
for (var i = 0; i < element.childNodes.length; i++) {
var child = element.childNodes[i];
if (
aria.Utils.attemptFocus(child) ||
aria.Utils.focusFirstDescendant(child)
) {
return true;
}
}
return false;
}; // end focusFirstDescendant
/**
* @description Find the last descendant node that is focusable.
* @param element
* DOM node for which to find the last focusable descendant.
* @returns {boolean}
* true if a focusable element is found and focus is set.
*/
// ✅
aria.Utils.focusLastDescendant = function (element) {
for (var i = element.childNodes.length - 1; i >= 0; i--) {
var child = element.childNodes[i];
if (
aria.Utils.attemptFocus(child) ||
aria.Utils.focusLastDescendant(child)
) {
return true;
}
}
return false;
}; // end focusLastDescendant
/**
* @description Set Attempt to set focus on the current node.
* @param element
* The node to attempt to focus on.
* @returns {boolean}
* true if element is focused.
*/
// ✅
aria.Utils.attemptFocus = function (element) {
if (!aria.Utils.isFocusable(element)) {
return false;
}
aria.Utils.IgnoreUtilFocusChanges = true;
try {
element.focus();
} catch (e) {
// continue regardless of error
}
aria.Utils.IgnoreUtilFocusChanges = false;
return document.activeElement === element;
}; // end attemptFocus
// ✅
/* Modals can open modals. Keep track of them with this array. */
aria.OpenDialogList = aria.OpenDialogList || new Array(0);
/**
* @returns {object} the last opened dialog (the current dialog)
*/
// ✅
aria.getCurrentDialog = function () {
if (aria.OpenDialogList && aria.OpenDialogList.length) {
return aria.OpenDialogList[aria.OpenDialogList.length - 1];
}
};
// ✅
aria.closeCurrentDialog = function () {
var currentDialog = aria.getCurrentDialog();
if (currentDialog) {
currentDialog.close();
return true;
}
return false;
};
// ✅
aria.handleEscape = function (event) {
var key = event.which || event.keyCode;
if (key === aria.KeyCode.ESC && aria.closeCurrentDialog()) {
event.stopPropagation();
}
};
// ✅
document.addEventListener("keyup", aria.handleEscape);
/**
* @class
* @description Dialog object providing modal focus management.
*
* Assumptions: The element serving as the dialog container is present in the
* DOM and hidden. The dialog container has role='dialog'.
* @param dialogId
* The ID of the element serving as the dialog container.
* @param focusAfterClosed
* Either the DOM node or the ID of the DOM node to focus when the
* dialog closes.
* @param focusFirst
* Optional parameter containing either the DOM node or the ID of the
* DOM node to focus when the dialog opens. If not specified, the
* first focusable element in the dialog will receive focus.
*/
// ✅
aria.Dialog = function (dialogId, focusAfterClosed, focusFirst) {
this.dialogNode = document.getElementById(dialogId);
if (this.dialogNode === null) {
throw new Error('No element found with id="' + dialogId + '".');
}
var validRoles = ["dialog", "alertdialog"];
var isDialog = (this.dialogNode.getAttribute("role") || "")
.trim()
.split(/\s+/g)
.some(function (token) {
return validRoles.some(function (role) {
return token === role;
});
});
if (!isDialog) {
throw new Error(
"Dialog() requires a DOM element with ARIA role of dialog or alertdialog.",
);
}
// Wrap in an individual backdrop element if one doesn't exist
// Native <dialog> elements use the ::backdrop pseudo-element, which
// works similarly.
var backdropClass = "dialog-backdrop";
if (this.dialogNode.parentNode.classList.contains(backdropClass)) {
this.backdropNode = this.dialogNode.parentNode;
} else {
this.backdropNode = document.createElement("div");
this.backdropNode.className = backdropClass;
this.dialogNode.parentNode.insertBefore(
this.backdropNode,
this.dialogNode,
);
// https://app.campsite.co/siblings/notes/r98cf524ok6z
// it moves existing position as sibling of backdropNode
// and appends it inside the backdropNode.
this.backdropNode.appendChild(this.dialogNode);
}
this.backdropNode.classList.add("active");
// Disable scroll on the body element
document.body.classList.add(aria.Utils.dialogOpenClass);
if (typeof focusAfterClosed === "string") {
this.focusAfterClosed = document.getElementById(focusAfterClosed);
} else if (typeof focusAfterClosed === "object") {
this.focusAfterClosed = focusAfterClosed;
} else {
throw new Error(
"the focusAfterClosed parameter is required for the aria.Dialog constructor.",
);
}
if (typeof focusFirst === "string") {
this.focusFirst = document.getElementById(focusFirst);
} else if (typeof focusFirst === "object") {
this.focusFirst = focusFirst;
} else {
this.focusFirst = null;
}
// Bracket the dialog node with two invisible, focusable nodes.
// While this dialog is open, we use these to make sure that focus never
// leaves the document even if dialogNode is the first or last node.
var preDiv = document.createElement("div");
this.preNode = this.dialogNode.parentNode.insertBefore(
preDiv,
this.dialogNode,
);
this.preNode.tabIndex = 0;
var postDiv = document.createElement("div");
this.postNode = this.dialogNode.parentNode.insertBefore(
postDiv,
this.dialogNode.nextSibling,
);
this.postNode.tabIndex = 0;
// If this modal is opening on top of one that is already open,
// get rid of the document focus listener of the open dialog.
if (aria.OpenDialogList.length > 0) {
aria.getCurrentDialog().removeListeners();
}
// focus trap.
this.addListeners();
aria.OpenDialogList.push(this);
// clear any input fields on the current dialog that might
// have been previously filled, but discarded
this.clearDialog();
// make visible, by resetting class. This removes the erstwhile default set 'hidden' class
this.dialogNode.className = "default_dialog";
if (this.focusFirst) {
this.focusFirst.focus();
} else {
aria.Utils.focusFirstDescendant(this.dialogNode);
}
this.lastFocus = document.activeElement;
}; // end Dialog constructor
// ✅
aria.Dialog.prototype.clearDialog = function () {
Array.prototype.map.call(
this.dialogNode.querySelectorAll("input"),
function (input) {
input.value = "";
},
);
};
/**
* @description
* Hides the current top dialog,
* removes listeners of the top dialog,
* restore listeners of a parent dialog if one was open under the one that just closed,
* and sets focus on the element specified for focusAfterClosed.
*/
// ✅
aria.Dialog.prototype.close = function () {
aria.OpenDialogList.pop();
this.removeListeners();
aria.Utils.remove(this.preNode);
aria.Utils.remove(this.postNode);
this.dialogNode.className = "hidden";
this.backdropNode.classList.remove("active");
this.focusAfterClosed.focus();
// If a dialog was open underneath this one, restore its listeners.
if (aria.OpenDialogList.length > 0) {
aria.getCurrentDialog().addListeners();
} else {
document.body.classList.remove(aria.Utils.dialogOpenClass);
}
}; // end close
/**
* @description
* Hides the current dialog and replaces it with another.
* @param newDialogId
* ID of the dialog that will replace the currently open top dialog.
* @param newFocusAfterClosed
* Optional ID or DOM node specifying where to place focus when the new dialog closes.
* If not specified, focus will be placed on the element specified by the dialog being replaced.
* @param newFocusFirst
* Optional ID or DOM node specifying where to place focus in the new dialog when it opens.
* If not specified, the first focusable element will receive focus.
*/
// ✅
aria.Dialog.prototype.replace = function (
newDialogId,
newFocusAfterClosed,
newFocusFirst,
) {
aria.OpenDialogList.pop();
this.removeListeners();
aria.Utils.remove(this.preNode);
aria.Utils.remove(this.postNode);
this.dialogNode.className = "hidden";
this.backdropNode.classList.remove("active");
var focusAfterClosed = newFocusAfterClosed || this.focusAfterClosed;
new aria.Dialog(newDialogId, focusAfterClosed, newFocusFirst);
}; // end replace
// ✅
aria.Dialog.prototype.addListeners = function () {
document.addEventListener("focus", this.trapFocus, true);
}; // end addListeners
// ✅
aria.Dialog.prototype.removeListeners = function () {
document.removeEventListener("focus", this.trapFocus, true);
}; // end removeListeners
// ✅
aria.Dialog.prototype.trapFocus = function (event) {
if (aria.Utils.IgnoreUtilFocusChanges) {
return;
}
var currentDialog = aria.getCurrentDialog();
if (currentDialog.dialogNode.contains(event.target)) {
currentDialog.lastFocus = event.target;
} else {
aria.Utils.focusFirstDescendant(currentDialog.dialogNode);
// this gets fired for example, when you hit shift+tab on
// first focussable element. What happens:
// 1. the focus goes to first div with tabindex = 0; which is
// outside of the dialog box, so this `else` gets called again
// 2. line no. 307 gets called and the focus comes back to first focussable element
// of dialog box
// 3. since lastFocus and activeElement are some ( first focussable element)
// it might be intended that the focus was for the last focussable element,
// (because we jumped back from first focussable element, and ended up on it again)
if (currentDialog.lastFocus == document.activeElement) {
aria.Utils.focusLastDescendant(currentDialog.dialogNode);
}
currentDialog.lastFocus = document.activeElement;
}
}; // end trapFocus
// ✅
window.openDialog = function (dialogId, focusAfterClosed, focusFirst) {
new aria.Dialog(dialogId, focusAfterClosed, focusFirst);
};
// ✅
window.closeDialog = function (closeButton) {
var topDialog = aria.getCurrentDialog();
if (topDialog.dialogNode.contains(closeButton)) {
topDialog.close();
}
}; // end closeDialog
// ✅
window.replaceDialog = function (
newDialogId,
newFocusAfterClosed,
newFocusFirst,
) {
var topDialog = aria.getCurrentDialog();
// this is a safety check. ChatGPT says this:
// 1. Accessibility features or assistive technologies might allow users to navigate outside of the dialog.
// 2. Programmatic focus changes could move focus away from the dialog.
// 3. User interactions, like clicking outside of the dialog, could cause focus to move away.
if (topDialog.dialogNode.contains(document.activeElement)) {
topDialog.replace(newDialogId, newFocusAfterClosed, newFocusFirst);
}
}; // end replaceDialog
})();
("use strict");
/**
* @namespace aria
*/
var aria = aria || {};
/**
* @description
* Key code constants
*/
aria.KeyCode = {
BACKSPACE: 8,
TAB: 9,
RETURN: 13,
SHIFT: 16,
ESC: 27,
SPACE: 32,
PAGE_UP: 33,
PAGE_DOWN: 34,
END: 35,
HOME: 36,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
DELETE: 46,
};
aria.Utils = aria.Utils || {};
// Polyfill src https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
// ✅
aria.Utils.matches = function (element, selector) {
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function (s) {
var matches = element.parentNode.querySelectorAll(s);
var i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {
// empty
}
return i > -1;
};
}
return element.matches(selector);
};
// ✅
aria.Utils.remove = function (item) {
if (item.remove && typeof item.remove === "function") {
return item.remove();
}
if (
item.parentNode &&
item.parentNode.removeChild &&
typeof item.parentNode.removeChild === "function"
) {
return item.parentNode.removeChild(item);
}
return false;
};
// ✅
aria.Utils.isFocusable = function (element) {
if (element.tabIndex < 0) {
return false;
}
if (element.disabled) {
return false;
}
switch (element.nodeName) {
case "A":
return !!element.href && element.rel != "ignore";
case "INPUT":
return element.type != "hidden";
case "BUTTON":
case "SELECT":
case "TEXTAREA":
return true;
default:
return false;
}
};
// NOT used anywhere
aria.Utils.getAncestorBySelector = function (element, selector) {
if (!aria.Utils.matches(element, selector + " " + element.tagName)) {
// Element is not inside an element that matches selector
return null;
}
// Move up the DOM tree until a parent matching the selector is found
var currentNode = element;
var ancestor = null;
while (ancestor === null) {
if (aria.Utils.matches(currentNode.parentNode, selector)) {
ancestor = currentNode.parentNode;
} else {
currentNode = currentNode.parentNode;
}
}
return ancestor;
};
// ✅
aria.Utils.hasClass = function (element, className) {
return new RegExp("(\\s|^)" + className + "(\\s|$)").test(element.className);
};
// ✅
aria.Utils.addClass = function (element, className) {
if (!aria.Utils.hasClass(element, className)) {
element.className += " " + className;
}
};
// ✅
aria.Utils.removeClass = function (element, className) {
var classRegex = new RegExp("(\\s|^)" + className + "(\\s|$)");
element.className = element.className.replace(classRegex, " ").trim();
};
// not used anywhere
aria.Utils.bindMethods = function (object /* , ...methodNames */) {
var methodNames = Array.prototype.slice.call(arguments, 1);
methodNames.forEach(function (method) {
object[method] = object[method].bind(object);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment