Expand/Collapse
Expand/Collapse
This pattern creates a button that toggles an element as hidden (collapsed) or not hidden (expanded). The element's current state and changes in the element's state are communicated to screen reader users. The HTML 5 elements <details>
and <summary>
provide this kind of functionality natively, but support for those elements is not universal. There is an option to use <details>
and <summary>
for this pattern in browsers that support them, or this pattern can be configured to use only generic <div>
elements.
Turn on a screen reader to experience this example in action.
Initial HTML Markup
<div class="deque-expander" id="gettysburg">
<div class="deque-expander-summary">
<span class="toggle-indicator"></span>
The Gettysburg Address, by Abraham Lincoln
</div>
<div class="deque-expander-content">
<p>Four score and seven years ago our fathers brought forth
on this continent, a new nation, conceived in Liberty, and
dedicated to the proposition that all men are created equal.</p>
<p>Now we are engaged in a great civil war, testing whether that
nation, or any nation so conceived and so dedicated, can long endure.</p>
<p>We are met on a great battle-field of that war. We have come to dedicate
a portion of that field, as a final resting place for those who here gave
their lives that that nation might live. It is altogether fitting and proper
that we should do this.</p>
<p>But, in a larger sense, we can not dedicate — we can not consecrate
— we can not hallow — this ground. The brave men, living and dead,
who struggled here, have consecrated it, far above our poor power to add or
detract. The world will little note, nor long remember what we say here, but
it can never forget what they did here. It is for us the living, rather, to
be dedicated here to the unfinished work which they who fought here have thus
far so nobly advanced. It is rather for us to be here dedicated to the great
task remaining before us — that from these honored dead we take increased
devotion to that cause for which they gave the last full measure of devotion
— that we here highly resolve that these dead shall not have died in vain
— that this nation, under God, shall have a new birth of freedom —
and that government of the people, by the people, for the people, shall not
perish from the earth.</p>
</div>
</div>
JavaScript
Required: The complete JavaScript file (for all patterns in the library): deque-patterns.min.js
Key parts of the JavaScript file
Note: The code below is functional only in the context of the complete JavaScript file.
In the @expander section:
function createExpander(container) {
var containerTag = container.tagName;
if (containerTag == 'DETAILS') {
if ((0, _isDetailsSupported.isDetailsSupported)() && !(0, _isIOS2.default)()) {
return html5Version(container);
} else {
return html4Version(container);
}
} else {
return html4Version(container);
}
}
function html5Version(container) {
var summary = container.querySelector('.deque-expander-summary');
if (summary.hasAttribute('aria-expanded')) {
return false;
}
summary.setAttribute('tabindex', '0');
summary.setAttribute('aria-expanded', 'false');
container.classList.add('deque-expander');
if (container.hasAttribute('open')) {
summary.setAttribute('aria-expanded', 'true');
container.setAttribute('open');
} else {
summary.setAttribute('aria-expanded', 'false');
container.removeAttribute('open');
}
// it would seem that browsers treat the <summary>
// element as if it were a button, i.e. automagically
// treat space and enter as 'click' events.
summary.setAttribute('role', 'button');
summary.setAttribute('aria-expanded', 'false');
function setOpenStatus() {
if (container.hasAttribute('open')) {
summary.setAttribute('aria-expanded', 'true');
} else {
summary.setAttribute('aria-expanded', 'false');
}
}
summary.addEventListener('click', function () {
setTimeout(setOpenStatus);
});
}
function html4Version(container) {
var containerTag = container.tagName;
var summary = container.querySelector('.deque-expander-summary');
if (summary.hasAttribute('aria-expanded')) {
return false;
}
if (containerTag == 'DETAILS') {
/* convert summary element to div */
var newSummary = document.createElement('div');
var summaryNodes = [],
summaryValues = []; //collects the names and values of the summary attributes into two arrays
for (var att, i = 0, atts = summary.attributes, n = atts.length; i < n; i++) {
att = atts[i];
summaryNodes.push(att.nodeName);
summaryValues.push(att.nodeValue);
}
for (var x = 0; x < summaryNodes.length; x++) {
newSummary.setAttribute(summaryNodes[x], summaryValues[x]); //puts the summary attributes onto the newly created div
}
newSummary.classList.add('deque-expander-summary');
newSummary.innerHTML = summary.innerHTML;
summary.parentNode.replaceChild(newSummary, summary);
/* convert details element to div */
var newContainer = document.createElement('div');
var detailsNodes = [],
detailsValues = []; //collects the names and values of the details attributes into two arrays
for (var attContainer, j = 0, attsContainer = container.attributes, m = attsContainer.length; j < m; j++) {
attContainer = attsContainer[j];
detailsNodes.push(attContainer.nodeName);
detailsValues.push(attContainer.nodeValue);
}
for (var y = 0; y < detailsNodes.length; y++) {
newContainer.setAttribute(detailsNodes[y], detailsValues[y]); //puts the details attributes onto the newly created div
}
newContainer.classList.add('deque-expander');
newContainer.innerHTML = container.innerHTML;
container.parentNode.replaceChild(newContainer, container);
container = newContainer;
summary = container.querySelector('.deque-expander-summary');
}
summary.setAttribute('tabindex', '0');
summary.setAttribute('aria-expanded', 'false');
summary.setAttribute('role', 'button');
var content = container.querySelector('.deque-expander-content');
content.classList.add('deque-hidden');
function toggle(e) {
var ua = window.navigator.userAgent;
var msie = ua.indexOf('Trident/');
var msedge = ua.indexOf('Edge');
if (msie > 0 || msedge > 0) {
window.onkeydown = function (e) {
return !(e.keyCode == 32);
};
}
e.stopPropagation();
e.preventDefault();
content.classList.toggle('deque-hidden');
if (content.classList.contains('deque-hidden')) {
summary.setAttribute('aria-expanded', 'false');
} else {
summary.setAttribute('aria-expanded', 'true');
}
}
(0, _keyboardUtils.onElementEnter)(summary, toggle);
(0, _keyboardUtils.onElementSpace)(summary, toggle);
summary.addEventListener('click', toggle);
summary.addEventListener('keyup', function (e) {
if (e.keyCode == 32) {
//toggle(e);
}
});
}
function activateAllExpanders() {
var expanders = document.querySelectorAll('.deque-expander');
for (var i = 0; i < expanders.length; i++) {
if (expanders[i]) {
if (expanders[i].querySelector('summary')) {
if (!expanders[i].querySelector('summary').hasAttribute('aria-expanded')) {
createExpander(expanders[i]);
}
}
if (expanders[i].querySelector('.deque-expander-summary')) {
if (!expanders[i].querySelector('.deque-expander-summary').hasAttribute('aria-expanded')) {
createExpander(expanders[i]);
}
}
/*
if(!(expanders[i].querySelector('summary').hasAttribute('aria-expanded')) || !(expanders[i].querySelector('.deque-expander-summary').hasAttribute('aria-expanded'))) {
console.log('iam in in');
createExpander(expanders[i]);
}
*/
}
}
}
activateAllExpanders();
In the @expander-isDetailsSupported section:
function isDetailsSupported() {
var el = document.createElement('details');
var diff;
// return early if possible; thanks @aFarkas!
if (!('open' in el)) {
return false;
}
document.body.appendChild(el);
el.innerHTML = '<summary>a</summary>b';
diff = el.offsetHeight;
el.open = true;
diff = diff != el.offsetHeight;
document.body.removeChild(el);
return diff;
}
In the @expander-isIOS section:
exports.default = function () {
return (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
);
};
In the @keyboardUtils section:
var KEYS = exports.KEYS = {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
SHIFT: 16,
CTRL: 17,
ALT: 18,
ESCAPE: 27,
SPACE: 32,
LEFT: 37,
RIGHT: 39,
UP: 38,
DOWN: 40,
F10: 121,
HOME: 36,
END: 35,
PAGE_UP: 33,
PAGE_DOWN: 34
};
function bindElementToEventValue(element, eventName, testValue, handler) {
function localHandler(e) {
if (e.which === testValue) {
handler(e);
}
}
element.addEventListener(eventName, localHandler);
return function () {
element.removeEventListener(eventName, localHandler);
};
}
function bindElementToKeypressValue(element, testValue, handler) {
return bindElementToEventValue(element, 'keypress', testValue, handler);
}
function bindElementToKeydownValue(element, testValue, handler) {
return bindElementToEventValue(element, 'keydown', testValue, handler);
}
function onElementEnter(element, handler) {
return bindElementToKeydownValue(element, KEYS.ENTER, handler);
}
function onElementEscape(element, handler) {
return bindElementToKeydownValue(element, KEYS.ESCAPE, handler);
}
function onElementSpace(element, handler) {
return bindElementToKeypressValue(element, KEYS.SPACE, handler);
}
function onElementLeft(element, handler) {
return bindElementToKeydownValue(element, KEYS.LEFT, handler);
}
function onElementRight(element, handler) {
return bindElementToKeydownValue(element, KEYS.RIGHT, handler);
}
function onElementUp(element, handler) {
return bindElementToKeydownValue(element, KEYS.UP, handler);
}
function onElementDown(element, handler) {
return bindElementToKeydownValue(element, KEYS.DOWN, handler);
}
function onElementHome(element, handler) {
return bindElementToKeydownValue(element, KEYS.HOME, handler);
}
function onElementEnd(element, handler) {
return bindElementToKeydownValue(element, KEYS.END, handler);
}
function onElementPageUp(element, handler) {
return bindElementToKeydownValue(element, KEYS.PAGE_UP, handler);
}
function onElementPageDown(element, handler) {
return bindElementToKeydownValue(element, KEYS.PAGE_DOWN, handler);
}
function onElementF10(element, handler) {
return bindElementToKeydownValue(element, KEYS.F10, handler);
}
function isAlphaNumeric(charCode) {
return charCode >= 48 && charCode <= 90 /* numbers, uppercase letters */
|| charCode >= 97 && charCode <= 122 /* lowercase letters */;
}
function onElementCharacter(element, handler) {
function localHandler(e) {
var charCode = e.which;
if (isAlphaNumeric(charCode)) {
handler(e);
}
}
element.addEventListener('keypress', localHandler);
return function () {
element.removeEventListener('keypress', localHandler);
};
}
function trapEnter(element) {
onElementEnter(element, function (e) {
e.stopPropagation();
e.preventDefault();
});
}
Note: No additional JavaScript initialization code is necessary for this pattern. All elements with class="deque-expander"
will be initialized automatically by the external JavaScript file.
CSS
Required: The complete CSS file (for all patterns in the library): deque-patterns.min.css
Key styles within the CSS file (other styles may also be necessary):
.deque-expander .deque-expander-summary {
display: block;
border: 0;
padding: 14px 36px 14px 12px;
color: #000000;
left: 0;
text-align: left;
background: rgba(0, 0, 0, 0.05);
}
.deque-expander .deque-expander-summary::-webkit-details-marker {
display: none;
}
.deque-expander .deque-expander-summary .toggle-indicator:before {
background: none;
content: '\E70D';
font-family: 'mwf-glyphs';
color: #5e5e5e;
float: left;
font-size: 1em;
font-weight: bold;
margin: 0 10px 0 0;
padding: 0;
text-align: center;
width: 20px;
}
.deque-expander .deque-expander-summary:hover {
cursor: default;
outline: 1px solid #990000;
background-color: rgba(0, 0, 0, 0.1);
}
.deque-expander .deque-expander-summary:active {
cursor: default;
background: rgba(0, 0, 0, 0.3);
outline: 1px solid #990000;
}
.deque-expander .deque-expander-summary:focus {
outline: 1px dashed #000000;
background: rgba(0, 0, 0, 0.1);
}
.deque-expander .deque-expander-content {
clear: both;
padding: 14px 36px 14px 12px;
}
.deque-expander .deque-expander-summary[aria-expanded=true] .toggle-indicator:before,
.deque-expander.open .deque-expander-summary[aria-expanded=true] .toggle-indicator:before {
content: '\E70E';
}
Fonts
Note: You will need to edit the src
for font-family:'mwf-glyphs'
in the external CSS file.
Implementation Instructions
Step 1: Add Dependencies
Add deque-patterns.min.css in the <head>
of the document.
<link rel="stylesheet" type="text/css" href="deque-patterns.min.css">
Add a script link to deque-patterns.min.js to the bottom of the page.
<script type="text/javascript" src="deque-patterns.min.js"></script>
Step 2: Add HTML
- Wrap the each individual expander HTML in a
<div>
or<span>
container withclass="deque-expander"
, for styling purposes and to allow the javascript to initialize all the expanders on the page. - Add a
<div>
or<span>
container withclass="deque-expander-summary"
. The inner text will be the title of the expander. - Create a
<div>
or<span>
container withclass="deque-expander-content"
for the content that will be exposed when the regions are expanded.