Proyectos de Subversion Moodle

Rev

Rev 1 | Mostrar el archivo completo | | | Autoría | Ultima modificación | Ver Log |

Rev 1 Rev 1441
Línea 19... Línea 19...
19
 * @module     theme_universe/aria
19
 * @module     theme_universe/aria
20
 * @copyright  2018 Damyon Wiese <damyon@moodle.com>
20
 * @copyright  2018 Damyon Wiese <damyon@moodle.com>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
22
 */
Línea 23... Línea 23...
23
 
23
 
24
import $ from 'jquery';
24
import Tab from 'theme_universe/bootstrap/tab';
-
 
25
import Pending from 'core/pending';
Línea 25... Línea 26...
25
import Pending from 'core/pending';
26
import * as FocusLockManager from 'core/local/aria/focuslock';
26
 
27
 
27
/**
28
/**
28
 * Drop downs from bootstrap don't support keyboard accessibility by default.
29
 * Drop downs from bootstrap don't support keyboard accessibility by default.
Línea 76... Línea 77...
76
 
77
 
77
        // Fix the focus on the menu items when the menu is opened.
78
        // Fix the focus on the menu items when the menu is opened.
78
        const menu = e.target.parentElement.querySelector('[role="menu"]');
79
        const menu = e.target.parentElement.querySelector('[role="menu"]');
79
        let menuItems = false;
80
        let menuItems = false;
80
        let foundMenuItem = false;
-
 
Línea 81... Línea 81...
81
        let textInput = false;
81
        let foundMenuItem = false;
82
 
82
 
83
        if (menu) {
-
 
84
            menuItems = menu.querySelectorAll('[role="menuitem"]');
83
        if (menu) {
85
            textInput = e.target.parentElement.querySelector('[data-action="search"]');
-
 
86
        }
84
            menuItems = menu.querySelectorAll('[role="menuitem"]');
87
 
85
        }
88
        if (menuItems && menuItems.length > 0) {
86
        if (menuItems && menuItems.length > 0) {
89
            // Up key opens the menu at the end.
87
            // Up key opens the menu at the end.
90
            if (trigger === 'ArrowUp') {
88
            if (trigger === 'ArrowUp') {
Línea 99... Línea 97...
99
                // The first menu entry, pretty reasonable.
97
                // The first menu entry, pretty reasonable.
100
                foundMenuItem = menuItems[0];
98
                foundMenuItem = menuItems[0];
101
            }
99
            }
102
        }
100
        }
Línea 103... Línea 101...
103
 
101
 
104
        if (textInput) {
-
 
105
            shiftFocus(textInput);
-
 
106
        }
-
 
107
        if (foundMenuItem && textInput === null) {
102
        if (foundMenuItem) {
108
            shiftFocus(foundMenuItem);
103
            shiftFocus(foundMenuItem);
109
        }
104
        }
Línea 110... Línea 105...
110
    };
105
    };
111
 
106
 
112
    // Search for menu items by finding the first item that has
107
    // Search for menu items by finding the first item that has
113
    // text starting with the typed character (case insensitive).
108
    // text starting with the typed character (case insensitive).
114
    document.addEventListener('keypress', e => {
109
    document.addEventListener('keypress', e => {
115
        if (e.target.matches('.dropdown [role="menu"] [role="menuitem"]')) {
110
        if (e.target.matches('[role="menu"] [role="menuitem"]')) {
116
            const menu = e.target.closest('[role="menu"]');
111
            const menu = e.target.closest('[role="menu"]');
117
            if (!menu) {
112
            if (!menu) {
118
                return;
113
                return;
Línea 138... Línea 133...
138
    // Keyboard navigation for arrow keys, home and end keys.
133
    // Keyboard navigation for arrow keys, home and end keys.
139
    document.addEventListener('keydown', e => {
134
    document.addEventListener('keydown', e => {
Línea 140... Línea 135...
140
 
135
 
141
        // We only want to set focus when users access the dropdown via keyboard as per
136
        // We only want to set focus when users access the dropdown via keyboard as per
142
        // guidelines defined in w3 aria practices 1.1 menu-button.
137
        // guidelines defined in w3 aria practices 1.1 menu-button.
143
        if (e.target.matches('[data-toggle="dropdown"]')) {
138
        if (e.target.matches('[data-bs-toggle="dropdown"]')) {
144
            handleMenuButton(e);
139
            handleMenuButton(e);
Línea 145... Línea 140...
145
        }
140
        }
146
 
141
 
147
        if (e.target.matches('.dropdown [role="menu"] [role="menuitem"]')) {
142
        if (e.target.matches('[role="menu"] [role="menuitem"]')) {
148
            const trigger = e.key;
143
            const trigger = e.key;
Línea 149... Línea 144...
149
            let next = false;
144
            let next = false;
Línea 196... Línea 191...
196
            }
191
            }
197
            return;
192
            return;
198
        }
193
        }
199
    });
194
    });
Línea -... Línea 195...
-
 
195
 
-
 
196
    // Trap focus if the dropdown is a dialog.
-
 
197
    document.addEventListener('shown.bs.dropdown', e => {
-
 
198
        const dialog = e.target.querySelector('.dropdown-menu[role="dialog"]');
-
 
199
        if (dialog) {
-
 
200
            // Use setTimeout to make sure the dialog is positioned correctly to prevent random scrolling.
-
 
201
            setTimeout(() => {
-
 
202
                FocusLockManager.trapFocus(dialog);
-
 
203
            });
-
 
204
        }
-
 
205
    });
-
 
206
 
200
 
207
    // Untrap focus when the dialog dropdown is closed.
-
 
208
    document.addEventListener('hidden.bs.dropdown', e => {
-
 
209
        const dialog = e.target.querySelector('.dropdown-menu[role="dialog"]');
-
 
210
        if (dialog) {
-
 
211
            FocusLockManager.untrapFocus();
-
 
212
        }
201
    $('.dropdown').on('hidden.bs.dropdown', e => {
213
 
202
        // We need to focus on the menu trigger.
214
        // We need to focus on the menu trigger.
-
 
215
        const trigger = e.target.querySelector('[data-bs-toggle="dropdown"]');
203
        const trigger = e.target.querySelector('[data-toggle="dropdown"]');
216
        // If it's a click event, then no element is focused because the clicked element is inside a closed dropdown.
204
        const focused = document.activeElement != document.body ? document.activeElement : null;
217
        const focused = e.clickEvent?.target || (document.activeElement !== document.body ? document.activeElement : null);
205
        if (trigger && focused && e.target.contains(focused)) {
218
        if (trigger && focused && e.target.contains(focused)) {
206
            shiftFocus(trigger, () => {
219
            shiftFocus(trigger, () => {
207
                if (document.activeElement === document.body) {
220
                if (document.activeElement === document.body) {
208
                    // If the focus is currently on the body, then we can safely assume that the focus needs to be updated.
221
                    // If the focus is currently on the body, then we can safely assume that the focus needs to be updated.
Línea 218... Línea 231...
218
 
231
 
219
/**
232
/**
220
 * A lot of Bootstrap's out of the box features don't work if dropdown items are not focusable.
233
 * A lot of Bootstrap's out of the box features don't work if dropdown items are not focusable.
221
 */
