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_boost/aria
19
 * @module     theme_boost/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_boost/bootstrap/tab';
25
import Pending from 'core/pending';
25
import Pending from 'core/pending';
Línea 26... Línea 26...
26
import * as FocusLockManager from 'core/local/aria/focuslock';
26
import * as FocusLockManager from 'core/local/aria/focuslock';
27
 
27
 
Línea 105... Línea 105...
105
    };
105
    };
Línea 106... Línea 106...
106
 
106
 
107
    // Search for menu items by finding the first item that has
107
    // Search for menu items by finding the first item that has
108
    // text starting with the typed character (case insensitive).
108
    // text starting with the typed character (case insensitive).
109
    document.addEventListener('keypress', e => {
109
    document.addEventListener('keypress', e => {
110
        if (e.target.matches('.dropdown [role="menu"] [role="menuitem"]')) {
110
        if (e.target.matches('[role="menu"] [role="menuitem"]')) {
111
            const menu = e.target.closest('[role="menu"]');
111
            const menu = e.target.closest('[role="menu"]');
112
            if (!menu) {
112
            if (!menu) {
113
                return;
113
                return;
114
            }
114
            }
Línea 133... Línea 133...
133
    // Keyboard navigation for arrow keys, home and end keys.
133
    // Keyboard navigation for arrow keys, home and end keys.
134
    document.addEventListener('keydown', e => {
134
    document.addEventListener('keydown', e => {
Línea 135... Línea 135...
135
 
135
 
136
        // 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
137
        // guidelines defined in w3 aria practices 1.1 menu-button.
137
        // guidelines defined in w3 aria practices 1.1 menu-button.
138
        if (e.target.matches('[data-toggle="dropdown"]')) {
138
        if (e.target.matches('[data-bs-toggle="dropdown"]')) {
139
            handleMenuButton(e);
139
            handleMenuButton(e);
Línea 140... Línea 140...
140
        }
140
        }
141
 
141
 
142
        if (e.target.matches('.dropdown [role="menu"] [role="menuitem"]')) {
142
        if (e.target.matches('[role="menu"] [role="menuitem"]')) {
143
            const trigger = e.key;
143
            const trigger = e.key;
Línea 144... Línea 144...
144
            let next = false;
144
            let next = false;
Línea 191... Línea 191...
191
            }
191
            }
192
            return;
192
            return;
193
        }
193
        }
194
    });
194
    });
Línea -... Línea 195...
-
 
195
 
195
 
196
    // Trap focus if the dropdown is a dialog.
196
    $('.dropdown').on('shown.bs.dropdown', e => {
197
    document.addEventListener('shown.bs.dropdown', e => {
197
        const dialog = e.target.querySelector(`#${e.relatedTarget.getAttribute('aria-controls')}[role="dialog"]`);
198
        const dialog = e.target.querySelector('.dropdown-menu[role="dialog"]');
198
        if (dialog) {
199
        if (dialog) {
199
            // Use setTimeout to make sure the dialog is positioned correctly to prevent random scrolling.
200
            // Use setTimeout to make sure the dialog is positioned correctly to prevent random scrolling.
200
            setTimeout(() => {
201
            setTimeout(() => {
201
                FocusLockManager.trapFocus(dialog);
202
                FocusLockManager.trapFocus(dialog);
202
            });
203
            });
203
        }
204
        }
Línea -... Línea 205...
-
 
205
    });
204
    });
206
 
205
 
207
    // Untrap focus when the dialog dropdown is closed.
206
    $('.dropdown').on('hidden.bs.dropdown', e => {
208
    document.addEventListener('hidden.bs.dropdown', e => {
207
        const dialog = e.target.querySelector(`#${e.relatedTarget.getAttribute('aria-controls')}[role="dialog"]`);
209
        const dialog = e.target.querySelector('.dropdown-menu[role="dialog"]');
208
        if (dialog) {
210
        if (dialog) {
Línea 209... Línea 211...
209
            FocusLockManager.untrapFocus();
211
            FocusLockManager.untrapFocus();
210
        }
212
        }
211
 
213
 
212
        // We need to focus on the menu trigger.
214
        // We need to focus on the menu trigger.
213
        const trigger = e.target.querySelector('[data-toggle="dropdown"]');
215
        const trigger = e.target.querySelector('[data-bs-toggle="dropdown"]');
214
        // If it's a click event, then no element is focused because the clicked element is inside a closed dropdown.
216
        // If it's a click event, then no element is focused because the clicked element is inside a closed dropdown.
215
        const focused = e.clickEvent?.target || (document.activeElement !== document.body ? document.activeElement : null);
217
        const focused = e.clickEvent?.target || (document.activeElement !== document.body ? document.activeElement : null);
Línea 229... Línea 231...
229
 
231
 
230
/**
232
/**
231
 * 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.
232
 */
234
 */
233
const comboboxFix = () => {
235
const comboboxFix = () => {
234
    $(document).on('show.bs.dropdown', e => {
236
    document.addEventListener('show.bs.dropdown', e => {
235
        if (e.relatedTarget.matches('[role="combobox"]')) {
237
        if (e.relatedTarget.matches('[role="combobox"]')) {
236
            const combobox = e.relatedTarget;
238
            const combobox = e.relatedTarget;
Línea 237... Línea 239...
237
            const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
239
            const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
Línea 253... Línea 255...
253
                }, 0);
255
                }, 0);
254
            }
256
            }
255
        }
257
        }
256
    });
258
    });
Línea 257... Línea 259...
257
 
259
 
258
    $(document).on('hidden.bs.dropdown', e => {
260
    document.addEventListener('hidden.bs.dropdown', e => {
259
        if (e.relatedTarget.matches('[role="combobox"]')) {
261
        if (e.relatedTarget.matches('[role="combobox"]')) {
260
            const combobox = e.relatedTarget;
262
            const combobox = e.relatedTarget;
Línea 261... Línea 263...
261
            const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
263
            const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
Línea 339... Línea 341...
339
                    combobox.setAttribute('aria-activedescendant', next.id);
341
                    combobox.setAttribute('aria-activedescendant', next.id);
340
                    next.scrollIntoView({block: 'nearest'});
342
                    next.scrollIntoView({block: 'nearest'});
341
                }
343
                }
342
            }
344
            }
343
        }
345
        }
344
    });
346
    }, true);
