<main>
<toggle-section open="false">
<h2>Section 1</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam non lectus sit amet nunc facilisis molestie. Praesent quis libero et mauris facilisis dignissim at sed nisi.</p>
<p>Nullam efficitur porttitor lectus, ac finibus nibh fermentum ac. Phasellus aliquam, nibh non efficitur pharetra, tellus diam posuere lectus, a consequat elit ex nec ligula.</p>
</toggle-section>
<toggle-section open="true">
<h2>Section 2</h2>
<p>Aliquam erat volutpat. Nulla facilisi. Nunc porttitor, elit non eleifend aliquam, est leo scelerisque nibh, nec faucibus odio urna ac nulla.</p>
<p>Maecenas laoreet in metus eget convallis. Vivamus at eleifend felis. Proin non vehicula neque. Etiam eleifend sapien ut nulla malesuada, ac condimentum nisl efficitur.</p>
</toggle-section>
</main>
html {
font-family: Arial, sans-serif;
}
body {
max-width: 40rem;
margin: 0 auto;
padding: 1em;
}
.controls {
text-align: right;
margin-bottom: 1em;
}
.controls li {
display: inline;
}
button {
background: #000;
color: #fff;
border: 0;
font-size: 0.85rem;
border-radius: 0.25rem;
}
/*
Custom elements are inline by default
*/
toggle-section {
display: block;
}
/*
Only applies if script runs and
`role="region"` is added
*/
toggle-section[role="region"] {
border-width: 2px 0;
border-style: solid;
}
toggle-section[role="region"] + toggle-section {
border-top: 0;
}
(function() {
// Check for <template> support
if ('content' in document.createElement('template')) {
const tmpl = document.createElement('template')
// Create the web component's template
// featuring a <slot> for the Light DOM content
tmpl.innerHTML = `
<h2>
<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>
</h2>
<div class="content" hidden>
<slot></slot>
</div>
<style>
h2 {
margin: 0;
}
h2 button {
all: inherit;
box-sizing: border-box;
display: flex;
justify-content: space-between;
width: 100%;
padding: 0.5em 0;
}
h2 button:focus svg {
outline: 2px solid;
}
button svg {
height: 1em;
margin-left: 0.5em;
}
[aria-expanded="true"] .vert {
display: none;
}
[aria-expanded] rect {
fill: currentColor;
}
</style>
`
// Check for latest Shadow DOM syntax support
if (document.head.attachShadow) {
class ToggleSection extends HTMLElement {
constructor() {
super()
// Make the host element a region
this.setAttribute('role', 'region')
// Create a `shadowRoot` and populate from template
this.attachShadow({ mode: 'open' })
this.shadowRoot.appendChild(tmpl.content.cloneNode(true))
// Assign the toggle button
this.btn = this.shadowRoot.querySelector('h2 button')
// Get the first element in Light DOM
// and cast its heading level (which should, but may not, exist)
const oldHeading = this.querySelector(':first-child')
let level = parseInt(oldHeading.tagName.substr(1))
// Get the Shadow DOM <h2>
this.heading = this.shadowRoot.querySelector('h2')
// If there is no level, there is no heading.
// Add a warning.
if (!level) {
console.warn('The first element inside each <toggle-section> should be a heading of an appropriate level.')
}
// If the level is a real integer and not 2
// set `aria-level` accordingly
if (level && level !== 2) {
this.heading.setAttribute('aria-level', level)
}
// Add the Light DOM heading label to the innerHTML of the toggle button
// and remove the now unwanted Light DOM heading
this.btn.innerHTML = oldHeading.textContent + this.btn.innerHTML
oldHeading.parentNode.removeChild(oldHeading)
// The main state switching function
this.switchState = () => {
let expanded = this.getAttribute('open') === 'true' || false
// Toggle `aria-expanded`
this.btn.setAttribute('aria-expanded', expanded)
// Toggle the `.content` element's visibility
this.shadowRoot.querySelector('.content').hidden = !expanded
}
// Change the component's `open` attribute value on click
// (which will, in turn, trigger switchState(), see below)
this.btn.onclick = () => {
this.setAttribute('open', this.getAttribute('open') === 'true' ? 'false' : 'true')
}
}
// Identify just the `open` attribute as an observed attribute
static get observedAttributes() {
return ['open']
}
// When `open` changes value, execute switchState()
attributeChangedCallback(name) {
if (name === 'open') {
this.switchState()
}
}
}
// Add our new custom element to the window for use
window.customElements.define('toggle-section', ToggleSection)
// Define the expand/collapse all template
const buttons = document.createElement('div')
buttons.innerHTML = `
<ul class="controls" aria-label="section controls">
<li><button id="expand">expand all</button></li>
<li><button id="collapse">collapse all</button></li>
</ul>
`
// Get the first `toggle-section` on the page
// and all toggle sections as a node list
const first = document.querySelector('toggle-section')
const all = document.querySelectorAll('toggle-section')
// Insert the button controls before the first <toggle-section>
first.parentNode.insertBefore(buttons, first)
// Place the click on the parent <ul>...
buttons.addEventListener('click', (e) => {
// ...then determine which button was the target
let expand = e.target.id === 'expand' ? true : false
// Iterate over the toggle sections to switch
// each one's state uniformly
Array.prototype.forEach.call(all, (t) => {
t.setAttribute('open', expand)
})
})
}
}
})()
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.