PageSourceSearch

https://www.nacacnet.org/wp-content/plugins/my-calendar/js/modal/a…essible-modal-window-aria.js?ver=3.8.623349

js nacacnet.org collected 2026-09-24 08:35:59 UTC 17,653 bytes, 417 lines download raw bytes

1/**
2 * ES2015 accessible modal window system, using ARIA
3 *
4 * Forked from van11y.net
5 *
6 * Website: https://van11y.net/accessible-modal/
7 * License MIT: https://github.com/nico3333fr/van11y-accessible-modal-window-aria/blob/master/LICENSE
8 */
9'use strict';
10
11(function (doc) {
12
13  'use strict';
14
15  var MODAL_ID_PREFIX = 'label_modal_';
16  var MODAL_CLASS_SUFFIX = 'modal';
17  var MODAL_PREFIX_CLASS_ATTR = 'data-modal-prefix-class';
18  var MODAL_TEXT_ATTR = 'data-modal-text';
19  var MODAL_CONTENT_ID_ATTR = 'data-modal-content-id';
20  var MODAL_DESCRIBEDBY_ID_ATTR = 'data-modal-describedby-id';
21  var MODAL_TITLE_ATTR = 'data-modal-title';
22  var MODAL_FOCUS_TO_ATTR = 'data-modal-focus-toid';
23  var MODAL_CLOSE_TEXT_ATTR = 'data-modal-close-text';
24
25  var MODAL_BUTTON_CLASS_SUFFIX = 'modal-close';
26  var MODAL_BUTTON_JS_ID = 'mc-modal-close';
27  var MODAL_BUTTON_JS_CLASS = 'mc-modal-close';
28  var MODAL_BUTTON_CONTENT_BACK_ID = 'data-content-back-id';
29  var MODAL_BUTTON_FOCUS_BACK_ID = 'data-focus-back';
30
31  var MODAL_WRAPPER_CLASS_SUFFIX = 'modal__wrapper';
32  var MODAL_CONTENT_CLASS_SUFFIX = 'modal__content';
33  var MODAL_CONTENT_JS_ID = 'mc-modal-content';
34
35  var MODAL_CLOSE_TEXT_CLASS_SUFFIX = 'modal-close__text';
36
37  var MODAL_TITLE_ID = 'modal-title';
38  var MODAL_TITLE_CLASS_SUFFIX = 'modal-title';
39
40  var FOCUSABLE_ELEMENTS_STRING = "a[href], area[href], input:not([type='hidden']):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]";
41  var WRAPPER_PAGE_JS = 'mc-modal-page';
42  var MODAL_JS_ID = 'mc-modal';
43  var MODAL_OVERLAY_ID = 'mc-modal-overlay';
44  var MODAL_OVERLAY_CLASS_SUFFIX = 'modal-overlay';
45  var MODAL_OVERLAY_TXT = 'Close modal';
46  var MODAL_OVERLAY_BG_ENABLED_ATTR = 'data-background-click';
47  var NO_SCROLL_CLASS = 'mc-no-scroll';
48  var ATTR_OPEN = 'open';
49
50  var findById = function findById(id) {
51    return doc.getElementById(id);
52  };
53
54  var addClass = function addClass(el, className) {
55    if (el.classList) {
56      el.classList.add(className);
57    }
58  };
59
60  var removeClass = function removeClass(el, className) {
61    if (el.classList) {
62      el.classList.remove(className);
63    }
64  };
65
66  var hasClass = function hasClass(el, className) {
67    if (el.classList) {
68      return el.classList.contains(className);
69    } else {
70		return false;
71	}
72  };
73
74  function wrapInner(parent, wrapper) {
75    if (typeof wrapper === "string") wrapper = document.createElement(wrapper);
76
77    parent.appendChild(wrapper);
78
79    while (parent.firstChild !== wrapper) wrapper.appendChild(parent.firstChild);
80  }
81
82  function remove(el) {
83    /* node.remove() is too modern for IE≤11 */
84    el.parentNode.removeChild(el);
85  }
86
87  /* gets an element el, search if it is child of parent class, returns id of the parent */
88  var searchParent = function searchParent(el, parentClass) {
89    var found = false;
90    var parentElement = el.parentNode;
91    while (parentElement && found === false) {
92      if (hasClass(parentElement, parentClass) === true) {
93        found = true;
94      } else {
95        parentElement = parentElement.parentNode;
96      }
97    }
98    if (found === true) {
99      return parentElement.getAttribute('id');
100    } else {
101      return '';
102    }
103  };
104
105  /**
106   * Create the template for an overlay
107   * @param  {Object} config
108   * @return {String}
109   */
110  var createOverlay = function createOverlay(config) {
111
112    var id = MODAL_OVERLAY_ID;
113    var overlayText = config.text || MODAL_OVERLAY_TXT;
114    var overlayClass = config.prefixClass + MODAL_OVERLAY_CLASS_SUFFIX;
115    var overlayBackgroundEnabled = config.backgroundEnabled === 'disabled' ? 'disabled' : 'enabled';
116
117    return '<span\n                    id="' + id + '"\n                    class="' + overlayClass + '"\n                    ' + MODAL_OVERLAY_BG_ENABLED_ATTR + '="' + overlayBackgroundEnabled + '"\n                    title="' + overlayText + '"\n                    >
117\n                    <span class="screen-reader-text">' + overlayText + '</span>\n                  </span>';
118  };
119
120  /**
121   * Create the template for a modal
122   * @param  {Object} config
123   * @return {String}
124   */
125  var createModal = function createModal(config) {
126	var dialog = document.createElement( 'dialog' );
127	var outerContent = document.createElement( 'div' );
128	var innerContent = document.createElement( 'div' );
129	var contentContainer = document.createElement( 'div' );
130	var titleContainer = document.createElement( 'div' );
131	contentContainer.setAttribute( 'id', MODAL_CONTENT_JS_ID );
132	titleContainer.classList.add( 'mc-modal-title-container' );
133
134	var id = MODAL_JS_ID;
135    var modalClassName = config.modalPrefixClass + MODAL_CLASS_SUFFIX;
136    var modalClassWrapper = config.modalPrefixClass + MODAL_WRAPPER_CLASS_SUFFIX;
137    var buttonCloseClassName = config.modalPrefixClass + MODAL_BUTTON_CLASS_SUFFIX;
138    var buttonCloseInner = '<span class="' + config.modalPrefixClass + MODAL_CLOSE_TEXT_CLASS_SUFFIX + '">\n                                          ' + config.modalCloseText + '\n                                         </span>';
139    var contentClassName = config.modalPrefixClass + MODAL_CONTENT_CLASS_SUFFIX;
140    var titleClassName = config.modalPrefixClass + MODAL_TITLE_CLASS_SUFFIX;
141    var title = config.modalTitle !== '' ? '<h1 id="' + MODAL_TITLE_ID + '" class="' + titleClassName + '">\n                                          ' + config.modalTitle + '\n                                         </h1>' : '';
142    var button_close = '<button type="button" class="' + MODAL_BUTTON_JS_CLASS + ' ' + buttonCloseClassName + '" id="' + MODAL_BUTTON_JS_ID + '" ' + MODAL_BUTTON_CONTENT_BACK_ID + '="' + config.modalContentId + '" ' + MODAL_BUTTON_FOCUS_BACK_ID + '="' + config.modalFocusBackId + '"><span class="dashicons dashicons-no" aria-hidden="true"></span>\n                               ' + buttonCloseInner + '\n                              </button>';
143    var content = config.modalText;
144
145    // If there is no content but an id we try to fetch content id
146    if (content === '' && config.modalContentId) {
147      var contentFromId = findById(config.modalContentId);
148	  var hasMultipleEvents = contentFromId.classList.contains( 'calendar-events' );
149	  if ( ! hasMultipleEvents && contentFromId ) {
150		// Legacy templates don't have this class inside the content.
151		var event = contentFromId.querySelector( '.mc-event' );
152		var classes;
153		if ( event ) {
154			classes = event.getAttribute( 'class' );
155		}
156		if ( classes ) {
157			dialog.setAttribute('class', classes);
158		}
159	  }
160      if (contentFromId) {
161		contentContainer.insertAdjacentElement( 'beforeEnd', contentFromId );
162      }
163    } else {
164		contentContainer.insertAdjacentHTML( 'beforeEnd', content );
165	}
166	dialog.setAttribute( 'id', id );
167	dialog.classList.add( modalClassName );
168	dialog.setAttribute( 'role', 'dialog' );
169	dialog.setAttribute( 'aria-describedby', config.modalDescribedById + ' ' + ATTR_OPEN );
170	dialog.setAttribute( 'aria-labelledby', MODAL_TITLE_ID );
171	outerContent.setAttribute( 'role', 'document' );
172	outerContent.classList.add( modalClassWrapper );
173	titleContainer.insertAdjacentHTML( 'afterBegin', title );
174	titleContainer.insertAdjacentHTML( 'afterBegin', button_close );
175	innerContent.classList.add( contentClassName );
176	innerContent.insertAdjacentElement( 'afterbegin', titleContainer );
177	innerContent.insertAdjacentElement( 'beforeEnd', contentContainer );
178	outerContent.insertAdjacentElement( 'afterBegin', innerContent );
179	dialog.insertAdjacentElement( 'afterBegin', outerContent );
180
181	return dialog;
182  };
183
184  var closeModal = function closeModal(config) {
185    if (config.modalFocusBackId !== '') {
186		var modalReturn = findById( config.modalFocusBackId );
187		var modalReturnContainer = modalReturn.closest( '.mc-events' ); // only works when control is inside container.
188		if ( modalReturnContainer && modalReturnContainer.tagName === 'ARTICLE' ) {
189			modalReturnContainer.insertAdjacentElement( 'beforeEnd', config.modalContent.firstChild );
190		} else {
191			modalReturnContainer = modalReturn.closest( '.mc-main' );
192			modalReturnContainer.insertAdjacentElement( 'beforeEnd', config.modalContent.firstChild );
193		}
194	}
195
196	remove(config.modal);
197    remove(config.overlay);
198  };
199
200  /** Find all modals inside a container
201   * @param  {Node} node Default document
202   * @return {Array}
203   */
204  var $listModals = function $listModals() {
205    var node = arguments.length <= 0 || arguments[0] === undefined ? doc : arguments[0];
206    return [].slice.call(node.querySelectorAll('.mc-modal'));
207  };
208
209  /**
210   * Build modals for a container
211   * @param  {Node} node
212   */
213  var attach = function attach(node) {
214    var addListeners = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1];
215
216    $listModals(node).forEach(function (modal_node) {
217
218      var iLisible = Math.random().toString(32).slice(2, 12);
219      var wrapperBody = findById(WRAPPER_PAGE_JS);
220      var body = doc.querySelector('body');
221
222	  // Only generate an ID if node does not already have one.
223	  if ( modal_node.hasAttribute('id') === false ) {
224      	modal_node.setAttribute('id', MODAL_ID_PREFIX + iLisible);
225	  }
226      modal_node.setAttribute('aria-haspopup', 'dialog');
227
228      if (wrapperBody === null || wrapperBody.length === 0) {
229        var wrapper = doc.createElement('DIV');
230        wrapper.setAttribute('id', WRAPPER_PAGE_JS);
231        wrapInner(body, wrapper);
232		wrapperBody = wrapper;
233      }
234    });
235
236    if (addListeners) {
237
238      /* listeners */
239      ['click', 'keydown'].forEach(function (eventName) {
240
241        doc.body.addEventListener(eventName, function (e) {
242
243          // click on link modal
244          var parentModalLauncher = searchParent(e.target, 'mc-modal');
245          if ((hasClass(e.target, 'mc-modal') === true || parentModalLauncher !== '') && eventName === 'click') {
246            var body = doc.querySelector('body');
247            var modalLauncher = parentModalLauncher !== '' ? findById(parentModalLauncher) : e.target;
248            var modalPrefixClass = modalLauncher.hasAttribute(MODAL_PREFIX_CLASS_ATTR) === true ? modalLauncher.getAttribute(MODAL_PREFIX_CLASS_ATTR) + '-' : '';
249            var modalText = modalLauncher.hasAttribute(MODAL_TEXT_ATTR) === true ? modalLauncher.getAttribute(MODAL_TEXT_ATTR) : '';
250            var modalContentId = modalLauncher.hasAttribute(MODAL_CONTENT_ID_ATTR) === true ? modalLauncher.getAttribute(MODAL_CONTENT_ID_ATTR) : '';
251            var modalDescribedById = modalLauncher.hasAttribute(MODAL_DESCRIBEDBY_ID_ATTR) === true ? modalLauncher.getAttribute(MODAL_DESCRIBEDBY_ID_ATTR) : '';
252            var modalTitle = modalLauncher.hasAttribute(MODAL_TITLE_ATTR) === true ? modalLauncher.getAttribute(MODAL_TITLE_ATTR) : '';
253            var modalCloseText = modalLauncher.hasAttribute(MODAL_CLOSE_TEXT_ATTR) === true ? modalLauncher.getAttribute(MODAL_CLOSE_TEXT_ATTR) : MODAL_OVERLAY_TXT;
254            var modalGiveFocusToId = modalLauncher.hasAttribute(MODAL_FOCUS_TO_ATTR) === true ? modalLauncher.getAttribute(MODAL_FOCUS_TO_ATTR) : '';
255
256            var wrapperBody = findById(WRAPPER_PAGE_JS);
257
258            // insert overlay
259            body.insertAdjacentHTML('beforeEnd', createOverlay({
260              text: modalCloseText,
261              backgroundEnabled: '',
262              prefixClass: modalPrefixClass
263            }));
264
265			// insert modal
266            body.insertAdjacentElement('beforeEnd', createModal({
267              modalText: modalText,
268              modalPrefixClass: modalPrefixClass,
269              backgroundEnabled: modalContentId,
270              modalTitle: modalTitle,
271              modalCloseText: modalCloseText,
272              modalCloseTitle: modalCloseText,
273              modalContentId: modalContentId,
274              modalDescribedById: modalDescribedById,
275              modalFocusBackId: modalLauncher.getAttribute('id')
276            }));
277
278            // hide page
279            wrapperBody.setAttribute('inert', '');
280
281            // add class noscroll to body
282            addClass(body, NO_SCROLL_CLASS);
283
284            // give focus to close button or specified element
285            var closeButton = findById(MODAL_BUTTON_JS_ID);
286            if (modalGiveFocusToId !== '') {
287              var focusTo = findById(modalGiveFocusToId);
288              if (focusTo) {
289                focusTo.focus();
290              } else {
291                closeButton.focus();
292              }
293            } else {
294              closeButton.focus();
295            }
296
297            e.preventDefault();
298          }
299
300          // click on close button or on overlay not blocked
301          var parentButton = searchParent(e.target, MODAL_BUTTON_JS_CLASS);
302          if ((e.target.getAttribute('id') === MODAL_BUTTON_JS_ID || parentButton !== '' || e.target.getAttribute('id') === MODAL_OVERLAY_ID || hasClass(e.target, MODAL_BUTTON_JS_CLASS) === true) && eventName === 'click') {
303            var body = doc.querySelector('body');
304            var wrapperBody = findById(WRAPPER_PAGE_JS);
305            var modal = findById(MODAL_JS_ID);
306            var modalContent = findById(MODAL_CONTENT_JS_ID) ? findById(MODAL_CONTENT_JS_ID) : '';
307            var overlay = findById(MODAL_OVERLAY_ID);
308            var modalButtonClose = findById(MODAL_BUTTON_JS_ID);
309            var modalFocusBackId = modalButtonClose.getAttribute(MODAL_BUTTON_FOCUS_BACK_ID);
310            var contentBackId = modalButtonClose.getAttribute(MODAL_BUTTON_CONTENT_BACK_ID);
311            var backgroundEnabled = overlay.getAttribute(MODAL_OVERLAY_BG_ENABLED_ATTR);
312
313            if (!(e.target.getAttribute('id') === MODAL_OVERLAY_ID && backgroundEnabled === 'disabled')) {
314
315              closeModal({
316                modal: modal,
317                modalContent: modalContent,
318                overlay: overlay,
319                modalFocusBackId: modalFocusBackId,
320                contentBackId: contentBackId,
321                backgroundEnabled: backgroundEnabled,
322                fromId: e.target.getAttribute('id')
323              });
324
325              // show back page
326              wrapperBody.removeAttribute('inert');
327
328              // remove class noscroll to body
329              removeClass(body, NO_SCROLL_CLASS);
330
331              if (modalFocusBackId) {
332                var contentFocus = findById(modalFocusBackId);
333                if (contentFocus) {
334                  contentFocus.focus();
335                }
336              }
337            }
338          }
339
340          // strike a key when modal opened
341          if (findById(MODAL_JS_ID) && eventName === 'keydown') {
342            var body = doc.querySelector('body');
343            var wrapperBody = findById(WRAPPER_PAGE_JS);
344            var modal = findById(MODAL_JS_ID);
345            var modalContent = findById(MODAL_CONTENT_JS_ID) ? findById(MODAL_CONTENT_JS_ID) : '';
346            var overlay = findById(MODAL_OVERLAY_ID);
347            var modalButtonClose = findById(MODAL_BUTTON_JS_ID);
348            var modalFocusBackId = modalButtonClose.getAttribute(MODAL_BUTTON_FOCUS_BACK_ID);
349            var contentBackId = modalButtonClose.getAttribute(MODAL_BUTTON_CONTENT_BACK_ID);
350            var listFocusables = [].slice.call(modal.querySelectorAll(FOCUSABLE_ELEMENTS_STRING));
351			listFocusables = listFocusables.filter( checkElementVisibility );
352			function checkElementVisibility(el) {
353				return el.checkVisibility();
354			}
355
356            // esc
357            if (e.keyCode === 27) {
358
359              closeModal({
360                modal: modal,
361                modalContent: modalContent,
362                overlay: overlay,
363                modalFocusBackId: modalFocusBackId,
364                contentBackId: contentBackId
365              });
366
367              // show back page
368              wrapperBody.removeAttribute('inert');
369
370              // remove class noscroll to body
371              removeClass(body, NO_SCROLL_CLASS);
372
373              if (modalFocusBackId) {
374                var contentFocus = findById(modalFocusBackId);
375                if (contentFocus) {
376                  contentFocus.focus();
377                }
378              }
379            }
380
381            // tab or shift tab in modal => capture focus
382            if (e.key === 'Tab' && listFocusables.indexOf(e.target) >= 0) {
383              // shift-tab on first element focusable => focus on last
384              if (e.shiftKey) {
385                if (e.target === listFocusables[0]) {
386                  listFocusables.at(-1).focus();
387                  e.preventDefault();
388                }
389              } else {
390                // tab on last element focusable => focus on first
391                if (e.target === listFocusables.at(-1)) {
392                  listFocusables[0].focus();
393                  e.preventDefault();
394                }
395              }
396            }
397
398            // tab outside modal => put it in focus
399            if (e.keyCode === 9 && listFocusables.indexOf(e.target) === -1) {
400              e.preventDefault();
401              listFocusables[0].focus();
402            }
403          }
404        }, true);
405      });
406    }
407  };
408
409  var onLoad = function onLoad() {
410    attach();
411    document.removeEventListener('DOMContentLoaded', onLoad);
412  };
413
414  document.addEventListener('DOMContentLoaded', onLoad);
415
416  window.accessibleModalWindowAria = attach;
417})(document);

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.