Línea 345... Línea 347...
345
 
347
 
346
    document.addEventListener('click', e => {
348
    document.addEventListener('click', e => {
347
        const option = e.target.closest('[role="listbox"] [role="option"]');
349
        const option = e.target.closest('[role="listbox"] [role="option"]');
348
        if (option) {
350
        if (option) {
Línea 378... Línea 380...
378
        }
380
        }
Línea 379... Línea 381...
379
 
381
 
380
        if (combobox.hasAttribute('value')) {
382
        if (combobox.hasAttribute('value')) {
381
            combobox.value = option.dataset.shortText || 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;
382
        } else {
388
            } else {
-
 
389
                combobox.textContent = option.dataset.shortText || option.textContent;
383
            combobox.textContent = option.dataset.shortText || option.textContent;
390
            }
Línea 384... Línea 391...
384
        }
391
        }
385
 
392
 
386
        if (combobox.dataset.inputElement) {
393
        if (combobox.dataset.inputElement) {
Línea 407... Línea 414...
407
        });
414
        });
408
    });
415
    });
409
};
416
};
Línea 410... Línea 417...
410
 
417
 
411
/**
418
/**
-
 
419
 * Changes the focus to the correct element based on the key that is pressed.
412
 * 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.
413
 * @param {KeyboardEvent} e
423
 * @param {boolean} updateTabIndex Whether to update the tabIndex of the elements.
414
 */
-
 
