1(function (global, factory) { 2 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 typeof define === 'function' && define.amd ? define(factory) : 4 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.A11yDialog = factory()); 5 }(this, (function () { 'use strict'; 6 7 var focusableSelectors = [ 8 'a[href]:not([tabindex^="-"])', 9 'area[href]:not([tabindex^="-"])', 10 'input:not([type="hidden"]):not([type="radio"]):not([disabled]):not([tabindex^="-"])', 11 'input[type="radio"]:not([disabled]):not([tabindex^="-"])', 12 'select:not([disabled]):not([tabindex^="-"])', 13 'textarea:not([disabled]):not([tabindex^="-"])', 14 'button:not([disabled]):not([tabindex^="-"])', 15 'iframe:not([tabindex^="-"])', 16 'audio[controls]:not([tabindex^="-"])', 17 'video[controls]:not([tabindex^="-"])', 18 '[contenteditable]:not([tabindex^="-"])', 19 '[tabindex]:not([tabindex^="-"])', 20 ]; 21 22 var TAB_KEY = 9; 23 var ESCAPE_KEY = 27; 24 25 /** 26 * Define the constructor to instantiate a dialog 27 * 28 * @constructor 29 * @param {Element} element 30 */ 31 function A11yDialog(element) { 32 // Prebind the functions that will be bound in addEventListener and 33 // removeEventListener to avoid losing references 34 this._show = this.show.bind(this); 35 this._hide = this.hide.bind(this); 36 this._maintainFocus = this._maintainFocus.bind(this); 37 this._bindKeypress = this._bindKeypress.bind(this); 38 39 this.$el = element; 40 this.shown = false; 41 this._id = this.$el.getAttribute('data-a11y-dialog') || this.$el.id; 42 this._previouslyFocused = null; 43 this._listeners = {}; 44 45 // Initialise everything needed for the dialog to work properly 46 this.create(); 47 } 48 49 /** 50 * Set up everything necessary for the dialog to be functioning 51 * 52 * @param {(NodeList | Element | string)} targets 53 * @return {this} 54 */ 55 A11yDialog.prototype.create = function () { 56 this.$el.setAttribute('aria-hidden', true); 57 this.$el.setAttribute('aria-modal', true); 58 this.$el.setAttribute('tabindex', -1); 59 60 if (!this.$el.hasAttribute('role')) { 61 this.$el.setAttribute('role', 'dialog'); 62 } 63 64 // Keep a collection of dialog openers, each of which will be bound a click 65 // event listener to open the dialog 66 this._openers = $$('[data-a11y-dialog-show="' + this._id + '"]'); 67 this._openers.forEach( 68 function (opener) { 69 opener.addEventListener('click', this._show); 70 }.bind(this) 71 ); 72 73 // Keep a collection of dialog closers, each of which will be bound a click 74 // event listener to close the dialog 75 this._closers = $$('[data-a11y-dialog-hide]', this.$el).concat( 76 $$('[data-a11y-dialog-hide="' + this._id + '"]') 77 ); 78 this._closers.forEach( 79 function (closer) { 80 closer.addEventListener('click', this._hide); 81 }.bind(this) 82 ); 83 84 // Execute all callbacks registered for the `create` event 85 this._fire('create'); 86 87 return this 88 }; 89 90 /** 91 * Show the dialog element, disable all the targets (siblings), trap the 92 * current focus within it, listen for some specific key presses and fire all 93 * registered callbacks for `show` event 94 * 95 * @param {Event} event 96 * @return {this} 97 */ 98 A11yDialog.prototype.show = function (event) { 99 // If the dialog is already open, abort 100 if (this.shown) { 101 return this 102 } 103 104 // Keep a reference to the currently focused element to be able to restore 105 // it later 106 this._previouslyFocused = document.activeElement; 107 this.$el.removeAttribute('aria-hidden'); 108 this.shown = true; 109 110 // Set the focus to the dialog element 111 moveFocusToDialog(this.$el); 112 113 // Bind a focus event listener to the body element to make sure the focus 114 // stays trapped inside the dialog while open, and start listening for some 115 // specific key presses (TAB and ESC) 116 document.body.addEventListener('focus', this._maintainFocus, true); 117 document.addEventListener('keydown', this._bindKeypress); 118 119 // Execute all callbacks registered for the `show` event 120 this._fire('show', event); 121 122 return this 123 }; 124 125 /** 126 * Hide the dialog element, enable all the targets (siblings), restore the 127 * focus to the previously active element, stop listening for some specific 128 * key presses and fire all registered callbacks for `hide` event 129 * 130 * @param {Event} event 131 * @return {this} 132 */ 133 A11yDialog.prototype.hide = function (event) { 134 // If the dialog is already closed, abort 135 if (!this.shown) { 136 return this 137 } 138 139 this.shown = false; 140 this.$el.setAttribute('aria-hidden', 'true'); 141 142 // If there was a focused element before the dialog was opened (and it has a 143 // `focus` method), restore the focus back to it 144 // See: https://github.com/KittyGiraudel/a11y-dialog/issues/108 145 if (this._previouslyFocused && this._previouslyFocused.focus) { 146 this._previouslyFocused.focus(); 147 } 148 149 // Remove the focus event listener to the body element and stop listening 150 // for specific key presses 151 document.body.removeEventListener('focus', this._maintainFocus, true); 152 document.removeEventListener('keydown', this._bindKeypress); 153 154 // Execute all callbacks registered for the `hide` event 155 this._fire('hide', event); 156 157 return this 158 }; 159 160 /** 161 * Destroy the current instance (after making sure the dialog has been hidden) 162 * and remove all associated listeners from dialog openers and closers 163 * 164 * @return {this} 165 */ 166 A11yDialog.prototype.destroy = function () { 167 // Hide the dialog to avoid destroying an open instance 168 this.hide(); 169 170 // Remove the click event listener from all dialog openers
vendor: 6,923 bytes, lines 171-394
171 this._openers.forEach( 172 function (opener) { 173 opener.removeEventListener('click', this._show); 174 }.bind(this) 175 ); 176 177 // Remove the click event listener from all dialog closers 178 this._closers.forEach( 179 function (closer) { 180 closer.removeEventListener('click', this._hide); 181 }.bind(this) 182 ); 183 184 // Execute all callbacks registered for the `destroy` event 185 this._fire('destroy'); 186 187 // Keep an object of listener types mapped to callback functions 188 this._listeners = {}; 189 190 return this 191 }; 192 193 /** 194 * Register a new callback for the given event type 195 * 196 * @param {string} type 197 * @param {Function} handler 198 */ 199 A11yDialog.prototype.on = function (type, handler) { 200 if (typeof this._listeners[type] === 'undefined') { 201 this._listeners[type] = []; 202 } 203 204 this._listeners[type].push(handler); 205 206 return this 207 }; 208 209 /** 210 * Unregister an existing callback for the given event type 211 * 212 * @param {string} type 213 * @param {Function} handler 214 */ 215 A11yDialog.prototype.off = function (type, handler) { 216 var index = (this._listeners[type] || []).indexOf(handler); 217 218 if (index > -1) { 219 this._listeners[type].splice(index, 1); 220 } 221 222 return this 223 }; 224 225 /** 226 * Iterate over all registered handlers for given type and call them all with 227 * the dialog element as first argument, event as second argument (if any). 228 * 229 * @access private 230 * @param {string} type 231 * @param {Event} event 232 */ 233 A11yDialog.prototype._fire = function (type, event) { 234 var listeners = this._listeners[type] || []; 235 236 listeners.forEach( 237 function (listener) { 238 listener(this.$el, event); 239 }.bind(this) 240 ); 241 }; 242 243 /** 244 * Private event handler used when listening to some specific key presses 245 * (namely ESCAPE and TAB) 246 * 247 * @access private 248 * @param {Event} event 249 */ 250 A11yDialog.prototype._bindKeypress = function (event) { 251 // This is an escape hatch in case there are nested dialogs, so the keypresses 252 // are only reacted to for the most recent one 253 if (!this.$el.contains(document.activeElement)) return 254 255 // If the dialog is shown and the ESCAPE key is being pressed, prevent any 256 // further effects from the ESCAPE key and hide the dialog, unless its role 257 // is 'alertdialog', which should be modal 258 if ( 259 this.shown && 260 event.which === ESCAPE_KEY && 261 this.$el.getAttribute('role') !== 'alertdialog' 262 ) { 263 event.preventDefault(); 264 this.hide(event); 265 } 266 267 // If the dialog is shown and the TAB key is being pressed, make sure the 268 // focus stays trapped within the dialog element 269 if (this.shown && event.which === TAB_KEY) { 270 trapTabKey(this.$el, event); 271 } 272 }; 273 274 /** 275 * Private event handler used when making sure the focus stays within the 276 * currently open dialog 277 * 278 * @access private 279 * @param {Event} event 280 */ 281 A11yDialog.prototype._maintainFocus = function (event) { 282 // If the dialog is shown and the focus is not within a dialog element (either 283 // this one or another one in case of nested dialogs) or within an element 284 // with the `data-a11y-dialog-focus-trap-ignore` attribute, move it back to 285 // its first focusable child. 286 // See: https://github.com/KittyGiraudel/a11y-dialog/issues/177 287 if ( 288 this.shown && 289 !event.target.closest('[aria-modal="true"]') && 290 !event.target.closest('[data-a11y-dialog-ignore-focus-trap]') 291 ) { 292 moveFocusToDialog(this.$el); 293 } 294 }; 295 296 /** 297 * Convert a NodeList into an array 298 * 299 * @param {NodeList} collection 300 * @return {Array<Element>} 301 */ 302 function toArray(collection) { 303 return Array.prototype.slice.call(collection) 304 } 305 306 /** 307 * Query the DOM for nodes matching the given selector, scoped to context (or 308 * the whole document) 309 * 310 * @param {String} selector 311 * @param {Element} [context = document] 312 * @return {Array<Element>} 313 */ 314 function $$(selector, context) { 315 return toArray((context || document).querySelectorAll(selector)) 316 } 317 318 /** 319 * Set the focus to the first element with `autofocus` with the element or the 320 * element itself 321 * 322 * @param {Element} node 323 */ 324 function moveFocusToDialog(node) { 325 var focused = node.querySelector('[autofocus]') || node; 326 327 focused.focus(); 328 } 329 330 /** 331 * Get the focusable children of the given element 332 * 333 * @param {Element} node 334 * @return {Array<Element>} 335 */ 336 function getFocusableChildren(node) { 337 return $$(focusableSelectors.join(','), node).filter(function (child) { 338 return !!( 339 child.offsetWidth || 340 child.offsetHeight || 341 child.getClientRects().length 342 ) 343 }) 344 } 345 346 /** 347 * Trap the focus inside the given element 348 * 349 * @param {Element} node 350 * @param {Event} event 351 */ 352 function trapTabKey(node, event) { 353 var focusableChildren = getFocusableChildren(node); 354 var focusedItemIndex = focusableChildren.indexOf(document.activeElement); 355 356 // If the SHIFT key is being pressed while tabbing (moving backwards) and 357 // the currently focused item is the first one, move the focus to the last 358 // focusable item from the dialog element 359 if (event.shiftKey && focusedItemIndex === 0) { 360 focusableChildren[focusableChildren.length - 1].focus(); 361 event.preventDefault(); 362 // If the SHIFT key is not being pressed (moving forwards) and the currently 363 // focused item is the last one, move the focus to the first focusable item 364 // from the dialog element 365 } else if ( 366 !event.shiftKey && 367 focusedItemIndex === focusableChildren.length - 1 368 ) { 369 focusableChildren[0].focus(); 370 event.preventDefault(); 371 } 372 } 373 374 function instantiateDialogs() { 375 $$('[data-a11y-dialog]').forEach(function (node) { 376 new A11yDialog(node); 377 }); 378 } 379 380 if (typeof document !== 'undefined') { 381 if (document.readyState === 'loading') { 382 document.addEventListener('DOMContentLoaded', instantiateDialogs); 383 } else { 384 if (window.requestAnimationFrame) { 385 window.requestAnimationFrame(instantiateDialogs); 386 } else { 387 window.setTimeout(instantiateDialogs, 16); 388 } 389 } 390 } 391 392 return A11yDialog; 393 394 })));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.