1/** 2 * Topics-bar dropdowns. 3 * 4 * CSS already opens a submenu on hover and on keyboard focus, so this script 5 * only covers what CSS cannot: 6 * 7 * - Escape closes an open menu and returns focus to its parent. 8 * - Touch devices have no hover, and the parent items are real links, so a first 9 * tap would navigate away before the submenu could be seen. The first tap 10 * opens; a second follows the link. 11 * - aria-expanded, kept in step with whatever opened the menu. 12 * 13 * Nothing here is required for the menu to work with a mouse or a keyboard -- 14 * if this file fails to load the dropdowns still open. 15 */ 16( function () { 17 'use strict'; 18 19 var menu = document.querySelector( '.highlight-menu' ); 20 if ( ! menu ) { 21 return; 22 } 23 24 var parents = [].slice.call( menu.querySelectorAll( '.menu-item-has-children' ) ); 25 if ( ! parents.length ) { 26 return; 27 } 28 29 // Touch is detected from the event itself rather than sniffed up front, so a 30 // hybrid laptop behaves correctly with whichever input is actually used. 31 var openedByTouch = null; 32 33 function link( item ) { 34 return item.querySelector( 'a' ); 35 } 36 37 function close( item ) { 38 item.classList.remove( 'is-open' ); 39 var a = link( item ); 40 if ( a ) { 41 a.setAttribute( 'aria-expanded', 'false' ); 42 } 43 if ( openedByTouch === item ) { 44 openedByTouch = null; 45 } 46 } 47 48 // Escape has to beat CSS. The menu opens on :focus-within, and Escape is 49 // supposed to return focus to the parent item -- which would satisfy 50 // :focus-within and reopen it immediately. This flag suppresses the panel 51 // while focus deliberately rests on a dismissed parent, and is cleared as 52 // soon as the user moves on. 53 function undismiss( item ) { 54 item.classList.remove( 'is-dismissed' ); 55 } 56 57 function closeAll( except ) { 58 parents.forEach( function ( item ) { 59 if ( item !== except ) { 60 close( item ); 61 } 62 } ); 63 } 64 65 parents.forEach( function ( item ) { 66 var a = link( item ); 67 if ( ! a ) { 68 return; 69 } 70 71 a.setAttribute( 'aria-haspopup', 'true' ); 72 a.setAttribute( 'aria-expanded', 'false' ); 73 74 a.addEventListener( 'click', function ( event ) { 75 // Only intercept touch. A mouse user has already seen the menu on 76 // hover, and a keyboard user reaches the children by tabbing. 77 if ( ! event.pointerType || 'touch' !== event.pointerType ) { 78 return; 79 } 80 81 if ( openedByTouch === item ) { 82 return; // Second tap: let the link do its job. 83 } 84 85 event.preventDefault(); 86 closeAll( item ); 87 item.classList.add( 'is-open' ); 88 a.setAttribute( 'aria-expanded', 'true' ); 89 openedByTouch = item; 90 } ); 91 92 // Mirror the hover state into aria so assistive tech is not told the 93 // menu is closed while it is plainly open. 94 item.addEventListener( 'mouseenter', function () { 95 undismiss( item ); 96 a.setAttribute( 'aria-expanded', 'true' ); 97 } ); 98 item.addEventListener( 'mouseleave', function () { 99 // Not while the item still holds focus: the menu is genuinely open 100 // for a keyboard user, and saying otherwise would be a lie to AT. 101 if ( openedByTouch !== item && ! item.contains( document.activeElement ) ) { 102 a.setAttribute( 'aria-expanded', 'false' ); 103 } 104 } ); 105 item.addEventListener( 'focusin', function () { 106 a.setAttribute( 'aria-expanded', 'true' ); 107 } ); 108 item.addEventListener( 'focusout', function ( event ) { 109 if ( ! item.contains( event.relatedTarget ) ) { 110 close( item ); 111 undismiss( item ); 112 } 113 } ); 114 } ); 115 116 document.addEventListener( 'keydown', function ( event ) { 117 if ( 'Escape' !== event.key ) { 118 return; 119 } 120 121 parents.forEach( function ( item ) { 122 var holdsFocus = item.contains( document.activeElement ); 123 if ( ! holdsFocus && ! item.classList.contains( 'is-open' ) ) { 124 return; 125 } 126 127 var a = link( item ); 128 close( item ); 129 130 if ( holdsFocus ) { 131 // Dismissed but still focused, so the panel must stay shut 132 // until focus moves. 133 item.classList.add( 'is-dismissed' ); 134 if ( a ) { 135 a.focus(); 136 } 137 } 138 } ); 139 } ); 140 141 document.addEventListener( 'click', function ( event ) { 142 if ( ! menu.contains( event.target ) ) { 143 closeAll( null ); 144 } 145 } ); 146}() );
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.