Skip to content

Instantly share code, notes, and snippets.

@cferdinandi
Last active November 28, 2023 21:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cferdinandi/054ba5806332d5aa2e8f69f4e3689308 to your computer and use it in GitHub Desktop.
Save cferdinandi/054ba5806332d5aa2e8f69f4e3689308 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<title>Accordions</title>
<style type="text/css">
body {
margin: 1em auto;
max-width: 30em;
width: 88%;
}
/*accordion-group {
--accordion-icon-expanded: " 👆";
--accordion-icon-collapsed: " 👇";
}*/
</style>
</head>
<body>
<h1>Accordions</h1>
<accordion-group headings="h2">
<h2>Yo, ho ho!</h2>
<div>Yo, ho ho and a bottle of rum!</div>
<h2>Ahoy, there!</h2>
<div>Ahoy there, matey!</div>
</accordion-group>
<script>
customElements.define('accordion-group', class extends HTMLElement {
/**
* Instantiate the Web Component
*/
constructor () {
// Get parent class properties
super();
// Set properties
this.headings = this.getAttribute('headings');
this.exclusive = this.hasAttribute('exclusive');
// Setup UI
this.setupDOM();
this.loadCSS();
this.handler = this.createHandler();
}
/**
* Add buttons and hide content on page load
*/
setupDOM () {
// Get all accordion headings
let headings = this.querySelectorAll(this.headings);
// Update content
for (let heading of headings) {
// Get the matching content
let content = heading.nextElementSibling;
if (!content) continue;
// Create a button, and copy heading content into it
let btn = document.createElement('button');
btn.innerHTML = heading.innerHTML;
// Wipe the heading content, and replace it with the button
heading.innerHTML = '';
heading.append(btn);
heading.setAttribute('trigger', '');
// Hide the content
content.setAttribute('hidden', '');
// Add ARIA
btn.setAttribute('aria-expanded', false);
}
}
/**
* Load accordion styles
*/
loadCSS () {
if (document.querySelector('[data-accordion-group-css]')) return;
let css = document.createElement('style');
css.innerHTML =
`/**
* Style the accordion buttons to look like headers
*/
accordion-group [trigger] > button {
background: transparent;
border: none;
display: block;
font: inherit;
margin: 0;
padding: 0;
text-align: left;
width: 100%;
}
/**
* Show expand/collapse icons
*/
accordion-group [trigger] > button[aria-expanded="true"]::after {
content: var(--accordion-icon-expanded, " –");
}
accordion-group [trigger] > button[aria-expanded="false"]::after {
content: var(--accordion-icon-collapsed, " +");
}`;
css.setAttribute('data-accordion-group-css', '');
document.head.append(css);
}
/**
* Create the event handler
*/
createHandler () {
return (event) => {
// Only run on accordion triggers
let trigger = event.target.closest('[trigger]');
if (!trigger) return;
let btn = trigger.firstElementChild;
// Get the content associated with the accordion
let content = trigger.nextElementSibling;
if (!content) return;
// If the content is expanded, hide it
// Otherwise, show it
if (btn.getAttribute('aria-expanded') === 'true') {
btn.setAttribute('aria-expanded', false);
content.setAttribute('hidden', '');
} else {
btn.setAttribute('aria-expanded', true);
content.removeAttribute('hidden');
}
// If not exclusive, all set
if (!this.exclusive) return;
// Otherwise, hide all other open accordions
let openAccordions = this.querySelectorAll('[trigger] > [aria-expanded="true"]');
for (let accordion of openAccordions) {
if (accordion === btn) continue;
accordion.setAttribute('aria-expanded', false);
accordion.parentNode.nextElementSibling.setAttribute('hidden', '');
}
};
}
/**
* Start listening to clicks
*/
connectedCallback () {
this.addEventListener('click', this.handler);
}
/**
* Stop listening to clicks
*/
disconnectedCallback () {
this.removeEventListener('click', this.handler);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment