PageSourceSearch

https://www.visitblekinge.se/modules/custom/common/wx_vendor_libraries/js/wcag.menu.js?tlvkaq

js visitblekinge.se collected 2026-09-24 21:34:01 UTC 15,669 bytes, 385 lines download raw bytes

1/**
2 * Custom WebX JS (Not external vendor library)
3 * Initialization of the WCAG menu functionality.
4 */
5
6/**
7 * setWcagMenuAriaLabels - Example config object
8 * @type {Object}
9 * @property {string} menuSelector - The selector for the main menu.
10 * @property {string} submenuSelector - The selector for the submenu.
11 * @property {string} toggleSelector - The selector for the dropdown toggle button.
12 * @example
13 * const config = {
14 *   menuSelector: '.menu-main',
15 *   submenuSelector: 'ul.dropdown-submenu',
16 *   toggleSelector: '.dropdown-toggle'
17 * };
18 * setWcagMenuAriaLabels(config);
19 */
20function setWcagMenuAriaLabels(config) {
21  if (!config) {
22    console.warn('function setWcagMenuAriaLabels: config object required');
23    return;
24  }
25  const { menuSelector, submenuSelector, toggleSelector } = config;
26  if (!menuSelector || !submenuSelector || !toggleSelector) {
27    console.warn('function setWcagMenuAriaLabels: ' + (menuSelector ? 'menuSelector ' : '') + (submenuSelector ? 'submenuSelector ' : '') + (toggleSelector ? 'toggleSelector ' : '') + 'is required');
28    return;
29  }
30  const menus = document.querySelectorAll(menuSelector);
31  menus.forEach((menu) => {
32    const toggleButtons = menu.querySelectorAll(toggleSelector);
33    toggleButtons.forEach((button) => {
34      const submenu = button.nextElementSibling;
35      const submenuList = submenu ? submenu.querySelector(submenuSelector) : null;
36      if (submenuList) {
37        submenuList.setAttribute('aria-labelledby', button.id);
38      }
39    });
40  });
41}
42/**/
43
44/**
45 * enableWcagMenuKeyboardNavigation - Example config object
46 * @type {Object}
47 * @property {string} menuSelector - The selector for the main menu.
48 * @property {string} toggleSelector - The selector for the dropdown toggle button.
49 * @property {string} level1PanelSelector - The selector for the dropdown menu.
50 * @property {string} level2PanelSelector - The selector for the dropdown submenu.
51 * @property {string} level1ToggleSelector - The selector for the top-level menu items.
52 * @property {string} level2ToggleSelector - The selector for the submenu items.
53 * @example
54 * const config = {
55 *   menuSelector: '.menu-main',
56 *   toggleSelector: '.dropdown-toggle',
57 *   level1PanelSelector: '.dropdown-menu',
58 *   level2PanelSelector: '.dropdown-menu-slide',
59 *   level1ToggleSelector: ':scope > li > .nav-link, :scope > li > .dropdown-toggle', // The :scope pseudo-class ensures that querySelectorAll only matches direct descendants of the current element, not from the entire document.
60 *   level2ToggleSelector: ':scope > ul > li > .nav-link, :scope > ul > li > .dropdown-toggle', // The :scope pseudo-class ensures that querySelectorAll only matches direct descendants of the current element, not from the entire document.
61 * };
62 * enableWcagMenuKeyboardNavigation(config);
63 */
64function enableWcagMenuKeyboardNavigation(config) {
65  if (!config) {
66    console.warn('function enableWcagMenuKeyboardNavigation: config object required');
67    return;
68  }
69  const { menuSelector, toggleSelector, level1PanelSelector, level2PanelSelector, level1ToggleSelector, level2ToggleSelector } = config;
70  if (!menuSelector || !toggleSelector || !level1PanelSelector || !level2PanelSelector || !level1ToggleSelector || !level2ToggleSelector) {
71    console.warn(
72      'function enableWcagMenuKeyboardNavigation: ' +
73        (menuSelector ? '' : 'menuSelector ') +
74        (toggleSelector ? '' : 'toggleSelector ') +
75        (level1PanelSelector ? '' : 'level1PanelSelector ') +
76        (level2PanelSelector ? '' : 'level2PanelSelector ') +
77        (level1ToggleSelector ? '' : 'level1ToggleSelector ') +
78        (level2ToggleSelector ? '' : 'level2ToggleSelector ') +
79        'is required'
80    );
81    return;
82  }
83
84  let currentIndex = -1;
85  let currentElement = null;
86
87  const menus = document.querySelectorAll(menuSelector);
88
89  menus.forEach((menu) => {
90    menu.addEventListener('keydown', function (event) {
91      if (currentIndex === -1) {
92        currentElement = document.activeElement;
93      }
94
95      // These variables only handle up to two levels (main and one submenu).
96      let isLevel1 = false;
97      let isLevel2 = false;
98      let level1Elements = [];
99      let level2Elements = [];
100
101      if (currentElement.classList.contains(toggleSelector.replace('.', '')) && currentElement.getAttribute('aria-expanded') === 'true') {
102        isLevel1 = true;
103        const dropdownMenu = currentElement.nextElementSibling;
104        level1Elements = dropdownMenu ? [currentElement, ...dropdownMenu.querySelectorAll(level2ToggleSelector)] : [];
105
106        if (level1Elements.includes(currentElement)) {
107          currentIndex = level1Elements.indexOf(currentElement);
108        } else {
109          currentIndex = -1;
110        }
111
112        if (currentElement.closest(level2PanelSelector)) {
113          isLevel2 = true;
114          const level2Menu = currentElement.closest(level2PanelSelector);
115          level2Elements = level2Menu ? [level2Menu.previousElementSibling, ...level2Menu.querySelectorAll(level2ToggleSelector)] : [];
116        }
117      } else if (currentElement.closest(level1PanelSelector)) {
118        isLevel1 = true;
119        const dropdownMenu = currentElement.closest(level1PanelSelector);
120        level1Elements = dropdownMenu ? [dropdownMenu.previousElementSibling, ...dropdownMenu.querySelectorAll(level2ToggleSelector)] : [];
121
122        if (currentElement.closest(level2PanelSelector)) {
123          isLevel2 = true;
124          const level2Menu = currentElement.closest(level2PanelSelector);
125          level2Elements = level2Menu ? [level2Menu.previousElementSibling, ...level2Menu.querySelectorAll(level2ToggleSelector)] : [];
126        }
127      }
128
129      if (event.key === 'Tab' && event.shiftKey) {
130        if (isLevel2) {
131          event.preventDefault();
132          event.stopPropagation();
133
134          currentIndex = (currentIndex - 1 + level2Elements.length) % level2Elements.length;
135          level2Elements[currentIndex].focus();
136          currentElement = level2Elements[currentIndex];
137        } else if (isLevel1) {
138          event.preventDefault();
139          event.stopPropagation();
140
141          currentIndex = (currentIndex - 1 + level1Elements.length) % level1Elements.length;
142          level1Elements[currentIndex].focus();
143          currentElement = level1Elements[currentIndex];
144        } else {
145          const topLevelElements = [...menu.querySelectorAll(level1ToggleSelector)];
146          if (currentIndex < topLevelElements.length - 2) {
147            event.preventDefault();
148            event.stopPropagation();
149            currentIndex = (currentIndex - 1 + topLevelElements.length) % topLevelElements.length;
150            topLevelElements[currentIndex].focus();
151            currentElement = topLevelElements[currentIndex];
152          }
153        }
154        return;
155      }
156
157      switch (event.key) {
158        case 'ArrowDown':
159        case 'ArrowRight':
160          if (isLevel2) {
161            currentIndex = (currentIndex + 1) % level2Elements.length;
162            level2Elements[currentIndex].focus();
163            currentElement = level2Elements[currentIndex];
164          } else if (isLevel1) {
165            event.preventDefault();
166            event.stopPropagation();
167
168            currentIndex = (currentIndex + 1) % level1Elements.length;
169            level1Elements[currentIndex].focus();
170            currentElement = level1Elements[currentIndex];
171          } else {
172            event.preventDefault();
173            event.stopPropagation();
174            const topLevelElements = [...menu.querySelectorAll(level1ToggleSelector)];
175            currentIndex = (currentIndex + 1) % topLevelElements.length;
176            topLevelElements[currentIndex].focus();
177            currentElement = topLevelElements[currentIndex];
178          }
179          break;
180
181        case 'Tab':
182          if (isLevel2) {
183            event.preventDefault();
184            event.stopPropagation();
185
186            currentIndex = (currentIndex + 1) % level2Elements.length;
187            level2Elements[currentIndex].focus();
188            currentElement = level2Elements[currentIndex];
189          } else if (isLevel1) {
190            event.preventDefault();
191            event.stopPropagation();
192
193            currentIndex = (currentIndex + 1) % level1Elements.length;
194            level1Elements[currentIndex].focus();
195            currentElement = level1Elements[currentIndex];
196          } else {
197            const topLevelElements = [...menu.querySelectorAll(level1ToggleSelector)];
198            if (currentIndex < topLevelElements.length - 2) {
199              event.preventDefault();
200              event.stopPropagation();
201              currentIndex = (currentIndex + 1) % topLevelElements.length;
202              topLevelElements[currentIndex].focus();
203              currentElement = topLevelElements[currentIndex];
204            }
205          }
206          break;
207
208        case 'ArrowUp':
209        case 'ArrowLeft':
210          event.preventDefault();
211          event.stopPropagation();
212          if (isLevel2) {
213            currentIndex = (currentIndex - 1 + level2Elements.length) % level2Elements.length;
214            level2Elements[currentIndex].focus();
215            currentElement = level2Elements[currentIndex];
216          } else if (isLevel1) {
217            currentIndex = (currentIndex - 1 + level1Elements.length) % level1Elements.length;
218            level1Elements[currentIndex].focus();
219            currentElement = level1Elements[currentIndex];
220          } else {
221            const topLevelElements = [...menu.querySelectorAll(level1ToggleSelector)];
222            currentIndex = (currentIndex - 1 + topLevelElements.length) % topLevelElements.length;
223            topLevelElements[currentIndex].focus();
224            currentElement = topLevelElements[currentIndex];
225          }
226          break;
227
228        case 'Enter':
229        case ' ':
230          event.preventDefault();
231          if (currentElement && typeof currentElement.click === 'function') {
232            currentElement.click();
233          }
234          break;
235
236        case 'Escape':
237        case 'Esc':
238          event.preventDefault();
239          event.stopPropagation();
240          const toggleExpandedSelector = menu.querySelector(`${toggleSelector}[aria-expanded="true"]`);
241          if (!toggleExpandedSelector) {
242            break;
243          }
244          toggleExpandedSelector.click();
245          toggleExpandedSelector.focus();
246          currentElement = toggleExpandedSelector;
247          currentIndex = -1;
248
249          break;
250      }
251    });
252  });
253}
254
255/**
256 * enableWcagMenuTabTrapping - Example config object
257 * @type {Object}
258 * @property {string} menuSelector - The selector for the main menu container.
259 * @property {string} toggleSelector - The selector for the menu toggle button (hamburger button).
260 * @property {string} closeSelector - The selector for close buttons within the menu (optional).
261 * @property {string} focusableElementsSelector - Custom selector for focusable elements (optional).
262 * @example
263 * const config = {
264 *   menuSelector: '.offcanvas-menu',
265 *   toggleSelector: '.hamburger-toggle',
266 *   closeSelector: '.close-menu',
267 *   focusableElementsSelector: 'a, button, [tabindex]:not([tabindex="-1"])'
268 * };
269 * enableWcagMenuTabTrapping(config);
270 */
271function enableWcagMenuTabTrapping(config) {
272  if (!config?.menuSelector || !config?.toggleSelector) {
273    console.warn('enableWcagMenuTabTrapping: menuSelector and toggleSelector are required');
274    return;
275  }
276
277  const { menuSelector, toggleSelector, closeSelector, focusableElementsSelector = 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])' } = config;
278
279  document.querySelectorAll(menuSelector).forEach((menu) => {
280    let isMenuOpen = false;
281    let focusableElements = [];
282    let lastFocusedElement = null;
283
284    const isVisible = (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement;
285
286    const getFocusableElements = () => {
287      const menuElements = Array.from(menu.querySelectorAll(focusableElementsSelector)).filter(isVisible);
288
289      // When menu is open, prefer close selector over toggle selector
290      if (isMenuOpen && closeSelector) {
291        const closeButtons = Array.from(menu.querySelectorAll(closeSelector)).filter(isVisible);
292        if (closeButtons.length > 0) {
293          // Filter out close buttons from menu elements to avoid duplicates
294          const otherMenuElements = menuElements.filter(el => !closeButtons.includes(el));
295          return [...closeButtons, ...otherMenuElements];
296        }
297      }
298
299      // When menu is closed or no close selector, use toggle selector
300      const toggleButtons = Array.from(document.querySelectorAll(toggleSelector)).filter(isVisible);
301      // Filter out toggle buttons from menu elements to avoid duplicates (in case they're inside the menu)
302      const otherMenuElements = menuElements.filter(el => !toggleButtons.includes(el));
303      return [...toggleButtons, ...otherMenuElements];
304    };
305
306    const isMenuOpenCheck = () =>
307      menu.classList.contains('show') ||
308      menu.classList.contains('open') ||
309      menu.getAttribute('aria-hidden') === 'false' ||
310      window.getComputedStyle(menu).display !== 'none' ||
311      document.body.classList.contains('menu-visible') ||
312      document.body.classList.contains('show-menu');
313
314    const trapFocus = (event) => {
315      if (!isMenuOpen || event.key !== 'Tab') {
316        if (event.key === 'Escape') {
317          event.preventDefault();
318          closeMenu();
319        }
320        return;
321      }
322
323      // Recalculate focusable elements to get current state (important for close/toggle selector switching)
324      const currentFocusableElements = getFocusableElements();
325      if (currentFocusableElements.length === 0) return;
326
327      event.preventDefault();
328      const currentIndex = currentFocusableElements.findIndex(el => el === document.activeElement);
329      const nextIndex = event.shiftKey
330        ? (currentIndex <= 0 ? currentFocusableElements.length - 1 : currentIndex - 1)
331        : (currentIndex >= currentFocusableElements.length - 1 ? 0 : currentIndex + 1);
332
333      currentFocusableElements[nextIndex].focus();
334    };
335
336    const openMenu = () => {
337      isMenuOpen = true;
338      focusableElements = getFocusableElements();
339      focusableElements[0]?.focus();
340      document.addEventListener('keydown', trapFocus);
341    };
342
343    const closeMenu = () => {
344      isMenuOpen = false;
345      focusableElements = [];
346      document.removeEventListener('keydown', trapFocus);
347      lastFocusedElement?.focus();
348      lastFocusedElement = null;
349    };
350
351    // Toggle button handling
352    document.querySelectorAll(toggleSelector).forEach((toggle) => {
353      toggle.addEventListener('click', () => {
354        lastFocusedElement = toggle;
355
356        if (isMenuOpen) {
357          closeMenu();
358          return;
359        }
360
361        // Otherwise check if menu opened after click
362        setTimeout(() => {
363          const menuIsOpen = isMenuOpenCheck();
364          if (menuIsOpen && !isMenuOpen) {
365            openMenu();
366          }
367        }, 10);
368      });
369    });
370
371    // Close button handling
372    if (closeSelector) {
373      menu.querySelectorAll(closeSelector).forEach((closeBtn) => {
374        closeBtn.addEventListener('click', () => setTimeout(closeMenu, 10));
375      });
376    }
377
378    // Click outside to close
379    document.addEventListener('click', (event) => {
380      if (isMenuOpen && !menu.contains(event.target) && !Array.from(document.querySelectorAll(toggleSelector)).some(btn => btn.contains(event.target))) {
381        closeMenu();
382      }
383    });
384  });
385}

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.