├── .gitignore
├── public
├── fonts
│ ├── 351114_0_0.eot
│ ├── 351114_0_0.ttf
│ ├── 351114_1_0.eot
│ ├── 351114_1_0.ttf
│ ├── 351114_0_0.woff
│ ├── 351114_0_0.woff2
│ ├── 351114_1_0.woff
│ └── 351114_1_0.woff2
├── css
│ ├── partials
│ │ └── fonts.css
│ └── app.css
├── js
│ └── app.js
└── index.html
├── postcss.config.js
├── README.md
├── package.json
└── tailwind.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | public/build
3 | yarn.lock
4 | .DS_Store
5 | .vscode
--------------------------------------------------------------------------------
/public/fonts/351114_0_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proeung/accessible-menu/HEAD/public/fonts/351114_0_0.eot
--------------------------------------------------------------------------------
/public/fonts/351114_0_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proeung/accessible-menu/HEAD/public/fonts/351114_0_0.ttf
--------------------------------------------------------------------------------
/public/fonts/351114_1_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proeung/accessible-menu/HEAD/public/fonts/351114_1_0.eot
--------------------------------------------------------------------------------
/public/fonts/351114_1_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proeung/accessible-menu/HEAD/public/fonts/351114_1_0.ttf
--------------------------------------------------------------------------------
/public/fonts/351114_0_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proeung/accessible-menu/HEAD/public/fonts/351114_0_0.woff
--------------------------------------------------------------------------------
/public/fonts/351114_0_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proeung/accessible-menu/HEAD/public/fonts/351114_0_0.woff2
--------------------------------------------------------------------------------
/public/fonts/351114_1_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proeung/accessible-menu/HEAD/public/fonts/351114_1_0.woff
--------------------------------------------------------------------------------
/public/fonts/351114_1_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proeung/accessible-menu/HEAD/public/fonts/351114_1_0.woff2
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-import'),
4 | require('tailwindcss/nesting'),
5 | require('tailwindcss'),
6 | require('autoprefixer'),
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/public/css/partials/fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'SofiaPro';
3 | font-weight: 300;
4 | src: url('../fonts/351114_1_0.eot');
5 | src: url('../fonts/351114_1_0.eot?#iefix') format('embedded-opentype'),
6 | url('../fonts/351114_1_0.woff2') format('woff2'),
7 | url('../fonts/351114_1_0.woff') format('woff'),
8 | url('../fonts/351114_1_0.ttf') format('truetype');
9 | }
10 |
11 | @font-face {
12 | font-family: 'SofiaPro';
13 | font-weight: 700;
14 | src: url('../fonts/351114_0_0.eot');
15 | src: url('../fonts/351114_0_0.eot?#iefix') format('embedded-opentype'),
16 | url('../fonts/351114_0_0.woff2') format('woff2'),
17 | url('../fonts/351114_0_0.woff') format('woff'),
18 | url('../fonts/351114_0_0.ttf') format('truetype');
19 | }
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # accessible-menu
2 |
3 | A sandbox static page that demonstrates WCAG accessible mega menu using Tailwind CSS v.3 and extending from Disclosure Menus.
4 |
5 | 
6 |
7 |
8 | To get started:
9 |
10 | 1. Install the dependencies:
11 |
12 | ```bash
13 | # Using npm
14 | npm install
15 |
16 | # Using Yarn
17 | yarn
18 | ```
19 |
20 | 2. Start the development server:
21 |
22 | ```bash
23 | # Using npm
24 | npm run serve
25 |
26 | # Using Yarn
27 | yarn run serve
28 | ```
29 |
30 | Now you should be able to see the project running at localhost:8080.
31 |
32 | 3. Open `public/index.html` in your editor and start experimenting!
33 |
34 | ## Building for production
35 |
36 | To build an optimized version of your CSS, simply run:
37 |
38 | ```bash
39 | # Using npm
40 | npm run production
41 |
42 | # Using Yarn
43 | yarn run production
44 | ```
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "accessible-menu",
3 | "version": "0.0.1",
4 | "description": "A sandbox app that generates WCAG accessible menus.",
5 | "author": {
6 | "name": "Putra Bonaccorsi",
7 | "url": "https://www.iamputra.com"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": ""
12 | },
13 | "scripts": {
14 | "serve": "cross-env NODE_ENV=development concurrently \"postcss public/css/app.css -o public/build/app.css --watch\" \"live-server ./public\"",
15 | "development": "cross-env NODE_ENV=development postcss public/css/app.css -o public/build/app.css",
16 | "production": "cross-env NODE_ENV=production postcss public/css/app.css -o public/build/app.css"
17 | },
18 | "dependencies": {
19 | "postcss-calc": "^7.0.1",
20 | "postcss-import": "^12.0.1",
21 | "postcss-nesting": "^7.0.1"
22 | },
23 | "devDependencies": {
24 | "autoprefixer": "^10.4.0",
25 | "concurrently": "^7.0.0",
26 | "cross-env": "^5.2.0",
27 | "live-server": "^1.2.1",
28 | "postcss": "^8.4.4",
29 | "postcss-cli": "^9.1.0",
30 | "tailwindcss": "^3.0.11"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors');
2 |
3 | module.exports = {
4 | content: [
5 | './public/**/*.{html,js}',
6 | ],
7 | theme: {
8 | colors: {
9 | transparent: 'transparent',
10 | current: 'currentColor',
11 | black: colors.black,
12 | white: colors.white,
13 | gray: colors.gray,
14 | purple: colors.purple,
15 | red: colors.red,
16 | blue: colors.blue,
17 | },
18 | screens: {
19 | 'sm': '640px',
20 | 'md': '768px',
21 | 'lg': '1024px',
22 | 'xl': '1280px',
23 | },
24 | container: {
25 | padding: {
26 | DEFAULT: '1rem',
27 | sm: '2rem',
28 | lg: '4rem',
29 | },
30 | },
31 | fontFamily: {
32 | 'sans': [
33 | 'SofiaPro',
34 | '-apple-system',
35 | 'BlinkMacSystemFont',
36 | 'Segoe UI',
37 | 'Roboto',
38 | 'Oxygen',
39 | 'Ubuntu',
40 | 'Cantarell',
41 | 'Fira Sans',
42 | 'Droid Sans',
43 | 'Helvetica Neue',
44 | ],
45 | 'sans-secondary': [
46 | 'Heebo',
47 | 'Roboto',
48 | 'Oxygen',
49 | 'Ubuntu',
50 | 'Cantarell',
51 | 'Fira Sans',
52 | 'Droid Sans',
53 | 'Helvetica Neue',
54 | ],
55 | },
56 | extend: {
57 | spacing: {
58 | '70': '4.375rem',
59 | '100': '6.25rem',
60 | '310': '19.375rem',
61 | },
62 | fontSize: {
63 | xs: '0.75rem', //12px
64 | sm: '0.875rem', //14px
65 | base: '1rem', //16px
66 | lg: '1.125rem', //18px
67 | xl: '1.25rem', //20px
68 | '2xl': '1.375rem', //22px
69 | '3xl': '1.875rem', //30px
70 | '4xl': '2.25rem', //36px
71 | '5xl': '2.625rem', //42px
72 | },
73 | },
74 | },
75 | plugins: [],
76 | }
77 |
--------------------------------------------------------------------------------
/public/css/app.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import "partials/fonts";
6 |
7 |
8 | @layer components {
9 |
10 | /* Hamburger Button */
11 | .hamburger-button.active,
12 | .hamburger-button[aria-expanded="true"] {
13 | .line-1 {
14 | @apply translate-y-0 rotate-45;
15 | }
16 |
17 | .line-2 {
18 | @apply opacity-0 translate-x-3;
19 | }
20 |
21 | .line-3 {
22 | @apply translate-y-0 -rotate-45;
23 | }
24 | }
25 |
26 | /* Accessible Menu */
27 | ul.accessible-menu {
28 |
29 | &.menu-is-open {
30 | @apply block lg:flex;
31 | }
32 |
33 | > li {
34 | @apply lg:flex;
35 | }
36 |
37 | li {
38 | @apply m-0;
39 |
40 | button {
41 | @apply border-b-2 text-lg text-gray-900 font-medium py-4 lg:py-8 px-4 w-full lg:w-auto lg:border-b-0;
42 | }
43 |
44 | &:last-child {
45 | button {
46 | @apply border-b-0;
47 | }
48 | }
49 | }
50 |
51 | button:focus,
52 | ul li a:focus {
53 | @apply outline outline-2 text-blue-900 outline-offset-[-2px] outline-blue-900 relative;
54 | }
55 |
56 | button:hover,
57 | button[aria-expanded="true"] {
58 | @apply text-blue-900;
59 | }
60 |
61 | button[aria-expanded="true"] {
62 | svg {
63 | @apply rotate-180;
64 | }
65 | }
66 |
67 | ul li a:focus {
68 | @apply outline-offset-[2px];
69 | }
70 |
71 | ul li a:hover {
72 | @apply text-blue-900 underline;
73 | }
74 |
75 | /* Submenu */
76 | ul.accessible-menu__sub-menu {
77 | top: calc(100% + 1px);
78 | @apply block bg-white invisible rounded-b-md shadow-lg list-none m-0 p-4 space-y-2 lg:absolute lg:right-0 lg:p-8 lg:space-y-4;
79 |
80 | li {
81 | @apply text-lg font-medium;
82 |
83 | a {
84 | @apply block lg:inline-block;
85 | }
86 | }
87 | }
88 |
89 | ul.accessible-menu__sub-menu--mega {
90 | @apply container lg:grid lg:grid-cols-3 lg:gap-20 space-y-6 lg:space-y-0;
91 |
92 | span {
93 | @apply block font-bold text-xl text-gray-600 mb-2 lg:mb-6 lg:text-2xl;
94 | }
95 |
96 | ul {
97 | @apply grid grid-cols-1 gap-3;
98 | }
99 | }
100 |
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/public/js/app.js:
--------------------------------------------------------------------------------
1 | const accessibleMenu = function (domNode) {
2 | this.rootNode = domNode;
3 | this.triggerNodes = [];
4 | this.controlledNodes = [];
5 | this.openIndex = null;
6 | this.useArrowKeys = true;
7 | };
8 |
9 | accessibleMenu.prototype.init = function () {
10 | const buttons = this.rootNode.querySelectorAll('button[aria-expanded][aria-controls]');
11 | for (var i = 0; i < buttons.length; i++) {
12 | const button = buttons[i];
13 | const menu = button.parentNode.querySelector('.accessible-menu__sub-menu');
14 | if (menu) {
15 | // Save ref to button and controlled menu.
16 | this.triggerNodes.push(button);
17 | this.controlledNodes.push(menu);
18 |
19 | // Collapse menus.
20 | button.setAttribute('aria-expanded', 'false');
21 | this.toggleMenu(menu, false);
22 |
23 | // Attach event listeners.
24 | menu.addEventListener('keydown', this.handleMenuKeyDown.bind(this));
25 | button.addEventListener('click', this.handleButtonClick.bind(this));
26 | button.addEventListener('keydown', this.handleButtonKeyDown.bind(this));
27 | }
28 | }
29 |
30 | this.rootNode.addEventListener('focusout', this.handleBlur.bind(this));
31 |
32 | // Show the menu help instructions when the first button is focused.
33 | const menuHelp = document.querySelector('.menu-help-instructions');
34 | if (buttons[0].getAttribute('aria-expanded') === 'false') {
35 | buttons[0].addEventListener('focusin', () => {
36 |
37 | window.setTimeout(() => {
38 | menuHelp.classList.remove('sr-only');
39 | }, 200);
40 | });
41 |
42 | buttons[0].addEventListener('focusout', () => {
43 | menuHelp.classList.add('sr-only');
44 | });
45 | }
46 | };
47 |
48 | accessibleMenu.prototype.toggleMenu = function (domNode, show) {
49 | if (domNode) {
50 | domNode.style.display = show ? 'grid' : 'none';
51 | domNode.style.visibility = show ? 'visible' : 'hidden';
52 | domNode.classList.toggle('menu-is-open', show);
53 | }
54 | };
55 |
56 | accessibleMenu.prototype.toggleExpand = function (index, expanded) {
57 | // Close open menu, if applicable.
58 | if (this.openIndex !== index) {
59 | this.toggleExpand(this.openIndex, false);
60 | }
61 |
62 | // Handle menu at called index.
63 | if (this.triggerNodes[index]) {
64 | this.openIndex = expanded ? index : null;
65 | this.triggerNodes[index].setAttribute('aria-expanded', expanded);
66 | this.toggleMenu(this.controlledNodes[index], expanded);
67 | }
68 | };
69 |
70 | accessibleMenu.prototype.controlFocusByKey = function (keyboardEvent, nodeList, currentIndex) {
71 | switch (keyboardEvent.key) {
72 | case 'ArrowUp':
73 | case 'ArrowLeft':
74 | keyboardEvent.preventDefault();
75 | if (currentIndex > -1) {
76 | const prevIndex = Math.max(0, currentIndex - 1);
77 | nodeList[prevIndex].focus();
78 | }
79 | break;
80 | case 'ArrowDown':
81 | case 'ArrowRight':
82 | keyboardEvent.preventDefault();
83 | if (currentIndex > -1) {
84 | const nextIndex = Math.min(nodeList.length - 1, currentIndex + 1);
85 | nodeList[nextIndex].focus();
86 | }
87 | break;
88 | case 'Home':
89 | keyboardEvent.preventDefault();
90 | nodeList[0].focus();
91 | break;
92 | case 'End':
93 | keyboardEvent.preventDefault();
94 | nodeList[nodeList.length - 1].focus();
95 | break;
96 | }
97 | };
98 |
99 | /* Event Handlers */
100 | accessibleMenu.prototype.handleBlur = function (event) {
101 | const menuContainsFocus = this.rootNode.contains(event.relatedTarget);
102 | if (!menuContainsFocus && this.openIndex !== null) {
103 | this.toggleExpand(this.openIndex, false);
104 | }
105 | };
106 |
107 | accessibleMenu.prototype.handleButtonKeyDown = function (event) {
108 | const targetButtonIndex = this.triggerNodes.indexOf(document.activeElement);
109 |
110 | // Close the menu on the escape key.
111 | if (event.key === 'Escape') {
112 | this.toggleExpand(this.openIndex, false);
113 | }
114 |
115 | // Move focus into the open menu if the current menu is open.
116 | else if (this.useArrowKeys && this.openIndex === targetButtonIndex && event.key === 'ArrowDown') {
117 | event.preventDefault();
118 | this.controlledNodes[this.openIndex].querySelector('a').focus();
119 | }
120 |
121 | // Handle arrow key navigation between top-level buttons, if set.
122 | else if (this.useArrowKeys) {
123 | this.controlFocusByKey(event, this.triggerNodes, targetButtonIndex);
124 | }
125 | };
126 |
127 | accessibleMenu.prototype.handleButtonClick = function (event) {
128 | const button = event.target;
129 | const buttonIndex = this.triggerNodes.indexOf(button);
130 | const buttonExpanded = button.getAttribute('aria-expanded') === 'true';
131 | this.toggleExpand(buttonIndex, !buttonExpanded);
132 | };
133 |
134 | accessibleMenu.prototype.handleMenuKeyDown = function (event) {
135 | if (this.openIndex === null) {
136 | return;
137 | }
138 |
139 | const menuLinks = Array.prototype.slice.call(this.controlledNodes[this.openIndex].querySelectorAll('a'));
140 | const currentIndex = menuLinks.indexOf(document.activeElement);
141 |
142 | // Close the menu on the escape key.
143 | if (event.key === 'Escape') {
144 | this.triggerNodes[this.openIndex].focus();
145 | this.toggleExpand(this.openIndex, false);
146 | }
147 |
148 | // Handle arrow key navigation within menu links, if set.
149 | else if (this.useArrowKeys) {
150 | this.controlFocusByKey(event, menuLinks, currentIndex);
151 | }
152 | };
153 |
154 | // Switch on/off arrow key navigation.
155 | accessibleMenu.prototype.updateKeyControls = function (useArrowKeys) {
156 | this.useArrowKeys = useArrowKeys;
157 | };
158 |
159 | // Initialize Menus.
160 | window.addEventListener('load', function (event) {
161 | const menus = document.querySelectorAll('.accessible-menu');
162 | const accessibleMenus = [];
163 |
164 | for (var i = 0; i < menus.length; i++) {
165 | accessibleMenus[i] = new accessibleMenu(menus[i]);
166 | accessibleMenus[i].init();
167 | }
168 |
169 | // Toggle Hamburger Button & Dropdown.
170 | const hamburgerButton = document.querySelector('.hamburger-button');
171 | hamburgerButton.addEventListener('click', function(menu) {
172 | hamburgerButton.classList.toggle('active');
173 |
174 | if (hamburgerButton.getAttribute('aria-expanded') === 'false') {
175 | // Set aria attribute.
176 | this.setAttribute("aria-expanded", 'true');
177 | //Apply toggle class to the first menu from arry.
178 | menus[0].classList.add('menu-is-open');
179 | } else {
180 | this.setAttribute("aria-expanded", 'false');
181 | menus[0].classList.remove('menu-is-open');
182 | }
183 | });
184 |
185 | }, false);
186 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Acessible Menu
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Acessible Menu Logo
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
Menu Navigation Tips
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 |
57 |
58 |
64 | Open Hamburger Menu
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
77 |
78 |
83 | Plan Your Visit
84 |
85 |
86 |
87 |
88 |
140 |
141 |
142 |
147 | Research & Collections
148 |
149 |
150 |
151 |
152 |
204 |
205 |
206 |
211 | Support
212 |
213 |
214 |
215 |
216 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 | accessible menu
262 | A sandbox static page that demonstrates WCAG accessible mega menu using Tailwind CSS v.3 and extending from Disclosure Menus.
263 |
264 |
265 |
266 |
267 |
268 | Keyboard Support
269 |
270 |
271 |
272 |
273 |
Tab Shift + Tab
274 |
Use the tab and shift-tab keys to access top-level menu links or to focus on the previous links.
275 |
276 |
277 |
278 |
279 |
280 |
Space or Enter
281 |
Use the "Space" or "Enter" keys to activate the visibility of the menu dropdown.
282 |
283 |
284 |
285 |
286 |
287 |
Escape
288 |
Use this key to close the current dropdown menu and return focus to the element that spawned it.
289 |
290 |
291 |
292 |
293 |
294 |
Up or Down arrow
295 |
Use the "Up" key to move focus to the previous item in the menu links. If the focus is on the first item, move focus to the last item.
296 | Use the "Down" key to move focus to the next item in the menu links. If the focus is on the last item, move focus to the first item.
297 |
298 |
299 |
300 |
301 |
302 |
Right or Left arrow
303 |
Use the "Right" key to move focus to the next item in the menu links. If the focus is on the last item, move focus to the first item.
304 | Use the "Left" key to move focus to the previous item in the menu links. If the focus is on the first item, move focus to the last item.
305 |
306 |
307 |
308 |
309 |
310 |
Home or End
311 |
Use the "Home" to move focus to the first item in the current menu level. Use "End" to move focus to the last item in the current menu level.
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
333 |
334 |
335 |
336 |
337 |
338 |
--------------------------------------------------------------------------------