-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtoggle.js
181 lines (152 loc) · 5.73 KB
/
toggle.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
(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: 1em 0;
padding: 1em 1em;
background: var(--cool-grey);
color: #000000;
}
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: #000000;
<!-- currentColor; color of +/- signs -->
}
</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
const oldHeading = this.querySelector(':first-child')
// and cast its heading level (which should, but may not, exist)
let level = parseInt(oldHeading.tagName.substr(1))
// Then take its `id` (may be null)
let id = oldHeading.id
// Get the Shadow DOM <h2>
this.heading = this.shadowRoot.querySelector('h2')
// If `id` exists, apply it
if (id) {
this.heading.id = id
}
// 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 but 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
}
this.btn.onclick = () => {
// Change the component's `open` attribute value on click
let open = this.getAttribute('open') === 'true' || false
this.setAttribute('open', open ? 'false' : 'true')
// Update the hash if the collapsible section's
// heading has an `id` and we are opening, not closing
if (this.heading.id && !open) {
history.pushState(null, null, '#' + this.heading.id)
}
}
}
connectedCallback() {
if (window.location.hash.substr(1) === this.heading.id) {
this.setAttribute('open', 'true')
this.btn.focus()
}
}
// 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)
})
})
}
}
})()