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'], function($) {
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 M.util.pending_js('core/aria:delayed-focus');
69 var delayedFocus = function() {
71 M.util.complete_js('core/aria:delayed-focus');
73 setTimeout(delayedFocus, 50);
76 $('.dropdown').on('shown.bs.dropdown', function(e) {
77 // We need to focus on the first menuitem.
78 var menu = $(e.target).find('[role="menu"]'),
80 foundMenuItem = false;
83 menuItems = $(menu).find('[role="menuitem"]');
85 if (menuItems && menuItems.length > 0) {
87 foundMenuItem = menuItems[menuItems.length - 1];
89 // The first menu entry, pretty reasonable.
90 foundMenuItem = menuItems[0];
94 shiftFocus(foundMenuItem);
97 // Search for menu items by finding the first item that has
98 // text starting with the typed character (case insensitive).
99 $('.dropdown [role="menu"] [role="menuitem"]').keypress(function(e) {
100 var trigger = String.fromCharCode(e.which || e.keyCode),
101 menu = $(e.target).closest('[role="menu"]'),
110 menuItems = $(menu).find('[role="menuitem"]');
115 trigger = trigger.toLowerCase();
116 for (i = 0; i < menuItems.length; i++) {
117 item = $(menuItems[i]);
118 itemText = item.text().trim().toLowerCase();
119 if (itemText.indexOf(trigger) == 0) {
126 // Keyboard navigation for arrow keys, home and end keys.
127 $('.dropdown [role="menu"] [role="menuitem"]').keydown(function(e) {
128 var trigger = e.which || e.keyCode,
130 menu = $(e.target).closest('[role="menu"]'),
136 menuItems = $(menu).find('[role="menuitem"]');
142 for (i = 0; i < menuItems.length - 1; i++) {
143 if (menuItems[i] == e.target) {
144 next = menuItems[i + 1];
149 // Wrap to first item.
153 } else if (trigger == 38) {
155 for (i = 1; i < menuItems.length; i++) {
156 if (menuItems[i] == e.target) {
157 next = menuItems[i - 1];
162 // Wrap to last item.
163 next = menuItems[menuItems.length - 1];
166 } else if (trigger == 36) {
170 } else if (trigger == 35) {
172 next = menuItems[menuItems.length - 1];
174 // Variable next is set if we do want to act on the keypress.
181 $('.dropdown').on('hidden.bs.dropdown', function(e) {
182 // We need to focus on the menu trigger.
183 var trigger = $(e.target).find('[data-toggle="dropdown"]');
189 // After page load, focus on any element with special autofocus attribute.
191 M.util.pending_js('core/aria:delayed-focus');
192 window.setTimeout(function() {
193 var alerts = $('[role="alert"][data-aria-autofocus="true"]');
194 if (alerts.length > 0) {
195 $(alerts[0]).attr('tabindex', '0');
196 $(alerts[0]).focus();
198 M.util.complete_js('core/aria:delayed-focus');