COMPONENT

C-ACCORDION

Demo Section

Each variation will be presented in the following sections.

Simple Accordion

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aliquid assumenda, ducimus facilis inventore iste labore laborum libero nam necessitatibus neque nulla numquam perspiciatis rem, repudiandae sed soluta veniam vero.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aliquid assumenda, ducimus facilis inventore iste labore laborum libero nam necessitatibus neque nulla numquam perspiciatis rem, repudiandae sed soluta veniam vero.

Single Open Accordion with Default Openings

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aliquid assumenda, ducimus facilis inventore iste labore laborum libero nam necessitatibus neque nulla numquam perspiciatis rem, repudiandae sed soluta veniam vero.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aliquid assumenda, ducimus facilis inventore iste labore laborum libero nam necessitatibus neque nulla numquam perspiciatis rem, repudiandae sed soluta veniam vero.

Technical Details

Bower versionGitter Chat

Accordion

Description

The component represents a simple accordion with transitions and max-height.

Accordions are elements used to expand and collapse content that is broken into logical sections, much like tabs.

The accordion is based on the blueprint of Veams-Components and is a wrap-with component to support flexible content with predefined surrounded markup.

The accordion is jQuery-free (we use Veams-Query) and contains some accessiblity functionality.


Requirements

  • Veams >= v5.0.0 - Veams Framework.

Installation

Installation with Veams

veams install vc accordion

Installation with Bower

bower install veams-component-accordion --save


Fields

c-accordion.hbs

Settings

  • settings.accContextClass {String} [default] - Context class of component.
  • settings.accClasses {String} - Modifier classes for component.
  • settings.accJsOptions {Object} - JavaScript options which gets stringified.

c-accordion__item.hbs

Settings

  • settings.accNoWrapper {Boolean} - Hide wrapper div .accordion__item.

Further Parameters

  • accItemId {String} - Id of the accordion item.
  • accButton {String} - Button text for accordion item.

JavaScript Options

The module gives you the possibility to override default options:

  • activeClass {String} [‘is-active’] - Define the active class for active elements.
  • accordionBtn {String} [’[data-js-item=‘accordion-btn’]’] - Define the element for accordion buttons.
  • accordionContent {String} [’[data-js-item=“accordion-content”]’] - Define the element for accordion content items.
  • calculatingClass {String} [‘is-calculating’] - Define the calculating class for the initial calculation cycle.
  • clickHandler {String} [‘click’] - Define a click handler for the buttons.
  • closeClass {String} [‘is-closed’] - Define the closing class for accordion content items.
  • dataMaxAttr {String} [‘data-js-height’] - Define the attribute in which the calculated height is saved.
  • openAllOnInit {Boolean} [false] - If set to true, all panels stays open on render.
  • openByHash {Boolean} [false] - If set to true, panel can be opened by url hash referencing the id of the panel.
  • openClass {Boolean} [‘is-open’] - Define the opening class for accordion content items.
  • openIndex {Number} [null] - Index of panel to be opened on init (zero based).
  • openOnViewports {Array} [ [‘desktop’, ‘tablet-large’, ‘tablet-small’] ] - Viewports on which the openIndex panel is opened on init.
  • singleOpen {Boolean} [false] - If set to true, only one panel can be opened at the same time.
  • tabMode {Boolean} [false] - If set to true, the accordion behaves like a tab module (click on active button will not close corresponding panel).
  • unresolvedClass {String} [‘is-unresolved’] - Define the unresolved class for the whole accordion which will be deleted after initialize() and render() is finished.

Sass Options

