Skip to content

Instantly share code, notes, and snippets.

@magyarn
Last active March 15, 2018 00:50
Show Gist options
  • Save magyarn/32d88d511a8126b49b3bf182566d2732 to your computer and use it in GitHub Desktop.
Save magyarn/32d88d511a8126b49b3bf182566d2732 to your computer and use it in GitHub Desktop.
Code Journal 2: Accessible Dropdown
<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>
// 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;
}
};
}
// 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;
}
<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