Skip to content

Commit

Permalink
feat(modal): a11y support, tabbing in modal, close icon focus/keypress
Browse files Browse the repository at this point in the history
This PR adds accessibility (a11y) support to the modal module

add proper aria attributes
make sure tabbing only takes place inside the modal and does not tab to fields hidden by the dimmer
make a close button a button and tabbable as well
I also separated the closeIcon from the previous close selector which was supposed to be able to also close the modal on each action button when the selector is enhanced.
Reason for separation of closeIcon is the new ability to trigger the closeicon by space or enter which is needed as accessibility requirement and was not possible before.

Attention
The aria attributes are only applied when modal is called dynamically via JS attributes (means without an already prepared modal DOM structure). When using existing Modal DOM nodes, adding proper aria attributes/labels is still the job of the developer as Modal is not adjusting existing DOM node attributes (for example an id has to be created for aria-labelledby).

However, the tabbing feature works independently of existing aria labels and was a general bug anyway.
  • Loading branch information
lubber-de authored Aug 9, 2021
1 parent 9088caa commit 2ecf719
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 25 deletions.
113 changes: 88 additions & 25 deletions src/definitions/modules/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ $.fn.modal = function(parameters) {

$module = $(this),
$context = $(settings.context),
$close = $module.find(selector.close),
$closeIcon = $module.find(selector.closeIcon),
$inputs,

$allModals,
$otherModals,
Expand All @@ -92,6 +93,7 @@ $.fn.modal = function(parameters) {
module = {

initialize: function() {
module.create.id();
if(!$module.hasClass('modal')) {
module.create.modal();
if(!$.isFunction(settings.onHidden)) {
Expand All @@ -116,12 +118,13 @@ $.fn.modal = function(parameters) {
$actions.empty();
}
settings.actions.forEach(function (el) {
var icon = el[fields.icon] ? '<i class="' + module.helpers.deQuote(el[fields.icon]) + ' icon"></i>' : '',
var icon = el[fields.icon] ? '<i '+(el[fields.text] ? 'aria-hidden="true"' : '')+' class="' + module.helpers.deQuote(el[fields.icon]) + ' icon"></i>' : '',
text = module.helpers.escape(el[fields.text] || '', settings.preserveHTML),
cls = module.helpers.deQuote(el[fields.class] || ''),
click = el[fields.click] && $.isFunction(el[fields.click]) ? el[fields.click] : function () {};
$actions.append($('<button/>', {
html: icon + text,
'aria-label': $('<div>'+(el[fields.text] || el[fields.icon] || '')+'</div>').text(),
class: className.button + ' ' + cls,
click: function () {
if (click.call(element, $module) === false) {
Expand All @@ -135,7 +138,6 @@ $.fn.modal = function(parameters) {
module.cache = {};
module.verbose('Initializing dimmer', $context);

module.create.id();
module.create.dimmer();

if ( settings.allowMultiple ) {
Expand All @@ -145,11 +147,9 @@ $.fn.modal = function(parameters) {
$module.addClass('top aligned');
}
module.refreshModals();

module.refreshInputs();
module.bind.events();
if(settings.observeChanges) {
module.observeChanges();
}
module.observeChanges();
module.instantiate();
if(settings.autoShow){
module.show();
Expand All @@ -166,16 +166,20 @@ $.fn.modal = function(parameters) {

create: {
modal: function() {
$module = $('<div/>', {class: className.modal});
$module = $('<div/>', {class: className.modal, role: 'dialog', 'aria-modal': true});
if (settings.closeIcon) {
$close = $('<i/>', {class: className.close})
$module.append($close);
$closeIcon = $('<i/>', {class: className.close, role: 'button', tabindex: 0, 'aria-label': settings.text.close})
$module.append($closeIcon);
}
if (settings.title !== '') {
$('<div/>', {class: className.title}).appendTo($module);
var titleId = '_' + module.get.id() + 'title';
$module.attr('aria-labelledby', titleId);
$('<div/>', {class: className.title, id: titleId}).appendTo($module);
}
if (settings.content !== '') {
$('<div/>', {class: className.content}).appendTo($module);
var descId = '_' + module.get.id() + 'desc';
$module.attr('aria-describedby', descId);
$('<div/>', {class: className.content, id: descId}).appendTo($module);
}
if (module.has.configActions()) {
$('<div/>', {class: className.actions}).appendTo($module);
Expand Down Expand Up @@ -228,15 +232,21 @@ $.fn.modal = function(parameters) {
;
$window.off(elementEventNamespace);
$dimmer.off(elementEventNamespace);
$close.off(eventNamespace);
$closeIcon.off(elementEventNamespace);
if($inputs) {
$inputs.off(elementEventNamespace);
}
$context.dimmer('destroy');
},

observeChanges: function() {
if('MutationObserver' in window) {
observer = new MutationObserver(function(mutations) {
module.debug('DOM tree modified, refreshing');
module.refresh();
if(settings.observeChanges) {
module.debug('DOM tree modified, refreshing');
module.refresh();
}
module.refreshInputs();
});
observer.observe(element, {
childList : true,
Expand All @@ -261,6 +271,23 @@ $.fn.modal = function(parameters) {
$allModals = $otherModals.add($module);
},

refreshInputs: function(){
if($inputs){
$inputs
.off('keydown' + elementEventNamespace)
;
}
$inputs = $module.find('[tabindex], :input').filter(':visible').filter(function() {
return $(this).closest('.disabled').length === 0;
});
$inputs.first()
.on('keydown' + elementEventNamespace, module.event.inputKeyDown.first)
;
$inputs.last()
.on('keydown' + elementEventNamespace, module.event.inputKeyDown.last)
;
},

attachEvents: function(selector, event) {
var
$toggle = $(selector)
Expand Down Expand Up @@ -289,6 +316,9 @@ $.fn.modal = function(parameters) {
.on('click' + eventNamespace, selector.approve, module.event.approve)
.on('click' + eventNamespace, selector.deny, module.event.deny)
;
$closeIcon
.on('keyup' + elementEventNamespace, module.event.closeKeyUp)
;
$window
.on('resize' + elementEventNamespace, module.event.resize)
;
Expand All @@ -307,7 +337,7 @@ $.fn.modal = function(parameters) {

get: {
id: function() {
return (Math.random().toString(16) + '000000000').substr(2, 8);
return id;
},
element: function() {
return $module;
Expand Down Expand Up @@ -346,10 +376,38 @@ $.fn.modal = function(parameters) {
close: function() {
module.hide();
},
closeKeyUp: function(event){
var
keyCode = event.which
;
if ((keyCode === settings.keys.enter || keyCode === settings.keys.space) && $module.hasClass(className.front)) {
module.hide();
}
},
inputKeyDown: {
first: function(event) {
var
keyCode = event.which
;
if (keyCode === settings.keys.tab && event.shiftKey) {
$inputs.last().focus();
event.preventDefault();
}
},
last: function(event) {
var
keyCode = event.which
;
if (keyCode === settings.keys.tab && !event.shiftKey) {
$inputs.first().focus();
event.preventDefault();
}
}
},
mousedown: function(event) {
var
$target = $(event.target),
isRtl = module.is.rtl();
isRtl = module.is.rtl()
;
initialMouseDownInModal = ($target.closest(selector.modal).length > 0);
if(initialMouseDownInModal) {
Expand Down Expand Up @@ -397,10 +455,9 @@ $.fn.modal = function(parameters) {
},
keyboard: function(event) {
var
keyCode = event.which,
escapeKey = 27
keyCode = event.which
;
if(keyCode == escapeKey) {
if(keyCode === settings.keys.escape) {
if(settings.closable) {
module.debug('Escape key pressed hiding modal');
if ( $module.hasClass(className.front) ) {
Expand Down Expand Up @@ -900,13 +957,10 @@ $.fn.modal = function(parameters) {
set: {
autofocus: function() {
var
$inputs = $module.find('[tabindex], :input').filter(':visible').filter(function() {
return $(this).closest('.disabled').length === 0;
}),
$autofocus = $inputs.filter('[autofocus]'),
$input = ($autofocus.length > 0)
? $autofocus.first()
: $inputs.first()
: ($inputs.length > 1 ? $inputs.filter(':not(i.close)') : $inputs).first()
;
if($input.length > 0) {
$input.focus();
Expand Down Expand Up @@ -1323,11 +1377,19 @@ $.fn.modal.settings = {
// called after deny selector match
onDeny : function(){ return true; },

keys : {
space : 32,
enter : 13,
escape : 27,
tab : 9,
},

selector : {
title : '> .header',
content : '> .content',
actions : '> .actions',
close : '> .close',
closeIcon: '> .close',
approve : '.actions .positive, .actions .approve, .actions .ok',
deny : '.actions .negative, .actions .deny, .actions .cancel',
modal : '.ui.modal',
Expand Down Expand Up @@ -1363,7 +1425,8 @@ $.fn.modal.settings = {
},
text: {
ok : 'Ok',
cancel: 'Cancel'
cancel: 'Cancel',
close : 'Close'
}
};

Expand Down
2 changes: 2 additions & 0 deletions src/definitions/modules/modal.less
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@
height: @closeHitbox;
padding: @closePadding;
}
.ui.modal > .close:focus,
.ui.modal > .close:hover {
opacity: 1;
outline: none;
}

/*--------------
Expand Down

0 comments on commit 2ecf719

Please sign in to comment.