1 // This file is part of Moodle - http://moodle.org/
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 * Enhancements to Bootstrap components for accessibility.
19 * @module theme_boost/aria
20 * @copyright 2018 Damyon Wiese <damyon@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 define(['jquery', 'core/pending'], function($, Pending) {
26 // Drop downs from bootstrap don't support keyboard accessibility by default.
28 setFocusEnd = function() {
31 getFocusEnd = function() {
32 var result = focusEnd;
37 // Special handling for "up" keyboard control.
38 $('[data-toggle="dropdown"]').keydown(function(e) {
39 var trigger = e.which || e.keyCode,
42 // Up key opens the menu at the end.
44 // Focus the end of the menu, not the beginning.
48 // Escape key only closes the menu, it doesn't open it.
50 expanded = $(e.target).attr('aria-expanded');
52 if (expanded == "false") {
57 // Space key or Enter key opens the menu.
58 if (trigger == 32 || trigger == 13) {
59 // Cancel random scroll.
61 // Open the menu instead.
66 // Special handling for navigation keys when menu is open.
67 var shiftFocus = function(element) {
68 var delayedFocus = function(pendingPromise) {
70 pendingPromise.resolve();
72 setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus'));
75 $('.dropdown').on('shown.bs.dropdown', function(e) {
76 // We need to focus on the first menuitem.
77 var menu = $(e.target).find('[role="menu"]'),
79 foundMenuItem = false;
82 menuItems = $(menu).find('[role="menuitem"]');
84 if (menuItems && menuItems.length > 0) {
86 foundMenuItem = menuItems[menuItems.length - 1];
88 // The first menu entry, pretty reasonable.
89 foundMenuItem = menuItems[0];
93 shiftFocus(foundMenuItem);
96 // Search for menu items by finding the first item that has
97 // text starting with the typed character (case insensitive).
98 $('.dropdown [role="menu"] [role="menuitem"]').keypress(function(e) {
99 var trigger = String.fromCharCode(e.which || e.keyCode),
100 menu = $(e.target).closest('[role="menu"]'),
109 menuItems = $(menu).find('[role="menuitem"]');
114 trigger = trigger.toLowerCase();
115 for (i = 0; i < menuItems.length; i++) {
116 item = $(menuItems[i]);
117 itemText = item.text().trim().toLowerCase();
118 if (itemText.indexOf(trigger) == 0) {
125 // Keyboard navigation for arrow keys, home and end keys.
126 $('.dropdown [role="menu"] [role="menuitem"]').keydown(function(e) {
127 var trigger = e.which || e.keyCode,
129 menu = $(e.target).closest('[role="menu"]'),
135 menuItems = $(menu).find('[role="menuitem"]');
141 for (i = 0; i < menuItems.length - 1; i++) {
142 if (menuItems[i] == e.target) {
143 next = menuItems[i + 1];
148 // Wrap to first item.
152 } else if (trigger == 38) {
154 for (i = 1; i < menuItems.length; i++) {
155 if (menuItems[i] == e.target) {
156 next = menuItems[i - 1];
161 // Wrap to last item.
162 next = menuItems[menuItems.length - 1];
165 } else if (trigger == 36) {
169 } else if (trigger == 35) {
171 next = menuItems[menuItems.length - 1];
173 // Variable next is set if we do want to act on the keypress.
180 $('.dropdown').on('hidden.bs.dropdown', function(e) {
181 // We need to focus on the menu trigger.
182 var trigger = $(e.target).find('[data-toggle="dropdown"]');
188 // After page load, focus on any element with special autofocus attribute.
190 window.setTimeout(function(pendingPromise) {
191 var alerts = $('[role="alert"][data-aria-autofocus="true"]');
192 if (alerts.length > 0) {
193 $(alerts[0]).attr('tabindex', '0');
194 $(alerts[0]).focus();
196 pendingPromise.resolve();
197 }, 300, new Pending('core/aria:delayed-focus'));