-
Notifications
You must be signed in to change notification settings - Fork 104
/
details.polyfill.js
240 lines (204 loc) · 8.28 KB
/
details.polyfill.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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
// <details> polyfill
// http://caniuse.com/#feat=details
// FF Support for HTML5's <details> and <summary>
// https://bugzilla.mozilla.org/show_bug.cgi?id=591737
// http://www.sitepoint.com/fixing-the-details-element/
;(function (global) {
'use strict'
var GOVUK = global.GOVUK || {}
GOVUK.details = {
NATIVE_DETAILS: typeof document.createElement('details').open === 'boolean',
KEY_ENTER: 13,
KEY_SPACE: 32,
// Create a started flag so we can prevent the initialisation
// function firing from both DOMContentLoaded and window.onload
started: false,
// Add event construct for modern browsers or IE
// which fires the callback with a pre-converted target reference
addEvent: function (node, type, callback) {
if (node.addEventListener) {
node.addEventListener(type, function (e) {
callback(e, e.target)
}, false)
} else if (node.attachEvent) {
node.attachEvent('on' + type, function (e) {
callback(e, e.srcElement)
})
}
},
removeEvent: function (node, type) {
if (node.removeEventListener) {
node.removeEventListener(type, function (e) {
}, false)
} else if (node.detachEvent) {
node.detachEvent('on' + type, function (e) {
})
}
},
// Cross-browser character code / key pressed
charCode: function (e) {
return (typeof e.which === 'number') ? e.which : e.keyCode
},
// Cross-browser preventing default action
preventDefault: function (e) {
if (e.preventDefault) {
e.preventDefault()
} else {
e.returnValue = false
}
},
// Handle cross-modal click events
addClickEvent: function (node, callback) {
GOVUK.details.addEvent(node, 'keypress', function (e, target) {
// When the key gets pressed - check if it is enter or space
if (GOVUK.details.charCode(e) === GOVUK.details.KEY_ENTER || GOVUK.details.charCode(e) === GOVUK.details.KEY_SPACE) {
if (target.nodeName.toLowerCase() === 'summary') {
// Prevent space from scrolling the page
// and enter from submitting a form
GOVUK.details.preventDefault(e)
// Click to let the click event do all the necessary action
if (target.click) {
target.click()
} else {
// except Safari 5.1 and under don't support .click() here
callback(e, target)
}
}
}
})
// Prevent keyup to prevent clicking twice in Firefox when using space key
GOVUK.details.addEvent(node, 'keyup', function (e, target) {
if (GOVUK.details.charCode(e) === GOVUK.details.KEY_SPACE) {
if (target.nodeName === 'SUMMARY') {
GOVUK.details.preventDefault(e)
}
}
})
GOVUK.details.addEvent(node, 'click', function (e, target) {
callback(e, target)
})
},
// Get the nearest ancestor element of a node that matches a given tag name
getAncestor: function (node, match) {
do {
if (!node || node.nodeName.toLowerCase() === match) {
break
}
node = node.parentNode
} while (node)
return node
},
// Initialisation function
addDetailsPolyfill: function (list, container) {
container = container || document.body
// If this has already happened, just return
// else set the flag so it doesn't happen again
if (GOVUK.details.started) {
return
}
GOVUK.details.started = true
// Get the collection of details elements, but if that's empty
// then we don't need to bother with the rest of the scripting
if ((list = container.getElementsByTagName('details')).length === 0) {
return
}
// else iterate through them to apply their initial state
var n = list.length
var i = 0
for (i; i < n; i++) {
var details = list[i]
// Save shortcuts to the inner summary and content elements
details.__summary = details.getElementsByTagName('summary').item(0)
details.__content = details.getElementsByTagName('div').item(0)
if (!details.__summary || !details.__content) {
return
}
// If the content doesn't have an ID, assign it one now
// which we'll need for the summary's aria-controls assignment
if (!details.__content.id) {
details.__content.id = 'details-content-' + i
}
// Add ARIA role="group" to details
details.setAttribute('role', 'group')
// Add role=button to summary
details.__summary.setAttribute('role', 'button')
// Add aria-controls
details.__summary.setAttribute('aria-controls', details.__content.id)
// Set tabIndex so the summary is keyboard accessible for non-native elements
// http://www.saliences.com/browserBugs/tabIndex.html
if (!GOVUK.details.NATIVE_DETAILS) {
details.__summary.tabIndex = 0
}
// Detect initial open state
var openAttr = details.getAttribute('open') !== null
if (openAttr === true) {
details.__summary.setAttribute('aria-expanded', 'true')
details.__content.setAttribute('aria-hidden', 'false')
} else {
details.__summary.setAttribute('aria-expanded', 'false')
details.__content.setAttribute('aria-hidden', 'true')
if (!GOVUK.details.NATIVE_DETAILS) {
details.__content.style.display = 'none'
}
}
// Create a circular reference from the summary back to its
// parent details element, for convenience in the click handler
details.__summary.__details = details
// If this is not a native implementation, create an arrow
// inside the summary
if (!GOVUK.details.NATIVE_DETAILS) {
var twisty = document.createElement('i')
if (openAttr === true) {
twisty.className = 'arrow arrow-open'
twisty.appendChild(document.createTextNode('\u25bc'))
} else {
twisty.className = 'arrow arrow-closed'
twisty.appendChild(document.createTextNode('\u25ba'))
}
details.__summary.__twisty = details.__summary.insertBefore(twisty, details.__summary.firstChild)
details.__summary.__twisty.setAttribute('aria-hidden', 'true')
}
}
// Bind a click event to handle summary elements
GOVUK.details.addClickEvent(container, function (e, summary) {
if (!(summary = GOVUK.details.getAncestor(summary, 'summary'))) {
return true
}
return GOVUK.details.statechange(summary)
})
},
// Define a statechange function that updates aria-expanded and style.display
// Also update the arrow position
statechange: function (summary) {
var expanded = summary.__details.__summary.getAttribute('aria-expanded') === 'true'
var hidden = summary.__details.__content.getAttribute('aria-hidden') === 'true'
summary.__details.__summary.setAttribute('aria-expanded', (expanded ? 'false' : 'true'))
summary.__details.__content.setAttribute('aria-hidden', (hidden ? 'false' : 'true'))
if (!GOVUK.details.NATIVE_DETAILS) {
summary.__details.__content.style.display = (expanded ? 'none' : '')
var hasOpenAttr = summary.__details.getAttribute('open') !== null
if (!hasOpenAttr) {
summary.__details.setAttribute('open', 'open')
} else {
summary.__details.removeAttribute('open')
}
}
if (summary.__twisty) {
summary.__twisty.firstChild.nodeValue = (expanded ? '\u25ba' : '\u25bc')
summary.__twisty.setAttribute('class', (expanded ? 'arrow arrow-closed' : 'arrow arrow-open'))
}
return true
},
destroy: function (node) {
GOVUK.details.removeEvent(node, 'click')
},
// Bind two load events for modern and older browsers
// If the first one fires it will set a flag to block the second one
// but if it's not supported then the second one will fire
init: function ($container) {
GOVUK.details.addEvent(document, 'DOMContentLoaded', GOVUK.details.addDetailsPolyfill)
GOVUK.details.addEvent(window, 'load', GOVUK.details.addDetailsPolyfill)
}
}
global.GOVUK = GOVUK
})(window)