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:
<details class="nav-menu"> <summary><svg aria-labelledby="nav-menu-svg-title" role="img" fill="currentColor" stroke="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"> <title id="nav-menu-svg-title">Menu</title> <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> </svg></summary> <div class="nav-menu-inner"> <ul> <li><a href="/">Item one</a></li> <li><a href="/">Item two</a></li> <li><a href="/">Item three</a></li> </ul> </div></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:
details.nav-menu > summary { list-style: none; display: inline; position: relative; cursor: pointer;}details.nav-menu > summary::-webkit-details-marker { display: none;}details .nav-menu-inner { position: absolute; top: 2rem; left: 10px; width: 180px; z-index: 1000; border: 1px solid black;}.nav-menu-inner a { display: block;}
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:
document.body.addEventListener('click', (ev) => { /* Close any open details elements that this click is outside of */ var target = ev.target; var detailsClickedWithin = null; while (target && target.tagName != 'DETAILS') { target = target.parentNode; } if (target && target.tagName == 'DETAILS') { detailsClickedWithin = target; } Array.from(document.getElementsByTagName('details')).filter( (details) => details.open && details != detailsClickedWithin ).forEach(details => details.open = false);});