jquery-dialog
Version:
jQuery plugin that creates the basic interactivity for an ARIA dialog widget.
216 lines (182 loc) • 7.45 kB
JavaScript
/**
* @file jQuery plugin that creates the basic interactivity for an ARIA dialog widget
* @author Ian McBurnie <ianmcburnie@hotmail.com>
* @version 0.5.2
* @requires jquery
* @requires jquery-next-id
* @requires jquery-focusable
* @requires jquery-keyboard-trap
* @requires jquery-screenreader-trap
*/
(function($) {
/**
* jQuery plugin that creates the basic interactivity for an ARIA dialog button
*
* @method "jQuery.fn.dialogButton"
* @return {jQuery} chainable jQuery class
*/
$.fn.dialogButton = function dialogButton(options) {
return this.each(function onEach() {
var $dialogButton = $(this);
var dialogId = $dialogButton.data('jquery-dialog-button-for');
var $dialog = $('#' + dialogId);
$dialog.on('dialogClose', function() {
$dialogButton.focus();
});
$dialogButton.on('click', function() {
$dialog.dialog(options);
});
});
};
/**
* jQuery plugin that creates the basic interactivity for an ARIA dialog widget
*
* @method "jQuery.fn.dialog"
* @return {jQuery} chainable jQuery class
*/
$.fn.dialog = function dialog(options) {
options = $.extend(
{},
$.fn.dialog.defaults,
options
);
return this.each(function onEach() {
var $dialog = $(this);
var dataHasTransitions = $dialog.data('jquery-dialog-transitions-enabled');
var hasTransitions = (dataHasTransitions === undefined) ? options.transitionsEnabled : dataHasTransitions;
var $body = $('body');
var $header = $dialog.find('.dialog__header');
var $dialogBody = $dialog.find('.dialog__body');
var $heading = $header.find('> h2');
var $dialogWindow = $dialog.find('.dialog__window'); // role=document is for older NVDA
var $closeButton = $header.find('.dialog__close, .dialog__back');
var $dialogForm = $dialogBody.find('.dialog__form');
var $dialogFormChoice = $('<input type="hidden" name="choice" />');
var $autoFocusable = $dialogWindow.find('[autofocus]');
var $focusable;
var openTimeout;
var closeTimeout;
var onKeyDown = function(e) {
if (e.keyCode === 27) {
close(e);
}
};
// submit button 'click' triggers before form 'submit' event
// use this opportunity to patch-in hidden value
var onFormSubmitClick = function() {
var value = this.value || this.innerText;
$dialogFormChoice.attr('value', value);
};
// dialog forms never submit, and always return serialzied data to caller
var onFormSubmit = function(e) {
e.preventDefault();
close(e, $(this).serializeArray());
};
/**
* @method open
* @return void
*/
var open = function() {
$dialog.prop('hidden', false);
// find all focusable elements inside dialog
$focusable = ($autoFocusable.length > 0) ? $autoFocusable : $dialog.focusable();
// dialog must always focus on an interactive element
// if none found, set focus to doc
if ($focusable.length === 0) {
$dialogWindow.attr('tabindex', '-1').focus();
} else {
$focusable.first().focus();
}
// prevent screen reader virtual cursor from leaving the dialog
$.trapScreenreader($dialog);
// prevent keyboard user from leaving the dialog
$.trapKeyboard($dialog, { deactivateOnFocusExit: false });
// add hook to body for CSS
$body.addClass('has-dialog');
// listen for key down events
$dialog.on('keydown', onKeyDown);
// let observers know that the dialog is open for business
$dialog.trigger('dialogOpen');
};
/**
* @method close
* @return void
*/
var close = function(e, data) {
$dialogForm.off('click', '[type=submit]', onFormSubmitClick);
$dialogForm.off('click', '[type=button]', close);
$dialogForm.off('submit', onFormSubmit);
$closeButton.off('click', close);
$dialog.off('keydown', onKeyDown);
$dialogFormChoice.remove();
$.untrapKeyboard();
$.untrapScreenreader();
$body.removeClass('has-dialog');
if (hasTransitions === true) {
window.clearTimeout(openTimeout);
$dialog.addClass('dialog--transition-out');
$dialogWindow.one('transitionend', function() {
$dialog.removeClass('dialog--transition-out');
$dialog.prop('hidden', true);
$dialog.trigger('dialogClose', [data]);
});
} else {
$dialog.prop('hidden', true);
$dialog.trigger('dialogClose', [data]);
}
};
$dialogForm.append($dialogFormChoice);
// assign a unique id to the dialog widget
$dialog.nextId('dialog');
// heading needs an id to create programmatic label for dialog
$heading.nextId($dialog.prop('id') + '-title');
// setup the programmatic label
$dialog.attr('aria-labelledby', $heading.prop('id'));
// ensure dialog has role dialog
$dialog.attr('role', 'dialog');
// ensure header has role banner
// found issue in voiceover where banner role will suppress announcement of dialog role
// $header.attr('role', 'banner');
$dialogForm.on('click', '[type=submit]', onFormSubmitClick);
$dialogForm.on('click', '[type=button]', close);
$dialogForm.on('submit', onFormSubmit);
$closeButton.on('click', close);
if (hasTransitions === true) {
// clean up any untriggered closeTimeout
window.clearTimeout(closeTimeout);
// prime the CSS transition (this class should set display to block)
$dialog.addClass('dialog--transition-in');
// wait a little time before triggering CSS transition
openTimeout = setTimeout(function() {
open();
$dialog.removeClass('dialog--transition-in');
}, 16);
} else {
open();
}
});
};
}(jQuery));
$.fn.dialog.defaults = {
transitionsEnabled: false,
transitionOutDuration: 0
};
/**
* The jQuery plugin namespace.
* @external "jQuery.fn"
* @see {@link http://learn.jquery.com/plugins/|jQuery Plugins}
*/
/**
* dialogOpen event
*
* @event dialogOpen
* @type {object}
* @property {object} event - event object
*/
/**
* dialogClose event
*
* @event dialogClose
* @type {object}
* @property {object} event - event object
*/