Smooth Accordion With Details Disclosure Element

Category: Accordion , Javascript , Recommended | January 4, 2019
Author:Giana
Views Total:6,307 views
Official Page:Go to website
Last Update:January 4, 2019
License:MIT

Preview:

Smooth Accordion With Details Disclosure Element

Description:

Create a smoothly collapsible accordion using JavaScript, CSS3 animations, and <details> and <summary> elements.

How to use it:

Code the accordion.

<div class="container collapse">
  <details>
    <summary>Accordion 1</summary>
    <div class="details-wrapper">
      <div class="details-styling">        
        Details 1
      </div>
    </div>
  </details>
  <details>
    <summary>Accordion 2</summary>
    <div class="details-wrapper">
      <div class="details-styling">        
        Details 2
      </div>
    </div>
  </details>
  <details>
    <summary>Accordion 3</summary>
    <div class="details-wrapper">
      <div class="details-styling">        
        Details 3
      </div>
    </div>
  </details>
</div>

The necessary CSS & CSS3 styles for the accordion.

/*
  Please wrap your collapsible content in a single element or so help me
  Must add a transition or it breaks because that's the whole purpose of this
  Only one transition-duration works (see explanation on line #141 in JS)
  You can add more to an inner wrapper (.details-styling)
*/
.collapse-init summary + * {
  transition: all 0.25s ease-in-out;
  overflow: hidden;
}
/*
  Closed state. Any CSS transitions work here
  The JS has a height calculation to make sliding opened/closed easier, but it's not necessary
  Remove the height prop for a simple toggle on/off (after all that work I did for you?)
*/
.collapse-init :not(.panel-active) summary + * {
  height: 0;
  opacity: 0;
  -webkit-transform: scale(0.9);
          transform: scale(0.9);
  -webkit-transform-origin: bottom center;
          transform-origin: bottom center;
}
.collapse-init summary {
  list-style: none;
}
.collapse-init summary::-webkit-details-marker {
  display: none;
}
.collapse-init summary::before {
  display: none;
}
.collapse-init summary {
  cursor: pointer;
}
etails {
  background: #fff;
  border: 1px solid #d6d1e0;
  border-bottom: 0;
  list-style: none;
}
details:first-child {
  border-radius: 6px 6px 0 0;
}
details:last-child {
  border-bottom: 1px solid #d6d1e0;
  border-radius: 0 0 6px 6px;
}
summary {
  display: block;
  transition: 0.2s;
  font-weight: 700;
  padding: 1em;
}
summary:focus {
  outline: 2px solid #5b13ec;
}
.collapse-init summary::after {
  border-right: 2px solid;
  border-bottom: 2px solid;
  content: '';
  float: right;
  width: 0.5em;
  height: 0.5em;
  margin-top: 0.25em;
  -webkit-transform: rotate(45deg);
          transform: rotate(45deg);
  transition: inherit;
}
[open] summary {
  background: #5b13ec;
  color: #f8f5fe;
}
[open] summary::after {
  margin-top: 0.5em;
  -webkit-transform: rotate(225deg);
          transform: rotate(225deg);
}

Load the details-element-polyfill for IE/Edge support.

<script src="https://cdn.jsdelivr.net/gh/javan/details-element-polyfill@master/dist/details-element-polyfill.js"></script>

The primary JavaScript for the accordion.