234
 */
222
const comboboxFix = () => {
235
const comboboxFix = () => {
223
    $(document).on('show.bs.dropdown', e => {
236
    document.addEventListener('show.bs.dropdown', e => {
224
        if (e.relatedTarget.matches('[role="combobox"]')) {
237
        if (e.relatedTarget.matches('[role="combobox"]')) {
225
            const combobox = e.relatedTarget;
238
            const combobox = e.relatedTarget;
Línea 226... Línea 239...
226
            const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
239
            const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
Línea 242... Línea 255...
242
                }, 0);
255
                }, 0);
243
            }
256
            }
244
        }
257
        }
245
    });
258
    });
Línea 246... Línea 259...
246
 
259
 
247
    $(document).on('hidden.bs.dropdown', e => {
260
    document.addEventListener('hidden.bs.dropdown', e => {
248
        if (e.relatedTarget.matches('[role="combobox"]')) {
261
        if (e.relatedTarget.matches('[role="combobox"]')) {
249
            const combobox = e.relatedTarget;
262
            const combobox = e.relatedTarget;
Línea 250... Línea 263...
250
            const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
263
            const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
Línea 295... Línea 308...
295
                        }
308
                        }
296
                    }
309
                    }
297
                    if (editable && !next) {
310
                    if (editable && !next) {
298
                        next = options[options.length - 1];
311
                        next = options[options.length - 1];
299
                    }
312
                    }
300
                } else if (trigger == 'Home') {
313
                } else if (trigger == 'Home' && !editable) {
301
                    next = options[0];
314
                    next = options[0];
302
                } else if (trigger == 'End') {
315
                } else if (trigger == 'End' && !editable) {
303
                    next = options[options.length - 1];
316
                    next = options[options.length - 1];
304
                } else if ((trigger == ' ' && !editable) || trigger == 'Enter') {
317
                } else if ((trigger == ' ' && !editable) || trigger == 'Enter') {
305
                    e.preventDefault();
318
                    e.preventDefault();
306
                    selectOption(combobox, activeOption);
319
                    selectOption(combobox, activeOption);
307
                } else if (!editable) {
320
                } else if (!editable) {
Línea 336... Línea 349...
336
        const option = e.target.closest('[role="listbox"] [role="option"]');
349
        const option = e.target.closest('[role="listbox"] [role="option"]');
337
        if (option) {
350
        if (option) {
338
            const listbox = option.closest('[role="listbox"]');
351
            const listbox = option.closest('[role="listbox"]');
339
            const combobox = document.querySelector(`[role="combobox"][aria-controls="${listbox.id}"]`);
352
            const combobox = document.querySelector(`[role="combobox"][aria-controls="${listbox.id}"]`);
340
            if (combobox) {
353
            if (combobox) {
341
                combobox.focus();
-
 
342
                selectOption(combobox, option);
354
                selectOption(combobox, option);
343
            }
355
            }
344
        }
356
        }
345
    });
