Test Seitennavigation mit Details / Summary

Was?

Die Navigation soll folgende Eigenschaften haben:

Wie?

Basis der Navigation ist eine verschachtelte Liste. Um die Liste und die Unterlisten ein- und ausblenden zu können, werden sie in ein details-Element gelegt. Die Bereichsüberschriften kommen in ein summary-Element:

HTML

<a id="skip-link" href="#main">zum Hauptinhalt</a>
<nav id="sitenav">
	<details>
		<summary>Menü</summary>
		<ul>
			<li><a href="/">Startseite</a>
			<li>
				<details>
					<summary>Bereich 1</summary>
					<ul>
						<li><a href="">Seite 1.1</a></li>
						<li><a href="">Seite 1.2</a></li>
						<li><a href="">Seite 1.3</a></li>
					</ul>
				</details>
			<li aria-current="page">
				<details>
					<summary>Bereich 2</summary>
					<ul>
						<li><a href="">Seite 2.1</a></li>
						<li><a href="">Seite 2.2</a></li>
						<li aria-current="page"><a href="#">aktuelle Seite</a></li>
						<li><a href="">Seite 2.4</a></li>
						<li><a href="">Seite 2.5</a></li>
						<li><a href="">Seite 2.6</a></li>
					</ul>
				</details>
			<li><a href="">Seite 3</a>
			<li><a href="">Seite 4</a>
			<li>
				<details>
					<summary>Breich 5</summary>
					<ul>
						<li><a href="">Seite 5.1</a></li>
						<li><a href="">Seite 5.2</a></li>
					</ul>
				</details>
			<li><a href="">Impressum</a></li>
		</ul>
	</details>
</nav>
<main id="main">
			...
</main>
				

Vor die Navigation wurde noch ein Skip-Link gesetzt, um Tastatur-Benutzern sowie Besuchern mit assistiven Techniken zu ermöglichen, direkt zum Inhalt zu springen. Per css wird dieser Link aus dem Viewport geschoben und nur bei Focus sichtbar.

Per css wird die Navigationsliste je nach Seitenbreite im Quer- oder Hochformat angeordnet. Da noch nicht alle Browser details/summary unterstützen und eine Feature-Detection und ein Polyfill nur per Javascript möglich sind, werden die Klassen für die absolute Positionierung und die Seitenbreite im Javascript gesetzt. Die Media Query für die Viewportbreite wird daher auch im Javascript durchgeführt.

Auf schmalen Viewports wird die Navigation hinter einem "Menü-Button" versteckt. Bei Klick öffnet sich die erste Ebene nach unten und die zweite nach rechts. Auf genügend breiten Viewports ist der "Menübutton" versteckt und die erste Ebene ist immer sichtbar und waagrecht angeordnet. Die zweite Ebene öffnet nach unten.

Um die Bedienung der Navigation noch etwas komfortabler für die Besucher zu gestalten, wird im Javascript dafür gesorgt, das nur ein Navigationsbereich offen ist, und das bei Mausklick bzw. Touch außerhalb der Navigation und bei der Escapetaste alle Navigationslisten geschlossen werden.

CSS

/* Platz für die Navigation */
body { margin-top: 3.5em; }

/* Skip-Link */
#skip-link { position: absolute; left: 0; top: 0; background: black; color: white; padding: 0.25em; transform: translateY(-100%); transition: 0.2s transform; z-index: 1001 } 
#skip-link:focus { transform: translateY(0); }			

/* Allgemeine Einstellungen und Einstellungen für schmale Viewports */
#sitenav *, #sitenav a { background-color: lightgray; color: black }
#sitenav { top: 1em; left: 2em; }
#sitenav ul { list-style-type: none; margin:0; padding-top:.2em; padding-left:1em; padding-right:1em; border-radius: .5em; }
#sitenav ul ul { margin-left: 7em; margin-top: -3.1em; padding-left: .1em; }
#sitenav li a { display: inline-block; margin-top:.1em; margin-bottom:.1em; }
#sitenav li a, #sitenav li summary { padding: .5em; margin: .3em; line-height: 1.4em; outline: none; }
#sitenav li a:hover, #sitenav li summary:hover, #sitenav li a:focus, #sitenav li summary:focus { outline: 1px solid #5050ff }
#sitenav li a { text-decoration: none; }
#sitenav li { padding: 0em; white-space: nowrap; vertical-align: middle; }
#sitenav li[aria-current] a[href='#']::before { content: "► "; font-family: arial_unicode_ms }
#sitenav ul details summary::after { font-family: arial_unicode_ms; }
#sitenav ul details:not(open) summary::after { content: ' ►'; }
#sitenav ul details[open] summary::after { content: ' ◄'; }
#sitenav > details:not(open) > summary::before { content: "☰"; font-size: 0.9em; } 
#sitenav > details[open] > summary::before { content: "×"; font-size: 1.2em; } 
#sitenav > details { border: 1px solid black; border-radius: .2em; max-width: -webkit-max-content; max-width: -moz-max-content; max-width: max-content; padding: .2em; }
#sitenav > details > ul { margin-left: -.5em; margin-top: .3em; }
#sitenav summary { cursor: pointer; max-width: -webkit-max-content; max-width: -moz-max-content; max-width: max-content; }
#sitenav summary::-webkit-details-marker { display: none; }
#sitenav summary { list-style-type:  none; }
#sitenav ul summary::before { content: ""; width: 0; }
#sitenav.withjs > details > ul > li[aria-current] { border-left: 2px solid black; }
#sitenav.withjs { position: absolute; }
#sitenav.withjs ul { position: absolute; }

/* Einstellungen für breite Viewports */
#sitenav.large { top: 0; left: 0; right: 0; }
#sitenav.large > details { border: none; border-radius: 0; padding: 0 } 
#sitenav.large > details > ul , #sitenav.large > details > ul > li { display: inline-block }
#sitenav.large ul details:not(open) summary::after { content: ' ▼'; }
#sitenav.large ul details[open] summary::after { content: ' ▲'; }
#sitenav.large > details > ul { width: 100%; box-sizing: border-box; }
#sitenav.large > details > ul { margin-left: 0; margin-top: 0; border-radius: 0; }
#sitenav.large > details > ul > li[aria-current] { border-bottom: 2px solid black; border-left: none; }
#sitenav.large ul ul { margin-left: -.5em; margin-top: -.1em; padding-left: .5em; border-radius: 0 0 .5em .5em;}
				

Javascript

window.addEventListener("DOMContentLoaded",function() { 
"use strict"

	// Polyfill für IE und Edge
	var native_details = ('open' in document.createElement("details"));
	if (!native_details) {
		var script = document.createElement('script');
		script.src = "details-polyfill.js";
		document.getElementsByTagName('head')[0].appendChild(script);
	}

	// Alle details und summary
	var navele = document.querySelector("#sitenav");
	var details = navele.querySelectorAll("details");
	var summary = navele.querySelectorAll("summary");

	// Fokussierbares Element hinter Navigation legen, für focusin-Event
	var div = document.createElement("div");
	div.tabIndex = 0;
	navele.appendChild(div);
	
	// Fürs css Klasse "withjs" setzen
	sitenav.className += " withjs";
	
	// Auf Seitenbreite reagieren und bei breiten Viewports Klasse "large" setzen
	var format;
	var mq = window.matchMedia("screen and (min-width:45em)");
	mq.addListener(mq_handler);
	mq_handler();				
	
	function mq_handler() {
		if(mq.matches) {
			details[0].setAttribute('open', 'open');
			summary[0].setAttribute('hidden', 'hidden');
			format = "large";
			sitenav.className += " large"
		}
		else {
			details[0].removeAttribute('open');
			summary[0].removeAttribute('hidden');
			format = "small";
			sitenav.className = sitenav.className.replace(" large","");
		}
	}
	
	// Eventhandler für Click, TAB und keydown
	window.addEventListener('click', klickhandler);
	window.addEventListener('touchstart', klickhandler);
	window.addEventListener('keydown', keyhandler);
	window.addEventListener('focusin', focusinhandler);
	
	// Aktion bei Klick und Tastendruck
	function klickhandler(e) {
		if (e.target.nodeName.toLowerCase() === 'summary') {
			closeAllwithout([e.target.parentNode,details[0]]);
		}
		else {
			if(!childOf(e.target,details[0])) closeAllwithout([]);
		}
	}

	// Bei Taste ESC Navigation schließen
	function keyhandler(e) { 
		var keyCode = e.keyCode;
		if(keyCode == 27) closeAllwithout([]);
	}
	
	// Bei Verlassen mit Tabulator Navigation schließen
	function focusinhandler(e) { 
		if(!childOf(e.target,details[0])) closeAllwithout([]);
	}
	
	// Alle Navigationselemente schließen, bis auf Elemente in nc
	function closeAllwithout(nc) {
		for(var i=1;i<details.length;i++) if(nc.indexOf(details[i]) == -1) details[i].removeAttribute('open');
		if(format == "small" && 	details[0].hasAttribute("open") && nc.indexOf(details[0]) == -1) {
			details[0].removeAttribute('open');
			summary[0].focus();
		}
	}
	
	// Ermitteln, ob child ein Kindelement von parent ist
	function childOf(child, parent) {
		while((child = child.parentNode) && child!==parent); 
		return !!child; 
	}
	
});
				

Dreiecksammlung

U+2BC5 ⯅ U+2BC6 ⯆ U+2BC7 ⯇ U+2BC8 (Nicht im Obstladen)

Default: U+25B2 ▲, U+25B6 ▶, U+25BA ►, U+25BC ▼, U+25C0 ◀, U+25C4 ◄

arial: U+25B2 ▲, U+25B6 ▶, U+25BA ►, U+25BC ▼, U+25C0 ◀, U+25C4 ◄

arial_unicode_ms: U+25B2 ▲, U+25B6 ▶, U+25BA ►, U+25BC ▼, U+25C0 ◀, U+25C4 ◄

Im Font arial_unicode_ms gewählt: U+25B2 ▲, U+25BA ►, U+25BC ▼, U+25C4 ◄

Links zu Details und Aria

Klick ...

The details and summary elements by Scott O'Hara

Stackoverflow: Adding aria attributes to details elements