A Pen by Nathan Magyar on CodePen.
Last active
March 15, 2018 00:50
-
-
Save magyarn/32d88d511a8126b49b3bf182566d2732 to your computer and use it in GitHub Desktop.
Code Journal 2: Accessible Dropdown
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<main> | |
<section class="card-section"> | |
<article class="card"> | |
<div class="table-header"> | |
<p>Title</p> | |
<p>Creator</p> | |
<p>Date Added</p> | |
<div> | |
<p>Actions</p> | |
</div> | |
</div> | |
<ul id="fileList"> | |
<li class="file-item"> | |
<p>HTML Tips and Tricks</p> | |
<p>Nathan Magyar</p> | |
<p>March 1, 2018</p> | |
<div class="actions"> | |
<button | |
class="action-btn" | |
id="actionButton1" | |
aria-haspopup="true" | |
aria-controls="menu1"> | |
<i class="material-icons settings-icon" id="actionIcon">settings</i> | |
</button> | |
<ul | |
class="action-menu hidden" | |
id="action-list" | |
role="menu" | |
aria-labelledby="actionButton1"> | |
<li role="menuitem"><button>Edit</button></li> | |
<li role="menuitem"><button>Copy</button></li> | |
<li role="menuitem"><button>Delete</button></li> | |
</ul> | |
</li> | |
<li class="file-item"> | |
<p>Successful Sass</p> | |
<p>Nathan Magyar</p> | |
<p>March 2, 2018</p> | |
<div class="actions"> | |
<button | |
class="action-btn" | |
id="actionButton1" | |
aria-haspopup="true" | |
aria-controls="menu1"> | |
<i class="material-icons settings-icon" id="actionIcon">settings</i> | |
</button> | |
<ul | |
class="action-menu hidden" | |
id="action-list" | |
role="menu" | |
aria-labelledby="actionButton1"> | |
<li role="menuitem"><button>Edit</button></li> | |
<li role="menuitem"><button>Copy</button></li> | |
<li role="menuitem"><button>Delete</button></li> | |
</ul> | |
</li> | |
<li class="file-item"> | |
<p>Jazzy JavaScript</p> | |
<p>Nathan Magyar</p> | |
<p>March 2, 2018</p> | |
<div class="actions"> | |
<button | |
class="action-btn" | |
id="actionButton1" | |
aria-haspopup="true" | |
aria-controls="menu1"> | |
<i class="material-icons settings-icon" id="actionIcon">settings</i> | |
</button> | |
<ul | |
class="action-menu hidden" | |
id="action-list" | |
role="menu" | |
aria-labelledby="actionButton1"> | |
<li role="menuitem"><button>Edit</button></li> | |
<li role="menuitem"><button>Copy</button></li> | |
<li role="menuitem"><button>Delete</button></li> | |
</ul> | |
</li> | |
<li class="file-item"> | |
<p>Vuetiful Vue Components</p> | |
<p>Nathan Magyar</p> | |
<p>March 2, 2018</p> | |
<div class="actions"> | |
<button | |
class="action-btn" | |
id="actionButton1" | |
aria-haspopup="true" | |
aria-controls="menu1"> | |
<i class="material-icons settings-icon" id="actionIcon">settings</i> | |
</button> | |
<ul | |
class="action-menu hidden" | |
id="action-list" | |
role="menu" | |
aria-labelledby="actionButton1"> | |
<li role="menuitem"><button>Edit</button></li> | |
<li role="menuitem"><button>Copy</button></li> | |
<li role="menuitem"><button>Delete</button></li> | |
</ul> | |
</li> | |
</ul> | |
</article> | |
</section> | |
<article class="notes"> | |
<h2 class="centered-title">Takeaways:</h2> | |
<ol class="takeaways-list"> | |
<li> | |
Keep event listeners out of the <code>html</code> when possible. Adding them in the same Javascript file where they are defined makes long-term maintenance easier, as you don't have to go hunting for the function calls in the <code>html</code> if you end up changing the function name. | |
</li> | |
<li>There's an important difference between <code>.firstChild</code> and <code>.firstElementChild</code>. The latter helps you get around additional unnecessary indexing in a <code>DOM node</code>. | |
</li> | |
<li> | |
If you want to use arrow keys to navigate a page element, it's useful to call <code>e.preventDefault()</code> to prevent the page from scrolling as it normally would with such a keyboard input. | |
</li> | |
<li>For next time, I'd like to work on improving my separation of concerns. That is, using classes strictly for visual styling purposes and id's for Javascript / behavioral adjustments of the DOM. That way people in the future, including myself, can alter the code without worrying if they're unintentionally breaking some JS or destroying a layout with their changes.</li> | |
</ol> | |
</article> | |
</main> | |
<footer> | |
<p>Made with Vanilla JS 🍦</p> | |
</footer> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Get all action buttons on the page | |
const actionBtns = document.querySelectorAll(".action-btn"); | |
// Add the openMenu event listener to each of the action buttons | |
for (let i = 0; i < actionBtns.length; i++) { | |
actionBtns[i].addEventListener("click", openMenu); | |
} | |
// Select and add necessary classes to make the actionMenu visible | |
// And animate the settings icon when the menu opens | |
// Make the menu keyboard accessible with the keyboardNav function | |
// Add an event listener to the body to close the menu when a user clicks outside of it | |
function openMenu(e) { | |
e.stopPropagation(); | |
const actionBtn = this; | |
const actionMenu = this.nextElementSibling; | |
const actionIcon = this.firstElementChild; | |
const openMenu = document.querySelector(".action-menu:not(.hidden)") | |
if (openMenu && openMenu.previousElementSibling !==actionBtn) {openMenu.classList.add("hidden")}; | |
actionMenu.classList.toggle("hidden"); | |
actionIcon.classList.toggle("open-menu"); | |
keyboardNav(actionMenu); | |
document.body.addEventListener("click", closeMenu); | |
} | |
// If the event target isn't the action button, a child of action-menu, or the settings icon | |
// Hide the menu that doesn't have a 'hidden' class and remove the body's event listener | |
function closeMenu(e) { | |
if ( | |
!e.target.parentElement.classList.contains("action-menu") | |
) { | |
document.querySelector(".action-menu:not(.hidden)").classList.add("hidden"); | |
document.body.removeEventListener("click", closeMenu); | |
} | |
}; | |
// Make keycodes more readable for reference in the keyboardNav function | |
const keyCodes = { | |
up: 38, | |
down: 40, | |
escape: 27, | |
tab: 9 | |
}; | |
// Enable the action menu to be navigable by arrow keys and tabbing | |
// If the user presses the escape key, hide/close the menu | |
function keyboardNav(actionMenu) { | |
const actionBtn = this.document.activeElement; | |
const actionList = actionMenu; | |
const firstAction = actionList.firstElementChild; | |
const lastAction = actionList.lastElementChild; | |
document.onkeydown = function(e) { | |
switch (e.keyCode) { | |
case keyCodes.up: | |
e.preventDefault(); | |
if (document.activeElement == actionBtn) { | |
document.activeElement.nextElementSibling.lastElementChild.firstElementChild.focus(); | |
} else if (document.activeElement == firstAction.firstElementChild) { | |
document.activeElement.parentNode.parentNode.lastElementChild.firstElementChild.focus(); | |
} else { | |
document.activeElement.parentNode.previousElementSibling.firstElementChild.focus(); | |
} | |
break; | |
case keyCodes.down: | |
e.preventDefault(); | |
if (document.activeElement == actionBtn) { | |
firstAction.firstElementChild.focus(); | |
} else if (document.activeElement == lastAction.firstElementChild) { | |
document.activeElement.parentNode.parentNode.firstElementChild.firstElementChild.focus(); | |
} else { | |
document.activeElement.parentNode.nextElementSibling.firstElementChild.focus(); | |
} | |
break; | |
case keyCodes.tab: | |
if ( | |
document.activeElement == actionBtn && | |
!actionList.classList.contains("hidden") | |
) { | |
e.preventDefault(); | |
document.activeElement.nextElementSibling.firstElementChild.firstElementChild.focus(); | |
} else if (document.activeElement == firstAction.firstElementChild) { | |
e.preventDefault(); | |
document.activeElement.parentNode.nextElementSibling.firstElementChild.focus(); | |
} else if ( | |
document.activeElement == | |
actionList.firstElementChild.nextElementSibling | |
) { | |
document.activeElement.parentNode.nextElementSibling.firstElementChild.focus(); | |
} else if (document.activeElement == lastAction.firstElementChild) { | |
e.preventDefault(); | |
document.activeElement.parentNode.parentNode.firstElementChild.firstElementChild.focus(); | |
} | |
break; | |
case keyCodes.escape: | |
actionList.classList.add("hidden"); | |
actionBtn.focus(); | |
break; | |
} | |
}; | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Variables | |
$black: #000; | |
$blue: #39A0ED; | |
$white: #FFF; | |
$green: #5DB856; | |
$grey: #999; | |
$grey-light: #F0F0F0; | |
$grey-dark: #C4C4C4; | |
$yellow: #F4D142; | |
$red: #F45042; | |
$red-light: #FFDAD7; | |
$red-dark: #CD4539; | |
$radius: 5px; | |
$radius2: 25px; | |
$shadow: 0px 5px 20px 10px rgba(0, 0, 0, .2); | |
$shadowB: 0px 5px 10px 5px rgba(0, 0, 0, .2); | |
// Media Queries | |
$mobile: "screen and (max-width: 375px)"; | |
$tablet: "screen and (min-width: 375.1px) and (max-width: 750px)"; | |
// Styles | |
body { | |
box-sizing: border-box; | |
margin: 0; | |
width: 100%; | |
height: 100%; | |
} | |
main { | |
padding: 0; | |
margin: 0; | |
} | |
ul { | |
padding-left: 0; | |
list-style: none; | |
} | |
.card-section { | |
padding: 2rem; | |
background-color: $red; | |
min-height: 200px; | |
display: flex; | |
flex-wrap: wrap; | |
justify-content: center; | |
} | |
.card { | |
font-family: 'Raleway', sans-serif; | |
margin: 0 auto; | |
width: 800px; | |
text-align: center; | |
background-color: $white; | |
border-radius: $radius; | |
margin: 0 1rem; | |
box-shadow: $shadow; | |
@media #{$tablet}, #{$mobile} { | |
margin: 1rem; | |
} | |
#fileList { | |
margin: 0; | |
} | |
.file-item, .table-header { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
padding: .25rem 1rem; | |
p { | |
width: 30%; | |
text-align: left; | |
margin: 0; | |
} | |
div { | |
width: 10%; | |
@media #{$tablet}, #{$mobile} { | |
width: 20%; | |
} | |
} | |
@media #{$tablet} { | |
p { | |
width: 40%; | |
} | |
p:nth-of-type(2) { | |
padding: 0 1em; | |
} | |
p:nth-of-type(3) { | |
display: none; | |
} | |
} | |
@media #{$mobile} { | |
p { | |
width: 80%; | |
} | |
p:nth-of-type(2), | |
p:nth-of-type(3) { | |
display: none; | |
} | |
} | |
} | |
.file-item:nth-of-type(even) { | |
background: $red-light; | |
} | |
.file-item:last-of-type { | |
border-radius: 0 0 5px 5px; | |
} | |
.table-header { | |
background-color: $red; | |
color: $white; | |
border-radius: 5px 5px 0 0; | |
padding: 1rem; | |
} | |
} | |
.action-btn { | |
border: none; | |
cursor: pointer; | |
width: 100%; | |
border-radius: 5px; | |
transition: all .2s ease; | |
background: transparent; | |
.settings-icon { | |
transition: all .2s ease; | |
} | |
&:focus { | |
background: $red; | |
color: $white; | |
outline: none | |
} | |
&:hover:not(:focus) { | |
.settings-icon { | |
color: $red; | |
} | |
} | |
} | |
ul.action-menu { | |
position: absolute; | |
padding: 1em; | |
box-shadow: $shadowB; | |
border-radius: $radius; | |
z-index: 1000; | |
width: 100px; | |
height: auto; | |
list-style: none; | |
padding: 0; | |
margin: 0; | |
text-align: left; | |
li { | |
&:first-of-type button { | |
border-radius: 5px 5px 0 0; | |
} | |
&:last-of-type button { | |
border-radius: 0 0 5px 5px; | |
} | |
button { | |
width: 100%; | |
height: 2em; | |
font-size: 1em; | |
background: white; | |
border: none; | |
text-align: left; | |
cursor: pointer; | |
transition: all .2s ease; | |
&:focus, &:hover { | |
background-color: $red-light; | |
outline: none; | |
} | |
} | |
} | |
} | |
// Cog Spinning Styles | |
@keyframes spin { | |
to { | |
transform: rotate(90deg) | |
} | |
} | |
.open-menu { | |
animation: spin .5s ease-out forwards; | |
} | |
// Misc | |
.centered-title { | |
text-align: center; | |
} | |
.hidden { | |
display: none; | |
} | |
// Takeaways Section | |
.notes { | |
font-family: 'Merriweather', serif; | |
font-weight: 300; | |
line-height: 1.6rem; | |
max-width: 800px; | |
margin: 0 auto; | |
padding: 0 1rem; | |
.takeaways-list { | |
list-style-position: inside; | |
padding-left: 0; | |
li { | |
margin-bottom: 1rem; | |
} | |
} | |
code { | |
font-size: 1rem; | |
color: $red; | |
background-color: $grey-light; | |
padding: .1rem .25rem; | |
border-radius: $radius; | |
border: .5px solid $grey-dark; | |
} | |
} | |
footer { | |
font-family: 'Merriweather', serif; | |
font-size: .8rem; | |
text-align: center; | |
padding: 2rem 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/styles/shCoreMidnight.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment