Skip to content

Commit

Permalink
feat #1592 WAI-ARIA - Modal dialog
Browse files Browse the repository at this point in the history
Mainly brings accessibility to modal dialogs:

- label
- accessible icons
- background elements hiding
- focus restoration (to the latest focused element before opening the dialog) when closing the dialog

This required some side changes:

- now action widgets (Button & Link) make sure their action is triggered only if the keyup event matches a previous keydown one
- icons are now accessible
- the MultiSelect widget can now be closed with the escape key
- various widgets have been fixed to handle the escape key more properly

closes #1592
  • Loading branch information
ymeine committed Feb 16, 2016
1 parent 65dd05d commit 8cd4434
Show file tree
Hide file tree
Showing 46 changed files with 2,592 additions and 96 deletions.
4 changes: 4 additions & 0 deletions src/aria/popups/Beans.js
Expand Up @@ -148,6 +148,10 @@ module.exports = Aria.beanDefinitions({
$type : "json:String",
$description : "The role attribute to add to the container, if wai is activated"
},
"labelId" : {
$type : "json:String",
$description : "The id of the element to use as a label for accessibility. Used only if wai is activated."
},
"waiAria" : {
$type : "json:Boolean",
$description : "If true, accessibility-related DOM attributes are enabled on this container, adding the role attribute if defined."
Expand Down
59 changes: 45 additions & 14 deletions src/aria/popups/Popup.js
Expand Up @@ -16,6 +16,7 @@ var Aria = require("../Aria");
var ariaPopupsPopupManager = require("./PopupManager");
require("./Beans");
var ariaUtilsDom = require("../utils/Dom");
var ariaUtilsString = require("../utils/String");
var ariaUtilsSize = require("../utils/Size");
var ariaUtilsEvent = require("../utils/Event");
var ariaUtilsDelegate = require("../utils/Delegate");
Expand Down Expand Up @@ -320,10 +321,21 @@ module.exports = Aria.classDefinition({
ariaUtilsDelegate.getMarkup(this._delegateId),
' style="position:absolute;top:-15000px;left:-15000px;visibility:hidden;display:block;"'
];
var role = cfg.role;
if (cfg.waiAria && role) {
html.push(' role="' + role + '"');

if (cfg.waiAria) {
var role = cfg.role;
if (role) {
role = ariaUtilsString.escapeForHTML(role, {attr: true});
html.push(' role="' + role + '"');
}

var labelId = cfg.labelId;
if (labelId) {
labelId = ariaUtilsString.escapeForHTML(labelId, {attr: true});
html.push(' aria-labelledby="' + labelId + '"');
}
}

html.push("></div>");
div.innerHTML = html.join('');

Expand Down Expand Up @@ -690,13 +702,21 @@ module.exports = Aria.classDefinition({
* @protected
*/
_show : function () {
// --------------------------------------------------- destructuring

// Insure that the top left corner is visible
if (this.modalMaskDomElement) {
var conf = this.conf;

var domElement = this.domElement;
var modalMaskDomElement = this.modalMaskDomElement;

// ------------------------------------------------------ processing

// Ensure that the top left corner is visible
if (modalMaskDomElement) {
if (this._containerOverflow == -1) {
this._containerOverflow = this.popupContainer.changeContainerOverflow("hidden");
}

var containerSize = this.popupContainer.getScrollSize();

// Compute the style after scrollbars are removed from the
Expand All @@ -712,12 +732,23 @@ module.exports = Aria.classDefinition({
modalMaskZIndex = this.computedStyle.zIndex;
}

this.modalMaskDomElement.style.cssText = ['left:0px;top:0px;', 'width:', containerSize.width, 'px;', 'height:',
containerSize.height, 'px;', 'z-index:', modalMaskZIndex, ';', 'position:absolute;display:block;'].join('');
modalMaskDomElement.style.cssText = [
'left:0px;',
'top:0px;',
'width:', containerSize.width, 'px;',
'height:', containerSize.height, 'px;',
'z-index:', modalMaskZIndex, ';',
'position:absolute;',
'display:block;'
].join('');

if (conf.waiAria) {
modalMaskDomElement.setAttribute('aria-hidden', 'true');
}

if (this.conf.animateIn) {
if (conf.animateIn) {
this._getMaskAnimator().start("fade", {
to : this.modalMaskDomElement,
to : modalMaskDomElement,
type : 1
});
}
Expand All @@ -739,19 +770,19 @@ module.exports = Aria.classDefinition({
if (this.computedStyle.bottom != null) {
popupPosition = popupPosition.concat('bottom:', this.computedStyle.bottom, 'px;');
}
this.domElement.style.cssText = popupPosition.concat(['z-index:', this.computedStyle.zIndex, ';',
domElement.style.cssText = popupPosition.concat(['z-index:', this.computedStyle.zIndex, ';',
'position:absolute;display:inline-block;']).join('');

if (this.conf.animateIn) {
this._startAnimation(this.conf.animateIn, {
to : this.domElement,
if (conf.animateIn) {
this._startAnimation(conf.animateIn, {
to : domElement,
type : 1
}, false);
}

if (ariaCoreBrowser.isIE7 && !this.isOpen) {
// Without the following line, the autocomplete does not initially display its content on IE7:
this.popupContainer.getContainerElt().appendChild(this.domElement);
this.popupContainer.getContainerElt().appendChild(domElement);
}

},
Expand Down
17 changes: 17 additions & 0 deletions src/aria/widgets/CfgBeans.js
Expand Up @@ -1354,6 +1354,10 @@ module.exports = Aria.beanDefinitions({
$type : "common:Callback",
$description : "Function to be called when the user clicks on the icon."
},
"label" : {
$type : "json:String",
$description : "The label to use for the icon (used for accessibility only)."
},
"sourceImage" : {
$type : "json:Object",
$description : "Configuration for custom image",
Expand All @@ -1374,6 +1378,11 @@ module.exports = Aria.beanDefinitions({
$default : 16
}
}
},
"role" : {
$type : "json:String",
$description : "The role (attribute) to give to the widget (only used when waiAria is true).",
$default : null
}
}
},
Expand Down Expand Up @@ -1973,11 +1982,19 @@ module.exports = Aria.beanDefinitions({
$description : "Whether the dialog has a close button in its title bar.",
$default : true
},
"closeLabel" : {
$type : "json:String",
$description : "The label to use for the close icon (can be used in various ways such as for tooltip or accessibility)."
},
"maximizable" : {
$type : "json:Boolean",
$description : "Whether the dialog has a maximize button in its title bar. Note that you can set this to false and programatically maximize the Dialog to achieve a fullscreen-only Dialog solution.",
$default : false
},
"maximizeLabel" : {
$type : "json:String",
$description : "The label to use for the maximize icon (can be used in various ways such as for tooltip or accessibility)."
},
"closeOnMouseClick" : {
$type : "json:Boolean",
$description : "Close the dialog when the user clicks outside of it",
Expand Down
2 changes: 1 addition & 1 deletion src/aria/widgets/GlobalStyle.tpl.css
Expand Up @@ -118,7 +118,7 @@

{if aria.core.Browser.isSafari || aria.core.Browser.isChrome || aria.core.Browser.isOpera}
*:focus {
outline: 0;
outline-width: 0;
}
{/if}

Expand Down
102 changes: 86 additions & 16 deletions src/aria/widgets/Icon.js
Expand Up @@ -13,6 +13,7 @@
* limitations under the License.
*/
var Aria = require("../Aria");
var ariaUtilsString = require("../utils/String");
var ariaWidgetsIconStyle = require("./IconStyle.tpl.css");
var ariaWidgetsWidget = require("./Widget");
var ariaCoreTplClassLoader = require("../core/TplClassLoader");
Expand Down Expand Up @@ -67,43 +68,102 @@ module.exports = Aria.classDefinition({
ICON_NOT_FOUND : "%1Icon was not found: %2"
},
$prototype : {

/**
* Override widget _widgetMarkup method.
* @protected
* @override
* @param {aria.templates.MarkupWriter} out the html output writer
*/
_widgetMarkup : function (out) {
// --------------------------------------------------- destructuring

var cfg = this._cfg;
var id = this._domId;
var icon = cfg.icon;
var tooltip = cfg.tooltip;
var tabIndex = cfg.tabIndex;
var waiAria = cfg.waiAria;
var label = cfg.label;
var role = cfg.role;

var id = this._domId;
var iconInfo = this._iconInfo;
var extraAttributes = this.extraAttributes;

if (tooltip != null && tooltip !== '') {
tooltip = 'title="' + tooltip + '" ';
} else {
tooltip = '';
// ------------------------------------------------------ processing

var attributes = [];

function addAttribute(key, value) {
value = '' + value;
value = ariaUtilsString.escapeForHTML(value, {attr: true});
attributes.push(key + '="' + value + '"');
}

var delegationMarkup = "";
// delegationMarkup ------------------------------------------------

var delegateManager = aria.utils.Delegate;
if (!this._delegateId) {
this._delegateId = delegateManager.add({
var delegateId = this._delegateId;

if (!delegateId) {
delegateId = delegateManager.add({
fn : this.delegate,
scope : this
});
this._delegateId = delegateId;
}
delegationMarkup = delegateManager.getMarkup(this._delegateId) + " ";

if (!iconInfo.spriteURL && cfg.icon) {
var classes = aria.widgets.AriaSkinInterface.getSkinObject("Icon", cfg.icon.split(":")[0], true).content[cfg.icon.split(":")[1]];
out.write(['<span id="', id, '" class="xWidget ', classes, '" ', this.extraAttributes, '></span>'].join(''));
var delegationMarkup = delegateManager.getMarkup(delegateId);

// icon ------------------------------------------------------------

addAttribute('id', id);


var style = null;

if (!iconInfo.spriteURL && icon) {
var parts = icon.split(":");
var skinclass = parts[0];
var contentKey = parts[1];

var classes = aria.widgets.AriaSkinInterface.getSkinObject("Icon", skinclass, true).content[contentKey];

addAttribute('class', ['xWidget'].concat(classes).join(' '));
} else {
out.write(['<span id="', id, '" class="', this._getIconClasses(iconInfo), '" ', tooltip,
delegationMarkup, 'style="', this._getIconStyle(iconInfo), '" ', this.extraAttributes,
'></span>'].join(''));
addAttribute('class', this._getIconClasses(iconInfo));
if (tooltip != null && tooltip !== '') {
tooltip = addAttribute('title', tooltip);
}
attributes.push(delegationMarkup);

style = this._getIconStyle(iconInfo);
}

if (tabIndex != null) {
tabIndex = this._calculateTabIndex();
addAttribute('tabindex', tabIndex);
}

if (waiAria && label) {
addAttribute('aria-label', label);
}

if (waiAria && role) {
addAttribute('role', role);
}

if (style) {
addAttribute('style', style);
}

attributes.push(extraAttributes);

attributes = attributes.join(' ');
var markup = '<span ' + attributes + '></span>';

// ---------------------------------------------------------- output

out.write(markup);
},

/**
Expand Down Expand Up @@ -202,6 +262,16 @@ module.exports = Aria.classDefinition({
return cssClasses;
},

_dom_onkeydown : function (domEvent) {
var keyCode = domEvent.keyCode;

if (keyCode == domEvent.KC_ENTER || keyCode == domEvent.KC_SPACE) {
return this._dom_onclick(domEvent);
}

return true;
},

/**
* The method called when the markup is clicked
* @param {aria.DomEvent} evt Event
Expand Down
4 changes: 4 additions & 0 deletions src/aria/widgets/IconStyle.tpl.css
Expand Up @@ -25,6 +25,10 @@

/* Icon class: ${info.skinClassName} */
{if info.skinClass.spriteURL}
.xICN${info.skinClassName}:focus {
outline-width: initial;
}

.xICN${info.skinClassName} {
{if !widgetSettings.middleAlignment}vertical-align:top;{/if}
font-size:1px;
Expand Down
23 changes: 11 additions & 12 deletions src/aria/widgets/action/Button.js
Expand Up @@ -350,14 +350,11 @@ module.exports = Aria.classDefinition({
* @private
*/
_dom_onclick : (ariaCoreBrowser.isChrome || ariaCoreBrowser.isOpera || ariaCoreBrowser.isSafari) ? function (domEvent) {
this._keyPressed = false;
return; // we don't catch onclick's for buttons on chrome & safari. we catch mouseup's instead
// we don't catch onclick's for buttons on chrome & safari. we catch mouseup's instead
} : function (domEvent) {
if (this._keyPressed) {
this._keyPressed = false;
return;
if (!this._keyPressed) {
this._performAction(domEvent);
}
this._performAction(domEvent);
},

/**
Expand All @@ -367,14 +364,15 @@ module.exports = Aria.classDefinition({
*/
_dom_onkeyup : function (domEvt) {
if (domEvt.keyCode == ariaDomEvent.KC_SPACE || domEvt.keyCode == ariaDomEvent.KC_ENTER) {
this._keyPressed = false;
this._updateState();
if (this._keyPressed) {
this._keyPressed = false;
this._updateState();

if (!this._performAction(domEvt)) {
domEvt.stopPropagation();
return false;
if (!this._performAction(domEvt)) {
domEvt.stopPropagation();
return false;
}
}
return true;
}
return true;
},
Expand All @@ -394,6 +392,7 @@ module.exports = Aria.classDefinition({
*/
_dom_onblur : function (domEvt) {
this._focused = false;
this._keyPressed = false;
this._updateState();
}
}
Expand Down

0 comments on commit 8cd4434

Please sign in to comment.