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.



The Gettysburg Address, by Abraham Lincoln

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.

Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure.

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.

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.

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 &mdash; we can not consecrate 
      &mdash; we can not hallow &mdash; 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 &mdash; that from these honored dead we take increased 
      devotion to that cause for which they gave the last full measure of devotion 
      &mdash; that we here highly resolve that these dead shall not have died in vain 
      &mdash; that this nation, under God, shall have a new birth of freedom &mdash;
      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');
	  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');
	      //container.setAttribute('open');
	    } else {
	      summary.setAttribute('aria-expanded', 'false');
	      // container.removeAttribute('open');
	    }
	  }
	
	  summary.addEventListener('click', function () {
	    setTimeout(setOpenStatus);
	  });
	}
	
	function html4Version(container) {
	  var containerTag = container.tagName;
	  var summary = container.querySelector('.deque-expander-summary');
	
	  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) {
	    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);
	}
	
	function activateAllExpanders() {
	  var expanders = document.querySelectorAll('.deque-expander');
	  for (var i = 0; i < expanders.length; i++) {
	    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 bindElementToKeypressValue(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"></li>

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 individaul expander HTML in a <div> or <span> container with class="deque-expander", for styling purposes and to allow the javascript to initialize all the expanders on the page.
    • Add a <div> or <span> container with class="deque-expander-summary". The inner text will be the title of the expander.
    • Create a <div> or <span> container with class="deque-expander-content" for the content that will be exposed when the regions are expanded.