Skip to content

Instantly share code, notes, and snippets.

@kimili
Last active April 14, 2021 17:14
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 kimili/2aa0fa07d8504702d7a0ce9d29f69236 to your computer and use it in GitHub Desktop.
Save kimili/2aa0fa07d8504702d7a0ce9d29f69236 to your computer and use it in GitHub Desktop.
Web Component for a toggle section: an expandable collapsible block with a heading.
class ToggleSection extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.render();
this.btn.addEventListener('click', () => {
this.setAttribute('open', this.getAttribute('open') === 'true' ? 'false' : 'true');
})
}
attributeChangedCallback(name) {
if (name === 'open') {
this.switchState()
}
}
render() {
const tmpl = document.createElement('template');
tmpl.innerHTML = getTemplateHTML();
this.setAttribute('role', 'region');
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(tmpl.content.cloneNode(true));
this.btn = this.shadowRoot.querySelector('h3 button');
const oldHeading = this.querySelector(':first-child');
let level = parseInt(oldHeading.tagName.substr(1));
this.heading = this.shadowRoot.querySelector('h3')
if (!level) {
console.warn('The first element inside each <toggle-section> should be a heading of an appropriate level.')
}
if (level && level !== 3) {
this.heading.setAttribute('aria-level', level)
}
this.btn.innerHTML = `${this.btn.innerHTML} <span>${oldHeading.textContent}</span>`;
oldHeading.parentNode.removeChild(oldHeading);
}
// The main state switching function
switchState() {
let expanded = this.getAttribute('open') === 'true' || false;
this.btn.setAttribute('aria-expanded', expanded);
this.shadowRoot.querySelector('.content').hidden = !expanded;
}
static get observedAttributes() {
return ['open']
}
};
const getTemplateHTML = () => {
const templateHTML = `
<h3>
<button aria-expanded="false">
<svg aria-hidden="true" focusable="false" viewBox="0 0 10 10">
<rect class="vert" height="8" width="2" y="1" x="4"/>
<rect height="2" width="8" y="4" x="1"/>
</svg>
</button>
</h3>
<div class="content" hidden>
<slot></slot>
</div>
<style>
h3 {
margin: 0;
font-family: var(--heading-font-family);
font-size: var(--heading-font-size);
}
h3 button {
all: inherit;
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
padding: 1em 0;
cursor: pointer;
}
h3 button span {
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
h3 button:focus span {
border-color: var(--primary-color);
}
button svg {
height: 1rem;
margin-right: 0.5em;
flex-shrink: 0;
}
[aria-expanded="true"] .vert {
display: none;
}
[aria-expanded] rect {
fill: currentColor;
}
div.content {
padding-bottom: 1em;
}
</style>`
return templateHTML;
};
const ToggleSectionComponent = () => {
if (!'content' in document.createElement('template') || !document.head.attachShadow) {
return;
}
window.customElements.define('toggle-section', ToggleSection);
};
export default ToggleSectionComponent;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment