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.
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);
});
Created 2020-10-31T20:12:10-07:00 · Edit