Alert Dialog on Page Load in Massachusetts.edu

Component
Software Product
Drupal

In the event of an important announcement, Massachusetts.edu uses an Alert feature that loads on every page in a dialog. Focus is forced to the dialog and access to the page is not possible until the visitor has dismissed the alert. 

HTML 

Page templates include an if statement that looks for the presence of the alert.

{% if alert %}
	{% include directory ~ "/templates/partials/alert.twig" with { alert: alert} %}
{% endif %}

The body tag receives the class "dialog_open".

<body class="dialog_open"> ...... </body>

The dialog code is constructed in alert.twig

<div class="js-dialog-wrapper dialog_wrapper" data-label="{{ alert.title }}" data-id="{{ alert.id }}">
	<div class="fs-row">
		<div class="fs-cell">
			<dialog class="js-dialog dialog dialog_open" tabindex="-1" open="open" role="alert" aria-modal="true">
				<div class="js-dialog-document dialog_document">
					<h2 class="dialog_alert_title">{{ alert.title }}</h2>

					{% if alert.subtitle %}
						<span class="dialog_subheading">{{ alert.subtitle }}</span>
					{% endif %}

					{% if alert.description %}
						<span class="dialog_alert_body">{{ alert.description }}</span>
					{% endif %}

					{% if alert.link_title %}
						<a class="dialog_alert_link" href="{{ alert.link_url }}">
							<span class="dialog_alert_link_text">{{ alert.link_title }}</span>
							{{ site.animated_circle_arrow("dialog_alert_link_icon") }}
						</a>
					{% endif %}

					<button class="js-dialog-close dialog_close" aria-label="Close">
						<span class="dialog_close_icon">{{ site.icon("close", directory) }}</span>
					</button>
				</div>
			</dialog>
		</div>
	</div>
</div>

CSS 

Hide the page content behind the dialog.

body.dialog_open { overflow: hidden; }

Dialog wrapper:

.dialog_wrapper {
	position: fixed;
	left: 50%;
	-webkit-transform: translateX(-50%);
	transform: translateX(-50%);
	top: calc(var(--vh) * 50);
	-webkit-transform: translate(-50%, -50%);
	transform: translate(-50%, -50%);
	opacity: 0;
	-webkit-transition: opacity 0.25s ease;
	transition: opacity 0.25s ease;
	visibility: hidden;
	z-index: 400;
}

.dialog_wrapper.visible {
	opacity: 1;
	visibility: visible;
}

 

Javascript

The dialog.js file contains all of the associated behaviors and class changes when an alert dialog is active.


Site.modules.Dialog = (function($, Site) {

	var $Dialog;
	var $DialogDocument;
	var $DialogTarget;
	var $DialogClose;
	var DialogID;

	function init() {
		$Dialog = $(".js-dialog");

		if ($Dialog.length) {
			$DialogDocument = $(".js-dialog-document");
			$DialogClose = $(".js-dialog-close");
			DialogID = $Dialog.closest(".js-dialog-wrapper").attr("data-id");

			var cookie = getCookie(DialogID.toString());

			if(cookie != 'true') {
				loadDialog($Dialog);
				bindUI();
				addDialogAccessibility();
			}
		}
	}

	function bindUI() {
		$(document).on("click touchstart", onDocumentClick);
		$DialogClose.on("click", onCloseClick);
		$DialogClose.on("keydown", onCloseKeydown);
		$DialogDocument.on("keydown", onDocumentKeydown);
		$DialogDocument.on("keyup", onDocumentKeyup);
	}

	function addDialogAccessibility() {
		$DialogDocument.attr("role", "alert");

		$(".js-dialog").each(function() {
			$(this).attr("aria-label", $(this).closest(".js-dialog-wrapper").data("label"));
		});
	}

	function removeDialogAccessibility() {
		$(".js-dialog").contents().unwrap();
		$DialogDocument.removeAttr("role tabindex");
	}

	function loadDialog(e) {
		$("body").addClass("dialog_open");
		e.closest(".js-dialog-wrapper").addClass("visible");
		e.focus();
	}

	function onCloseClick(e) {
		$("body").removeClass("dialog_open");
		$Dialog.removeAttr("open").removeClass("dialog_open").closest(".js-dialog-wrapper").removeClass("visible");
		setCookie(DialogID.toString(), true);
	}

	function onCloseKeydown(e) {
		if (e.keyCode === 9) { // tab
			if (!(e.shiftKey)) {
				$(".js-dialog")
					.focus();
			}
		}
	}

	function onDocumentKeydown(e) {
		if ($DialogDocument.is(":focus")) {
			if (e.keyCode === 9) { // tab
				if (e.shiftKey) {
					e.preventDefault();

					$(".js-dialog-close")
						.focus();
				}
			}
		}
	}

	function onDocumentKeyup(e) {
		if (e.keyCode === 27) { // escape
			onCloseClick();
		}
	}

	function onDocumentClick(e) {
		if ($(".page_wrapper").hasClass("dialog_open")) {
			if (!($(event.target).closest(".js-dialog").length || $(event.target).closest(".js-dialog-trigger").length)) {
				onCloseClick();
			}
		}
	}

	function setCookie(key, value) {
		var expires = new Date();
		expires.setTime(expires.getTime() + 604800000); //1 week
		document.cookie = key + '=' + value + ';expires=' + expires.toUTCString();
	}

	function getCookie(key) {
		var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
		return keyValue ? keyValue[2] : null;
	}

	Site.onInit.push(init);

	return {
		addDialogAccessibility: addDialogAccessibility,
		removeDialogAccessibility: removeDialogAccessibility
	};

})(jQuery, Site);