From 5fdad059c129ed0965931e3d7e837e51406d7d4a Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 5 May 2020 13:43:31 -0500 Subject: [PATCH 01/47] initial update to the menu button links --- .../menu-button/css/menu-button-links.css | 87 +++++ .../menu-button/images/down-arrow-focus.svg | 4 + examples/menu-button/images/down-arrow.svg | 4 + .../menu-button/images/up-arrow-focus.svg | 4 + examples/menu-button/images/up-arrow.svg | 4 + examples/menu-button/js/menu-button-links.js | 310 ++++++++++++++++++ examples/menu-button/menu-button-links.html | 21 +- 7 files changed, 418 insertions(+), 16 deletions(-) create mode 100644 examples/menu-button/css/menu-button-links.css create mode 100644 examples/menu-button/images/down-arrow-focus.svg create mode 100644 examples/menu-button/images/down-arrow.svg create mode 100644 examples/menu-button/images/up-arrow-focus.svg create mode 100644 examples/menu-button/images/up-arrow.svg create mode 100644 examples/menu-button/js/menu-button-links.js diff --git a/examples/menu-button/css/menu-button-links.css b/examples/menu-button/css/menu-button-links.css new file mode 100644 index 0000000000..ca212d714b --- /dev/null +++ b/examples/menu-button/css/menu-button-links.css @@ -0,0 +1,87 @@ +.menu-button-links { + margin: 0; + margin-top: 0.5em; + margin-bottom: 0.5em; + padding: 7px; + font-size: 110%; + list-style: none; + background-color: #eee; + border: #eee solid 1px; + border-radius: 5px; +} + +.menu-button-links button { + padding: 6px; + display: inline-block; + position: relative; + border: 0 solid black; + background-color: #eee; + border: 0px solid #eee; + font-size: 0.9em; + color: black; + border-radius: 5px; +} + +.menu-button-links button::after { + content: url('../images/down-arrow.svg'); + padding-left: 0.25em; +} + +.menu-button-links button:focus::after { + content: url('../images/down-arrow-focus.svg'); +} + +.menu-button-links button[aria-expanded="true"]::after { + content: url('../images/up-arrow-focus.svg'); +} + +.menu-button-links [role="menu"] { + display: none; + position: absolute; + margin: 0; + padding: 0; +} + +.menu-button-links [role="menuitem"], +.menu-button-links [role="separator"] { + margin: 0; + padding: 6px; + display: block; + width: 24em; + background-color: #eee; + border: 0px solid #eee; + color: black; + border-radius: 5px; +} + +.menu-button-links [role="separator"] { + padding-top: 3px; + background-image: url('../images/separator.svg'); + background-position: center; + background-repeat: repeat-x; +} + +/* focus styling */ + +.menu-button-links.focus { + padding: 6px; + border: #034575 solid 2px; +} + +.menu-button-links [role="menu"] { + padding: 7px 4px; + border: 2px solid #034575; + border-radius: 5px; + background-color: #eee; +} + +.menu-button-links button:focus, +.menu-button-links button[aria-expanded=true], +.menu-button-links [role="menuitem"]:focus { + padding: 2px; + border: 4px solid #034575; + background: #034575; + color: #fff; + outline: none; + margin: 0; +} diff --git a/examples/menu-button/images/down-arrow-focus.svg b/examples/menu-button/images/down-arrow-focus.svg new file mode 100644 index 0000000000..f637806269 --- /dev/null +++ b/examples/menu-button/images/down-arrow-focus.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/menu-button/images/down-arrow.svg b/examples/menu-button/images/down-arrow.svg new file mode 100644 index 0000000000..c30c32e123 --- /dev/null +++ b/examples/menu-button/images/down-arrow.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/menu-button/images/up-arrow-focus.svg b/examples/menu-button/images/up-arrow-focus.svg new file mode 100644 index 0000000000..37574ee8a9 --- /dev/null +++ b/examples/menu-button/images/up-arrow-focus.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/menu-button/images/up-arrow.svg b/examples/menu-button/images/up-arrow.svg new file mode 100644 index 0000000000..4494c0f0db --- /dev/null +++ b/examples/menu-button/images/up-arrow.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/menu-button/js/menu-button-links.js b/examples/menu-button/js/menu-button-links.js new file mode 100644 index 0000000000..8ac4ea868e --- /dev/null +++ b/examples/menu-button/js/menu-button-links.js @@ -0,0 +1,310 @@ +/* +* This content is licensed according to the W3C Software License at +* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document +* +* File: menu-button-links.js +* +* Desc: Creates a menu button that opens a menu of links +*/ + +var MenuButtonLinks = function (domNode) { + + this.domNode = domNode; + this.buttonNode = domNode.querySelector('button'); + this.menuNode = domNode.querySelector('[role="menu"]'); + this.menuitemNodes = [] + this.firstMenuitem = false; + this.lastMenuitem = false; + this.firstChars = []; + + this.buttonNode.addEventListener('keydown', this.handleButtonKeydown.bind(this)); + this.buttonNode.addEventListener('click', this.handleButtonClick.bind(this)); + + var nodes = domNode.querySelectorAll('[role="menuitem"]'); + + for (var i = 0; i < nodes.length; i++) { + var menuitem = nodes[i]; + this.menuitemNodes.push(menuitem); + menuitem.tabIndex = -1; + this.firstChars.push(menuitem.textContent.trim()[0].toLowerCase()); + + menuitem.addEventListener('keydown', this.handleMenuitemKeydown.bind(this)); + menuitem.addEventListener('click', this.handleMenuitemClick.bind(this)); + + menuitem.addEventListener('mouseover', this.handleMenuitemMouseover.bind(this)); + + if( !this.firstMenuitem) { + this.firstMenuitem = menuitem; + } + this.lastMenuitem = menuitem; + } + + domNode.addEventListener('focusin', this.handleFocusin.bind(this)); + domNode.addEventListener('focusout', this.handleFocusout.bind(this)); + + window.addEventListener('mousedown', this.handleBackgroundMousedown.bind(this), true); +}; + +MenuButtonLinks.prototype.setFocusToMenuitem = function (newMenuitem) { + + this.menuitemNodes.forEach(function(item) { + if (item === newMenuitem) { + item.tabIndex = 0; + newMenuitem.focus(); + } + else { + item.tabIndex = -1; + } + }); +}; + +MenuButtonLinks.prototype.setFocusToFirstMenuitem = function (currentMenuitem) { + this.setFocusToMenuitem(this.firstMenuitem); +}; + +MenuButtonLinks.prototype.setFocusToLastMenuitem = function (currentMenuitem) { + this.setFocusToMenuitem(this.lastMenuitem); +}; + +MenuButtonLinks.prototype.setFocusToPreviousMenuitem = function (currentMenuitem) { + var newMenuitem, index; + + if (currentMenuitem === this.firstMenuitem) { + newMenuitem = this.lastMenuitem; + } + else { + index = this.menuitemNodes.indexOf(currentMenuitem); + newMenuitem = this.menuitemNodes[ index - 1 ]; + } + + this.setFocusToMenuitem(newMenuitem); + + return newMenuitem; +}; + +MenuButtonLinks.prototype.setFocusToNextMenuitem = function (currentMenuitem) { + var newMenuitem, index; + + if (currentMenuitem === this.lastMenuitem) { + newMenuitem = this.firstMenuitem; + } + else { + index = this.menuitemNodes.indexOf(currentMenuitem); + newMenuitem = this.menuitemNodes[ index + 1 ]; + } + this.setFocusToMenuitem(newMenuitem); + + return newMenuitem; +}; + +MenuButtonLinks.prototype.setFocusByFirstCharacter = function (currentMenuitem, char) { + var start, index; + + if (char.length > 1) { + return; + } + + char = char.toLowerCase(); + + // Get start index for search based on position of currentItem + start = this.menuitemNodes.indexOf(currentMenuitem) + 1; + if (start >= this.menuitemNodes.length) { + start = 0; + } + + // Check remaining slots in the menu + index = this.firstChars.indexOf(char, start); + + // If not found in remaining slots, check from beginning + if (index === -1) { + index = this.firstChars.indexOf(char, 0); + } + + // If match was found... + if (index > -1) { + this.setFocusToMenuitem(this.menuitemNodes[index]); + } +}; + +// Utilities + +MenuButtonLinks.prototype.getIndexFirstChars = function (startIndex, char) { + for (var i = startIndex; i < this.firstChars.length; i++) { + if (char === this.firstChars[i]) { + return i; + } + } + return -1; +}; + +// Popup menu methods + +MenuButtonLinks.prototype.openPopup = function () { + + var rect = this.menuNode.getBoundingClientRect(); + + this.menuNode.style.display = 'block'; + + this.buttonNode.setAttribute('aria-expanded', 'true'); + +}; + +MenuButtonLinks.prototype.closePopup = function () { + if (this.isOpen()) { + this.buttonNode.setAttribute('aria-expanded', 'false'); + this.menuNode.style.display = 'none'; + } +}; + +MenuButtonLinks.prototype.isOpen = function () { + return this.buttonNode.getAttribute('aria-expanded') === 'true'; +}; + +// Menu event handlers + +MenuButtonLinks.prototype.handleFocusin = function (event) { + this.domNode.classList.add('focus'); +}; + +MenuButtonLinks.prototype.handleFocusout = function (event) { + this.domNode.classList.remove('focus'); +}; + +MenuButtonLinks.prototype.handleButtonKeydown = function (event) { + var tgt = event.currentTarget, + key = event.key, + flag = false; + + switch (key) { + case ' ': + case 'Enter': + case 'ArrowDown': + case 'Down': + this.openPopup(); + this.setFocusToFirstMenuitem(); + flag = true; + break; + + case 'Esc': + case 'Escape': + this.closePopup(); + flag = true; + break; + + case 'Up': + case 'ArrowUp': + this.openPopup(); + this.setFocusToLastMenuitem(); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } +}; + +MenuButtonLinks.prototype.handleButtonClick = function (event) { + this.openPopup(); + this.setFocusToFirstMenuitem(); +}; + +MenuButtonLinks.prototype.isPrintableCharacter = function(str) { + return str.length === 1 && str.match(/\S/); +}; + +MenuButtonLinks.prototype.handleMenuitemKeydown = function (event) { + var tgt = event.currentTarget, + key = event.key, + flag = false; + + switch (key) { + case ' ': + case 'Enter': + this.closePopup(); + window.location.href=tgt.href; + flag = true; + break; + + case 'Esc': + case 'Escape': + this.closePopup(); + this.buttonNode.focus(); + flag = true; + break; + + case 'Up': + case 'ArrowUp': + this.setFocusToPreviousMenuitem(tgt); + flag = true; + break; + + case 'ArrowDown': + case 'Down': + this.setFocusToNextMenuitem(tgt); + flag = true; + break; + + + case 'Home': + case 'PageUp': + this.setFocusToFirstMenuitem(menuId, tgt); + flag = true; + break; + + case 'End': + case 'PageDown': + this.setFocusToLastMenuitem(menuId, tgt); + flag = true; + break; + + case 'Tab': + this.closePopup(); + break; + + default: + if (this.isPrintableCharacter(key)) { + this.setFocusByFirstCharacter(tgt, key); + flag = true; + } + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } +}; + +MenuButtonLinks.prototype.handleMenuitemClick = function (event) { + var tgt = event.currentTarget; + this.closePopup(); + window.location.href=tgt.href; +}; + +MenuButtonLinks.prototype.handleMenuitemMouseover = function (event) { + var tgt = event.currentTarget; + tgt.focus(); +}; + +MenuButtonLinks.prototype.handleBackgroundMousedown = function (event) { + if (!this.domNode.contains(event.target)) { + this.closePopup(); + this.buttonNode.focus(); + event.stopPropagation(); + event.preventDefault(); + } +}; + +// Initialize menu buttons + +window.addEventListener('load', function () { + var menuButtons = document.querySelectorAll('.menu-button-links'); + for(var i=0; i < menuButtons.length; i++) { + var menuButton = new MenuButtonLinks(menuButtons[i]); + } +}); diff --git a/examples/menu-button/menu-button-links.html b/examples/menu-button/menu-button-links.html index 7e5fa8663f..c45a1d0fc9 100644 --- a/examples/menu-button/menu-button-links.html +++ b/examples/menu-button/menu-button-links.html @@ -12,10 +12,8 @@ - - - - + +