Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
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.
7
//
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.
12
//
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/>.
15
 
16
/**
17
 * ARIA helpers related to the aria-hidden attribute.
18
 *
19
 * @module     core/local/aria/aria-hidden.
20
 * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
import {getList} from 'core/normalise';
24
import Selectors from './selectors';
25
 
26
// The map of MutationObserver objects for an object.
27
const childObserverMap = new Map();
28
const siblingObserverMap = new Map();
29
 
30
/**
31
 * Determine whether the browser supports the MutationObserver system.
32
 *
33
 * @method
34
 * @returns {Bool}
35
 */
36
const supportsMutationObservers = () => (MutationObserver && typeof MutationObserver === 'function');
37
 
38
/**
39
 * Disable element focusability, disabling the tabindex for child elements which are normally focusable.
40
 *
41
 * @method
42
 * @param {HTMLElement} target
43
 */
44
const disableElementFocusability = target => {
45
    if (!(target instanceof HTMLElement)) {
46
        // This element is not an HTMLElement.
47
        // This can happen for Text Nodes.
48
        return;
49
    }
50
 
51
    if (target.matches(Selectors.elements.focusable)) {
52
        disableAndStoreTabIndex(target);
53
    }
54
 
55
    target.querySelectorAll(Selectors.elements.focusable).forEach(disableAndStoreTabIndex);
56
};
57
 
58
/**
59
 * Remove the current tab-index and store it for later restoration.
60
 *
61
 * @method
62
 * @param {HTMLElement} element
63
 */
64
const disableAndStoreTabIndex = element => {
65
    if (typeof element.dataset.ariaHiddenTabIndex !== 'undefined') {
66
        // This child already has a hidden attribute.
67
        // Do not modify it as the original value will be lost.
68
        return;
69
    }
70
 
71
    // Store the old tabindex in a data attribute.
72
    if (element.getAttribute('tabindex')) {
73
        element.dataset.ariaHiddenTabIndex = element.getAttribute('tabindex');
74
    } else {
75
        element.dataset.ariaHiddenTabIndex = '';
76
    }
77
    element.setAttribute('tabindex', -1);
78
};
79
 
80
/**
81
 * Re-enable element focusability, restoring any tabindex.
82
 *
83
 * @method
84
 * @param {HTMLElement} target
85
 */
86
const enableElementFocusability = target => {
87
    if (!(target instanceof HTMLElement)) {
88
        // This element is not an HTMLElement.
89
        // This can happen for Text Nodes.
90
        return;
91
    }
92
 
93
    if (target.matches(Selectors.elements.focusableToUnhide)) {
94
        restoreTabIndex(target);
95
    }
96
 
97
    target.querySelectorAll(Selectors.elements.focusableToUnhide).forEach(restoreTabIndex);
98
};
99
 
100
/**
101
 * Restore the tab-index of the supplied element.
102
 *
103
 * When disabling focusability the current tab-index is stored in the ariaHiddenTabIndex data attribute.
104
 * This is used to restore the tab-index, but only whilst the parent nodes remain unhidden.
105
 *
106
 * @method
107
 * @param {HTMLElement} element
108
 */
109
const restoreTabIndex = element => {
110
    if (element.closest(Selectors.aria.hidden)) {
111
        // This item still has a hidden parent, or is hidden itself. Do not unhide it.
112
        return;
113
    }
114
 
115
    const oldTabIndex = element.dataset.ariaHiddenTabIndex;
116
    if (oldTabIndex === '') {
117
        element.removeAttribute('tabindex');
118
    } else {
119
        element.setAttribute('tabindex', oldTabIndex);
120
    }
121
 
122
    delete element.dataset.ariaHiddenTabIndex;
123
};
124
 
125
/**
126
 * Update the supplied DOM Module to be hidden.
127
 *
128
 * @method
129
 * @param {HTMLElement} target
130
 * @returns {Array}
131
 */
132
export const hide = target => getList(target).forEach(_hide);
133
 
