1- const updateMenuPositionForSubMenu = ( currentMenuSupplier ) => {
2- const currentMenu = currentMenuSupplier ( ) ;
3- const subMenu = currentMenu ?. getElementsByClassName ( 'pure-menu-children' ) ?. [ 0 ] ;
1+ // Improve interactions with dropdown menus.
2+ ( function ( ) {
3+ const OPEN_MENU_SELECTOR = ".nav-container details[open]" ;
44
5- subMenu ?. style . setProperty ( '--menu-x' , `${ currentMenu . getBoundingClientRect ( ) . x } px` ) ;
6- }
5+ function updateMenuPositionForSubMenu ( ) {
6+ const currentMenu = document . querySelector ( OPEN_MENU_SELECTOR ) ;
7+ const subMenu = currentMenu ?. getElementsByClassName ( 'pure-menu-children' ) ?. [ 0 ] ;
78
8- // Allow menus to be open and used by keyboard.
9- ( function ( ) {
10- var currentMenu ;
11- var backdrop = document . createElement ( "div" ) ;
12- backdrop . style = "display:none;position:fixed;width:100%;height:100%;z-index:1" ;
13- document . documentElement . insertBefore ( backdrop , document . querySelector ( "body" ) ) ;
9+ subMenu ?. style . setProperty ( '--menu-x' , `${ currentMenu . getBoundingClientRect ( ) . x } px` ) ;
10+ }
1411
15- addEventListener ( 'resize' , ( ) => updateMenuPositionForSubMenu ( ( ) => currentMenu ) ) ;
12+ addEventListener ( 'resize' , updateMenuPositionForSubMenu ) ;
1613
1714 function previous ( allItems , item ) {
1815 var i = 1 ;
@@ -37,216 +34,43 @@ const updateMenuPositionForSubMenu = (currentMenuSupplier) => {
3734 function last ( allItems ) {
3835 return allItems [ allItems . length - 1 ] ;
3936 }
40- function closeMenu ( ) {
41- if ( this === backdrop ) {
42- document . documentElement . focus ( ) ;
43- } else if ( currentMenu . querySelector ( ".pure-menu-link:focus" ) ) {
44- currentMenu . firstElementChild . focus ( ) ;
37+ function closeMenu ( ignore ) {
38+ const menus = Array . prototype . slice . call (
39+ document . querySelectorAll ( OPEN_MENU_SELECTOR ) ) ;
40+ for ( const menu of menus ) {
41+ if ( menu !== ignore ) {
42+ menu . open = false ;
43+ }
4544 }
46- currentMenu . className = currentMenu . className . replace ( "pure-menu-active" , "" ) ;
47- currentMenu = null ;
48- backdrop . style . display = "none" ;
49- }
50- backdrop . onclick = closeMenu ;
51- function openMenu ( newMenu ) {
52- updateMenuPositionForSubMenu ( ( ) => newMenu ) ;
53- currentMenu = newMenu ;
54- newMenu . className += " pure-menu-active" ;
55- backdrop . style . display = "block" ;
5645 }
5746 function menuOnClick ( e ) {
58- if ( this . getAttribute ( "href" ) != "#" ) {
59- return ;
60- }
61- if ( this . parentNode === currentMenu ) {
62- closeMenu ( ) ;
63- this . blur ( ) ;
47+ if ( ! this . open ) {
48+ this . focus ( ) ;
6449 } else {
65- if ( currentMenu ) closeMenu ( ) ;
66-
67- openMenu ( this . parentNode ) ;
50+ closeMenu ( this ) ;
51+ updateMenuPositionForSubMenu ( ) ;
6852 }
69- e . preventDefault ( ) ;
70- e . stopPropagation ( ) ;
7153 } ;
7254 function menuKeyDown ( e ) {
73- if ( currentMenu ) {
74- var children = currentMenu . querySelector ( ".pure-menu-children" ) ;
75- var currentLink = children . querySelector ( ".pure-menu-link:focus" ) ;
76- var currentItem ;
77- if ( currentLink && currentLink . parentNode . className . indexOf ( "pure-menu-item" ) !== - 1 ) {
78- currentItem = currentLink . parentNode ;
79- }
80- var allItems = [ ] ;
81- if ( children ) {
82- allItems = children . querySelectorAll ( ".pure-menu-item .pure-menu-link" ) ;
83- }
84- var switchTo = null ;
85- switch ( e . key . toLowerCase ( ) ) {
86- case "escape" :
87- case "esc" :
88- closeMenu ( ) ;
89- e . preventDefault ( ) ;
90- e . stopPropagation ( ) ;
91- return ;
92- case "arrowdown" :
93- case "down" :
94- if ( currentLink ) {
95- // Arrow down when an item other than the last is focused: focus next item.
96- // Arrow down when the last item is focused: jump to top.
97- switchTo = ( next ( allItems , currentLink ) || allItems [ 0 ] ) ;
98- } else {
99- // Arrow down when a menu is open and nothing is focused: focus first item.
100- switchTo = allItems [ 0 ] ;
101- }
102- break ;
103- case "arrowup" :
104- case "up" :
105- if ( currentLink ) {
106- // Arrow up when an item other than the first is focused: focus previous item.
107- // Arrow up when the first item is focused: jump to bottom.
108- switchTo = ( previous ( allItems , currentLink ) || last ( allItems ) ) ;
109- } else {
110- // Arrow up when a menu is open and nothing is focused: focus last item.
111- switchTo = last ( allItems ) ;
112- }
113- break ;
114- case "tab" :
115- if ( ! currentLink ) {
116- // if the menu is open, we should focus trap into it
117- // this is the behavior of the WAI example
118- // it is not the same as GitHub, but GitHub allows you to tab yourself out
119- // of the menu without closing it (which is horrible behavior)
120- switchTo = e . shiftKey ? last ( allItems ) : allItems [ 0 ] ;
121- } else if ( e . shiftKey && currentLink === allItems [ 0 ] ) {
122- // if you tab your way out of the menu, close it
123- // this is neither what GitHub nor the WAI example do,
124- // but is a rationalization of GitHub's behavior: we don't want users who know how to
125- // use tab and enter, but don't know that they can close menus with Escape,
126- // to find themselves completely trapped in the menu
127- closeMenu ( ) ;
128- e . preventDefault ( ) ;
129- e . stopPropagation ( ) ;
130- } else if ( ! e . shiftKey && currentLink === last ( allItems ) ) {
131- // same as above.
132- // if you tab your way out of the menu, close it
133- closeMenu ( ) ;
134- }
135- break ;
136- case "enter" :
137- case "return" :
138- // enter and return have the default browser behavior,
139- // but they also close the menu
140- // this behavior is identical between both the WAI example, and GitHub's
141- setTimeout ( function ( ) {
142- closeMenu ( ) ;
143- } , 100 ) ;
144- break ;
145- case "space" :
146- case " " :
147- // space closes the menu, and activates the current link
148- // this behavior is identical between both the WAI example, and GitHub's
149- if ( document . activeElement instanceof HTMLAnchorElement && ! document . activeElement . hasAttribute ( "aria-haspopup" ) ) {
150- // It's supposed to copy the behaviour of the WAI Menu Bar
151- // page, and of GitHub's menus. I've been using these two
152- // sources to judge what is basically "industry standard"
153- // behaviour for menu keyboard activity on the web.
154- //
155- // On GitHub, here's what I notice:
156- //
157- // 1 If you click open a menu, the menu button remains
158- // focused. If, in this stage, I press space, the menu will
159- // close.
160- //
161- // 2 If I use the arrow keys to focus a menu item, and then
162- // press space, the menu item will be activated. For
163- // example, clicking "+", then pressing down, then pressing
164- // space will open the New Repository page.
165- //
166- // Behaviour 1 is why the
167- // `!document.activeElement.hasAttribute("aria-haspopup")`
168- // condition is there. It's to make sure the menu-link on
169- // things like the About dropdown don't get activated.
170- // Behaviour 2 is why this code is required at all; I want to
171- // activate the currently highlighted menu item.
172- document . activeElement . click ( ) ;
173- }
174- setTimeout ( function ( ) {
175- closeMenu ( ) ;
176- } , 100 ) ;
177- e . preventDefault ( ) ;
178- e . stopPropagation ( ) ;
179- break ;
180- case "home" :
181- // home: focus first menu item.
182- // This is the behavior of WAI, while GitHub scrolls,
183- // but it's unlikely that a user will try to scroll the page while the menu is open,
184- // so they won't do it on accident.
185- switchTo = allItems [ 0 ] ;
186- break ;
187- case "end" :
188- // end: focus last menu item.
189- // This is the behavior of WAI, while GitHub scrolls,
190- // but it's unlikely that a user will try to scroll the page while the menu is open,
191- // so they won't do it on accident.
192- switchTo = last ( allItems ) ;
193- break ;
194- case "pageup" :
195- // page up: jump five items up, stopping at the top
196- // the number 5 is used so that we go one page in the
197- // inner-scrolled Dependencies and Versions fields
198- switchTo = currentItem || allItems [ 0 ] ;
199- for ( var n = 0 ; n < 5 ; ++ n ) {
200- if ( switchTo . previousElementSibling && switchTo . previousElementSibling . className == 'pure-menu-item' ) {
201- switchTo = switchTo . previousElementSibling ;
202- }
203- }
204- break ;
205- case "pagedown" :
206- // page down: jump five items down, stopping at the bottom
207- // the number 5 is used so that we go one page in the
208- // inner-scrolled Dependencies and Versions fields
209- switchTo = currentItem || last ( allItems ) ;
210- for ( var n = 0 ; n < 5 ; ++ n ) {
211- if ( switchTo . nextElementSibling && switchTo . nextElementSibling . className == 'pure-menu-item' ) {
212- switchTo = switchTo . nextElementSibling ;
213- }
214- }
215- break ;
216- }
217- if ( switchTo ) {
218- var switchToLink = switchTo . querySelector ( "a" ) ;
219- if ( switchToLink ) {
220- switchToLink . focus ( ) ;
221- } else {
222- switchTo . focus ( ) ;
223- }
224- e . preventDefault ( ) ;
225- e . stopPropagation ( ) ;
226- }
227- } else if ( e . target . parentNode . className && e . target . parentNode . className . indexOf ( "pure-menu-has-children" ) !== - 1 ) {
228- switch ( e . key . toLowerCase ( ) ) {
229- case "arrowdown" :
230- case "down" :
231- case "space" :
232- case " " :
233- openMenu ( e . target . parentNode ) ;
234- e . preventDefault ( ) ;
235- e . stopPropagation ( ) ;
236- break ;
237- }
55+ const key = e . key . toLowerCase ( ) ;
56+ if ( ( key === "escape" || key === "esc" ) &&
57+ document . querySelector ( OPEN_MENU_SELECTOR ) !== null )
58+ {
59+ closeMenu ( ) ;
60+ e . preventDefault ( ) ;
61+ e . stopPropagation ( ) ;
23862 }
239- } ;
240- var menus = Array . prototype . slice . call ( document . querySelectorAll ( ".pure-menu-has-children" ) ) ;
241- var menusLength = menus . length ;
242- var menu ;
243- for ( var i = 0 ; i < menusLength ; ++ i ) {
244- menu = menus [ i ] ;
245- menu . firstElementChild . setAttribute ( "aria-haspopup" , "menu" ) ;
246- menu . firstElementChild . nextElementSibling . setAttribute ( "role" , "menu" ) ;
247- menu . firstElementChild . addEventListener ( "click" , menuOnClick ) ;
24863 }
249- document . documentElement . addEventListener ( "keydown" , menuKeyDown ) ;
64+
65+ const setEvents = ( menus ) => {
66+ menus = Array . prototype . slice . call ( menus ) ;
67+ for ( const menu of menus ) {
68+ menu . addEventListener ( "toggle" , menuOnClick ) ;
69+ menu . addEventListener ( "keydown" , menuKeyDown ) ;
70+ }
71+ } ;
72+ setEvents ( document . querySelectorAll ( ".nav-container details" ) ) ;
73+
25074 document . documentElement . addEventListener ( "keydown" , function ( ev ) {
25175 if ( ev . key == "y" && ev . target . tagName != "INPUT" ) {
25276 let permalink = document . getElementById ( "permalink" ) ;
0 commit comments