diff --git a/addon/components/es-navbar.js b/addon/components/es-navbar.js index d7d591cf..e6ad5fea 100644 --- a/addon/components/es-navbar.js +++ b/addon/components/es-navbar.js @@ -20,8 +20,7 @@ export default class EsNavbar extends Component { } toggleMenu() { - let menu = this.element.querySelector('ul[role="menubar"]'); - + let menu = this.element.querySelector('.navbar-toggler'); menu.setAttribute('aria-expanded', menu.getAttribute('aria-expanded') !== 'true'); } } diff --git a/addon/components/es-navbar/link/component.js b/addon/components/es-navbar/link/component.js index 57c4abfb..07bca943 100644 --- a/addon/components/es-navbar/link/component.js +++ b/addon/components/es-navbar/link/component.js @@ -3,242 +3,104 @@ import layout from './template'; import { computed } from '@ember/object'; import { equal } from '@ember/object/computed'; import { inject as service } from '@ember/service'; -import { next } from '@ember/runloop'; +import { schedule, next } from '@ember/runloop'; export default Component.extend({ layout, tagName: 'li', - tabIndex: 0, - - role: 'menuitem', - - attributeBindings: ['role'], + classNames: ['navbar-list-item'], classNameBindings: ['isDropdown:dropdown'], isDropdown: equal('link.type', 'dropdown'), + isDropdownOpen: false, - keyCode: Object.freeze({ - 'TAB': 9, - 'RETURN': 13, - 'ESC': 27, - 'SPACE': 32, - 'PAGEUP': 33, - 'PAGEDOWN': 34, - 'END': 35, - 'HOME': 36, - 'LEFT': 37, - 'UP': 38, - 'RIGHT': 39, - 'DOWN': 40 + // because aria-expanded requires a string value instead of a boolean + isExpanded: computed('isDropdownOpen', function() { + return this.isDropdownOpen ? 'true' : 'false'; }), navbar: service(), - didInsertElement() { - this.element.tabIndex = -1; - - this.get('navbar').register(this); - this.domNode = this.element.querySelector('ul[role="menu"]'); + actions: { + toggleDropdown() { + this.toggleProperty('isDropdownOpen'); - if(this.domNode) { - this.element.querySelector('a').onmousedown = () => this.expand(); - let links = Array.from(this.domNode.querySelectorAll('a')) + if (this.isDropdownOpen) { + // if it's open, let's make sure it can do some things + schedule('afterRender', this, function() { - links.forEach((ancor) => { - ancor.addEventListener('blur', () => this.handleBlur()); - }); + // move focus to the first item in the dropdown + this.processFirstElementFocus(); + this.processKeyPress(); + }); + } } }, - handleBlur() { - next(this, function() { - let subItems = Array.from(this.element.querySelectorAll('ul[role="menu"] li')); - let focused = subItems.find(item => document.activeElement === item.querySelector('a')); - - // debugger - if(!focused) { - this.closePopupMenu(); - } - }) + closeDropdown() { + // set the isDropdownOpen to false, which will make the dropdown go away + this.set('isDropdownOpen', false); }, - openPopupMenu() { - // Get position and bounding rectangle of controller object's DOM node - var rect = this.element.getBoundingClientRect(); - - // Set CSS properties - if(this.domNode) { - this.domNode.style.display = 'block'; - this.domNode.style.top = rect.height + 'px'; - this.domNode.style.zIndex = 1000; - } - - this.set('expanded', true); + openDropdown() { //might not need this + // open the dropdown and set the focus to the first item inside + this.set('isDropdownOpen', true); + this.processFirstElementFocus(); }, - closePopupMenu(force) { - var controllerHasHover = this.hasHover; - - var hasFocus = this.hasFocus; - - if (!this.isMenubarItem) { - controllerHasHover = false; - } + processBlur() { + next(this, function() { + let subItems = Array.from(this.element.querySelectorAll('.navbar-dropdown-list li')); + let focused = subItems.find(item => document.activeElement === item.querySelector('a')); - if (force || (!hasFocus && !this.hasHover && !controllerHasHover)) { - if(this.domNode) { - this.domNode.style.display = 'none'; - this.domNode.style.zIndex = 0; + //if the dropdown isn't focused, close it + if (!focused) { + this.closeDropdown(); } - this.set('expanded', false); - } - }, - - expanded: computed({ - get() { - return this.element.getAttribute('aria-expanded') === 'true'; - }, - set(key, value) { - this.element.setAttribute('aria-expanded', value); - } - }).volatile(), - - setFocusToFirstItem() { - let element = this.element.querySelector('ul[role="menu"] li a') - if (element) { - element.focus(); - } + }); }, - setFocusToLastItem() { - this.element.querySelector('ul[role="menu"] li a:last-of-type').focus(); + processClick() { + // TODO handle mouseclick outside the current dropdown }, - setFocusToNextItem() { - let subItems = Array.from(this.element.querySelectorAll('ul[role="menu"] li')); - - let focused = subItems.find(item => document.activeElement === item.querySelector('a')); - let focusedIndex = subItems.indexOf(focused); - - let nextItem = subItems[(focusedIndex + 1) % subItems.length]; - - if (!nextItem) { - return; - } - - nextItem.querySelector('a').focus(); + processFirstElementFocus() { + // Identify the first item in the dropdown list & set focus on it + let firstFocusable = this.element.querySelector('.navbar-dropdown-list li:first-of-type a'); + firstFocusable.focus(); }, - setFocusToPreviousItem() { - let subItems = Array.from(this.element.querySelectorAll('ul[role="menu"] li')); - - let focused = subItems.find(item => document.activeElement === item.querySelector('a')); - let focusedIndex = subItems.indexOf(focused); + processKeyPress() { + // add event listeners + let dropdownList = this.element.querySelector('.navbar-dropdown-list'); - let nextIndex = focusedIndex - 1; + //...for certain keypress events + dropdownList.addEventListener('keydown', event => { - if (nextIndex < 0) { - nextIndex = subItems.length - 1; - } + // ESC key should close the dropdown and return focus to the toggle + if (event.keyCode === 27 && this.isDropdownOpen) { + this.closeDropdown(); + this.returnFocus(); - let nextItem = subItems[nextIndex]; - - if (!nextItem) { - return; - } + // if focus leaves the open dropdown via keypress, close it (without trying to otherwise control focus) + } else if (this.isDropdownOpen) { + this.processBlur(); - nextItem.querySelector('a').focus(); + } else { + return; + } + }); }, - keyDown(event) { - let flag = false; - let clickEvent; - let mousedownEvent; - - switch (event.keyCode) { - case this.keyCode.RETURN: - case this.keyCode.SPACE: - // Create simulated mouse event to mimic the behavior of ATs - // and let the event handler handleClick do the housekeeping. - mousedownEvent = new MouseEvent('mousedown', { - 'view': window, - 'bubbles': true, - 'cancelable': true - }); - clickEvent = new MouseEvent('click', { - 'view': window, - 'bubbles': true, - 'cancelable': true - }); - - document.activeElement.dispatchEvent(mousedownEvent); - document.activeElement.dispatchEvent(clickEvent); - - flag = true; - break; - case this.keyCode.DOWN: - if(this.get('expanded')) { - this.setFocusToNextItem(); - } else { - this.openPopupMenu(); - this.setFocusToFirstItem(); - } - flag = true; - break; - - case this.keyCode.LEFT: - this.get('navbar').setFocusToPreviousItem(this); - flag = true; - break; - - case this.keyCode.RIGHT: - this.get('navbar').setFocusToNextItem(this); - flag = true; - break; - - case this.keyCode.UP: - if(this.get('expanded')) { - this.setFocusToPreviousItem(); - } else { - this.openPopupMenu(); - this.setFocusToLastItem(); - } - break; - - case this.keyCode.HOME: - case this.keyCode.PAGEUP: - this.setFocusToFirstItem(); - flag = true; - break; - - case this.keyCode.END: - case this.keyCode.PAGEDOWN: - this.setFocusToLastItem(); - flag = true; - break; - - case this.keyCode.TAB: - this.closePopupMenu(true); - break; - - case this.keyCode.ESC: - this.closePopupMenu(true); - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } + returnFocus() { + // after that rendering bit happens, we need to return the focus to the trigger + schedule('afterRender', this, function() { + let dropdownTrigger = this.element.querySelector('.navbar-list-item-dropdown-toggle'); + dropdownTrigger.focus(); + }); }, - expand() { - next(this, () => { - if(this.get('expanded')) { - this.closePopupMenu(); - } else { - this.openPopupMenu(); - this.setFocusToFirstItem(); - } - }) + willDestroyElement() { + document.removeEventListener('keydown', this.triggerDropdown); + // document.removeEventListener('click', this.triggerDropdown); } }); diff --git a/addon/components/es-navbar/link/template.hbs b/addon/components/es-navbar/link/template.hbs index fb7f6f1e..ad5bcb7e 100644 --- a/addon/components/es-navbar/link/template.hbs +++ b/addon/components/es-navbar/link/template.hbs @@ -1,40 +1,36 @@ {{#if (eq link.type "link")}} {{/if}} {{#if (eq link.type "dropdown")}} - - + + {{#if isDropdownOpen}} + + {{/if}} {{/if}} diff --git a/addon/styles/components/es-navbar.css b/addon/styles/components/es-navbar.css index 8a186529..1b887f64 100644 --- a/addon/styles/components/es-navbar.css +++ b/addon/styles/components/es-navbar.css @@ -1,23 +1,29 @@ -nav.es-navbar { - width: 100%; - max-width: var(--container-width-large); - padding: 1rem; - font-size: var(--text-preset-2); +.es-navbar { + background-color: #1C1E23; + color: white; display: flex; flex-direction: column; + /* font-size: var(--text-preset-2); */ + font-size: 1.2em; + font-weight: 500; + max-width: var(--container-width-large); + padding: 0.5rem; + width: 100%; } .navbar-brand { display: block; - padding: 0 2rem; - height: 6rem; + padding: 0 1rem; + height: 3rem; + /* TODO using order is not ideal for accessibility */ order: 2; } -.navbar-nav { +.navbar-list { padding: 0; width: 100%; - margin: 1rem 0 0 0; + margin: 0.5rem 0 0 0; + /* TODO using order is not ideal for accessibility */ order: 3; } @@ -27,9 +33,10 @@ nav.es-navbar { border-radius: 4px; border-color: var(--color-light); margin-left: auto; + /* TODO using order is not ideal for accessibility */ order: 1; - margin-bottom: -6rem; - height: 6rem; + margin-bottom: -3rem; + height: 3rem; } .navbar-toggler .navbar-toggler-icon { @@ -37,108 +44,129 @@ nav.es-navbar { } .navbar-toggler-icon { - display: block; - width: 5rem; - height: 3rem; - vertical-align: middle; - content: ""; - background: center center no-repeat; background-size: 100% 100%; + background: center center no-repeat; + content: ""; + display: block; + height: 1.5rem; + width: 2.5rem; } -.navbar-nav >li { +.navbar-list-item { list-style: none; position: relative; } -.navbar-link__dropdown { - display: block; - padding: 2rem; - line-height: 2rem; +.navbar-list-item-link { + color: inherit; + padding: 1rem; text-decoration: none; - color: #FFF; - white-space: nowrap; - font-weight: 600; -} - -.dropdown .navbar-link__dropdown::after { - content: ''; - display: inline-block; - width: 0; - height: 0; - vertical-align: middle; - margin-left: 1rem; - border-top: 5px dashed; - border-top: 5px solid var(--color-orange); - border-right: 5px solid transparent; - border-left: 5px solid transparent; -} - -li[aria-expanded=true] .navbar-link__dropdown { - background: #FFFFFF; - color: var(--color-orange); -} - -ul[role="menu"] { - display: none; - padding: 0; - background: #FFFFFF; - box-shadow: 5px 5px 5px 0 rgba(50,50,50,0.2); -} - -ul[role="menu"] hr { - margin: 0; } -ul[role="menu"] li { - list-style: none; +.navbar-list-item-dropdown-toggle { + background-color: transparent; + border-color: transparent; + color: #FFF; display: block; + padding: 1rem; + text-decoration: none; white-space: nowrap; - background: #FFF; } -ul[role="menu"] li> a { - padding: 2rem; - font-weight: 600; - display: block; - text-decoration: none; - color: var(--color-orange); +.navbar-list-item-dropdown-toggle:hover { + cursor: pointer; } -ul[role="menu"] li> a:hover, ul[role="menu"] li> a:focus { - background: var(--color-orange); - color: var(--color-light); -} + .navbar-list-item-dropdown-toggle::after { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px dashed; + border-top: 5px solid white; + content: ''; + display: inline-block; + height: 0; + margin-left: 0.5rem; + vertical-align: middle; + width: 0; + } + .navbar-list-item-dropdown-toggle[aria-expanded="true"] { + color: var(--color-ember-orange); + } + .navbar-list-item-dropdown-toggle[aria-expanded="true"]:after { + border-top-color: var(--color-ember-orange); + } -ul[role="menubar"][aria-expanded="true"] li{ - background: blue; -} -@media (min-width: 992px) { - nav.es-navbar { - display: flex; - flex-direction: row; - } - .navbar-brand { + .navbar-dropdown-list { + background: #FFFFFF; + box-shadow: 5px 5px 5px 0 rgba(50, 50, 50, 0.2); padding: 0; - order: 1; } - .navbar-nav { - order: 2; - margin: 0 2rem; - padding: 0; - display: flex; - align-items: center; + + li.separator { + margin: 0; + /* TODO fix this */ + border-bottom: 1px solid black; } - .navbar-toggler { - order: 3; + + .navbar-dropdown-list-item { + background: #FFF; + display: block; + list-style: none; + white-space: nowrap; } - ul[role="menu"] { - display: none; - position: absolute; - padding: 0; - background: #FFFFFF; - box-shadow: 5px 5px 5px 0 rgba(50,50,50,0.2); + + .navbar-dropdown-list-item-link { + color: var(--color-ember-orange); + display: block; + padding: 1rem; + text-decoration: none; + } + + .navbar-dropdown-list-item-link:hover, + .navbar-dropdown-list-item-link:focus { + background: var(--color-ember-orange); + color: var(--color-light); } -} \ No newline at end of file + + + /* TODO This rule doesn't make sense so I'm commenting it out for now +ul[role="menubar"][aria-expanded="true"] li { + background: blue; +} */ + + @media (min-width: 992px) { + nav.es-navbar { + display: flex; + flex-direction: row; + padding: 0; + } + + .navbar-brand { + padding: 0; + /* TODO using order is not ideal for accessibility */ + order: 1; + } + + .navbar-list { + /* TODO using order is not ideal for accessibility */ + order: 2; + align-items: center; + display: flex; + margin: 0 1rem; + padding: 0; + } + + .navbar-toggler { + display: none; /* TODO remove this properly or else screen readers will still pick up on it */ + } + + .navbar-dropdown-list { + background: #FFFFFF; + box-shadow: 5px 5px 5px 0 rgba(50, 50, 50, 0.2); + padding: 0; + position: absolute; + z-index: 100; + } + } \ No newline at end of file diff --git a/addon/templates/components/es-navbar.hbs b/addon/templates/components/es-navbar.hbs index 223158b5..c752a6e0 100644 --- a/addon/templates/components/es-navbar.hbs +++ b/addon/templates/components/es-navbar.hbs @@ -1,47 +1,12 @@ -