There are multiple global variables which you can change:

  • $accordion-toggle-duration [300ms !default] - Speed of toggling.
  • $accordion-transition-method [ease !default] - Transition method of toggle effect.
  • $accordion-icon-color [#666 !default] - + icon color.
  • $accordion-icon-width [30px !default] - + icon width.
  • $accordion-icon-height [2px !default] - + icon height.
  • $accordion-btn-color [$accordion-icon-color !default] - Accordion button color.
  • $accordion-btn-bg-color [rgba(255, 255, 255, 0.5) !default] - Background color of the accordion button.
  • $accordion-padding [1rem !default] - Default padding which will be used in the accordion.
{
	"variations": {
		"simple": {
			"docs": {
				"variationName": "Simple Accordion"
			},
			"settings": {
				"accContextClass": "default",
				"accClasses": false
			},
			"content": {
				"items": [
					{
						"itemId": "test-1",
						"itemBtnText": "Item 1"
					},
					{
						"itemId": "test-2",
						"itemBtnText": "Item 2"
					}
				]
			}
		},
		"singleOpen": {
			"docs": {
				"variationName": "Single Open Accordion with Default Openings"
			},
			"settings": {
				"accContextClass": "default",
				"accClasses": "is-some-modifier",
				"accJsOptions": {
					"openIndex": 0,
					"singleOpen": true
				}
			},
			"content": {
				"items": [
					{
						"itemId": "test-2-1",
						"itemBtnText": "Item 1"
					},
					{
						"itemId": "test-2-2",
						"itemBtnText": "Item 2"
					}
				]
			}
		}
	}
}

c-accordion-usage

{{! WrapWith START: Accordion }}
{{#wrapWith "c-accordion" settings=this.settings}}
{{! WrapWith START: Item }}
	{{#each content.items}}
		{{#wrapWith "c-accordion__item" accItemId=this.itemId accButton=itemBtnText}}
			Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aliquid assumenda, ducimus facilis inventore iste labore laborum libero nam necessitatibus neque nulla numquam perspiciatis rem, repudiandae sed soluta veniam vero.
		{{/wrapWith}}
	{{/each}}
{{! WrapWith END: Item }}
{{/wrapWith}}
{{! WrapWith END: Accordion }}

c-accordion.hbs

<div class="c-accordion{{#if options.settings.accContextClass}}--{{options.settings.accContextClass}}{{else}}--default{{/if}}{{#if options.settings.accClasses}} {{options.settings.accClasses}}{{/if}}"
     data-css="c-accordion"
     data-js-module="accordion"{{#if options.settings.accJsOptions}}
     data-js-options='{{stringify options.settings.accJsOptions}}'{{/if}} role="tablist">
	{{{yield}}}
</div>

c-accordion__item.hbs

{{#unless options.settings.accNoWrapper}}
	<div class="accordion__item">
{{/unless}}
	<div class="accordion__header">
		<a href="#{{options.accItemId}}"
		   class="accordion__btn"
		   data-js-item="accordion-btn" role="tab"
		   aria-controls="{{options.accItemId}}">
			<span class="accordion__btn-inner">{{options.accButton}}</span>
		</a>
	</div>
	<div class="accordion__content"
	     id="{{options.accItemId}}"
	     data-js-item="accordion-content" role="tabpanel"
	     aria-labelledby="{{options.accItemId}}">
		<div class="accordion__content-inner">
			{{{yield}}}
		</div>
	</div>
{{#unless options.settings.accNoWrapper}}
	</div>
{{/unless}}
/* ===================================================
Accordion
=================================================== */

/* ---------------------------------------------------
Global Variables
--------------------------------------------------- */
$accordion-toggle-duration: 300ms !default;
$accordion-transition-method: ease !default;
$accordion-icon-color: #666 !default;
$accordion-icon-width: 30px !default;
$accordion-icon-height: 2px !default;
$accordion-btn-color: $accordion-icon-color !default;
$accordion-btn-bg-color: rgba(255, 255, 255, 0.5) !default;
$accordion-padding: 1rem !default;

/* ---------------------------------------------------
Global Styles
--------------------------------------------------- */
[data-css="c-accordion"] {
	position: relative;

	/*
	Unresolved
	----------------------- */
	&.is-unresolved {
	}

	/*
	Wrapper Item
	----------------------- */
	.accordion__item {
	}

	/*
	Header/ Toggle Button
	----------------------- */
	.accordion__header {
	}
	.accordion__btn {
	}
	.accordion__icon {
	}

	/*
	Content/ Toggle Content
	----------------------- */
	.accordion__content {
	}
	.accordion__content-inner {
	}

	/*
	Modifiers
	----------------------- */
	.is-closed {
		height: 0;
		transition: height $accordion-transition-method $accordion-toggle-duration;
		overflow: hidden;
	}

	.is-open {
		transition: height $accordion-transition-method $accordion-toggle-duration;
		overflow: hidden;
	}

	// !IMPORTANT for calculation
	.is-calculating {
		position: absolute !important;
		visibility: hidden !important;
		display: block !important;
		height: auto !important;
	}
}

/* ---------------------------------------------------
Context: Default
--------------------------------------------------- */
.c-accordion--default {
	/*
	Header/ Toggle Button
	----------------------- */
	.accordion__btn {
		color: $accordion-btn-color;
		display: block;
		font-size: $accordion-padding * 2;
		padding: ($accordion-padding * 2.4) $accordion-padding ($accordion-padding * 2.2) $accordion-padding;
		position: relative;
		text-decoration: none;
		text-transform: uppercase;
		background-color: $accordion-btn-bg-color;
		margin-bottom: 2px;

		&::before,
		&::after {
			@extend %pseudo;

			transition: bottom $accordion-toggle-duration linear $accordion-toggle-duration, transform $accordion-toggle-duration linear;
			background-color: $accordion-icon-color;
			height: $accordion-icon-height;
			right: $accordion-padding;
			top: 50%;
			width: $accordion-icon-width;
		}

		&::after {
			transform: rotate(90deg);
		}

		&.is-active {

			&::after {
				transform: rotate(0deg);
			}
		}
	}

	/*
	Icon
	----------------------- */
	.accordion__icon {
		position: relative;
	}

	/*
	Content
	----------------------- */
	.accordion__content-inner {
		padding: $accordion-padding 0;
	}
}

accordion.js

/**
 * Represents a simple accordion with transitions and max-height.
 *
 * @module Accordion
 * @version v3.0.2
 *
 * @author Sebastian Fitzner
 * @author Andy Gutsche
 */

/**
 * Requirements
 */
import { Veams } from 'app';
import VeamsComponent from 'veams/src/js/common/component';

const $ = Veams.$;
const Helpers = Veams.helpers;

/**
 * Class Accordion
 */
class Accordion extends VeamsComponent {
	constructor(obj) {
		let options = {
			activeClass: 'is-active',
			accordionBtn: '[data-js-item="accordion-btn"]',
			accordionContent: '[data-js-item="accordion-content"]',
			calculatingClass: 'is-calculating',
			clickHandler: 'click',
			closeClass: 'is-closed',
			dataMaxAttr: 'data-js-height',
			openAllOnInit: false,
			openByHash: false,
			openClass: 'is-open',
			openIndex: null,
			openOnViewports: [
				'tablet-small',
				'tablet-large',
				'desktop'
			], // array: viewport names - eg.: ['mobile', 'tablet', 'desktop-small', 'desktop']
			removeStyles: false, // TODO
			singleOpen: false,
			tabMode: false,
			unresolvedClass: 'is-unresolved'
		};

		super(obj, options);
	}

	/** =================================================
	 * GETTER & SETTER
	 * ================================================ */

	/**
	 * Get module information
	 */
	static get info() {
		return {
			name: 'Accordion',
			version: '3.0.2',
			vc: true,
			mod: false // set to true if source was modified in project
		};
	}

	set $accordionContents(items) {
		this._$accordionContents = items;
	}

	get $accordionContents() {
		return this._$accordionContents;
	}

	set $accordionBtns(items) {
		this._$accordionBtns = items;
	}

	get $accordionBtns() {
		return this._$accordionBtns;
	}

	set $target(item) {
		this._$target = item;
	}

	get $target() {
		return this._$target;
	}

	set $btn(item) {
		this._$btn = item;
	}

	get $btn() {
		return this._$btn;
	}

	/** =================================================
	 * EVENTS
	 * ================================================ */
	get events() {
		return {
			'{{this.options.clickHandler}} {{this.options.accordionBtn}}': 'handleClick'
		}
	}

	get subscribe() {
		return {
			'{{Veams.EVENTS.resize}}': 'render',
			'{{Veams.EVENTS.accordion.closeAll}}': 'closeAll',
			'{{Veams.EVENTS.accordion.openAll}}': 'openAll'
		}
	}

	/** =================================================
	 * STANDARD METHODS
	 * ================================================= */

	/**
	 * Init method to save all necessary references.
	 */
	initialize() {
		this.$accordionContents = $(this.options.accordionContent, this.$el);
		this.$accordionBtns = $(this.options.accordionBtn, this.$el);
		this.$target = null;
		this.$btn = null;
		this.openIndex = this.options.openIndex;

		if (this.options.openByHash) {
			let idx = this.getIndexByHash();

			this.openIndex = typeof idx === 'number' ? idx : this.options.openIndex;
		}
		else if (this.options.tabMode && !this.options.openIndex) {
			this.openIndex = 0;
		}
	}

	/**
	 * Bind all events
	 */
	bindEvents() {
		let fnOnHashChange = this.onHashChange.bind(this);

		// Global events
		if (this.options.openByHash) {
			$(window).on(Veams.EVENTS.hashchange, fnOnHashChange);
		}
	}

	render() {
		if (!Veams.currentMedia) {
			console.warn('Accordion: Veams.currentMedia is necessary to support the slider module!');
			return;
		}

		this.removeStyles();
		this.saveHeights(this.$accordionContents);
		this.closeAll();

		if (this.options.openAllOnInit) {
			this.openAll();
		}

		// Open on index if set in options
		if (typeof this.openIndex === 'number') {
			if (this.options.tabMode || this.options.openOnViewports.indexOf(Veams.currentMedia) !== -1) {
				this.activateBtn(this.$accordionBtns.eq(this.openIndex));
				this.slideDown(this.$accordionContents.eq(this.openIndex));
			}
		}

		if (this.$el.hasClass(this.options.unresolvedClass)) {
			this.$el.removeClass(this.options.unresolvedClass);
		}
	}

	/** =================================================
	 * CUSTOM ACCORDION METHODS
	 * ================================================= */

	/**
	 * Get index of accordion content referenced by hash
	 *
	 * @return {number|boolean} - index of element or false if no match
	 */
	getIndexByHash() {
		let hash = document.location.hash.split('#');
		let retVal = false;
		let i = 0;

		if (hash < 2) {
			return false;
		}

		for (i; i < this.$accordionContents.length; i++) {
			if (this.$accordionContents[i].id === hash[1]) {
				retVal = i;
				break;
			}
		}

		return retVal;
	}

	/**
	 * Open accordion content referenced by hash
	 *
	 * @param {object} e - event object
	 */
	onHashChange(e) {
		let idx = this.getIndexByHash();

		if (typeof idx === 'number') {

			if (this.options.singleOpen) {
				this.closeAll();
			}

			this.activateBtn(this.$accordionBtns.eq(idx));
			this.slideDown(this.$accordionContents.eq(idx));

		}
	}

	/**
	 * Save heights of all accordion contents.
	 *
	 * @param {Array} items - array of items
	 */
	saveHeights(items) {
		Helpers.forEach(items, (idx, item) => {
			this.saveHeight(item);
		});
	}

	/**
	 * Save the height of the node item.
	 *
	 * @param {Object} item - item to calculate the height
	 */
	saveHeight(item) {
		let $el = $(item);

		// the el is hidden so:
		// making the el block so we can measure its height but still be hidden
		$el.addClass(this.options.calculatingClass);

		let wantedHeight = $el.outerHeight();

		// reverting to the original values
		$el.removeClass(this.options.calculatingClass);

		// save height in data attribute
		$el.attr(this.options.dataMaxAttr, wantedHeight);
	}

	/**
	 * Handle the click,
	 * get the id of the clicked button and
	 * execute the toggleContent method.
	 *
	 * @param {Object} e - event object
	 * @param {object} currentTarget - Target to which listener was attached.
	 */
	handleClick(e, currentTarget) {
		this.$btn = currentTarget ? $(currentTarget) : $(e.currentTarget);
		let targetId = this.$btn.attr('href');

		e.preventDefault();

		if (this.options.tabMode && this.$btn.hasClass(this.options.activeClass)) {
			return;
		}

		this.toggleContent(targetId);
	}

	/**
	 * Toggle the accordion content by using the id of the accordion button.
	 *
	 * @param {String} id - id of the target
	 *
	 * @public
	 */
	toggleContent(id) {
		this.$target = this.$el.find(id);

		if (this.$target.hasClass(this.options.openClass)) {
			this.slideUp(this.$target);
			this.deactivateBtn(this.$btn);
		} else {

			if (this.options.singleOpen || this.options.tabMode) {
				this.closeAll();
			}

			this.activateBtn(this.$btn);
			this.slideDown(this.$target);
		}
	}

	/**
	 * Mimics the slideUp functionality of jQuery by using height and transition.
	 *
	 * @param {Object} $item - jQuery object of item
	 */
	slideUp($item) {
		$item
			.css('height', 0)
			.removeAttr('style')
			.attr('aria-expanded', 'false')
			.removeClass(this.options.openClass)
			.addClass(this.options.closeClass);
	}

	/**
	 * Mimics the slideDown functionality of jQuery by using height and transition.
	 *
	 * @param {Object} $item - jQuery object of item
	 */
	slideDown($item) {
		$item
			.css('height', $item.attr('data-js-height') + 'px')
			.attr('aria-expanded', 'true')
			.removeClass(this.options.closeClass)
			.addClass(this.options.openClass);
	}

	/**
	 * Adds active class to the clicked button.
	 *
	 * @param {Object} $item - jQuery object of button
	 */
	activateBtn($item) {
		$item.addClass(this.options.activeClass);
	}

	/**
	 * Removes active class from the button.
	 *
	 * @param {Object} $item - jQuery object of button
	 */
	deactivateBtn($item) {
		$item.removeClass(this.options.activeClass);
	}

	/**
	 * Remove all styles of the accordion content elements
	 */
	removeStyles() {
		this.$accordionContents.removeAttr('style');
	}

	/**
	 * Close all accordion contents and active buttons
	 *
	 * @public
	 */
	closeAll() {
		Helpers.forEach(this.$accordionContents, (idx, item) => {
			this.slideUp($(item));
		});
		Helpers.forEach(this.$accordionBtns, (idx, item) => {
			this.deactivateBtn($(item));
		});
	}

	/**
	 * Close all accordion contents and active buttons
	 *
	 * @public
	 */
	openAll() {
		Helpers.forEach(this.$accordionContents, (idx, item) => {
			this.slideDown($(item));
		});
		Helpers.forEach(this.$accordionBtns, (idx, item) => {
			this.activateBtn($(item));
		});
	}
}

// Returns constructor
export default Accordion;

Simple Accordion

<div class="c-accordion--default" data-css="c-accordion" data-js-module="accordion" role="tablist">
	<div class="accordion__item">
		<div class="accordion__header">
			<a href="#test-1" class="accordion__btn" data-js-item="accordion-btn" role="tab" aria-controls="test-1">
			<span class="accordion__btn-inner">Item 1</span>
		</a>
		</div>
		<div class="accordion__content" id="test-1" data-js-item="accordion-content" role="tabpanel" aria-labelledby="test-1">
			<div class="accordion__content-inner">
				Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aliquid assumenda, ducimus facilis inventore iste labore laborum libero nam necessitatibus neque nulla numquam perspiciatis rem, repudiandae sed soluta veniam vero.
			</div>
		</div>
	</div>
	<div class="accordion__item">
		<div class="accordion__header">
			<a href="#test-2" class="accordion__btn" data-js-item="accordion-btn" role="tab" aria-controls="test-2">
			<span class="accordion__btn-inner">Item 2</span>
		</a>
		</div>
		<div class="accordion__content" id="test-2" data-js-item="accordion-content" role="tabpanel" aria-labelledby="test-2">
			<div class="accordion__content-inner">
				Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aliquid assumenda, ducimus facilis inventore iste labore laborum libero nam necessitatibus neque nulla numquam perspiciatis rem, repudiandae sed soluta veniam vero.
			</div>
		</div>
	</div>
</div>

Single Open Accordion with Default Openings

<div class="c-accordion--default is-some-modifier" data-css="c-accordion" data-js-module="accordion" data-js-options='{&quot;openIndex&quot;:0,&quot;singleOpen&quot;:true}' role="tablist">
	<div class="accordion__item">
		<div class="accordion__header">
			<a href="#test-2-1" class="accordion__btn" data-js-item="accordion-btn" role="tab" aria-controls="test-2-1">
			<span class="accordion__btn-inner">Item 1</span>
		</a>
		</div>
		<div class="accordion__content" id="test-2-1" data-js-item="accordion-content" role="tabpanel" aria-labelledby="test-2-1">
			<div class="accordion__content-inner">
				Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aliquid assumenda, ducimus facilis inventore iste labore laborum libero nam necessitatibus neque nulla numquam perspiciatis rem, repudiandae sed soluta veniam vero.
			</div>
		</div>
	</div>
	<div class="accordion__item">
		<div class="accordion__header">
			<a href="#test-2-2" class="accordion__btn" data-js-item="accordion-btn" role="tab" aria-controls="test-2-2">
			<span class="accordion__btn-inner">Item 2</span>
		</a>
		</div>
		<div class="accordion__content" id="test-2-2" data-js-item="accordion-content" role="tabpanel" aria-labelledby="test-2-2">
			<div class="accordion__content-inner">
				Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aliquid assumenda, ducimus facilis inventore iste labore laborum libero nam necessitatibus neque nulla numquam perspiciatis rem, repudiandae sed soluta veniam vero.
			</div>
		</div>
	</div>
</div>