MDL-71669 editor_atto: Set proper roles to toolbar menus

* Menu button fix
  - Added aria-haspopup, aria-controls, and aria-expanded attributes.
* Menu fixes
  - Added aria-labelledby that points to the menu button label.
  - Removed the dialog role in the menu's container.
  - The ul tag needs the "menu" role.
  - The li tag needs the role "none" instead of the "presentation" role

Reference:
https://www.w3.org/TR/wai-aria-practices-1.1/#menubutton
This commit is contained in:
Jun Pataleta 2021-06-01 12:04:12 +08:00
parent 338b60f43b
commit 9d943c4ff9
8 changed files with 141 additions and 42 deletions

View File

@ -29,9 +29,9 @@ var LOGNAME = 'moodle-editor_atto-menu';
var MENUDIALOGUE = '' +
'<div class="open {{config.buttonClass}} atto_menu" ' +
'style="min-width:{{config.innerOverlayWidth}};">' +
'<ul class="dropdown-menu">' +
'<ul class="dropdown-menu" role="menu" id="{{config.buttonId}}_menu" aria-labelledby="{{config.buttonId}}">' +
'{{#each config.items}}' +
'<li role="presentation" class="atto_menuentry">' +
'<li role="none" class="atto_menuentry">' +
'<a href="#" role="menuitem" data-index="{{@index}}" {{#each data}}data-{{@key}}="{{this}}"{{/each}}>' +
'{{{text}}}' +
'</a>' +
@ -67,12 +67,19 @@ Y.extend(Menu, M.core.dialogue, {
*/
_menuHandlers: null,
/**
* The menu button that controls this menu.
*/
_menuButton: null,
initializer: function(config) {
var headertext,
var headerText,
bb;
this._menuHandlers = [];
this._menuButton = document.getElementById(config.buttonId);
// Create the actual button.
var template = Y.Handlebars.compile(MENUDIALOGUE),
menu = Y.Node.create(template({
@ -83,14 +90,24 @@ Y.extend(Menu, M.core.dialogue, {
bb = this.get('boundingBox');
bb.addClass('editor_atto_controlmenu');
bb.addClass('editor_atto_menu');
bb.one('.moodle-dialogue-wrap')
.removeClass('moodle-dialogue-wrap')
.addClass('moodle-dialogue-content');
headertext = Y.Node.create('<h3/>')
// Get the dialogue container for this menu.
var content = bb.one('.moodle-dialogue-wrap');
content.removeClass('moodle-dialogue-wrap')
.addClass('moodle-dialogue-content');
// Remove the dialog role attribute.
content.removeAttribute('role');
// Remove aria-labelledby in the container. The aria-labelledby attribute is properly set in the menu's template.
content.removeAttribute('aria-labelledby');
// Render heading if necessary.
headerText = this.get('headerText').trim();
if (headerText) {
var heading = Y.Node.create('<h3/>')
.addClass('accesshide')
.setHTML(this.get('headerText'));
this.get('bodyContent').prepend(headertext);
.setHTML(headerText);
this.get('bodyContent').prepend(heading);
}
// Hide the header and footer node entirely.
this.headerNode.hide();
@ -152,6 +169,11 @@ Y.extend(Menu, M.core.dialogue, {
e.preventDefault();
}
// Remove menu button's aria-expanded attribute when this menu is hidden.
if (this._menuButton) {
this._menuButton.removeAttribute('aria-expanded');
}
return Menu.superclass.hide.call(this, arguments);
},

View File

@ -1 +1 @@
YUI.add("moodle-editor_atto-menu",function(i,e){var s='<div class="open {{config.buttonClass}} atto_menu" style="min-width:{{config.innerOverlayWidth}};"><ul class="dropdown-menu">{{#each config.items}}<li role="presentation" class="atto_menuentry"><a href="#" role="menuitem" data-index="{{@index}}" {{#each data}}data-{{@key}}="{{this}}"{{/each}}>{{{text}}}</a></li>{{/each}}</ul></div>',t=function(){t.superclass.constructor.apply(this,arguments)};i.extend(t,M.core.dialogue,{_menuHandlers:null,initializer:function(e){var t,n,o,a;this._menuHandlers=[],o=i.Handlebars.compile(s),a=i.Node.create(o({config:e})),this.set("bodyContent",a),(n=this.get("boundingBox")).addClass("editor_atto_controlmenu"),n.addClass("editor_atto_menu"),n.one(".moodle-dialogue-wrap").removeClass("moodle-dialogue-wrap").addClass("moodle-dialogue-content"),t=i.Node.create("<h3/>").addClass("accesshide").setHTML(this.get("headerText")),this.get("bodyContent").prepend(t),this.headerNode.hide(),this.footerNode.hide(),this._setupHandlers()},_setupHandlers:function(){var e=this.get("contentBox");this._menuHandlers.push(e.delegate("key",this._chooseMenuItem,"32, enter",".atto_menuentry",this),e.delegate("key",this._handleKeyboardEvent,"down:38,40",".dropdown-menu",this),e.on("focusoutside",this.hide,this),e.delegate("key",this.hide,"down:37,39,esc",".dropdown-menu",this))},_chooseMenuItem:function(e){e.target.simulate("click"),e.preventDefault()},hide:function(e){if(!0!==this.get("preventHideMenu"))return e&&e.preventDefault(),t.superclass.hide.call(this,arguments)},_handleKeyboardEvent:function(e){var t,n,o,a,i,s,d;for(e.preventDefault(),t=e.currentTarget.all('a[role="menuitem"]'),n=!1,a=1,i=o=0,s=e.target.ancestor('a[role="menuitem"]',!0);!n&&o<t.size();)t.item(o)===s?n=!0:o++;if(n){for(38===e.keyCode&&(a=-1);(o+=a)<0?o=t.size()-1:o>=t.size()&&(o=0),d=t.item(o),++i<t.size()&&d!==s&&d.hasAttribute("hidden"););d&&d.focus(),e.preventDefault(),e.stopImmediatePropagation()}}},{NAME:"menu",ATTRS:{headerText:{value:""}}}),i.Base.modifyAttrs(t,{width:{value:"auto"},hideOn:{value:[{eventName:"clickoutside"}]},extraClasses:{value:["editor_atto_menu"]},responsive:{value:!1},visible:{value:!1},center:{value:!1},closeButton:{value:!1}}),i.namespace("M.editor_atto").Menu=t},"@VERSION@",{requires:["moodle-core-notification-dialogue","node","event","event-custom"]});
YUI.add("moodle-editor_atto-menu",function(s,e){var u='<div class="open {{config.buttonClass}} atto_menu" style="min-width:{{config.innerOverlayWidth}};"><ul class="dropdown-menu" role="menu" id="{{config.buttonId}}_menu" aria-labelledby="{{config.buttonId}}">{{#each config.items}}<li role="none" class="atto_menuentry"><a href="#" role="menuitem" data-index="{{@index}}" {{#each data}}data-{{@key}}="{{this}}"{{/each}}>{{{text}}}</a></li>{{/each}}</ul></div>',t=function(){t.superclass.constructor.apply(this,arguments)};s.extend(t,M.core.dialogue,{_menuHandlers:null,_menuButton:null,initializer:function(e){var t,n,o,a,i,d;this._menuHandlers=[],this._menuButton=document.getElementById(e.buttonId),o=s.Handlebars.compile(u),a=s.Node.create(o({config:e})),this.set("bodyContent",a),(n=this.get("boundingBox")).addClass("editor_atto_controlmenu"),n.addClass("editor_atto_menu"),(i=n.one(".moodle-dialogue-wrap")).removeClass("moodle-dialogue-wrap").addClass("moodle-dialogue-content"),i.removeAttribute("role"),i.removeAttribute("aria-labelledby"),(t=this.get("headerText").trim())&&(d=s.Node.create("<h3/>").addClass("accesshide").setHTML(t),this.get("bodyContent").prepend(d)),this.headerNode.hide(),this.footerNode.hide(),this._setupHandlers()},_setupHandlers:function(){var e=this.get("contentBox");this._menuHandlers.push(e.delegate("key",this._chooseMenuItem,"32, enter",".atto_menuentry",this),e.delegate("key",this._handleKeyboardEvent,"down:38,40",".dropdown-menu",this),e.on("focusoutside",this.hide,this),e.delegate("key",this.hide,"down:37,39,esc",".dropdown-menu",this))},_chooseMenuItem:function(e){e.target.simulate("click"),e.preventDefault()},hide:function(e){if(!0!==this.get("preventHideMenu"))return e&&e.preventDefault(),this._menuButton&&this._menuButton.removeAttribute("aria-expanded"),t.superclass.hide.call(this,arguments)},_handleKeyboardEvent:function(e){var t,n,o,a,i,d,s;for(e.preventDefault(),t=e.currentTarget.all('a[role="menuitem"]'),n=!1,a=1,i=o=0,d=e.target.ancestor('a[role="menuitem"]',!0);!n&&o<t.size();)t.item(o)===d?n=!0:o++;if(n){for(38===e.keyCode&&(a=-1);(o+=a)<0?o=t.size()-1:o>=t.size()&&(o=0),s=t.item(o),++i<t.size()&&s!==d&&s.hasAttribute("hidden"););s&&s.focus(),e.preventDefault(),e.stopImmediatePropagation()}}},{NAME:"menu",ATTRS:{headerText:{value:""}}}),s.Base.modifyAttrs(t,{width:{value:"auto"},hideOn:{value:[{eventName:"clickoutside"}]},extraClasses:{value:["editor_atto_menu"]},responsive:{value:!1},visible:{value:!1},center:{value:!1},closeButton:{value:!1}}),s.namespace("M.editor_atto").Menu=t},"@VERSION@",{requires:["moodle-core-notification-dialogue","node","event","event-custom"]});

View File

@ -29,9 +29,9 @@ var LOGNAME = 'moodle-editor_atto-menu';
var MENUDIALOGUE = '' +
'<div class="open {{config.buttonClass}} atto_menu" ' +
'style="min-width:{{config.innerOverlayWidth}};">' +
'<ul class="dropdown-menu">' +
'<ul class="dropdown-menu" role="menu" id="{{config.buttonId}}_menu" aria-labelledby="{{config.buttonId}}">' +
'{{#each config.items}}' +
'<li role="presentation" class="atto_menuentry">' +
'<li role="none" class="atto_menuentry">' +
'<a href="#" role="menuitem" data-index="{{@index}}" {{#each data}}data-{{@key}}="{{this}}"{{/each}}>' +
'{{{text}}}' +
'</a>' +
@ -67,12 +67,19 @@ Y.extend(Menu, M.core.dialogue, {
*/
_menuHandlers: null,
/**
* The menu button that controls this menu.
*/
_menuButton: null,
initializer: function(config) {
var headertext,
var headerText,
bb;
this._menuHandlers = [];
this._menuButton = document.getElementById(config.buttonId);
// Create the actual button.
var template = Y.Handlebars.compile(MENUDIALOGUE),
menu = Y.Node.create(template({
@ -83,14 +90,24 @@ Y.extend(Menu, M.core.dialogue, {
bb = this.get('boundingBox');
bb.addClass('editor_atto_controlmenu');
bb.addClass('editor_atto_menu');
bb.one('.moodle-dialogue-wrap')
.removeClass('moodle-dialogue-wrap')
.addClass('moodle-dialogue-content');
headertext = Y.Node.create('<h3/>')
// Get the dialogue container for this menu.
var content = bb.one('.moodle-dialogue-wrap');
content.removeClass('moodle-dialogue-wrap')
.addClass('moodle-dialogue-content');
// Remove the dialog role attribute.
content.removeAttribute('role');
// Remove aria-labelledby in the container. The aria-labelledby attribute is properly set in the menu's template.
content.removeAttribute('aria-labelledby');
// Render heading if necessary.
headerText = this.get('headerText').trim();
if (headerText) {
var heading = Y.Node.create('<h3/>')
.addClass('accesshide')
.setHTML(this.get('headerText'));
this.get('bodyContent').prepend(headertext);
.setHTML(headerText);
this.get('bodyContent').prepend(heading);
}
// Hide the header and footer node entirely.
this.headerNode.hide();
@ -152,6 +169,11 @@ Y.extend(Menu, M.core.dialogue, {
e.preventDefault();
}
// Remove menu button's aria-expanded attribute when this menu is hidden.
if (this._menuButton) {
this._menuButton.removeAttribute('aria-expanded');
}
return Menu.superclass.hide.call(this, arguments);
},

View File

@ -179,9 +179,11 @@ Y.namespace('M.editor_atto').EditorPlugin = EditorPlugin;
var MENUTEMPLATE = '' +
'<button class="{{buttonClass}} atto_hasmenu" ' +
'id="{{id}}" ' +
'tabindex="-1" ' +
'type="button" ' +
'title="{{title}}">' +
'title="{{title}}" ' +
'aria-haspopup="true" ' +
'aria-controls="{{id}}_menu">' +
'<span class="editor_atto_menu_icon"></span>' +
'<span class="editor_atto_menu_expand"></span>' +
'</button>';
@ -527,13 +529,18 @@ EditorPluginButtons.prototype = {
}
// Create the actual button.
var id = 'atto_' + pluginname + '_menubutton_' + Y.stamp(this);
var template = Y.Handlebars.compile(MENUTEMPLATE);
button = Y.Node.create(template({
buttonClass: buttonClass,
config: config,
title: title
title: title,
id: id
}));
// Add this button id to the config. It will be used in the menu later.
config.buttonId = id;
window.require(['core/templates'], function(Templates) {
Templates.renderPix(config.icon, config.iconComponent, title).then(function(iconhtml) {
button.one(CSS.MENUICON).append(iconhtml);
@ -585,7 +592,8 @@ EditorPluginButtons.prototype = {
return;
}
if (e.currentTarget.ancestor('button', true).hasAttribute(DISABLED)) {
var menuButton = e.currentTarget.ancestor('button', true);
if (menuButton.hasAttribute(DISABLED)) {
// Exit early if the clicked button was disabled.
return;
}
@ -627,6 +635,9 @@ EditorPluginButtons.prototype = {
// Display the menu.
menuDialogue.show();
// Indicate that the menu is expanded.
menuButton.setAttribute("aria-expanded", true);
// Position it next to the button which opened it.
menuDialogue.align(this.buttons[config.buttonName], [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);

File diff suppressed because one or more lines are too long

View File

@ -179,9 +179,11 @@ Y.namespace('M.editor_atto').EditorPlugin = EditorPlugin;
var MENUTEMPLATE = '' +
'<button class="{{buttonClass}} atto_hasmenu" ' +
'id="{{id}}" ' +
'tabindex="-1" ' +
'type="button" ' +
'title="{{title}}">' +
'title="{{title}}" ' +
'aria-haspopup="true" ' +
'aria-controls="{{id}}_menu">' +
'<span class="editor_atto_menu_icon"></span>' +
'<span class="editor_atto_menu_expand"></span>' +
'</button>';
@ -525,13 +527,18 @@ EditorPluginButtons.prototype = {
}
// Create the actual button.
var id = 'atto_' + pluginname + '_menubutton_' + Y.stamp(this);
var template = Y.Handlebars.compile(MENUTEMPLATE);
button = Y.Node.create(template({
buttonClass: buttonClass,
config: config,
title: title
title: title,
id: id
}));
// Add this button id to the config. It will be used in the menu later.
config.buttonId = id;
window.require(['core/templates'], function(Templates) {
Templates.renderPix(config.icon, config.iconComponent, title).then(function(iconhtml) {
button.one(CSS.MENUICON).append(iconhtml);
@ -583,7 +590,8 @@ EditorPluginButtons.prototype = {
return;
}
if (e.currentTarget.ancestor('button', true).hasAttribute(DISABLED)) {
var menuButton = e.currentTarget.ancestor('button', true);
if (menuButton.hasAttribute(DISABLED)) {
// Exit early if the clicked button was disabled.
return;
}
@ -625,6 +633,9 @@ EditorPluginButtons.prototype = {
// Display the menu.
menuDialogue.show();
// Indicate that the menu is expanded.
menuButton.setAttribute("aria-expanded", true);
// Position it next to the button which opened it.
menuDialogue.align(this.buttons[config.buttonName], [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);

View File

@ -29,9 +29,11 @@
var MENUTEMPLATE = '' +
'<button class="{{buttonClass}} atto_hasmenu" ' +
'id="{{id}}" ' +
'tabindex="-1" ' +
'type="button" ' +
'title="{{title}}">' +
'title="{{title}}" ' +
'aria-haspopup="true" ' +
'aria-controls="{{id}}_menu">' +
'<span class="editor_atto_menu_icon"></span>' +
'<span class="editor_atto_menu_expand"></span>' +
'</button>';
@ -377,13 +379,18 @@ EditorPluginButtons.prototype = {
}
// Create the actual button.
var id = 'atto_' + pluginname + '_menubutton_' + Y.stamp(this);
var template = Y.Handlebars.compile(MENUTEMPLATE);
button = Y.Node.create(template({
buttonClass: buttonClass,
config: config,
title: title
title: title,
id: id
}));
// Add this button id to the config. It will be used in the menu later.
config.buttonId = id;
window.require(['core/templates'], function(Templates) {
Templates.renderPix(config.icon, config.iconComponent, title).then(function(iconhtml) {
button.one(CSS.MENUICON).append(iconhtml);
@ -435,7 +442,8 @@ EditorPluginButtons.prototype = {
return;
}
if (e.currentTarget.ancestor('button', true).hasAttribute(DISABLED)) {
var menuButton = e.currentTarget.ancestor('button', true);
if (menuButton.hasAttribute(DISABLED)) {
// Exit early if the clicked button was disabled.
return;
}
@ -477,6 +485,9 @@ EditorPluginButtons.prototype = {
// Display the menu.
menuDialogue.show();
// Indicate that the menu is expanded.
menuButton.setAttribute("aria-expanded", true);
// Position it next to the button which opened it.
menuDialogue.align(this.buttons[config.buttonName], [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);

View File

@ -27,9 +27,9 @@ var LOGNAME = 'moodle-editor_atto-menu';
var MENUDIALOGUE = '' +
'<div class="open {{config.buttonClass}} atto_menu" ' +
'style="min-width:{{config.innerOverlayWidth}};">' +
'<ul class="dropdown-menu">' +
'<ul class="dropdown-menu" role="menu" id="{{config.buttonId}}_menu" aria-labelledby="{{config.buttonId}}">' +
'{{#each config.items}}' +
'<li role="presentation" class="atto_menuentry">' +
'<li role="none" class="atto_menuentry">' +
'<a href="#" role="menuitem" data-index="{{@index}}" {{#each data}}data-{{@key}}="{{this}}"{{/each}}>' +
'{{{text}}}' +
'</a>' +
@ -65,12 +65,19 @@ Y.extend(Menu, M.core.dialogue, {
*/
_menuHandlers: null,
/**
* The menu button that controls this menu.
*/
_menuButton: null,
initializer: function(config) {
var headertext,
var headerText,
bb;
this._menuHandlers = [];
this._menuButton = document.getElementById(config.buttonId);
// Create the actual button.
var template = Y.Handlebars.compile(MENUDIALOGUE),
menu = Y.Node.create(template({
@ -81,14 +88,24 @@ Y.extend(Menu, M.core.dialogue, {
bb = this.get('boundingBox');
bb.addClass('editor_atto_controlmenu');
bb.addClass('editor_atto_menu');
bb.one('.moodle-dialogue-wrap')
.removeClass('moodle-dialogue-wrap')
.addClass('moodle-dialogue-content');
headertext = Y.Node.create('<h3/>')
// Get the dialogue container for this menu.
var content = bb.one('.moodle-dialogue-wrap');
content.removeClass('moodle-dialogue-wrap')
.addClass('moodle-dialogue-content');
// Remove the dialog role attribute.
content.removeAttribute('role');
// Remove aria-labelledby in the container. The aria-labelledby attribute is properly set in the menu's template.
content.removeAttribute('aria-labelledby');
// Render heading if necessary.
headerText = this.get('headerText').trim();
if (headerText) {
var heading = Y.Node.create('<h3/>')
.addClass('accesshide')
.setHTML(this.get('headerText'));
this.get('bodyContent').prepend(headertext);
.setHTML(headerText);
this.get('bodyContent').prepend(heading);
}
// Hide the header and footer node entirely.
this.headerNode.hide();
@ -150,6 +167,11 @@ Y.extend(Menu, M.core.dialogue, {
e.preventDefault();
}
// Remove menu button's aria-expanded attribute when this menu is hidden.
if (this._menuButton) {
this._menuButton.removeAttribute('aria-expanded');
}
return Menu.superclass.hide.call(this, arguments);
},