mirror of
https://github.com/moodle/moodle.git
synced 2025-04-16 05:54:19 +02:00
MDL-62139 output: Accessible action menus
Add label on the "gear" menu and default keyboard and focus controls.
This commit is contained in:
parent
c2f1dbcf49
commit
191a1c7c23
@ -26,6 +26,7 @@ $string['abouttobeinstalled'] = 'about to be installed';
|
||||
$string['action'] = 'Action';
|
||||
$string['actionchoice'] = 'What do you want to do with the file \'{$a}\'?';
|
||||
$string['actions'] = 'Actions';
|
||||
$string['actionsmenu'] = 'Actions menu';
|
||||
$string['active'] = 'Active';
|
||||
$string['activeusers'] = 'Active users';
|
||||
$string['activities'] = 'Activities';
|
||||
|
@ -4293,7 +4293,7 @@ class action_menu implements renderable, templatable {
|
||||
$pixicon = '<b class="caret"></b>';
|
||||
$linkclasses[] = 'textmenu';
|
||||
} else {
|
||||
$title = new lang_string('actions', 'moodle');
|
||||
$title = new lang_string('actionsmenu', 'moodle');
|
||||
$this->actionicon = new pix_icon(
|
||||
't/edit_menu',
|
||||
'',
|
||||
@ -4482,8 +4482,9 @@ class action_menu implements renderable, templatable {
|
||||
$primary->menutrigger = $this->menutrigger;
|
||||
$primary->triggerextraclasses = $this->triggerextraclasses;
|
||||
} else {
|
||||
$primary->title = get_string('actions');
|
||||
$actionicon = new pix_icon('t/edit_menu', '', 'moodle', ['class' => 'iconsmall actionmenu', 'title' => '']);
|
||||
$primary->title = get_string('actionsmenu');
|
||||
$iconattributes = ['class' => 'iconsmall actionmenu', 'title' => $primary->title];
|
||||
$actionicon = new pix_icon('t/edit_menu', '', 'moodle', $iconattributes);
|
||||
}
|
||||
|
||||
if ($actionicon instanceof pix_icon) {
|
||||
|
@ -32,5 +32,5 @@
|
||||
<a href="{{url}}" class="{{classes}}" {{#attributes}}{{name}}={{#quote}}{{value}}{{/quote}} {{/attributes}}{{#showtext}}aria-labelledby="actionmenuaction-{{instance}}"{{/showtext}}>{{#icon}}{{#pix}}{{key}}, {{component}}, {{title}}{{/pix}}{{/icon}}{{#showtext}}<span class="menu-action-text" id="actionmenuaction-{{instance}}">{{{text}}}</span>{{/showtext}}</a>
|
||||
{{/disabled}}
|
||||
{{#disabled}}
|
||||
<span class="currentlink" role="menuitem">{{#icon}}{{#pix}}{{key}}, {{component}}, {{title}}{{/pix}}{{/icon}}{{{text}}}</span>
|
||||
<span class="currentlink" role="button">{{#icon}}{{#pix}}{{key}}, {{component}}, {{title}}{{/pix}}{{/icon}}{{{text}}}</span>
|
||||
{{/disabled}}
|
||||
|
@ -27,4 +27,4 @@
|
||||
"triggerextraclasses": ""
|
||||
}
|
||||
}}
|
||||
<a href="#" class="{{triggerextraclasses}} toggle-display {{#menutrigger}}textmenu{{/menutrigger}}" id="action-menu-toggle-{{instance}}" title="{{title}}" role="menuitem">{{{actiontext}}}{{{menutrigger}}}{{#icon}}{{#pix}}{{key}}, {{component}}, {{title}}{{/pix}}{{/icon}}{{#rawicon}}{{{.}}}{{/rawicon}}{{#menutrigger}}<b class="caret"></b>{{/menutrigger}}</a>
|
||||
<a tabindex="0" class="{{triggerextraclasses}} toggle-display {{#menutrigger}}textmenu{{/menutrigger}}" id="action-menu-toggle-{{instance}}" aria-label="{{title}}" role="menuitem" aria-controls="action-menu-{{instance}}-menu">{{{actiontext}}}{{{menutrigger}}}{{#icon}}{{#pix}}{{key}}, {{component}}, {{title}}{{/pix}}{{/icon}}{{#rawicon}}{{{.}}}{{/rawicon}}{{#menutrigger}}<b class="caret"></b>{{/menutrigger}}</a>
|
||||
|
@ -86,6 +86,8 @@ test
|
||||
$this->setAdminUser();
|
||||
$PAGE->set_url('/');
|
||||
$CFG->theme = 'clean';
|
||||
$PAGE->reset_theme_and_output();
|
||||
$PAGE->initialise_theme_and_output();
|
||||
|
||||
// Set the configuration.
|
||||
set_config('customusermenuitems', $data);
|
||||
@ -94,7 +96,8 @@ test
|
||||
$dividercount += 2;
|
||||
|
||||
// The basic entry count will additionally include the wrapper menu, Dashboard, Profile, Logout and switch roles link.
|
||||
$entrycount += 4;
|
||||
// On clean theme only, the trigger is also a menuitem.
|
||||
$entrycount += 5;
|
||||
|
||||
$output = $OUTPUT->user_menu($USER);
|
||||
preg_match_all('/<a [^>]+role="menuitem"[^>]+>/', $output, $results);
|
||||
|
1
theme/boost/amd/build/aria.min.js
vendored
Normal file
1
theme/boost/amd/build/aria.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
define(["jquery"],function(a){return{init:function(){var b=!1,c=function(){b=!0},d=function(){var a=b;return b=!1,a};a('[data-toggle="dropdown"]').keydown(function(b){var d,e=b.which||b.keyCode;38==e&&c(),27==e&&(d=a(b.target).attr("aria-expanded"),b.preventDefault(),"false"==d&&a(b.target).click()),32!=e&&13!=e||(b.preventDefault(),a(b.target).click())});var e=function(b){var c=function(){a(this).focus()}.bind(b);setTimeout(c,50)};a(".dropdown").on("shown.bs.dropdown",function(b){var c=a(b.target).find('[role="menu"]'),f=!1,g=!1;c&&(f=a(c).find('[role="menuitem"]')),f&&f.length>0&&(g=d()?f[f.length-1]:f[0]),g&&e(g)}),a('.dropdown [role="menu"] [role="menuitem"]').keypress(function(b){var c,d,f=String.fromCharCode(b.which||b.keyCode),g=a(b.target).closest('[role="menu"]'),h=0,i=!1;if(g&&(i=a(g).find('[role="menuitem"]')))for(f=f.toLowerCase(),h=0;h<i.length;h++)if(c=a(i[h]),d=c.text().trim().toLowerCase(),0==d.indexOf(f)){e(c);break}}),a('.dropdown [role="menu"] [role="menuitem"]').keydown(function(b){var c=b.which||b.keyCode,d=!1,f=a(b.target).closest('[role="menu"]'),g=0,h=!1;if(f&&(h=a(f).find('[role="menuitem"]'))){if(40==c){for(g=0;g<h.length-1;g++)if(h[g]==b.target){d=h[g+1];break}d||(d=h[0])}else if(38==c){for(g=1;g<h.length;g++)if(h[g]==b.target){d=h[g-1];break}d||(d=h[h.length-1])}else 36==c?d=h[0]:35==c&&(d=h[h.length-1]);d&&(b.preventDefault(),e(d))}}),a(".dropdown").on("hidden.bs.dropdown",function(b){var c=a(b.target).find('[data-toggle="dropdown"]');c&&e(c)})}}});
|
2
theme/boost/amd/build/loader.min.js
vendored
2
theme/boost/amd/build/loader.min.js
vendored
@ -1 +1 @@
|
||||
define(["jquery","./tether","core/event"],function(a,b,c){return window.jQuery=a,window.Tether=b,require(["theme_boost/util","theme_boost/alert","theme_boost/button","theme_boost/carousel","theme_boost/collapse","theme_boost/dropdown","theme_boost/modal","theme_boost/scrollspy","theme_boost/tab","theme_boost/tooltip","theme_boost/popover"],function(){a("body").popover({trigger:"focus",selector:"[data-toggle=popover][data-trigger!=hover]"}),a("html").popover({container:"body",selector:"[data-toggle=popover][data-trigger=hover]",trigger:"hover",delay:{hide:500}}),c.getLegacyEvents().done(function(b){a(document).on(b.FILTER_CONTENT_UPDATED,function(){a("body").popover({selector:'[data-toggle="popover"]',trigger:"focus"})})})}),{}});
|
||||
define(["jquery","./tether","core/event"],function(a,b,c){return window.jQuery=a,window.Tether=b,require(["theme_boost/aria","theme_boost/util","theme_boost/alert","theme_boost/button","theme_boost/carousel","theme_boost/collapse","theme_boost/dropdown","theme_boost/modal","theme_boost/scrollspy","theme_boost/tab","theme_boost/tooltip","theme_boost/popover"],function(b){a("body").popover({trigger:"focus",selector:"[data-toggle=popover][data-trigger!=hover]"}),a("html").popover({container:"body",selector:"[data-toggle=popover][data-trigger=hover]",trigger:"hover",delay:{hide:500}}),c.getLegacyEvents().done(function(b){a(document).on(b.FILTER_CONTENT_UPDATED,function(){a("body").popover({selector:'[data-toggle="popover"]',trigger:"focus"})})}),b.init()}),{}});
|
188
theme/boost/amd/src/aria.js
Normal file
188
theme/boost/amd/src/aria.js
Normal file
@ -0,0 +1,188 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Enhancements to Bootstrap components for accessibility.
|
||||
*
|
||||
* @module theme_boost/aria
|
||||
* @copyright 2018 Damyon Wiese <damyon@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(['jquery'], function($) {
|
||||
return {
|
||||
init: function() {
|
||||
// Drop downs from bootstrap don't support keyboard accessibility by default.
|
||||
var focusEnd = false,
|
||||
setFocusEnd = function() {
|
||||
focusEnd = true;
|
||||
},
|
||||
getFocusEnd = function() {
|
||||
var result = focusEnd;
|
||||
focusEnd = false;
|
||||
return result;
|
||||
};
|
||||
|
||||
// Special handling for "up" keyboard control.
|
||||
$('[data-toggle="dropdown"]').keydown(function(e) {
|
||||
var trigger = e.which || e.keyCode,
|
||||
expanded;
|
||||
|
||||
// Up key opens the menu at the end.
|
||||
if (trigger == 38) {
|
||||
// Focus the end of the menu, not the beginning.
|
||||
setFocusEnd();
|
||||
}
|
||||
|
||||
// Escape key only closes the menu, it doesn't open it.
|
||||
if (trigger == 27) {
|
||||
expanded = $(e.target).attr('aria-expanded');
|
||||
e.preventDefault();
|
||||
if (expanded == "false") {
|
||||
$(e.target).click();
|
||||
}
|
||||
}
|
||||
|
||||
// Space key or Enter key opens the menu.
|
||||
if (trigger == 32 || trigger == 13) {
|
||||
// Cancel random scroll.
|
||||
e.preventDefault();
|
||||
// Open the menu instead.
|
||||
$(e.target).click();
|
||||
}
|
||||
});
|
||||
|
||||
// Special handling for navigation keys when menu is open.
|
||||
var shiftFocus = function(element) {
|
||||
var delayedFocus = function() {
|
||||
$(this).focus();
|
||||
}.bind(element);
|
||||
setTimeout(delayedFocus, 50);
|
||||
};
|
||||
|
||||
$('.dropdown').on('shown.bs.dropdown', function(e) {
|
||||
// We need to focus on the first menuitem.
|
||||
var menu = $(e.target).find('[role="menu"]'),
|
||||
menuItems = false,
|
||||
foundMenuItem = false;
|
||||
|
||||
if (menu) {
|
||||
menuItems = $(menu).find('[role="menuitem"]');
|
||||
}
|
||||
if (menuItems && menuItems.length > 0) {
|
||||
if (getFocusEnd()) {
|
||||
foundMenuItem = menuItems[menuItems.length - 1];
|
||||
} else {
|
||||
// The first menu entry, pretty reasonable.
|
||||
foundMenuItem = menuItems[0];
|
||||
}
|
||||
}
|
||||
if (foundMenuItem) {
|
||||
shiftFocus(foundMenuItem);
|
||||
}
|
||||
});
|
||||
// Search for menu items by finding the first item that has
|
||||
// text starting with the typed character (case insensitive).
|
||||
$('.dropdown [role="menu"] [role="menuitem"]').keypress(function(e) {
|
||||
var trigger = String.fromCharCode(e.which || e.keyCode),
|
||||
menu = $(e.target).closest('[role="menu"]'),
|
||||
i = 0,
|
||||
menuItems = false,
|
||||
item,
|
||||
itemText;
|
||||
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
menuItems = $(menu).find('[role="menuitem"]');
|
||||
if (!menuItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
trigger = trigger.toLowerCase();
|
||||
for (i = 0; i < menuItems.length; i++) {
|
||||
item = $(menuItems[i]);
|
||||
itemText = item.text().trim().toLowerCase();
|
||||
if (itemText.indexOf(trigger) == 0) {
|
||||
shiftFocus(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation for arrow keys, home and end keys.
|
||||
$('.dropdown [role="menu"] [role="menuitem"]').keydown(function(e) {
|
||||
var trigger = e.which || e.keyCode,
|
||||
next = false,
|
||||
menu = $(e.target).closest('[role="menu"]'),
|
||||
i = 0,
|
||||
menuItems = false;
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
menuItems = $(menu).find('[role="menuitem"]');
|
||||
if (!menuItems) {
|
||||
return;
|
||||
}
|
||||
// Down key.
|
||||
if (trigger == 40) {
|
||||
for (i = 0; i < menuItems.length - 1; i++) {
|
||||
if (menuItems[i] == e.target) {
|
||||
next = menuItems[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!next) {
|
||||
// Wrap to first item.
|
||||
next = menuItems[0];
|
||||
}
|
||||
|
||||
} else if (trigger == 38) {
|
||||
// Up key.
|
||||
for (i = 1; i < menuItems.length; i++) {
|
||||
if (menuItems[i] == e.target) {
|
||||
next = menuItems[i - 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!next) {
|
||||
// Wrap to last item.
|
||||
next = menuItems[menuItems.length - 1];
|
||||
}
|
||||
|
||||
} else if (trigger == 36) {
|
||||
// Home key.
|
||||
next = menuItems[0];
|
||||
|
||||
} else if (trigger == 35) {
|
||||
// End key.
|
||||
next = menuItems[menuItems.length - 1];
|
||||
}
|
||||
// Variable next is set if we do want to act on the keypress.
|
||||
if (next) {
|
||||
e.preventDefault();
|
||||
shiftFocus(next);
|
||||
}
|
||||
return;
|
||||
});
|
||||
$('.dropdown').on('hidden.bs.dropdown', function(e) {
|
||||
// We need to focus on the menu trigger.
|
||||
var trigger = $(e.target).find('[data-toggle="dropdown"]');
|
||||
if (trigger) {
|
||||
shiftFocus(trigger);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
@ -28,7 +28,8 @@ define(['jquery', './tether', 'core/event'], function(jQuery, Tether, Event) {
|
||||
window.jQuery = jQuery;
|
||||
window.Tether = Tether;
|
||||
|
||||
require(['theme_boost/util',
|
||||
require(['theme_boost/aria',
|
||||
'theme_boost/util',
|
||||
'theme_boost/alert',
|
||||
'theme_boost/button',
|
||||
'theme_boost/carousel',
|
||||
@ -39,7 +40,7 @@ define(['jquery', './tether', 'core/event'], function(jQuery, Tether, Event) {
|
||||
'theme_boost/tab',
|
||||
'theme_boost/tooltip',
|
||||
'theme_boost/popover'],
|
||||
function() {
|
||||
function(Aria) {
|
||||
|
||||
// We do twice because: https://github.com/twbs/bootstrap/issues/10547
|
||||
jQuery('body').popover({
|
||||
@ -63,8 +64,11 @@ define(['jquery', './tether', 'core/event'], function(jQuery, Tether, Event) {
|
||||
selector: '[data-toggle="popover"]',
|
||||
trigger: 'focus'
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Aria.init();
|
||||
});
|
||||
|
||||
|
||||
|
@ -52,4 +52,4 @@
|
||||
</div>
|
||||
|
||||
{{/primary}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -78,7 +78,7 @@
|
||||
}
|
||||
}}
|
||||
<div class="dropdown">
|
||||
<a href="#" class="{{triggerextraclasses}} dropdown-toggle" id="dropdown-{{instance}}" title="{{title}}" role="menuitem" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a href="#" tabindex="0" class="btn {{triggerextraclasses}} dropdown-toggle" id="dropdown-{{instance}}" aria-label="{{title}}" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" aria-controls="action-menu-{{instance}}-menu">
|
||||
{{{actiontext}}}
|
||||
{{{menutrigger}}}
|
||||
{{#icon}}
|
||||
@ -92,7 +92,7 @@
|
||||
{{/menutrigger}}
|
||||
</a>
|
||||
{{#secondary}}
|
||||
<div class="dropdown-menu dropdown-menu-right {{classes}}"{{#attributes}} {{name}}="{{value}}"{{/attributes}}>
|
||||
<div class="dropdown-menu dropdown-menu-right {{classes}}"{{#attributes}} {{name}}="{{value}}"{{/attributes}} id="dropdown-menu-{{instance}}">
|
||||
{{#items}}
|
||||
{{#actionmenulink}}
|
||||
<a href="{{url}}" class="dropdown-item {{classes}}" {{#attributes}}{{name}}={{#quote}}{{value}}{{/quote}} {{/attributes}}{{#showtext}}aria-labelledby="actionmenuaction-{{instance}}"{{/showtext}}>
|
||||
@ -107,7 +107,7 @@
|
||||
</a>
|
||||
{{/actionmenulink}}
|
||||
{{#actionmenufiller}}
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-divider" role="presentation"><span class="filler"> </span></div>
|
||||
{{/actionmenufiller}}
|
||||
{{^actionmenulink}}
|
||||
{{^actionmenufiller}}
|
||||
|
@ -10,7 +10,7 @@
|
||||
<a class="dropdown-item" href="{{{url}}}" {{#title}}title="{{{title}}}"{{/title}}>{{{text}}}</a>
|
||||
{{/divider}}
|
||||
{{#divider}}
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-divider" role="presentation"></div>
|
||||
{{/divider}}
|
||||
{{/children}}
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user