├── LICENSE ├── README.md ├── css ├── main.css └── mega-menu.css ├── img └── mega-menu.gif ├── index.html └── js └── accessible-mega-menu.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matt Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Accessible Mega Menu 2 | 3 | This demonstrates how to implement a keyboard- and screen reader- accessible mega menu as a jQuery plugin. Content for the links and text within the mega menu comes from the [Web Content Accessibility Guidelines (WCAG) 2.0](http://www.w3.org/TR/WCAG/). 4 | 5 | ![Mega menu image](img/mega-menu.gif) 6 | 7 | ## Keyboard Accessibility 8 | 9 | The accessible mega menu supports keyboard interaction modeled after the behavior described in the [WAI-ARIA Menu or Menu bar (widget) design pattern](http://www.w3.org/TR/wai-aria-practices/#menu), however it also respects users' general expectations for the behavior of links in a global navigation. The accessible mega menu implementation permits tab focus on each of the six top-level menu items. 10 | 11 | - When one of the menu items has focus, pressing the `Enter` key, `Spacebar` or `Down` arrow will open the submenu panel, and pressing the `Left` or `Right` arrow key will shift focus to the adjacent menu item. 12 | - Links within the submenu panels are included in the tab order when the panel is open. Links can also be navigated with the arrow keys or by typing the first character in the link name, which speeds up keyboard navigation considerably. 13 | - Pressing the `Escape` key closes the submenu and restores focus to the parent menu item. 14 | 15 | ## Screen Reader Accessibility 16 | 17 | The accessible mega menu models its use of WAI-ARIA roles, states, and properties after those described in the [WAI-ARIA Menu or Menu bar (widget) design pattern](http://www.w3.org/TR/wai-aria-practices/#menu) with some notable exceptions, so that it behaves better with screen reader user expectations for global navigation. The accessible mega menu doesn't use `role="menu"` for the menu container and `role="menuitem"` for each of the links therein; if it did, assistive technology will no longer interpret the links as _links_, but instead as _menu items_, and the links in the global navigation will no longer show up when a screen reader user executes a shortcut command to bring up a list of links in the page. 18 | 19 | This approach maintains the semantic structure of the submenu panels in the accessible mega menu by omitting `role="menu"` and `role="menuitem"` for the global navigation; the links are organized into lists and separated by headings. 20 | 21 | ## Usage 22 | 23 | ### HTML 24 | 25 | The HTML for the accessible mega menu is a `nav` element — or any other container element — containing a list. Each list item contains a link which is followed by a `div` or any other container element which will serve as the pop up panel. 26 | 27 | The panel can contain any HTML content; in the following example, each panel contains three lists of links. You can explicitly define groups within the panel, between which a user can navigate quickly using the left and right arrow keys; in the following example, the CSS class `.sub-nav-group` identifies a navigable group. 28 | 29 | ```html 30 | 74 | ``` 75 | 76 | By default, the accessible mega menu uses the the following CSS classes to define the top-level navigation items, panels, groups within the panels, and the hover, focus, and open states. It also defines a prefix for unique ID strings, which are required to indicate the relationship of a top-level navigation item to the panel it controls. 77 | 78 | ```javascript 79 | defaults = { 80 | // unique ID's are required to indicate aria-owns, aria-controls and aria-labelledby 81 | uuidPrefix: "menu", 82 | 83 | // default CSS class used to define the megamenu styling 84 | menuClass: "menu", 85 | 86 | // default CSS class for a top-level navigation item in the megamenu 87 | topNavItemClass: "menu-top-nav-item", 88 | 89 | // default CSS class for a megamenu panel */ 90 | panelClass: "menu-panel", 91 | 92 | // default CSS class for a group of items within a megamenu panel 93 | panelGroupClass: "menu-panel-group", 94 | 95 | // default CSS class for the hover state 96 | hoverClass: "hover", 97 | 98 | // default CSS class for the focus state 99 | focusClass: "focus", 100 | 101 | // default CSS class for the open state 102 | openClass: "open", 103 | }; 104 | ``` 105 | 106 | You can optionally override the defaults to use the CSS classes you may have already defined for your mega menu. 107 | 108 | ### JavaScript 109 | 110 | Be sure to include jQuery and the `accessible-mega-menu.js` plugin script. The following initializes the first nav element in the document as an `accessibleMegaMenu`, with optional CSS class overrides. 111 | 112 | ```javascript 113 | $("nav:first").accessibleMegaMenu({ 114 | // prefix for generated unique id attributes, which are required 115 | // to indicate aria-owns, aria-controls and aria-labelledby 116 | uuidPrefix: "megamenu", 117 | 118 | // CSS class used to define the megamenu styling 119 | menuClass: "nav-menu", 120 | 121 | // CSS class for a top-level navigation item in the megamenu 122 | topNavItemClass: "nav-item", 123 | 124 | // CSS class for a megamenu panel 125 | panelClass: "sub-nav", 126 | 127 | // CSS class for a group of items within a megamenu panel 128 | panelGroupClass: "sub-nav-group", 129 | 130 | // CSS class for the hover state 131 | hoverClass: "hover", 132 | 133 | // CSS class for the focus state 134 | focusClass: "focus", 135 | 136 | // CSS class for the open state 137 | openClass: "open", 138 | }); 139 | ``` 140 | 141 | ### CSS 142 | 143 | `AccessibleMegaMenu` handles the showing and hiding of panels by adding or removing a CSS class. No inline styles are added to hide elements or create animation between states. 144 | 145 | This CSS example enables the showing/hiding of and the layout of lists panels in the mega menu. 146 | 147 | ```css 148 | /* mega menu list */ 149 | .nav-menu { 150 | display: block; 151 | list-style: none; 152 | margin: 0; 153 | padding: 0; 154 | position: relative; 155 | z-index: 15; 156 | } 157 | 158 | /* a top level navigation item in the mega menu */ 159 | .nav-item { 160 | display: inline-block; 161 | list-style: none; 162 | margin: 0; 163 | padding: 0; 164 | } 165 | 166 | /* first descendant link within a top level navigation item */ 167 | .nav-item > a { 168 | border: 1px solid transparent; 169 | display: inline-block; 170 | margin: 0 0 -1px 0; 171 | padding: 0.5em 1em; 172 | position: relative; 173 | } 174 | 175 | /* focus/open states of first descendant link within a top level navigation item */ 176 | .nav-item > a:focus, 177 | .nav-item > a.open { 178 | border: 1px solid #dedede; 179 | } 180 | 181 | /* open state of first descendant link within a top level 182 | navigation item */ 183 | .nav-item > a.open { 184 | background-color: #fff; 185 | border-bottom: none; 186 | z-index: 1; 187 | } 188 | 189 | /* sub-navigation panel */ 190 | .sub-nav { 191 | background-color: #fff; 192 | border: 1px solid #dedede; 193 | display: none; 194 | margin-top: -1px; 195 | padding: 0.5em 1em; 196 | position: absolute; 197 | top: 2.2em; 198 | } 199 | 200 | /* sub-navigation panel open state */ 201 | .sub-nav.open { 202 | display: block; 203 | } 204 | 205 | /* list of items within sub-navigation panel */ 206 | .sub-nav ul { 207 | display: inline-block; 208 | margin: 0 1em 0 0; 209 | padding: 0; 210 | vertical-align: top; 211 | } 212 | 213 | /* list item within sub-navigation panel */ 214 | .sub-nav li { 215 | display: block; 216 | line-height: 1.5; 217 | list-style-type: none; 218 | margin: 0; 219 | padding: 0; 220 | } 221 | ``` 222 | 223 | Putting it all together, here is the completed example: 224 | 225 | ```html 226 | 227 | 228 | 229 | 230 | Simple Accessible Mega Menu Example 231 | 308 | 309 | 310 | 354 | 355 | 356 | 361 | 362 | 363 | 364 | 365 | 366 | 394 | 395 | 396 | ``` 397 | 398 | ## Support 399 | 400 | Current versions of Chrome, Firefox, Safari, and Edge. 401 | 402 | ## License 403 | 404 | [The MIT License (MIT)](http://allthingssmitty.mit-license.org/) 405 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | /* general styling */ 2 | :root { 3 | --active-blue: #225fd7; 4 | --mute-gray: #b3b3b3; 5 | --deep-gray: #454545; 6 | } 7 | 8 | html, 9 | body { 10 | height: 100%; 11 | margin: 0; 12 | padding: 0; 13 | width: 100%; 14 | } 15 | 16 | html { 17 | font-size: 14px; 18 | line-height: 1.5; 19 | } 20 | 21 | body { 22 | background-color: #f2f4f5; 23 | color: #333; 24 | cursor: default; 25 | font-family: sans-serif; 26 | } 27 | 28 | code, 29 | pre { 30 | background-color: rgba(0, 0, 0, 0.06); 31 | border-radius: 0.25rem; 32 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 33 | padding: 0.25rem; 34 | } 35 | 36 | code { 37 | white-space: nowrap; 38 | } 39 | 40 | h1 { 41 | font-size: 1.75rem; 42 | } 43 | 44 | h2 { 45 | font-size: 1.5rem; 46 | } 47 | 48 | h3 { 49 | font-size: 1.25rem; 50 | } 51 | 52 | .layout { 53 | background-color: #fff; 54 | box-shadow: 0 0 5px 1px #333; 55 | left: 0; 56 | margin: auto; 57 | padding: 0 15px 15px; 58 | position: absolute; 59 | right: 0; 60 | width: 900px; 61 | } 62 | 63 | :link { 64 | color: var(--active-blue); 65 | } 66 | 67 | :link:focus { 68 | outline: 2px solid var(--active-blue); 69 | text-decoration: underline; 70 | } 71 | 72 | .skip-nav { 73 | float: left; 74 | font-weight: 700; 75 | padding: 10px; 76 | } 77 | 78 | .skip-nav:focus { 79 | visibility: visible; 80 | } 81 | 82 | .skip-nav a { 83 | color: var(--deep-gray); 84 | left: -500px; 85 | overflow: hidden; 86 | position: absolute; 87 | } 88 | 89 | .skip-nav a:active, 90 | .skip-nav a:focus { 91 | left: 0; 92 | overflow: visible; 93 | position: static; 94 | } 95 | 96 | /* syntax highlight */ 97 | .highlight { 98 | margin-bottom: 16px; 99 | } 100 | 101 | .highlight pre { 102 | background-color: #f7f7f7; 103 | border-radius: 3px; 104 | line-height: 1.45; 105 | margin-bottom: 0; 106 | overflow: auto; 107 | padding: 16px; 108 | word-break: normal; 109 | } 110 | 111 | /* syntax highlighting */ 112 | .pl-c { 113 | color: #969896; 114 | } 115 | 116 | .pl-c1, 117 | .pl-s .pl-v { 118 | color: #0086b3; 119 | } 120 | 121 | .pl-e, 122 | .pl-en { 123 | color: #795da3; 124 | } 125 | 126 | .pl-ent, 127 | .pl-sr .pl-cce { 128 | color: #63a35c; 129 | } 130 | 131 | .pl-k { 132 | color: #a71d5d; 133 | } 134 | 135 | .pl-pds, 136 | .pl-pse .pl-s1, 137 | .pl-s, 138 | .pl-s .pl-sr, 139 | .pl-sr .pl-cce, 140 | .pl-sr .pl-sra, 141 | .pl-sr .pl-sre { 142 | color: #183691; 143 | } 144 | 145 | .pl-s .pl-s1, 146 | .pl-smi { 147 | color: #333; 148 | } 149 | 150 | .pl-sr .pl-cce { 151 | font-weight: bold; 152 | } 153 | 154 | .pl-v { 155 | color: #ed6a43; 156 | } 157 | -------------------------------------------------------------------------------- /css/mega-menu.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --active-blue: #225fd7; 3 | --mute-gray: #b3b3b3; 4 | --soft-white: #f5f8fa; 5 | --deep-gray: #454545; 6 | } 7 | 8 | .menu { 9 | background-color: #dfe2e2; 10 | border: 1px solid var(--mute-gray); 11 | border-top: none; 12 | box-sizing: border-box; 13 | color: var(--deep-gray); 14 | height: 3.077em; 15 | list-style: none; 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | .menu h2, 21 | .menu h3, 22 | .menu h4 { 23 | display: inline; 24 | font-size: 1em; 25 | line-height: inherit; 26 | padding: 0; 27 | } 28 | 29 | .menu:focus { 30 | outline: 2px solid var(--active-blue); 31 | } 32 | 33 | .menu-top-nav-item { 34 | border-right: 1px solid var(--mute-gray); 35 | box-sizing: border-box; 36 | color: var(--deep-gray); 37 | float: left; 38 | position: relative; 39 | text-shadow: 0 1px 0 #fff; 40 | } 41 | 42 | .menu-top-nav-item a[aria-haspopup] { 43 | color: var(--deep-gray); 44 | display: block; 45 | height: 3em; 46 | line-height: 3em; 47 | padding: 0 12px; 48 | position: relative; 49 | text-decoration: none; 50 | z-index: inherit; 51 | } 52 | 53 | .menu-top-nav-item a[aria-haspopup]:hover, 54 | .menu-top-nav-item a[aria-haspopup]:focus { 55 | color: #333; 56 | outline-offset: -1px; 57 | } 58 | 59 | .menu-top-nav-item a[aria-haspopup]:hover, 60 | .menu-top-nav-item a[aria-haspopup]:focus, 61 | .menu-top-nav-item a[aria-haspopup].open { 62 | background: var(--soft-white) none; 63 | margin-left: 0; 64 | z-index: 1002; 65 | } 66 | 67 | .menu-top-nav-item a[aria-haspopup].open { 68 | background: var(--soft-white) none; 69 | padding-bottom: 2px; 70 | } 71 | 72 | .menu-top-nav-item:first-child a[aria-haspopup].open { 73 | border-left: 1px solid var(--mute-gray); 74 | left: -1px; 75 | margin-right: -1px; 76 | } 77 | 78 | .menu-panel { 79 | background-color: var(--soft-white); 80 | border: 1px solid var(--mute-gray); 81 | box-shadow: 0 4px 4px -3px #171717; 82 | color: var(--deep-gray); 83 | cursor: default; 84 | left: -1px; 85 | line-height: normal; 86 | margin: 0; 87 | max-height: 0; 88 | opacity: 0; 89 | overflow: hidden; 90 | padding: 0 12px; 91 | position: absolute; 92 | top: -9999em; 93 | transition: opacity 250ms ease 250ms, max-height 500ms ease, 94 | visibility 0s linear 500ms, top 0s linear 500ms; 95 | visibility: hidden; 96 | } 97 | 98 | .menu-panel.open { 99 | max-height: 600px; 100 | opacity: 1; 101 | top: 3em; 102 | transition: opacity 250ms ease, max-height 500ms ease, visibility 0s linear 0s, 103 | top 0s linear 0s; 104 | visibility: visible; 105 | z-index: 1001; 106 | } 107 | 108 | .menu-panel a { 109 | color: var(--active-blue); 110 | display: inline-block; 111 | font-size: 0.92em; 112 | font-weight: bold; 113 | line-height: 1.25; 114 | margin: 0.35em 0; 115 | text-decoration: none; 116 | } 117 | 118 | .menu-panel ol { 119 | display: -webkit-flex; 120 | display: -ms-flexbox; 121 | display: flex; 122 | -webkit-flex-wrap: wrap; 123 | -ms-flex-wrap: wrap; 124 | flex-wrap: wrap; 125 | list-style: none; 126 | margin: 0; 127 | padding: 0; 128 | } 129 | 130 | .menu-panel > ol > li { 131 | border-left: 1px solid var(--mute-gray); 132 | float: left; 133 | margin-bottom: 1em; 134 | margin-right: 1em; 135 | padding-left: 1em; 136 | } 137 | 138 | .menu-panel > ol > li:first-of-type { 139 | border-left: none; 140 | box-shadow: none; 141 | padding-left: 0; 142 | } 143 | 144 | .menu-panel > p { 145 | color: var(--deep-gray); 146 | font-size: 1.2em; 147 | line-height: 1.1em; 148 | } 149 | 150 | .menu-panel a:hover, 151 | .menu-panel a:focus { 152 | color: var(--active-blue); 153 | text-decoration: underline; 154 | } 155 | 156 | .menu-panel hr { 157 | border: 0; 158 | border-top: 1px solid var(--mute-gray); 159 | } 160 | 161 | .menu-panel.cols-1, 162 | .menu-panel.cols-3, 163 | .menu-panel.cols-4, 164 | .menu-panel.cols-4b { 165 | width: 874px; 166 | } 167 | 168 | .menu-panel.cols-1 { 169 | left: -22.9em; 170 | } 171 | 172 | .menu-panel.cols-1 > ol > li { 173 | width: auto; 174 | } 175 | 176 | .menu-panel.cols-3 { 177 | left: -13.55em; 178 | } 179 | 180 | .menu-panel.cols-3 > ol > li { 181 | width: 30%; 182 | } 183 | 184 | .menu-panel.cols-4 > ol > li, 185 | .menu-panel.cols-4b > ol > li { 186 | width: 22%; 187 | } 188 | 189 | .menu-panel.cols-4b { 190 | left: -7.4em; 191 | } 192 | 193 | .menu-panel-group h3 > a { 194 | color: var(--deep-gray); 195 | font-size: 1.2em; 196 | font-weight: bold; 197 | line-height: 1.1em; 198 | margin-bottom: 6px; 199 | padding-bottom: 0; 200 | padding-top: 0.4em; 201 | } 202 | 203 | .menu-panel-group h3 > a:hover, 204 | .menu-panel-group > h3 > a:focus { 205 | color: #333; 206 | } 207 | 208 | .menu-panel-group ol { 209 | -webkit-flex-direction: column; 210 | -ms-flex-direction: column; 211 | flex-direction: column; 212 | } 213 | 214 | .menu-panel-group li > a { 215 | padding-left: 2.5em; 216 | text-indent: -2.5em; 217 | width: auto; 218 | } 219 | -------------------------------------------------------------------------------- /img/mega-menu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllThingsSmitty/accessible-mega-menu/dcee77d086a3ea2764ae64729f3574e3d10741ce/img/mega-menu.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Accessible Mega Menu 6 | 7 | 8 | 9 | 10 |
11 | Skip to main content 12 |
13 |
14 | 580 |
581 |

An Accessible Mega Menu

582 |

583 | Content for the links and text within the mega menu comes from the 584 | Web Content Accessibility Guidelines (WCAG) 2.0. 587 |

588 |

Keyboard Accessibility

589 |

590 | The accessible mega menu supports keyboard interaction modeled after 591 | the behavior described in the 592 | WAI-ARIA Menu or Menu bar (widget) design pattern, however it also respects users' general expectations for the 595 | behavior of links in a global navigation. The accessible mega menu 596 | implementation permits tab focus on each of the six top-level menu 597 | items. 598 |

599 | 618 |

Screen Reader Accessibility

619 |

620 | The accessible mega menu models its use of WAI-ARIA roles, states, and 621 | properties after those described in the 622 | WAI-ARIA Menu or Menu bar (widget) design pattern 625 | with some notable exceptions, so that it behaves better with screen 626 | reader user expectations for global navigation. The accessible mega 627 | menu doesn't use role="menu" for the menu container and 628 | role="menuitem" for each of the links therein; if it did, 629 | assistive technology will no longer interpret the links as 630 | links, but instead as menu items, and the links in the 631 | global navigation will no longer show up when a screen reader user 632 | executes a shortcut command to bring up a list of links in the page. 633 |

634 |

635 | This approach maintains the semantic structure of the submenu panels 636 | in the accessible mega menu by omitting role="menu" and 637 | role="menuitem" for the global navigation; the links are 638 | organized into lists and separated by headings. 639 |

640 |

Usage

641 |

HTML

642 |

643 | The HTML for the accessible mega menu is a nav element 644 | — or any other container element — containing a list. Each 645 | list item contains a link which is followed by a div or 646 | any other container element which will serve as the pop up panel. 647 |

648 |

649 | The panel can contain any HTML content; in the following example, each 650 | panel contains three lists of links. You can explicitly define groups 651 | within the panel, between which a user can navigate quickly using the 652 | left and right arrow keys; in the following example, the CSS class 653 | .sub-nav-group identifies a navigable group. 654 |

655 |
656 |
657 | <nav>
658 |   <ul class="nav-menu">
659 |     <li class="nav-item">
660 |       <a href="?music">Music</a>
661 |       <div class="sub-nav">
662 |         <ul class="sub-nav-group">
663 |           <li><a href="?music&amp;genre=0">Alternative</a></li>
664 |           <li><a href="?music&amp;genre=3">Country</a></li>
665 |           <li>&#8230;</li>
666 |         </ul>
667 |         <ul class="sub-nav-group">
668 |           <li><a href="?music&amp;genre=1">Dance</a></li>
669 |           <li><a href="?music&amp;genre=4">Electronic</a></li>
670 |           <li>&#8230;</li>
671 |         </ul>
672 |         <ul class="sub-nav-group">
673 |           <li><a href="?music&amp;genre=2">Hip-Hop/Rap</a></li>
674 |           <li><a href="?music&amp;genre=5">Jazz</a></li>
675 |           <li>&#8230;</li>
676 |         </ul>
677 |       </div>
678 |     </li>
679 |     <li class="nav-item">
680 |       <a href="?movies">Movies</a>
681 |       <div class="sub-nav">
682 |         <ul class="sub-nav-group">
683 |           <li><a href="?movies&amp;genre=10">New Release</a></li>
684 |           <li><a href="?movies&amp;genre=13">Comedy</a></li>
685 |           <li>&#8230;</li>
686 |         </ul>
687 |         <ul class="sub-nav-group">
688 |           <li><a href="?movies&amp;genre=11">Drama</a></li>
689 |           <li><a href="?movies&amp;genre=14">Sci-Fi</a></li>
690 |           <li>&#8230;</li>
691 |         </ul>
692 |         <ul class="sub-nav-group">
693 |           <li><a href="?movies&amp;genre=12">Horror</a></li>
694 |           <li><a href="?movies&amp;genre=15">Documentary</a></li>
695 |           <li>&#8230;</li>
696 |         </ul>
697 |       </div>
698 |     </li>
699 |   </ul>
700 | </nav>
701 |           
702 |
703 |

704 | By default, the accessible mega menu uses the the following CSS 705 | classes to define the top-level navigation items, panels, groups 706 | within the panels, and the hover, focus, and open states. It also 707 | defines a prefix for unique ID strings, which are required to indicate 708 | the relationship of a top-level navigation item to the panel it 709 | controls. 710 |

711 |
712 |
713 | defaults = {
714 |   // unique ID's are required to indicate aria-owns, aria-controls and aria-labelledby
715 |   uuidPrefix: 'menu',
716 | 
717 |   // default CSS class used to define the megamenu styling
718 |   menuClass: 'menu',
719 | 
720 |   // default CSS class for a top-level navigation item in the megamenu
721 |   topNavItemClass: 'menu-top-nav-item',
722 | 
723 |   // default CSS class for a megamenu panel */
724 |   panelClass: 'menu-panel',
725 | 
726 |   // default CSS class for a group of items within a megamenu panel
727 |   panelGroupClass: 'menu-panel-group',
728 | 
729 |   // default CSS class for the hover state
730 |   hoverClass: 'hover',
731 | 
732 |   // default CSS class for the focus state
733 |   focusClass: 'focus',
734 | 
735 |   // default CSS class for the open state
736 |   openClass: 'open' 
737 | }
738 |           
739 |
740 |

741 | You can optionally override the defaults to use the CSS classes you 742 | may have already defined for your mega menu. 743 |

744 |

JavaScript

745 |

746 | Be sure to include the latest version of jQuery and the 747 | accessible-mega-menu.js script. The following initializes 748 | the first nav element in the document as an 749 | accessibleMegaMenu, with optional CSS class overrides. 750 |

751 |
752 |
753 | $("nav:first").accessibleMegaMenu({
754 |   // prefix for generated unique id attributes, which are required
755 |   // to indicate aria-owns, aria-controls and aria-labelledby
756 |   uuidPrefix: "accessible-megamenu",
757 | 
758 |   // CSS class used to define the megamenu styling
759 |   menuClass: 'nav-menu',
760 | 
761 |   // CSS class for a top-level navigation item in the megamenu
762 |   topNavItemClass: 'nav-item',
763 | 
764 |   // CSS class for a megamenu panel
765 |   panelClass: 'sub-nav',
766 | 
767 |   // CSS class for a group of items within a megamenu panel
768 |   panelGroupClass: 'sub-nav-group',
769 | 
770 |   // CSS class for the hover state
771 |   hoverClass: 'hover',
772 | 
773 |   // CSS class for the focus state
774 |   focusClass: 'focus',
775 | 
776 |   // CSS class for the open state
777 |   openClass: 'open'
778 | });
779 |           
780 |
781 |

CSS

782 |

783 | AccessibleMegaMenu handles the showing and hiding of 784 | panels by adding or removing a CSS class. No inline styles are added 785 | to hide elements or create animation between states. 786 |

787 |

788 | This CSS example enables the showing/hiding of and the layout of lists 789 | panels in the mega menu. 790 |

791 |
792 |
793 | /* mega menu list */
794 | .nav-menu {
795 |   display: block;
796 |   list-style: none;
797 |   margin: 0;
798 |   padding: 0;
799 |   position: relative;
800 |   z-index: 15;
801 | }
802 | 
803 | /* a top level navigation item in the mega menu */
804 | .nav-item {
805 |   display: inline-block;
806 |   list-style: none;
807 |   margin: 0;
808 |   padding: 0;
809 | }
810 | 
811 | /* first descendant link within a top level navigation item */
812 | .nav-item > a {
813 |   border: 1px solid transparent;
814 |   display: inline-block;
815 |   margin: 0 0 -1px 0;
816 |   padding: 0.5em 1em;
817 |   position: relative;
818 | }
819 | 
820 | /* focus/open states of first descendant link within a top level navigation item */
821 | .nav-item > a:focus,
822 | .nav-item > a.open {
823 |   border: 1px solid #dedede;
824 | }
825 | 
826 | /* open state of first descendant link within a top level 
827 |    navigation item */
828 | .nav-item > a.open {
829 |   background-color: #fff;
830 |   border-bottom: none;
831 |   z-index: 1;
832 | }
833 | 
834 | /* sub-navigation panel */
835 | .sub-nav {
836 |   background-color: #fff;
837 |   border: 1px solid #dedede;
838 |   display: none;
839 |   margin-top: -1px;
840 |   padding: 0.5em 1em;
841 |   position: absolute;
842 |   top: 2.2em;
843 | }
844 | 
845 | /* sub-navigation panel open state */
846 | .sub-nav.open {
847 |   display: block;
848 | }
849 | 
850 | /* list of items within sub-navigation panel */
851 | .sub-nav ul {
852 |   display: inline-block;
853 |   margin: 0 1em 0 0;
854 |   padding: 0;
855 |   vertical-align: top;
856 | }
857 | 
858 | /* list item within sub-navigation panel */
859 | .sub-nav li {
860 |   display: block;
861 |   list-style-type: none;
862 |   margin: 0;
863 |   padding: 0;
864 | }
865 |           
866 |
867 |

Browser Support

868 |

869 | The accessible mega menu works in current versions of Chrome, Firefox, 870 | Safari, and Edge. 871 |

872 |
873 |
874 | 879 | 880 | 881 | 882 | -------------------------------------------------------------------------------- /js/accessible-mega-menu.js: -------------------------------------------------------------------------------- 1 | if (jQuery) { 2 | (function ($) { 3 | "use strict"; 4 | $(document).ready(function () { 5 | // initialize the megamenu 6 | $(".megamenu").accessibleMegaMenu(); 7 | }); 8 | })(jQuery); 9 | } 10 | 11 | /*global jQuery */ 12 | (function ($, window, document) { 13 | "use strict"; 14 | var pluginName = "accessibleMegaMenu", 15 | defaults = { 16 | uuidPrefix: "menu", // unique ID's are required to indicate aria-owns, aria-controls and aria-labelledby 17 | menuClass: "menu", // default CSS class used to define the megamenu styling 18 | topNavItemClass: "menu-top-nav-item", // default CSS class for a top-level navigation item in the megamenu 19 | panelClass: "menu-panel", // default CSS class for a megamenu panel 20 | panelGroupClass: "menu-panel-group", // default CSS class for a group of items within a megamenu panel 21 | hoverClass: "hover", // default CSS class for the hover state 22 | focusClass: "focus", // default CSS class for the focus state 23 | openClass: "open", // default CSS class for the open state 24 | }, 25 | Keyboard = { 26 | BACKSPACE: 8, 27 | COMMA: 188, 28 | DELETE: 46, 29 | DOWN: 40, 30 | END: 35, 31 | ENTER: 13, 32 | ESCAPE: 27, 33 | HOME: 36, 34 | LEFT: 37, 35 | PAGE_DOWN: 34, 36 | PAGE_UP: 33, 37 | PERIOD: 190, 38 | RIGHT: 39, 39 | SPACE: 32, 40 | TAB: 9, 41 | UP: 38, 42 | keyMap: { 43 | 48: "0", 44 | 49: "1", 45 | 50: "2", 46 | 51: "3", 47 | 52: "4", 48 | 53: "5", 49 | 54: "6", 50 | 55: "7", 51 | 56: "8", 52 | 57: "9", 53 | 59: ";", 54 | 65: "a", 55 | 66: "b", 56 | 67: "c", 57 | 68: "d", 58 | 69: "e", 59 | 70: "f", 60 | 71: "g", 61 | 72: "h", 62 | 73: "i", 63 | 74: "j", 64 | 75: "k", 65 | 76: "l", 66 | 77: "m", 67 | 78: "n", 68 | 79: "o", 69 | 80: "p", 70 | 81: "q", 71 | 82: "r", 72 | 83: "s", 73 | 84: "t", 74 | 85: "u", 75 | 86: "v", 76 | 87: "w", 77 | 88: "x", 78 | 89: "y", 79 | 90: "z", 80 | 96: "0", 81 | 97: "1", 82 | 98: "2", 83 | 99: "3", 84 | 100: "4", 85 | 101: "5", 86 | 102: "6", 87 | 103: "7", 88 | 104: "8", 89 | 105: "9", 90 | 190: ".", 91 | }, 92 | }; 93 | 94 | function AccessibleMegaMenu(element, options) { 95 | this.element = element; 96 | 97 | // merge optional settings and defaults into settings 98 | this.settings = $.extend({}, defaults, options); 99 | 100 | this._defaults = defaults; 101 | this._name = pluginName; 102 | 103 | this.mouseTimeoutID = null; 104 | this.focusTimeoutID = null; 105 | this.mouseFocused = false; 106 | this.justFocused = false; 107 | 108 | this.init(); 109 | } 110 | 111 | AccessibleMegaMenu.prototype = (function () { 112 | /* private attributes and methods ------------------------ */ 113 | var uuid = 0, 114 | keydownTimeoutDuration = 1000, 115 | keydownSearchString = "", 116 | isTouch = 117 | typeof window.hasOwnProperty === "function" && 118 | !!window.hasOwnProperty("ontouchstart"), 119 | _getPlugin, 120 | _addUniqueId, 121 | _togglePanel, 122 | _clickHandler, 123 | _clickOutsideHandler, 124 | _DOMAttrModifiedHandler, 125 | _focusInHandler, 126 | _focusOutHandler, 127 | _keyDownHandler, 128 | _mouseDownHandler, 129 | _mouseOverHandler, 130 | _mouseOutHandler, 131 | _toggleExpandedEventHandlers; 132 | 133 | /** 134 | * @name jQuery.fn.accessibleMegaMenu~_getPlugin 135 | * @desc Returns the parent accessibleMegaMenu instance for a given element 136 | * @param {jQuery} element 137 | * @memberof jQuery.fn.accessibleMegaMenu 138 | * @inner 139 | * @private 140 | */ 141 | _getPlugin = function (element) { 142 | return $(element) 143 | .closest(":data(plugin_" + pluginName + ")") 144 | .data("plugin_" + pluginName); 145 | }; 146 | 147 | /** 148 | * @name jQuery.fn.accessibleMegaMenu~_addUniqueId 149 | * @desc Adds a unique id and element. 150 | * The id string starts with the 151 | * string defined in settings.uuidPrefix. 152 | * @param {jQuery} element 153 | * @memberof jQuery.fn.accessibleMegaMenu 154 | * @inner 155 | * @private 156 | */ 157 | _addUniqueId = function (element) { 158 | element = $(element); 159 | var settings = this.settings; 160 | if (!element.attr("id")) { 161 | element.attr( 162 | "id", 163 | settings.uuidPrefix + "-" + new Date().getTime() + "-" + ++uuid 164 | ); 165 | } 166 | }; 167 | 168 | /** 169 | * @name jQuery.fn.accessibleMegaMenu~_togglePanel 170 | * @desc Toggle the display of mega menu panels in response to an event. 171 | * The optional boolean value 'hide' forces all panels to hide. 172 | * @param {event} event 173 | * @param {Boolean} [hide] Hide all mega menu panels when true 174 | * @memberof jQuery.fn.accessibleMegaMenu 175 | * @inner 176 | * @private 177 | */ 178 | _togglePanel = function (event, hide) { 179 | var target = $(event.target), 180 | that = this, 181 | settings = this.settings, 182 | menu = this.menu, 183 | topli = target.closest("." + settings.topNavItemClass), 184 | panel = target.hasClass(settings.panelClass) 185 | ? target 186 | : target.closest("." + settings.panelClass), 187 | newfocus; 188 | 189 | _toggleExpandedEventHandlers.call(this, true); 190 | 191 | if (hide) { 192 | topli = menu 193 | .find( 194 | "." + 195 | settings.topNavItemClass + 196 | " ." + 197 | settings.openClass + 198 | ":first" 199 | ) 200 | .closest("." + settings.topNavItemClass); 201 | if ( 202 | !( 203 | topli.is(event.relatedTarget) || 204 | topli.has(event.relatedTarget).length > 0 205 | ) 206 | ) { 207 | if ( 208 | (event.type === "mouseout" || event.type === "focusout") && 209 | topli.has(document.activeElement).length > 0 210 | ) { 211 | return; 212 | } 213 | topli 214 | .find("[aria-expanded]") 215 | .attr("aria-expanded", "false") 216 | .removeClass(settings.openClass) 217 | .filter("." + settings.panelClass) 218 | .attr("aria-hidden", "true"); 219 | if ( 220 | (event.type === "keydown" && event.keyCode === Keyboard.ESCAPE) || 221 | event.type === "DOMAttrModified" 222 | ) { 223 | newfocus = topli.find(":tabbable:first"); 224 | setTimeout(function () { 225 | menu 226 | .find("[aria-expanded]." + that.settings.panelClass) 227 | .off("DOMAttrModified.menu"); 228 | newfocus.focus(); 229 | that.justFocused = false; 230 | }, 99); 231 | } 232 | } else if (topli.length === 0) { 233 | menu 234 | .find("[aria-expanded=true]") 235 | .attr("aria-expanded", "false") 236 | .removeClass(settings.openClass) 237 | .filter("." + settings.panelClass) 238 | .attr("aria-hidden", "true"); 239 | } 240 | } else { 241 | clearTimeout(that.focusTimeoutID); 242 | topli 243 | .siblings() 244 | .find("[aria-expanded]") 245 | .attr("aria-expanded", "false") 246 | .removeClass(settings.openClass) 247 | .filter("." + settings.panelClass) 248 | .attr("aria-hidden", "true"); 249 | topli 250 | .find("[aria-expanded]") 251 | .attr("aria-expanded", "true") 252 | .addClass(settings.openClass) 253 | .filter("." + settings.panelClass) 254 | .attr("aria-hidden", "false"); 255 | if ( 256 | event.type === "mouseover" && 257 | target.is(":tabbable") && 258 | topli.length === 1 && 259 | panel.length === 0 && 260 | menu.has(document.activeElement).length > 0 261 | ) { 262 | target.focus(); 263 | that.justFocused = false; 264 | } 265 | 266 | _toggleExpandedEventHandlers.call(that); 267 | } 268 | }; 269 | 270 | /** 271 | * @name jQuery.fn.accessibleMegaMenu~_clickHandler 272 | * @desc Handle click event on mega menu item 273 | * @param {event} Event object 274 | * @memberof jQuery.fn.accessibleMegaMenu 275 | * @inner 276 | * @private 277 | */ 278 | _clickHandler = function (event) { 279 | var target = $(event.currentTarget), 280 | topli = target.closest("." + this.settings.topNavItemClass), 281 | panel = target.closest("." + this.settings.panelClass); 282 | if ( 283 | topli.length === 1 && 284 | panel.length === 0 && 285 | topli.find("." + this.settings.panelClass).length === 1 286 | ) { 287 | if (!target.hasClass(this.settings.openClass)) { 288 | event.preventDefault(); 289 | event.stopPropagation(); 290 | _togglePanel.call(this, event); 291 | this.justFocused = false; 292 | } else { 293 | if (this.justFocused) { 294 | event.preventDefault(); 295 | event.stopPropagation(); 296 | this.justFocused = false; 297 | } else if (isTouch) { 298 | event.preventDefault(); 299 | event.stopPropagation(); 300 | _togglePanel.call( 301 | this, 302 | event, 303 | target.hasClass(this.settings.openClass) 304 | ); 305 | } 306 | } 307 | } 308 | }; 309 | 310 | /** 311 | * @name jQuery.fn.accessibleMegaMenu~_clickOutsideHandler 312 | * @desc Handle click event outside of a the megamenu 313 | * @param {event} Event object 314 | * @memberof jQuery.fn.accessibleMegaMenu 315 | * @inner 316 | * @private 317 | */ 318 | _clickOutsideHandler = function (event) { 319 | if ($(event.target).closest(this.menu).length === 0) { 320 | event.preventDefault(); 321 | event.stopPropagation(); 322 | _togglePanel.call(this, event, true); 323 | } 324 | }; 325 | 326 | /** 327 | * @name jQuery.fn.accessibleMegaMenu~_DOMAttrModifiedHandler 328 | * @desc Handle DOMAttrModified event on panel to respond to Windows 8 Narrator ExpandCollapse pattern 329 | * @param {event} Event object 330 | * @memberof jQuery.fn.accessibleMegaMenu 331 | * @inner 332 | * @private 333 | */ 334 | _DOMAttrModifiedHandler = function (event) { 335 | if ( 336 | event.originalEvent.attrName === "aria-expanded" && 337 | event.originalEvent.newValue === "false" && 338 | $(event.target).hasClass(this.settings.openClass) 339 | ) { 340 | event.preventDefault(); 341 | event.stopPropagation(); 342 | _togglePanel.call(this, event, true); 343 | } 344 | }; 345 | 346 | /** 347 | * @name jQuery.fn.accessibleMegaMenu~_focusInHandler 348 | * @desc Handle focusin event on mega menu item. 349 | * @param {event} Event object 350 | * @memberof jQuery.fn.accessibleMegaMenu 351 | * @inner 352 | * @private 353 | */ 354 | _focusInHandler = function (event) { 355 | clearTimeout(this.focusTimeoutID); 356 | var target = $(event.target), 357 | panel = target.closest("." + this.settings.panelClass); 358 | target 359 | .addClass(this.settings.focusClass) 360 | .on("click.menu", $.proxy(_clickHandler, this)); 361 | this.justFocused = !this.mouseFocused; 362 | this.mouseFocused = false; 363 | if (this.panels.not(panel).filter("." + this.settings.openClass).length) { 364 | _togglePanel.call(this, event); 365 | } 366 | }; 367 | 368 | /** 369 | * @name jQuery.fn.accessibleMegaMenu~_focusOutHandler 370 | * @desc Handle focusout event on mega menu item. 371 | * @param {event} Event object 372 | * @memberof jQuery.fn.accessibleMegaMenu 373 | * @inner 374 | * @private 375 | */ 376 | _focusOutHandler = function (event) { 377 | this.justFocused = false; 378 | var that = this, 379 | target = $(event.target), 380 | topli = target.closest("." + this.settings.topNavItemClass), 381 | keepOpen = false; 382 | target.removeClass(this.settings.focusClass).off("click.menu"); 383 | 384 | if (window.cvox) { 385 | // If ChromeVox is running... 386 | that.focusTimeoutID = setTimeout(function () { 387 | window.cvox.Api.getCurrentNode(function (node) { 388 | if (topli.has(node).length) { 389 | // and the current node being voiced is in 390 | // the mega menu, clearTimeout, 391 | // so the panel stays open. 392 | clearTimeout(that.focusTimeoutID); 393 | } else { 394 | that.focusTimeoutID = setTimeout( 395 | function (scope, event, hide) { 396 | _togglePanel.call(scope, event, hide); 397 | }, 398 | 275, 399 | that, 400 | event, 401 | true 402 | ); 403 | } 404 | }); 405 | }, 25); 406 | } else { 407 | that.focusTimeoutID = setTimeout(function () { 408 | _togglePanel.call(that, event, true); 409 | }, 300); 410 | } 411 | }; 412 | 413 | /** 414 | * @name jQuery.fn.accessibleMegaMenu~_keyDownHandler 415 | * @desc Handle keydown event on mega menu. 416 | * @param {event} Event object 417 | * @memberof jQuery.fn.accessibleMegaMenu 418 | * @inner 419 | * @private 420 | */ 421 | _keyDownHandler = function (event) { 422 | var that = 423 | this.constructor === AccessibleMegaMenu ? this : _getPlugin(this), // determine the AccessibleMegaMenu plugin instance 424 | settings = that.settings, 425 | target = $( 426 | $(this).is("." + settings.hoverClass + ":tabbable") 427 | ? this 428 | : event.target 429 | ), // if the element is hovered the target is this, otherwise, its the focused element 430 | menu = that.menu, 431 | topnavitems = that.topnavitems, 432 | topli = target.closest("." + settings.topNavItemClass), 433 | tabbables = menu.find(":tabbable"), 434 | panel = target.hasClass(settings.panelClass) 435 | ? target 436 | : target.closest("." + settings.panelClass), 437 | panelGroups = panel.find("." + settings.panelGroupClass), 438 | currentPanelGroup = target.closest("." + settings.panelGroupClass), 439 | next, 440 | keycode = event.keyCode || event.which, 441 | start, 442 | i, 443 | o, 444 | label, 445 | found = false, 446 | newString = Keyboard.keyMap[event.keyCode] || "", 447 | regex, 448 | isTopNavItem = topli.length === 1 && panel.length === 0; 449 | 450 | if ( 451 | target.is("input:focus, select:focus, textarea:focus, button:focus") 452 | ) { 453 | // if the event target is a form element we should handle keydown normally 454 | return; 455 | } 456 | 457 | if (target.is("." + settings.hoverClass + ":tabbable")) { 458 | $("html").off("keydown.menu"); 459 | } 460 | 461 | switch (keycode) { 462 | case Keyboard.ESCAPE: 463 | _togglePanel.call(that, event, true); 464 | break; 465 | case Keyboard.DOWN: 466 | event.preventDefault(); 467 | if (isTopNavItem) { 468 | _togglePanel.call(that, event); 469 | found = 470 | topli.find("." + settings.panelClass + " :tabbable:first").focus() 471 | .length === 1; 472 | } else { 473 | found = 474 | tabbables 475 | .filter(":gt(" + tabbables.index(target) + "):first") 476 | .focus().length === 1; 477 | } 478 | 479 | if ( 480 | !found && 481 | window.opera && 482 | opera.toString() === "[object Opera]" && 483 | (event.ctrlKey || event.metaKey) 484 | ) { 485 | tabbables = $(":tabbable"); 486 | i = tabbables.index(target); 487 | found = 488 | $( 489 | ":tabbable:gt(" + $(":tabbable").index(target) + "):first" 490 | ).focus().length === 1; 491 | } 492 | break; 493 | case Keyboard.UP: 494 | event.preventDefault(); 495 | if (isTopNavItem && target.hasClass(settings.openClass)) { 496 | _togglePanel.call(that, event, true); 497 | next = topnavitems.filter( 498 | ":lt(" + topnavitems.index(topli) + "):last" 499 | ); 500 | if (next.children("." + settings.panelClass).length) { 501 | found = 502 | next 503 | .children() 504 | .attr("aria-expanded", "true") 505 | .addClass(settings.openClass) 506 | .filter("." + settings.panelClass) 507 | .attr("aria-hidden", "false") 508 | .find(":tabbable:last") 509 | .focus() === 1; 510 | } 511 | } else if (!isTopNavItem) { 512 | found = 513 | tabbables 514 | .filter(":lt(" + tabbables.index(target) + "):last") 515 | .focus().length === 1; 516 | } 517 | 518 | if ( 519 | !found && 520 | window.opera && 521 | opera.toString() === "[object Opera]" && 522 | (event.ctrlKey || event.metaKey) 523 | ) { 524 | tabbables = $(":tabbable"); 525 | i = tabbables.index(target); 526 | found = 527 | $( 528 | ":tabbable:lt(" + $(":tabbable").index(target) + "):first" 529 | ).focus().length === 1; 530 | } 531 | break; 532 | case Keyboard.RIGHT: 533 | event.preventDefault(); 534 | if (isTopNavItem) { 535 | found = 536 | topnavitems 537 | .filter(":gt(" + topnavitems.index(topli) + "):first") 538 | .find(":tabbable:first") 539 | .focus().length === 1; 540 | } else { 541 | if (panelGroups.length && currentPanelGroup.length) { 542 | // if the current panel contains panel groups, and we are able to focus the first tabbable element of the next panel group 543 | found = 544 | panelGroups 545 | .filter( 546 | ":gt(" + panelGroups.index(currentPanelGroup) + "):first" 547 | ) 548 | .find(":tabbable:first") 549 | .focus().length === 1; 550 | } 551 | 552 | if (!found) { 553 | found = topli.find(":tabbable:first").focus().length === 1; 554 | } 555 | } 556 | break; 557 | case Keyboard.LEFT: 558 | event.preventDefault(); 559 | if (isTopNavItem) { 560 | found = 561 | topnavitems 562 | .filter(":lt(" + topnavitems.index(topli) + "):last") 563 | .find(":tabbable:first") 564 | .focus().length === 1; 565 | } else { 566 | if (panelGroups.length && currentPanelGroup.length) { 567 | // if the current panel contains panel groups, and we are able to focus the first tabbable element of the previous panel group 568 | found = 569 | panelGroups 570 | .filter( 571 | ":lt(" + panelGroups.index(currentPanelGroup) + "):last" 572 | ) 573 | .find(":tabbable:first") 574 | .focus().length === 1; 575 | } 576 | 577 | if (!found) { 578 | found = topli.find(":tabbable:first").focus().length === 1; 579 | } 580 | } 581 | break; 582 | case Keyboard.TAB: 583 | i = tabbables.index(target); 584 | if ( 585 | event.shiftKey && 586 | isTopNavItem && 587 | target.hasClass(settings.openClass) 588 | ) { 589 | _togglePanel.call(that, event, true); 590 | next = topnavitems.filter( 591 | ":lt(" + topnavitems.index(topli) + "):last" 592 | ); 593 | if (next.children("." + settings.panelClass).length) { 594 | found = next 595 | .children() 596 | .attr("aria-expanded", "true") 597 | .addClass(settings.openClass) 598 | .filter("." + settings.panelClass) 599 | .attr("aria-hidden", "false") 600 | .find(":tabbable:last") 601 | .focus(); 602 | } 603 | } else if (event.shiftKey && i > 0) { 604 | found = 605 | tabbables.filter(":lt(" + i + "):last").focus().length === 1; 606 | } else if (!event.shiftKey && i < tabbables.length - 1) { 607 | found = 608 | tabbables.filter(":gt(" + i + "):first").focus().length === 1; 609 | } else if (window.opera && opera.toString() === "[object Opera]") { 610 | tabbables = $(":tabbable"); 611 | i = tabbables.index(target); 612 | if (event.shiftKey) { 613 | found = 614 | $( 615 | ":tabbable:lt(" + $(":tabbable").index(target) + "):last" 616 | ).focus().length === 1; 617 | } else { 618 | found = 619 | $( 620 | ":tabbable:gt(" + $(":tabbable").index(target) + "):first" 621 | ).focus().length === 1; 622 | } 623 | } 624 | 625 | if (found) { 626 | event.preventDefault(); 627 | } 628 | break; 629 | case Keyboard.SPACE: 630 | if (isTopNavItem) { 631 | event.preventDefault(); 632 | _clickHandler.call(that, event); 633 | } else { 634 | return true; 635 | } 636 | break; 637 | case Keyboard.ENTER: 638 | return true; 639 | break; 640 | default: 641 | // alphanumeric filter 642 | clearTimeout(this.keydownTimeoutID); 643 | 644 | keydownSearchString += 645 | newString !== keydownSearchString ? newString : ""; 646 | 647 | if (keydownSearchString.length === 0) { 648 | return; 649 | } 650 | 651 | this.keydownTimeoutID = setTimeout(function () { 652 | keydownSearchString = ""; 653 | }, keydownTimeoutDuration); 654 | 655 | if (isTopNavItem && !target.hasClass(settings.openClass)) { 656 | tabbables = tabbables.filter( 657 | ":not(." + settings.panelClass + " :tabbable)" 658 | ); 659 | } else { 660 | tabbables = topli.find(":tabbable"); 661 | } 662 | 663 | if (event.shiftKey) { 664 | tabbables = $(tabbables.get().reverse()); 665 | } 666 | 667 | for (i = 0; i < tabbables.length; i++) { 668 | o = tabbables.eq(i); 669 | if (o.is(target)) { 670 | start = keydownSearchString.length === 1 ? i + 1 : i; 671 | break; 672 | } 673 | } 674 | 675 | regex = new RegExp( 676 | "^" + 677 | keydownSearchString.replace( 678 | /[\-\[\]{}()*+?.,\\\^$|#\s]/g, 679 | "\\$&" 680 | ), 681 | "i" 682 | ); 683 | 684 | for (i = start; i < tabbables.length; i++) { 685 | o = tabbables.eq(i); 686 | label = $.trim(o.text()); 687 | if (regex.test(label)) { 688 | found = true; 689 | o.focus(); 690 | break; 691 | } 692 | } 693 | if (!found) { 694 | for (i = 0; i < start; i++) { 695 | o = tabbables.eq(i); 696 | label = $.trim(o.text()); 697 | if (regex.test(label)) { 698 | o.focus(); 699 | break; 700 | } 701 | } 702 | } 703 | break; 704 | } 705 | that.justFocused = false; 706 | }; 707 | 708 | /** 709 | * @name jQuery.fn.accessibleMegaMenu~_mouseDownHandler 710 | * @desc Handle mousedown event on mega menu. 711 | * @param {event} Event object 712 | * @memberof accessibleMegaMenu 713 | * @inner 714 | * @private 715 | */ 716 | _mouseDownHandler = function (event) { 717 | if ( 718 | $(event.target).is(this.settings.panelClass) || 719 | $(event.target).closest(":focusable").length 720 | ) { 721 | this.mouseFocused = true; 722 | } 723 | this.mouseTimeoutID = setTimeout(function () { 724 | clearTimeout(this.focusTimeoutID); 725 | }, 1); 726 | }; 727 | 728 | /** 729 | * @name jQuery.fn.accessibleMegaMenu~_mouseOverHandler 730 | * @desc Handle mouseover event on mega menu. 731 | * @param {event} Event object 732 | * @memberof jQuery.fn.accessibleMegaMenu 733 | * @inner 734 | * @private 735 | */ 736 | _mouseOverHandler = function (event) { 737 | clearTimeout(this.mouseTimeoutID); 738 | $(event.target).addClass(this.settings.hoverClass); 739 | _togglePanel.call(this, event); 740 | if ($(event.target).is(":tabbable")) { 741 | $("html").on("keydown.menu", $.proxy(_keyDownHandler, event.target)); 742 | } 743 | }; 744 | 745 | /** 746 | * @name jQuery.fn.accessibleMegaMenu~_mouseOutHandler 747 | * @desc Handle mouseout event on mega menu. 748 | * @param {event} Event object 749 | * @memberof jQuery.fn.accessibleMegaMenu 750 | * @inner 751 | * @private 752 | */ 753 | _mouseOutHandler = function (event) { 754 | var that = this; 755 | $(event.target).removeClass(that.settings.hoverClass); 756 | 757 | that.mouseTimeoutID = setTimeout(function () { 758 | _togglePanel.call(that, event, true); 759 | }, 250); 760 | if ($(event.target).is(":tabbable")) { 761 | $("html").off("keydown.menu"); 762 | } 763 | }; 764 | 765 | _toggleExpandedEventHandlers = function (hide) { 766 | var menu = this.menu; 767 | if (hide) { 768 | $("html").off( 769 | "mouseup.outside-menu, touchend.outside-menu, mspointerup.outside-menu, pointerup.outside-menu" 770 | ); 771 | 772 | menu 773 | .find("[aria-expanded]." + this.settings.panelClass) 774 | .off("DOMAttrModified.menu"); 775 | } else { 776 | $("html").on( 777 | "mouseup.outside-menu, touchend.outside-menu, mspointerup.outside-menu, pointerup.outside-menu", 778 | $.proxy(_clickOutsideHandler, this) 779 | ); 780 | 781 | /* Narrator in Windows 8 automatically toggles the aria-expanded property on double tap or click. 782 | To respond to the change to collapse the panel, we must add a listener for a DOMAttrModified event. */ 783 | menu 784 | .find("[aria-expanded=true]." + this.settings.panelClass) 785 | .on("DOMAttrModified.menu", $.proxy(_DOMAttrModifiedHandler, this)); 786 | } 787 | }; 788 | 789 | /* public attributes and methods ------------------------- */ 790 | return { 791 | constructor: AccessibleMegaMenu, 792 | 793 | /** 794 | * @lends jQuery.fn.accessibleMegaMenu 795 | * @desc Initializes an instance of the accessibleMegaMenu plugins 796 | * @memberof jQuery.fn.accessibleMegaMenu 797 | * @instance 798 | */ 799 | init: function () { 800 | var settings = this.settings, 801 | nav = $(this.element), 802 | menu = nav.children().first(), 803 | topnavitems = menu.children(); 804 | this.start(settings, nav, menu, topnavitems); 805 | }, 806 | 807 | start: function (settings, nav, menu, topnavitems) { 808 | var that = this; 809 | this.settings = settings; 810 | this.menu = menu; 811 | this.topnavitems = topnavitems; 812 | 813 | nav.attr("role", "navigation"); 814 | menu.addClass(settings.menuClass); 815 | topnavitems.each(function (i, topnavitem) { 816 | var topnavitemlink, topnavitempanel; 817 | topnavitem = $(topnavitem); 818 | topnavitem.addClass(settings.topNavItemClass); 819 | topnavitemlink = topnavitem.find(":tabbable:first"); 820 | topnavitempanel = topnavitem.children(":not(:tabbable):last"); 821 | _addUniqueId.call(that, topnavitemlink); 822 | if (topnavitempanel.length) { 823 | _addUniqueId.call(that, topnavitempanel); 824 | topnavitemlink.attr({ 825 | "aria-haspopup": true, 826 | "aria-controls": topnavitempanel.attr("id"), 827 | "aria-expanded": false, 828 | }); 829 | 830 | topnavitempanel 831 | .attr({ 832 | role: "group", 833 | "aria-expanded": false, 834 | "aria-hidden": true, 835 | }) 836 | .addClass(settings.panelClass) 837 | .not("[aria-labelledby]") 838 | .attr("aria-labelledby", topnavitemlink.attr("id")); 839 | } 840 | }); 841 | 842 | this.panels = menu.find("." + settings.panelClass); 843 | 844 | menu 845 | .on( 846 | "focusin.menu", 847 | ":focusable, ." + settings.panelClass, 848 | $.proxy(_focusInHandler, this) 849 | ) 850 | .on( 851 | "focusout.menu", 852 | ":focusable, ." + settings.panelClass, 853 | $.proxy(_focusOutHandler, this) 854 | ) 855 | .on("keydown.menu", $.proxy(_keyDownHandler, this)) 856 | .on("mouseover.menu", $.proxy(_mouseOverHandler, this)) 857 | .on("mouseout.menu", $.proxy(_mouseOutHandler, this)) 858 | .on("mousedown.menu", $.proxy(_mouseDownHandler, this)); 859 | 860 | if (isTouch) { 861 | menu.on("touchstart.menu", $.proxy(_clickHandler, this)); 862 | } 863 | 864 | menu.find("hr").attr("role", "separator"); 865 | 866 | if ($(document.activeElement).closest(menu).length) { 867 | $(document.activeElement).trigger("focusin.menu"); 868 | } 869 | }, 870 | 871 | /** 872 | * @desc Get default values 873 | * @example $(selector).accessibleMegaMenu('getDefaults'); 874 | * @return {object} 875 | * @memberof jQuery.fn.accessibleMegaMenu 876 | * @instance 877 | */ 878 | getDefaults: function () { 879 | return this._defaults; 880 | }, 881 | 882 | /** 883 | * @desc Get any option set to plugin using its name (as string) 884 | * @example $(selector).accessibleMegaMenu('getOption', some_option); 885 | * @param {string} opt 886 | * @return {string} 887 | * @memberof jQuery.fn.accessibleMegaMenu 888 | * @instance 889 | */ 890 | getOption: function (opt) { 891 | return this.settings[opt]; 892 | }, 893 | 894 | /** 895 | * @desc Get all options 896 | * @example $(selector).accessibleMegaMenu('getAllOptions'); 897 | * @return {object} 898 | * @memberof jQuery.fn.accessibleMegaMenu 899 | * @instance 900 | */ 901 | getAllOptions: function () { 902 | return this.settings; 903 | }, 904 | 905 | /** 906 | * @desc Set option 907 | * @example $(selector).accessibleMegaMenu('setOption', 'option_name', 'option_value', reinitialize); 908 | * @param {string} opt - Option name 909 | * @param {string} val - Option value 910 | * @param {boolean} [reinitialize] - boolean to re-initialize the menu. 911 | * @memberof jQuery.fn.accessibleMegaMenu 912 | * @instance 913 | */ 914 | setOption: function (opt, value, reinitialize) { 915 | this.settings[opt] = value; 916 | if (reinitialize) { 917 | this.init(); 918 | } 919 | }, 920 | }; 921 | })(); 922 | 923 | $.fn[pluginName] = function (options) { 924 | return this.each(function () { 925 | if (!$.data(this, "plugin_" + pluginName)) { 926 | $.data( 927 | this, 928 | "plugin_" + pluginName, 929 | new $.fn[pluginName].AccessibleMegaMenu(this, options) 930 | ); 931 | } 932 | }); 933 | }; 934 | 935 | $.fn[pluginName].AccessibleMegaMenu = AccessibleMegaMenu; 936 | 937 | /* :focusable and :tabbable selectors from 938 | https://raw.github.com/jquery/jquery-ui/master/ui/jquery.ui.core.js */ 939 | 940 | /** 941 | * @private 942 | */ 943 | function visible(element) { 944 | return ( 945 | $.expr.filters.visible(element) && 946 | !$(element) 947 | .parents() 948 | .addBack() 949 | .filter(function () { 950 | return $.css(this, "visibility") === "hidden"; 951 | }).length 952 | ); 953 | } 954 | 955 | /** 956 | * @private 957 | */ 958 | function focusable(element, isTabIndexNotNaN) { 959 | var map, 960 | mapName, 961 | img, 962 | nodeName = element.nodeName.toLowerCase(); 963 | if ("area" === nodeName) { 964 | map = element.parentNode; 965 | mapName = map.name; 966 | if (!element.href || !mapName || map.nodeName.toLowerCase() !== "map") { 967 | return false; 968 | } 969 | img = $("img[usemap=#" + mapName + "]")[0]; 970 | return !!img && visible(img); 971 | } 972 | return ( 973 | (/input|select|textarea|button|object/.test(nodeName) 974 | ? !element.disabled 975 | : "a" === nodeName 976 | ? element.href || isTabIndexNotNaN 977 | : isTabIndexNotNaN) && 978 | // the element and all of its ancestors must be visible 979 | visible(element) 980 | ); 981 | } 982 | 983 | $.extend($.expr[":"], { 984 | data: $.expr.createPseudo 985 | ? $.expr.createPseudo(function (dataName) { 986 | return function (elem) { 987 | return !!$.data(elem, dataName); 988 | }; 989 | }) // support: jQuery <1.8 990 | : function (elem, i, match) { 991 | return !!$.data(elem, match[3]); 992 | }, 993 | 994 | focusable: function (element) { 995 | return focusable(element, !isNaN($.attr(element, "tabindex"))); 996 | }, 997 | 998 | tabbable: function (element) { 999 | var tabIndex = $.attr(element, "tabindex"), 1000 | isTabIndexNaN = isNaN(tabIndex); 1001 | return ( 1002 | (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN) 1003 | ); 1004 | }, 1005 | }); 1006 | })(jQuery, window, document); 1007 | --------------------------------------------------------------------------------