357
    });
Línea 366... Línea 378...
366
            }
378
            }
367
            option.setAttribute('aria-selected', 'true');
379
            option.setAttribute('aria-selected', 'true');
368
        }
380
        }
Línea 369... Línea 381...
369
 
381
 
370
        if (combobox.hasAttribute('value')) {
382
        if (combobox.hasAttribute('value')) {
371
            combobox.value = option.textContent.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
383
            combobox.value = option.dataset.shortText || option.textContent.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
-
 
384
        } else {
-
 
385
            const selectedOptionContainer = combobox.querySelector('[data-selected-option]');
-
 
386
            if (selectedOptionContainer) {
-
 
387
                selectedOptionContainer.textContent = option.dataset.shortText || option.textContent;
372
        } else {
388
            } else {
-
 
389
                combobox.textContent = option.dataset.shortText || option.textContent;
373
            combobox.textContent = option.textContent;
390
            }
Línea 374... Línea 391...
374
        }
391
        }
375
 
392
 
376
        if (combobox.dataset.inputElement) {
393
        if (combobox.dataset.inputElement) {
Línea 397... Línea 414...
397
        });
414
        });
398
    });
415
    });
399
};
416
};
Línea 400... Línea 417...
400
 
417
 
401
/**
418
/**
-
 
419
 * Changes the focus to the correct element based on the key that is pressed.
402
 * Changes the focus to the correct tab based on the key that is pressed.
420
 * @param {NodeList} elements A NodeList of focusable elements to navigate between.
-
 
421
 * @param {KeyboardEvent} e The keyboard event that triggers the roving focus.
-
 
422
 * @param {boolean} vertical Whether the navigation is vertical.
403
 * @param {KeyboardEvent} e
423
 * @param {boolean} updateTabIndex Whether to update the tabIndex of the elements.
404
 */
-
 
405
const updateTabFocus = e => {
-
 
406
    const tabList = e.target.closest('[role="tablist"]');
424
 */
407
    const vertical = tabList.getAttribute('aria-orientation') == 'vertical';
425
const rovingFocus = (elements, e, vertical, updateTabIndex) => {
408
    const rtl = window.right_to_left();
426
    const rtl = window.right_to_left();
409
    const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');
427
    const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');
410
    const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');
428
    const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');
411
    const tabs = Array.prototype.filter.call(
-
 
412
        tabList.querySelectorAll('[role="tab"]'),
-
 
Línea 413... Línea 429...
413
        tab => !!tab.offsetHeight); // We only work with the visible tabs.
429
    const keys = [arrowNext, arrowPrevious, 'Home', 'End'];
414
 
430
 
415
    for (let i = 0; i < tabs.length; i++) {
431
    if (!keys.includes(e.key)) {
Línea -... Línea 432...
-
 
432
        return;
-
 
433
    }
-
 
434
 
-
 
435
    const focusElement = index => {
-
 
436
        elements[index].focus();
-
 
437
        if (updateTabIndex) {
-
 
438
            elements.forEach((element, i) => element.setAttribute('tabindex', i === index ? '0' : '-1'));
-
 
439
        }
-
 
440
    };
-
 
441
 
416
        tabs[i].index = i;
442
    const currentIndex = Array.prototype.indexOf.call(elements, e.target);
417
    }
443
    let nextIndex;
418
 
444
 
419
    switch (e.key) {
445
    switch (e.key) {
420
        case arrowNext:
-
 
421
            e.preventDefault();
-
 
422
            if (e.target.index !== undefined && tabs[e.target.index + 1]) {
446
        case arrowNext:
423
                tabs[e.target.index + 1].focus();
-
 
424
            } else {
447
            e.preventDefault();
425
                tabs[0].focus();
448
            nextIndex = (currentIndex + 1 < elements.length) ? currentIndex + 1 : 0;
426
            }
449
            focusElement(nextIndex);
427
            break;
450
            break;
428
        case arrowPrevious:
451
        case arrowPrevious:
429
            e.preventDefault();
-
 
430
            if (e.target.index !== undefined && tabs[e.target.index - 1]) {
-
 
431
                tabs[e.target.index - 1].focus();
-
 
432
            } else {
452
            e.preventDefault();
433
                tabs[tabs.length - 1].focus();
453
            nextIndex = (currentIndex - 1 >= 0) ? currentIndex - 1 : elements.length - 1;
434
            }
454
            focusElement(nextIndex);
435
            break;
455
            break;
436
        case 'Home':
456
        case 'Home':
437
            e.preventDefault();
457
            e.preventDefault();
438
            tabs[0].focus();
458
            focusElement(0);
439
            break;
459
            break;
440
        case 'End':
460
        case 'End':
441
            e.preventDefault();
461
            e.preventDefault();
Línea 442... Línea 462...
442
            tabs[tabs.length - 1].focus();
462
            focusElement(elements.length - 1);
443
    }
463
    }
444
};
464
};
445
 