var _createClass = function () {function defineProperties(target, props) {for (var i = 0; i < props.length; i++) {var descriptor = props[i];descriptor.enumerable = descriptor.enumerable || false;descriptor.configurable = true;if ("value" in descriptor) descriptor.writable = true;Object.defineProperty(target, descriptor.key, descriptor);}}return function (Constructor, protoProps, staticProps) {if (protoProps) defineProperties(Constructor.prototype, protoProps);if (staticProps) defineProperties(Constructor, staticProps);return Constructor;};}();function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError("Cannot call a class as a function");}}miscPolyfillsForIE();
// main function
Collapse = function () {
  function Collapse(container) {var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};_classCallCheck(this, Collapse);
    var defaults = {
      accordion: false,
      initClass: 'collapse-init',
      activeClass: 'panel-active',
      heightClass: 'collapse-reading-height' };

    this.settings = Object.assign({}, defaults, options);
    this._container = container;
    this._panels = container.querySelectorAll("details");
    this.events = {
      openingPanel: new CustomEvent('openingPanel'),
      openedPanel: new CustomEvent('openedPanel'),
      closingPanel: new CustomEvent('closingPanel'),
      closedPanel: new CustomEvent('closedPanel') };
  }
  // Sets height of panel content
  _createClass(Collapse, [{ key: '_setPanelHeight', value: function _setPanelHeight(panel) {
      var contents = panel.querySelector("summary + *");
      contents.style.height = contents.scrollHeight + "px";
    }
    // Removes height of panel content
  }, { key: '_removePanelHeight', value: function _removePanelHeight(panel) {
      var contents = panel.querySelector("summary + *");
      contents.style.height = null;
    }
    //=== Open panel
  }, { key: 'open', value: function open(panel) {
      panel.dispatchEvent(this.events.openingPanel);
      panel.open = true;
    }
    // Add height and active class, this triggers opening animation
  }, { key: '_afterOpen', value: function _afterOpen(panel) {
      this._setPanelHeight(panel);
      panel.classList.add(this.settings.activeClass);
    }
    // Remove height on animation end since it's no longer needed
  }, { key: '_endOpen', value: function _endOpen(panel) {
      panel.dispatchEvent(this.events.openedPanel);
      this._removePanelHeight(panel);
    }
    //=== Close panel, not toggling the actual [open] attr!
  }, { key: 'close', value: function close(panel) {
      panel.dispatchEvent(this.events.closingPanel);
      this._afterClose(panel);
    }
    // Set height, wait a beat, then remove height to trigger closing animation
  }, { key: '_afterClose', value: function _afterClose(panel) {var _this = this;
      this._setPanelHeight(panel);
      setTimeout(function () {
        panel.classList.remove(_this.settings.activeClass);
        _this._removePanelHeight(panel);
      }, 100); //help, this is buggy and hacky
    }
    // Actually closes panel once animation finishes
  }, { key: '_endClose', value: function _endClose(panel) {
      panel.dispatchEvent(this.events.closedPanel);
      panel.open = false;
    }
    //=== Toggles panel... just in case anyone needs this
  }, { key: 'toggle', value: function toggle(panel) {
      panel.open ? this.close(panel) : this.open(panel);
    }
    //=== Accordion closes all panels except the current passed panel 
  }, { key: 'openSinglePanel', value: function openSinglePanel(panel) {var _this2 = this;
      this._panels.forEach(function (element) {
        if (panel == element && !panel.open) {
          _this2.open(element);
        } else {
          _this2.close(element);
        }
      });
    }
    //=== Opens all panels just because
  }, { key: 'openAll', value: function openAll() {var _this3 = this;
      this._panels.forEach(function (element) {
        _this3.open(element);
      });
    }
    //=== Closes all panels just in case
  }, { key: 'closeAll', value: function closeAll() {var _this4 = this;
      this._panels.forEach(function (element) {
        _this4.close(element);
      });
    }
    // Now put it all together
  }, { key: '_attachEvents', value: function _attachEvents() {var _this5 = this;
      this._panels.forEach(function (panel) {
        var toggler = panel.querySelector("summary");
        var contents = panel.querySelector("summary + *");
        // On panel open
        panel.addEventListener("toggle", function (e) {
          var isReadingHeight = panel.classList.contains(_this5.settings.heightClass);
          if (panel.open && !isReadingHeight) {
            _this5._afterOpen(panel);
          }
        });
        toggler.addEventListener("click", function (e) {
          // If accordion, stop default toggle behavior
          if (_this5.settings.accordion) {
            _this5.openSinglePanel(panel);
            e.preventDefault();
          }
          // On attempting close, stop default close behavior to substitute our own
          else if (panel.open) {
              _this5.close(panel);
              e.preventDefault();
            }
          // On open, proceed as normal (see toggle listener above)
        });
        /*
              transitionend fires once for each animated property, 
              but we want it to fire once for each click. 
              So let's make sure to watch only a single property
              Note this makes complex animations with multiple transition-durations impossible
              Sorry
            */
        var propToWatch = '';
        // On panel finishing open/close animation
        contents.addEventListener("transitionend", function (e) {
          // Ignore transitions from child elements
          if (e.target !== contents) {
            return;
          }
          // Set property to watch on first fire
          if (!propToWatch) propToWatch = e.propertyName;
          // If watched property matches currently animating property
          if (e.propertyName == propToWatch) {
            var wasOpened = panel.classList.contains(_this5.settings.activeClass);
            wasOpened ? _this5._endOpen(panel) : _this5._endClose(panel);
          }
        });
      });
    } }, { key: 'init', value: function init()
    {
      // Attach functionality
      this._attachEvents();
      // If accordion, open the first panel
      if (this.settings.accordion) {
        this.openSinglePanel(this._panels[0]);
      }
      // For styling purposes
      this._container.classList.add(this.settings.initClass);
      return this;
    } }]);return Collapse;}();
// initialize the accordion
var makeMePretty = document.querySelector(".collapse");
var accordion = new Collapse(makeMePretty, { accordion: true }).init();

You Might Be Interested In:


Leave a Reply