JSFiddle - React, Tailwind, and code Playground

by Arnold Daniels

HTML

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
<aside class="menu">
  <p class="menu-label">
    General
  </p>
  <ul class="menu-list">
    <li><a>Dashboard</a></li>
    <li><a>Customers</a></li>
  </ul>
  <p class="menu-label">
    Administration
  </p>
  <ul class="menu-list collapse">
    <li><a>Team Settings</a></li>
    <li>
      <details>
        <summary><a class="is-active">Manage Your Team</a></summary>
        <ul>
          <li><a>Members</a></li>
          <li><a>Plugins</a></li>
          <li><a>Add a member</a></li>
        </ul>
      </details>
    </li>
    <li><a>Invitations</a></li>
    <li><a>Cloud Storage Environment Settings</a></li>
    <li><a>Authentication</a></li>
  </ul>
  <p class="menu-label">
    Transactions
  </p>
  <ul class="menu-list">
    <li><a>Payments</a></li>
    <li><a>Transfers</a></li>
    <li><a>Balance</a></li>
  </ul>
</aside>

SCSS

body {
  padding: 20px;
}

ul.collapse {
  ul {
    overflow: hidden;
    height: 100%;
  }
  li:not(.in) > ul {
    height: 0;
  }

  details {
    transition: height 0.4s;
    overflow: hidden;
  }

  summary {
    list-style: none;
  }
  summary::-webkit-details-marker {
    display: none;
  }
  summary:focus {
    outline: none;
  }
}

JavaScript

function delegateEvent(type, selector, handler) {
    document.addEventListener(type, function(event) {
        for (var target = event.target; target && target != this; target = target.parentNode) {
            if (target.matches(selector)) {
                handler.call(target, event);
                break;
            }
        }
    }, false);
}

function smoothAnimateDetails(details, summary, event) {
		if (details.style.height !== '') {
    		event.preventDefault();    
    		return; // Still in animation
    }

		transitionTime = durationToMs(getComputedStyle(details).transitionDuration);
    
    if (isNaN(transitionTime) || transitionTime <= 0) {
    		return;
    }

    const isOpen = details.open;
    details.style.height = details.offsetHeight + "px";
    
    setTimeout(function() {
      details.style.height = (isOpen ? summary.offsetHeight : details.scrollHeight) + "px";
    }, 0);
    
    if (isOpen) event.preventDefault(); // Don't hide the content during transition
    
    setTimeout(function() {
    		details.open = !isOpen;
        details.style.height = '';
    }, transitionTime);
}

function durationToMs(duration) {
		const groups = /^(?<amount>[\d\.]*)(?<unit>.*)$/.exec(duration).groups
    const amount = Number.parseFloat(groups.amount);
    
    return groups.unit === 'ms' ? amount : groups.unit === 's' ? amount * 1000 : NaN;
}

delegateEvent('click', 'summary', function(event) {
		smoothAnimateDetails(this.parentNode, this, event);
    event.stopPropagation();
});