-
-
Save cferdinandi/387c9b7db4997248f674040fe526fbe0 to your computer and use it in GitHub Desktop.
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
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Toggle Tabs</title> | |
<style type="text/css"> | |
body { | |
margin: 1em auto; | |
max-width: 30em; | |
width: 88%; | |
} | |
toggle-tabs { | |
/* The navigation list */ | |
--toggle-tab-list-margin: 0 0 2em; | |
/* Toggle nav links */ | |
--toggle-tab-link-color: currentColor; | |
--toggle-tab-link-margin: 0 0 0.25em; | |
--toggle-tab-link-padding: 0.5em 1em; | |
/* Toggle nav link :hover */ | |
--toggle-tab-list-hover-bg-color: #f7f7f7; | |
/* The active toggle nav link */ | |
--toggle-tab-link-active-bg-color: #e5e5e5; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Toggle Tabs</h1> | |
<toggle-tabs> | |
<ul tabs> | |
<li><a href="#wizard">Wizard</a></li> | |
<li><a href="#sorcerer">Sorcerer</a></li> | |
<li><a href="#druid">Druid</a></li> | |
</ul> | |
<div id="wizard"> | |
Wizards gain their magic through study... | |
</div> | |
<div id="sorcerer"> | |
Sorcerers get their power from an otherworldly being... | |
</div> | |
<div id="druid"> | |
Druids get their power from nature. | |
</div> | |
</toggle-tabs> | |
<script> | |
customElements.define('toggle-tabs', class extends HTMLElement { | |
/** | |
* Instantiate the Web Component | |
*/ | |
constructor () { | |
// Get parent class properties | |
super(); | |
// Define properties | |
this.tabList = this.querySelector('[tabs]'); | |
// Setup UI | |
this.setupDOM(); | |
this.loadCSS(); | |
this.clickHandler = this.createClickHandler(); | |
this.keyHandler = this.createKeyHandler(); | |
} | |
/** | |
* Add buttons and hide content on page load | |
*/ | |
setupDOM () { | |
// Only run if there are tabs | |
if (!this.tabList) return; | |
// Get the list items and links | |
let listItems = this.tabList.querySelectorAll('li'); | |
let links = this.tabList.querySelectorAll('a'); | |
// Add ARIA to list | |
this.tabList.setAttribute('role', 'tablist'); | |
// Add ARIA to the list items | |
for (let item of listItems) { | |
item.setAttribute('role', 'presentation'); | |
} | |
// Add ARIA to the links and content | |
let instance = this; | |
links.forEach(function (link, index) { | |
// Get the the target element | |
let tabPane = instance.querySelector(link.hash); | |
if (!tabPane) return; | |
// Add [role] and [aria-selected] attributes | |
link.setAttribute('role', 'tab'); | |
link.setAttribute('aria-selected', index === 0 ? true : false); | |
// If it's not the active (first) tab, remove focus | |
if (index > 0) { | |
link.setAttribute('tabindex', -1); | |
} | |
// If there's no ID, add one | |
if (!link.id) { | |
link.id = `tab_${tabPane.id}`; | |
} | |
// Add ARIA to tab pane | |
tabPane.setAttribute('role', 'tabpanel'); | |
tabPane.setAttribute('aria-labelledby', link.id); | |
// If not the active pane, hide it | |
if (index > 0) { | |
tabPane.setAttribute('hidden', ''); | |
} | |
}); | |
} | |
/** | |
* Load accordion styles | |
*/ | |
loadCSS () { | |
if (document.querySelector('[data-toggle-tabs-css]')) return; | |
let css = document.createElement('style'); | |
css.innerHTML = | |
`toggle-tabs [role="tablist"] { | |
list-style: none; | |
margin: var(--toggle-tab-list-margin, 0 0 2em); | |
padding: 0; | |
} | |
toggle-tabs [role="tablist"] li { | |
display: inline-block; | |
} | |
toggle-tabs [role="tab"] { | |
color: var(--toggle-tab-link-color, currentColor); | |
margin: var(--toggle-tab-link-margin, 0 0 0.25em); | |
padding: var(--toggle-tab-link-padding, 0.5em 1em); | |
text-decoration: none; | |
} | |
toggle-tabs [role="tab"]:active, | |
toggle-tabs [role="tab"]:hover { | |
background-color: var(--toggle-tab-list-hover-bg-color, #f7f7f7); | |
} | |
toggle-tabs [role="tab"][aria-selected="true"] { | |
background-color: var(--toggle-tab-link-active-bg-color, #e5e5e5); | |
}`; | |
css.setAttribute('data-toggle-tabs-css', ''); | |
document.head.append(css); | |
} | |
/** | |
* Create the event handler | |
*/ | |
createClickHandler () { | |
return (event) => { | |
// Only run on tab links | |
if (!event.target.matches('[role="tab"]')) return; | |
// Prevent the link from updating the URL | |
event.preventDefault(); | |
// Ignore the currently active tab | |
if (event.target.matches('[aria-selected="true"]')) return; | |
// Toggle tab visibility | |
this.toggle(event.target); | |
}; | |
} | |
createKeyHandler () { | |
return (event) => { | |
// Only run for left and right arrow keys | |
if (!['ArrowLeft', 'ArrowRight'].includes(event.code)) return; | |
// Only run if element in focus is on a tab | |
let tab = document.activeElement.closest('[role="tab"]'); | |
if (!tab) return; | |
// Only run if focused tab is in this component | |
if (!this.tabList.contains(tab)) return; | |
// Get the currently active tab | |
let currentTab = this.tabList.querySelector('[role="tab"][aria-selected="true"]'); | |
// Get the parent list item | |
let listItem = currentTab.closest('li'); | |
// If right arrow, get the next sibling | |
// Otherwise, get the previous | |
let nextListItem = event.code === 'ArrowRight' ? listItem.nextElementSibling : listItem.previousElementSibling; | |
if (!nextListItem) return; | |
let nextTab = nextListItem.querySelector('a'); | |
// Toggle tab visibility | |
this.toggle(nextTab); | |
nextTab.focus(); | |
}; | |
} | |
/** | |
* Toggle tab visibility | |
* @param {Node} tab The tab to show | |
*/ | |
toggle (tab) { | |
// Get the target tab pane | |
let tabPane = this.querySelector(tab.hash); | |
if (!tabPane) return; | |
// Get the current tab and content | |
let currentTab = tab.closest('[role="tablist"]').querySelector('[aria-selected="true"]'); | |
let currentPane = document.querySelector(currentTab.hash); | |
// Update the selected tab | |
tab.setAttribute('aria-selected', true); | |
currentTab.setAttribute('aria-selected', false); | |
// Update the visible tabPane | |
tabPane.removeAttribute('hidden'); | |
currentPane.setAttribute('hidden', ''); | |
// Make sure current tab can be focused and other tabs cannot | |
tab.removeAttribute('tabindex'); | |
currentTab.setAttribute('tabindex', -1); | |
} | |
/** | |
* Start listening to clicks | |
*/ | |
connectedCallback () { | |
if (!this.tabList) return; | |
this.tabList.addEventListener('click', this.clickHandler); | |
document.addEventListener('keydown', this.keyHandler); | |
} | |
/** | |
* Stop listening to clicks | |
*/ | |
disconnectedCallback () { | |
this.tabList.removeEventListener('click', this.clickHandler); | |
document.removeEventListener('keydown', this.keyHandler); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
@adamjohnson I didn't implement it myself because it's optional and would ramp up the complexity of the code quite a bit. But as a boilerplate, I highly encourage folks to add things like this if they want to. These tools are designed to be starting points.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
What are your thoughts about adding support for Home and End functionality?
Reference: ARIA Authoring Practices Guidelines - Tabs