Dropdown menu with details summary#
I added dropdown menus to Datasette 0.51 - see #1064.
I implemented them using the HTML <details><summary>
element. The HTML looked like this:
1<details class="nav-menu">2 <summary><svg aria-labelledby="nav-menu-svg-title" role="img"3 fill="currentColor" stroke="currentColor" xmlns="http://www.w3.org/2000/svg"4 viewBox="0 0 16 16" width="16" height="16">5 <title id="nav-menu-svg-title">Menu</title>6 <path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path>7 </svg></summary>8 <div class="nav-menu-inner">9 <ul>10 <li><a href="/">Item one</a></li>11 <li><a href="/">Item two</a></li>12 <li><a href="/">Item three</a></li>13 </ul>14 </div>15</details>
See the top right corner of https://latest-with-plugins.datasette.io/ for a demo.
This displays an SVG icon which, when clicked, expands to show the menu. The SVG icon uses aria-labelledby="nav-menu-svg-title" role="img"
and a <title id="nav-menu-svg-title">
element for accessibility.
I styled the menu using a variant of the following CSS:
1details.nav-menu > summary {2 list-style: none;3 display: inline;4 position: relative;5 cursor: pointer;6}7details.nav-menu > summary::-webkit-details-marker {8 display: none;9}10details .nav-menu-inner {11 position: absolute;12 top: 2rem;13 left: 10px;14 width: 180px;15 z-index: 1000;16 border: 1px solid black;17}18.nav-menu-inner a {19 display: block;20}
list-style: none;
hides the default reveal arrow from most browsers. ::-webkit-details-marker { display:none }
handles the rest.
The summary
element uses position: relative;
and the details .nav-menu-inner
uses position: absolute
- this positions the open dropdown menu in the right place.
Click outside the box to close the menu#
The above uses no JavaScript at all, but comes with one downside: it’s usual with menus to clear them if you click outside the menu, but here you need to click on the exact icon again to hide it.
I solved that with the following JavaScript, run at the bottom of the page:
1document.body.addEventListener('click', (ev) => {2 /* Close any open details elements that this click is outside of */3 var target = ev.target;4 var detailsClickedWithin = null;5 while (target && target.tagName != 'DETAILS') {6 target = target.parentNode;7 }8 if (target && target.tagName == 'DETAILS') {9 detailsClickedWithin = target;10 }11 Array.from(document.getElementsByTagName('details')).filter(12 (details) => details.open && details != detailsClickedWithin13 ).forEach(details => details.open = false);14});