465
 
446
/**
466
/**
447
 * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.
467
 * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.
448
 */
468
 */
-
 
469
const tabElementFix = () => {
-
 
470
    document.addEventListener('keydown', e => {
-
 
471
        if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {
449
const tabElementFix = () => {
472
            if (e.target.matches('[role="tablist"] [role="tab"]')) {
-
 
473
                const tabList = e.target.closest('[role="tablist"]');
-
 
474
                const tabs = Array.prototype.filter.call(
-
 
475
                    tabList.querySelectorAll('[role="tab"]'),
-
 
476
                    tab => !!tab.offsetHeight
450
    document.addEventListener('keydown', e => {
477
                ); // We only work with the visible tabs.
451
        if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {
478
                const vertical = tabList.getAttribute('aria-orientation') == 'vertical';
452
            if (e.target.matches('[role="tablist"] [role="tab"]')) {
479
 
Línea 453... Línea 480...
453
                updateTabFocus(e);
480
                rovingFocus(tabs, e, vertical, false);
454
            }
481
            }
455
        }
482
        }
456
    });
483
    });
457
 
484
 
458
    document.addEventListener('click', e => {
485
    document.addEventListener('click', e => {
459
        if (e.target.matches('[role="tablist"] [data-toggle="tab"], [role="tablist"] [data-toggle="pill"]')) {
486
        if (e.target.matches('[role="tablist"] [data-bs-toggle="tab"], [role="tablist"] [data-bs-toggle="pill"]')) {
460
            const tabs = e.target.closest('[role="tablist"]').querySelectorAll('[data-toggle="tab"], [data-toggle="pill"]');
487
            const tabs = e.target.closest('[role="tablist"]').querySelectorAll('[data-bs-toggle="tab"], [data-bs-toggle="pill"]');
461
            e.preventDefault();
488
            e.preventDefault();
462
            $(e.target).tab('show');
489
            Tab.getOrCreateInstance(e.target).show();
Línea 473... Línea 500...
473
 *
500
 *
474
 * @see {@link https://www.w3.org/TR/wai-aria-practices-1.1/#disclosure|WAI-ARIA Authoring Practices 1.1 - Disclosure (Show/Hide)}
501
 * @see {@link https://www.w3.org/TR/wai-aria-practices-1.1/#disclosure|WAI-ARIA Authoring Practices 1.1 - Disclosure (Show/Hide)}
475
 */
502
 */
476
const collapseFix = () => {
503
const collapseFix = () => {
477
    document.addEventListener('keydown', e => {
504
    document.addEventListener('keydown', e => {
478
        if (e.target.matches('[data-toggle="collapse"]')) {
505
        if (e.target.matches('[data-bs-toggle="collapse"]')) {
479
            // Pressing space should toggle expand/collapse.
506
            // Pressing space should toggle expand/collapse.
480
            if (e.key === ' ') {
507
            if (e.key === ' ') {
481
                e.preventDefault();
508
                e.preventDefault();
482
                e.target.click();
509
                e.target.click();
483
            }
510
            }
484
        }
511
        }
485
    });
512
    });
486
};
513
};
Línea -... Línea 514...
-
 
514
 
-
 
515
/**
-
 
516
 * Fix accessibility issues
-
 
517
 */
-
 
518
const toolbarFix = () => {
-
 
519
    document.addEventListener('keydown', e => {
-
 
520
        if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {
-
 
521
            if (e.target.matches('[role="toolbar"] button')) {
-
 
522
                const buttons = e.target.closest('[role="toolbar"]').querySelectorAll('button');
-
 
523
                rovingFocus(buttons, e, false, true);
-
 
524
            }
-
 
525
        }
-
 
526
    });
-
 
527
};
487
 
528
 
488
export const init = () => {
529
export const init = () => {
489
    dropdownFix();
530
    dropdownFix();
490
    comboboxFix();
531
    comboboxFix();
491
    autoFocus();
532
    autoFocus();
492
    tabElementFix();
533
    tabElementFix();
-
 
534
    collapseFix();
493
    collapseFix();
535
    toolbarFix();