134
const _hide = target => {
135
    if (!(target instanceof HTMLElement)) {
136
        // This element is not an HTMLElement.
137
        // This can happen for Text Nodes.
138
        return;
139
    }
140
 
141
    if (target.closest(Selectors.aria.hidden)) {
142
        // This Element, or a parent Element, is already hidden.
143
        // Stop processing.
144
        return;
145
    }
146
 
147
    // Set the aria-hidden attribute to true.
148
    target.setAttribute('aria-hidden', true);
149
 
150
    // Based on advice from https://dequeuniversity.com/rules/axe/3.3/aria-hidden-focus, upon setting the aria-hidden
151
    // attribute, all focusable elements underneath that element should be modified such that they are not focusable.
152
    disableElementFocusability(target);
153
 
154
    if (supportsMutationObservers()) {
155
        // Add a MutationObserver to check for new children to the tree.
156
        const mutationObserver = new MutationObserver(mutationList => {
157
            mutationList.forEach(mutation => {
158
                if (mutation.type === 'childList') {
159
                    mutation.addedNodes.forEach(disableElementFocusability);
160
                } else if (mutation.type === 'attributes') {
161
                    // The tabindex has been updated on a hidden attribute.
162
                    // Ensure that it is stored, ad set to -1 to prevent breakage.
163
                    const element = mutation.target;
164
                    const proposedTabIndex = element.getAttribute('tabindex');
165
 
166
                    if (proposedTabIndex !== "-1") {
167
                        element.dataset.ariaHiddenTabIndex = proposedTabIndex;
168
                        element.setAttribute('tabindex', -1);
169
                    }
170
                }
171
            });
172
        });
173
 
174
        mutationObserver.observe(target, {
175
            // Watch for changes to the entire subtree.
176
            subtree: true,
177
 
178
            // Watch for new nodes.
179
            childList: true,
180
 
181
            // Watch for attribute changes to the tabindex.
182
            attributes: true,
183
            attributeFilter: ['tabindex'],
184
        });
185
        childObserverMap.set(target, mutationObserver);
186
    }
187
};
188
 
189
/**
190
 * Reverse the effect of the hide action.
191
 *
192
 * @method
193
 * @param {HTMLElement} target
194
 * @returns {Array}
195
 */
196
export const unhide = target => getList(target).forEach(_unhide);
197
 
198
const _unhide = target => {
199
    if (!(target instanceof HTMLElement)) {
200
        return;
201
    }
202
 
203
    // Note: The aria-hidden attribute should be removed, and not set to false.
204
    // The presence of the attribute is sufficient for some browsers to treat it as being true, regardless of its value.
205
    target.removeAttribute('aria-hidden');
206
 
207
    // Restore the tabindex across all child nodes of the target.
208
    enableElementFocusability(target);
209
 
210
    // Remove the focusability MutationObserver watching this tree.
211
    if (childObserverMap.has(target)) {
212
        childObserverMap.get(target).disconnect();
213
        childObserverMap.delete(target);
214
    }
215
};
216
 
217
/**
218
 * Correctly mark all siblings of the supplied target Element as hidden.
219
 *
220
 * @method
221
 * @param {HTMLElement} target
222
 * @returns {Array}
223
 */
224
export const hideSiblings = target => getList(target).forEach(_hideSiblings);
225
 
226
const _hideSiblings = target => {
227
    if (!(target instanceof HTMLElement)) {
228
        return;
229
    }
230
 
231
    if (!target.parentElement) {
232
        return;
233
    }
234
 
235
    target.parentElement.childNodes.forEach(node => {
236
        if (node === target) {
237
            // Skip self;
238
            return;
239
        }
240
 
241
        hide(node);
242
    });
243
 
244
    if (supportsMutationObservers()) {
245
        // Add a MutationObserver to check for new children to the tree.
246
        const newNodeObserver = new MutationObserver(mutationList => {
247
            mutationList.forEach(mutation => {
248
                mutation.addedNodes.forEach(node => {
249
                    if (target.contains(node)) {
250
                        // Skip self, and children of self.
251
                        return;
252
                    }
253
 
254
                    hide(node);
255
                });
256
            });
257
        });
258
 
259
        newNodeObserver.observe(target.parentElement, {childList: true, subtree: true});
260
        siblingObserverMap.set(target.parentElement, newNodeObserver);
261
    }
262
};
263
 
264
/**
265
 * Correctly reverse the hide action of all children of the supplied target Element.
266
 *
267
 * @method
268
 * @param {HTMLElement} target
269
 * @returns {Array}
270
 */
271
export const unhideSiblings = target => getList(target).forEach(_unhideSiblings);
272
 
273
const _unhideSiblings = target => {
274
    if (!(target instanceof HTMLElement)) {
275
        return;
276
    }
277
 
278
    if (!target.parentElement) {
279
        return;
280
    }
281
 
282
    target.parentElement.childNodes.forEach(node => {
283
        if (node === target) {
284
            // Skip self;
285
            return;
286
        }
287
 
288
        unhide(node);
289
    });
290
 
291
    // Remove the sibling MutationObserver watching this tree.
292
    if (siblingObserverMap.has(target.parentElement)) {
293
        siblingObserverMap.get(target.parentElement).disconnect();
294
        siblingObserverMap.delete(target.parentElement);
295
    }
296
};