415
const updateTabFocus = e => {
-
 
416
    const tabList = e.target.closest('[role="tablist"]');
424
 */
417
    const vertical = tabList.getAttribute('aria-orientation') == 'vertical';
425
const rovingFocus = (elements, e, vertical, updateTabIndex) => {
418
    const rtl = window.right_to_left();
426
    const rtl = window.right_to_left();
419
    const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');
427
    const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');
420
    const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');
428
    const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');
421
    const tabs = Array.prototype.filter.call(
-
 
422
        tabList.querySelectorAll('[role="tab"]'),
-
 
Línea 423... Línea 429...
423
        tab => !!tab.offsetHeight); // We only work with the visible tabs.
429
    const keys = [arrowNext, arrowPrevious, 'Home', 'End'];
424
 
430
 
425
    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
 
426
        tabs[i].index = i;
442
    const currentIndex = Array.prototype.indexOf.call(elements, e.target);
427
    }
443
    let nextIndex;
428
 
444
 
429
    switch (e.key) {
445
    switch (e.key) {
430
        case arrowNext:
-
 
431
            e.preventDefault();
-
 
432
            if (e.target.index !== undefined && tabs[e.target.index + 1]) {
446
        case arrowNext:
433
                tabs[e.target.index + 1].focus();
-
 
434
            } else {
447
            e.preventDefault();
435
                tabs[0].focus();
448
            nextIndex = (currentIndex + 1 < elements.length) ? currentIndex + 1 : 0;
436
            }
449
            focusElement(nextIndex);
437
            break;
450
            break;
438
        case arrowPrevious:
451
        case arrowPrevious:
439
            e.preventDefault();
-
 
440
            if (e.target.index !== undefined && tabs[e.target.index - 1]) {
-
 
441
                tabs[e.target.index - 1].focus();
-
 
442
            } else {
452
            e.preventDefault();
443
                tabs[tabs.length - 1].focus();
453
            nextIndex = (currentIndex - 1 >= 0) ? currentIndex - 1 : elements.length - 1;
444
            }
454
            focusElement(nextIndex);
445
            break;
455
            break;
446
        case 'Home':
456
        case 'Home':
447
            e.preventDefault();
457
            e.preventDefault();
448
            tabs[0].focus();
458
            focusElement(0);
449
            break;
459
            break;
450
        case 'End':
460
        case 'End':
451
            e.preventDefault();
461
            e.preventDefault();
Línea 452... Línea 462...
452
            tabs[tabs.length - 1].focus();
462
            focusElement(elements.length - 1);
453
    }
463
    }
454
};
464
};
455
 
465
 
456
/**
466
/**
457
 * 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.
458
 */
468
 */
-
 
469
const tabElementFix = () => {
-
 
470
    document.addEventListener('keydown', e => {
-
 
471
        if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {
459
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
460
    document.addEventListener('keydown', e => {
477
                ); // We only work with the visible tabs.
461
        if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {
478
                const vertical = tabList.getAttribute('aria-orientation') == 'vertical';
462
            if (e.target.matches('[role="tablist"] [role="tab"]')) {
479
 
Línea 463... Línea 480...
463
                updateTabFocus(e);
480
                rovingFocus(tabs, e, vertical, false);
464
            }
481
            }
465
        }
482
        }
466
    });
483
    });
467
 
484
 
468
    document.addEventListener('click', e => {
485
    document.addEventListener('click', e => {
469
        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"]')) {
470
            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"]');
471
            e.preventDefault();
488
            e.preventDefault();
472
            $(e.target).tab('show');
489
            Tab.getOrCreateInstance(e.target).show();
Línea 483... Línea 500...
483
 *
500
 *
484
 * @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)}
485
 */
502
 */
486
const collapseFix = () => {
503
const collapseFix = () => {
487
    document.addEventListener('keydown', e => {
504
    document.addEventListener('keydown', e => {
488
        if (e.target.matches('[data-toggle="collapse"]')) {
505
        if (e.target.matches('[data-bs-toggle="collapse"]')) {
489
            // Pressing space should toggle expand/collapse.
506
            // Pressing space should toggle expand/collapse.
490
            if (e.key === ' ') {
507
            if (e.key === ' ') {
491
                e.preventDefault();
508
                e.preventDefault();
492
                e.target.click();
509
                e.target.click();
493
            }
510
            }
494
        }
511
        }
495
    });
512
    });
496
};
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
};
497
 
528
 
498
export const init = () => {
529
export const init = () => {
499
    dropdownFix();
530
    dropdownFix();
500
    comboboxFix();
531
    comboboxFix();
501
    autoFocus();
532
    autoFocus();
502
    tabElementFix();
533
    tabElementFix();
-
 
534
    collapseFix();
503
    collapseFix();
535
    toolbarFix();