beatovich: Experiment details/summary

hallo

Weiter unten ist eine komplette html Testseite (inclusive css/js).

Ich experimentiere mit verschachtelten detail-Elementen. Im vorliegenden Fall wird nur ein Thread in der Verschachtelung darestellt (js).

Im js gibt es aber etwas seltsames

			col.forEach( function(item){
				if(item === thisEl){
					if( item.open == ""){
						item.removeAttribute("open");
						item.querySelector("summary").setAttribute("aria-expanded","true");
					}
					else{
						item.querySelector("summary").setAttribute("aria-expanded","false");
					}
				}
				else{
					item.removeAttribute("open");
					item.querySelector("summary").setAttribute("aria-expanded","false");
				}
			} );

richtig: wenn ich removeAttribute("open") ausführe muss ich gleichzeitig setAttribute("aria-expanded","true") anweisen.

Das ist logisch total falsch aber es 'funzt'! Zumindest auf Firefox 59

Andere Browser?

Die Frage ist zudem, ist es sinnvoll weitere aria attribute einzubetten?

Wer will kann gern einen codepen oder so von diesem Code machen. Vielleicht wird daraus ja etwas fürs Wiki.

Und ach ja: wer macht mit CSS etwas schönes daraus?


<!doctype html>
<html lang="de">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Test </title>
	<style>
html{font-size:3.2mm; line-height:1.5; height:100%; }
body{ height:100%; padding: 1rem;}
*{margin:0;padding:0; font-size:1rem;box-sizing: border-box; }
:focus{outline: 2px solid orange; z-index:20; }
h1{margin: 1rem 0; font-size:2rem;}

nav {  background:#fff; width:20rem; margin: 1rem 0; }
.nav { padding: 0 0 0 2rem;}
nav, .nav {  /*! width:40rem; */  }
nav summary { position:relative; background:#fff; transition: background-color 1s; white-space: nowrap; padding: 0 1rem; }
nav summary[aria-expanded="true"] { background:#eee;  white-space: nowrap; padding: 0 1rem; }
nav summary:focus { z-index:10; }
nav details {border:0 solid #ccc; border-width: 0 0 0.1rem 0.1rem;/*! background:#fff; */  margin-right:auto; }
nav div:not(.nav) { overflow:auto; padding: 0.5rem 1rem; /*! box-shadow: 1px 1px 2px 0px #666; */ }
nav + * { margin-top:2rem; }

	</style>
	
</head>
<body class="noscript">

<h1>Navigation mit Details/Summary</h2>
<p>Exclusiv geschaltete detail Elemente sollen jeweils nur einen "thread" in dieser Verschachtelung anzeigen.</p>
<p>Reagiert auf Tastatur und Click</p>

<nav>
	<details>
		<summary>Level 1 A</summary>
		<div class="nav">
			<details>
				<summary>Level 2 A</summary>
				<div>Lorem dolores ipsum sunt </div>
			</details>
			<details>
				<summary>Level 2 B</summary>
				<div class="nav">
					<details>
						<summary>Level 3 A</summary>
						<div>Lorem dolores ipsum sunt </div>
					</details>
					<details>
						<summary>Level 3 B</summary>
						<div>Lorem dolores ipsum sunt </div>
					</details>
				</div>
				<div>Lorem dolores ipsum sunt </div>

			</details>
		</div>
	</details>
	<details>
		<summary>Level 1 B</summary>
		<div>Lorem dolores ipsum sunt </div>
	</details>
	<details>
		<summary>Level 1 C</summary>
		<div>content</div>
	</details>
	<details>
		<summary>Level 1 D</summary>
		<div>content</div>
	</details>
</nav>

<p>Other Content</p>

<script>
//<!--
function init(){
  document.body.classList.remove("noscript");
  document.querySelectorAll(".tablist [aria-expanded], .details [aria-expanded]")
    .forEach( function(item){
	    item.setAttribute("aria-expanded",false);
  });
  document.querySelectorAll("details").forEach( function(detail){
	  detail.querySelector("summary").setAttribute("aria-expanded","false");
	detail.addEventListener(
		"click", 
		function(ev){
			ev.stopPropagation();
			let thisEl=this;
			console.log(thisEl.querySelector("summary"));
			thisEl.querySelector("summary").setAttribute("aria-expanded","true");
			var col = this.parentElement.querySelectorAll("details");
			col.forEach( function(item){
				if(item === thisEl){
					if( item.open == ""){
						item.removeAttribute("open");
						item.querySelector("summary").setAttribute("aria-expanded","true");
					}
					else{
						item.querySelector("summary").setAttribute("aria-expanded","false");
					}
				}
				else{
					item.removeAttribute("open");
					item.querySelector("summary").setAttribute("aria-expanded","false");
				}
			} );
		}
	);
});
}

init();
// -->
</script>

</body>
</html>

--
Neu im Forum! Signaturen kann man ausblenden!
  1. hallo

    kleine Bugfixes

    //<!-- function init(){ document.body.classList.remove("noscript"); document.querySelectorAll(".tablist [aria-expanded], .details [aria-expanded]") .forEach( function(item){ item.setAttribute("aria-expanded",false); }); document.querySelectorAll("details").forEach( function(detail){ detail.querySelector("summary").setAttribute("aria-expanded","false"); detail.addEventListener( "click", function(ev){

    			if( ev.target.nodeName !== "SUMMARY") return;
    
      	ev.stopPropagation();
      	let thisEl=this;
      	console.log(thisEl.querySelector("summary"));
      	thisEl.querySelector("summary").setAttribute("aria-expanded","true");
      	var col = this.parentElement.querySelectorAll("details");
      	col.forEach( function(item){
      		if(item === thisEl){
      			if( item.open == ""){
      				item.removeAttribute("open");
      				item.querySelector("summary").setAttribute("aria-expanded","true");
      			}
      			else{
      				item.querySelector("summary").setAttribute("aria-expanded","false");
      			}
      		}
      		else{
      			item.removeAttribute("open");
      			item.querySelector("summary").setAttribute("aria-expanded","false");
      		}
      	} );
      }
    

    ); }); }

    // replace init();
    
    document.addEventListener('DOMContentLoaded', init, false);
    

    // --> </script>

    </body> </html>

    
    
    -- 
    Neu im Forum!
    Signaturen kann man ausblenden!
    
    --
    Neu im Forum! Signaturen kann man ausblenden!
    1. Lieber beatovich,

      //<!-- function init(){

      was hat bitteschön ein HTML-Kommentar im JavaScript zu suchen? Welche (potenziell steinzeitlichen) Browser, die JavaScript nicht verstehen, möchtest Du denn noch unterstützen?

      Wenn Du XML-konform JavaScript im HTML-Code unterbringen willst, dann notiere das lieber so:

      <script>//<![CDATA[
      function init () {}
      //]]></script>
      

      Liebe Grüße,

      Felix Riesterer.

      1. Hallo Felix und beatovich,

        abgesehen davon, dass es obsolet ist, ist es falsch, auch wenn es in der MDN so drin steht.

        <script>
        //<!–−
        alert("Hello World");
        //-->
        </script>
        

        gibt auf einem Browser, der kein Script versteht, // aus. Das will man nicht, deswegen akzeptiert JavaScript im Browser <!-- als einzeiligen Kommentar[1]. Allerdings kommt der Foren-Highlighter damit nicht klar...

        Für HTML muss es so aussehen:

        <script>
        <!–−
        alert("Hello World");
        //-->
        </script>
        

        Und wenn man auch XHTML richtig supporten will, dann - so sagt MDN - schreibt man diesen Schwall (und achtet im JavaScript darauf, in die Sequenz ]]> mindestens ein Space einzustreuen)

        <script type="text/javascript"><!--//--><![CDATA[//><!--
                ...
        //--><!]]></script>
        

        Rolf

        --
        sumpsi - posui - clusi

        1. ECMA Script 262, Anhang B.1.3 ↩︎

        1. hallo

          <script>
          <!–−
          alert("Hello World");
          //-->
          </script>
          

          Das soll einer verstehen… naja… Im Endeffekt werden externe JS Dateien eingebunden.

          Aber das wesentliche Problem oder Kopfzerbrechen ist dieses offensichtlich unlogische Script, das aber zum richtigen Ergebnis führt.

          if( item.open == ""){
             item.removeAttribute("open");
             item.querySelector("summary").setAttribute("aria-expanded","true");
          }
          else{
             item.querySelector("summary").setAttribute("aria-expanded","false");
          }
          

          item meint hier ein details Element.

          logisch wäre:

          • ist details open,
          • dann lösche open
          • und definiere für summary (firstChild von details) aria-expanded = false.

          Getestet auf Firefox 59. Habe hier keinen Chorme zum Testen.

          --
          Neu im Forum! Signaturen kann man ausblenden!
          1. Hallo beatovich,

            du hast mehrere Probleme.

            • HTMLDetailsElement.open ist ein bool, kein String
            • click ist das falsche Event. Korrekt ist toggle, dann kommst Du dem click-Handling des Browsers nicht in die Quere.

            Folgender Eventhandler funktioniert bei mir mit Chrome prächtig:

            	detail.addEventListener("toggle", function(ev) { 
            			let thisEl = this;
                  this.querySelector("summary").setAttribute("aria-expanded", this.open ? "true" : "false");
                  if (this.open) {
            			  var siblings = this.parentElement.querySelectorAll("details");
            			  siblings.forEach( function(sibling) {
                      sibling.open = sibling === thisEl;
              			});
                  }
            		});
            

            Um thisEl zu vermeiden, könnte man ev.target verwenden, oder als Callback für forEach eine Arrow-Function verwenden. Arrow-Functions erzeugen keinen neuen this-Kontext.

            Ich hatte den forEach Callback zuerst als

               if (sibling !== thisEl) sibling.open = false;
            

            geschrieben, aber die blinde Zuweisung funktioniert genauso gut.

            Rolf

            --
            sumpsi - posui - clusi
            1. 	detail.addEventListener("toggle", function(ev) { 
              			let thisEl = this;
                    this.querySelector("summary").setAttribute("aria-expanded", this.open ? "true" : "false");
                    if (this.open) {
              			  var siblings = this.parentElement.querySelectorAll("details");
              			  siblings.forEach( function(sibling) {
                        sibling.open = sibling === thisEl;
                			});
                    }
              		});
              

              Um thisEl zu vermeiden, könnte man ev.target verwenden

              Oder einfach detail. Wenn der Event-Handler in einem anderen Scope läge, dann wäre ev.target auf jeden Fall this vorzuziehen. Niemand weiß, was this ist.

              1. Hallo 1unitedpower,

                doch, was this ist, weiß man. Innerhalb eines Event-Handlers ist es das Event-Target. Solange man da drin bleibt und keinen neuen function-Kontexte erzeugt...

                Aber Du hast recht, detail wird auch funktionieren, weil die Registrierung mit einer forEach Schleife erfolgt und daher eine Closure existiert. Ich persönlich vermeide allerdings die Nutzung von Closure-Daten, wenn es sich irgendwie vermeiden lässt.

                Da weiß ich allerdings nicht genau Bescheid. Sind Closures Quanten? Existieren sie, wenn sie keiner beobachtet? Sprich: Wenn der JS-JIT sieht, dass ich aus meiner Funktion nicht in den Parent-Scope hinausgreife, erzeugt er dann die Closure nicht?

                Rolf

                --
                sumpsi - posui - clusi
                1. hallo

                  Hallo 1unitedpower,

                  doch, was this ist, weiß man. Innerhalb eines Event-Handlers ist es das Event-Target. Solange man da drin bleibt und keinen neuen function-Kontexte erzeugt...

                  in meinem Fall ist es das Element, welches den Event-Listener bekommt.

                  Ich habe die Vorschläge von 1unitedpower getestet. Sie waren leider nicht zielführend.

                  Ich werde Rolfs Hinweis mit dem toggle Event als nächsten probieren.

                  --
                  Neu im Forum! Signaturen kann man ausblenden!
                  1. Ich habe die Vorschläge von 1unitedpower getestet. Sie waren leider nicht zielführend.

                    Dann stell uns doch mal das reduzierte Beispiel zur Verfügung.

                    1. hallo

                      Ich habe die Vorschläge von 1unitedpower getestet. Sie waren leider nicht zielführend.

                      Dann stell uns doch mal das reduzierte Beispiel zur Verfügung.

                      Ein reduziertes Beispiel wäre:

                      <body>
                        <details><summary>A</summary>
                          <p>content</p>
                        </details>
                        <details><summary>B</summary>
                          <div>
                            <details><summary>B1</summary>
                               <p>content</p>
                            </details>
                            <details><summary>B2</summary>
                               <p>content</p>
                            </details>
                          </div>
                        </details>
                      </body>
                      
                      • Click auf geschlossenes A muss B, B1 und B2 schliessen.
                      • Click auf geschlossenes B muss A schliessen
                      • Click auf offenes B muss B, B1 und B2 schliessen.
                      • Click auf offenes A muss B, B1 und B2 schliessen.
                      • Click auf geschlossenes B1 muss B2 schliessen und umgekehrt.
                      • Click auf offenes B1 muss B1 schliessen.

                      oder allgemein:

                      • Es soll in einem verschachtelten Baum von details-Elementen nur ein Thread sichtbar sein.
                      --
                      Neu im Forum! Signaturen kann man ausblenden!
                2. doch, was this ist, weiß man.

                  Kannst du die dir dynamic-scoping Relgen von JavaScript wirklich merken? Ich kann das nicht, für die folgenden Beispiele muss ich lange überlegen, was this anstelle das Kommentars wäre. Der statische Scope ist da wesentlich einfacherer.

                  function click1 () { /*...*/ }
                  const click2 = () => { /*...*/ }
                  const click3 = function () { /*...*/ }
                  const click4 = ((...args) => click2(args))();
                  const click5 = function click2 () { /*...*/ }
                  element.addEventListener('click', click1);
                  element.addEventListener('click', click2);
                  element.addEventListener('click', click3);
                  element.addEventListener('click', click4);
                  element.addEventListener('click', click5);
                  element.addEventListener('click', function () { /*...*/ )});
                  element.addEventListener('click', () => { /*...*/ });
                  element.addEventListener('click', ({click: function () { /*..*/ }}).click);
                  element.addEventListener('click', ({click () { /*..*/ }}).click);
                  element.addEventListener('click', ({click : () => { /*..*/ }}).click);
                  
                  1. Hallo 1unitedpower,

                    nettes Quiz 😂. Ja, ich denke, ich habe das soweit verstanden und musste eigentlich nicht überlegen. Du hast aber insofern recht, dass schon eine halbe Sekunde, die man überlegen muss, zu viel ist. Der Umgang mit this ist einer der ugly parts von JavaScript.

                    Aber die Regeln sind:

                    • das Eventhandling des DOM versucht, das this des Handlers an das event-Target zu binden. Das ist HTML-DOM, nicht JavaScript. Der Rest schon.

                    • eine function bindet ein eigenes this (je nach Aufrufart), ein Lambda bindet sich an das this, das bei seiner Definition gültig ist. Ein Lambda kann nicht mit call oder apply zwangsgebunden werden.

                    • function foo(args) {} und foo = function(args) {} sind mMn äquivalent

                    • Eine Eventhandler-Funktion hat deswegen event.target als this und ein Lambda nicht.

                    • Hinter click4 steht eine IIFE mit einer Lambda-Funktion. Die ruft click2 auf, und zwar sofort. D.h. click4 enthält das, was click2 zurückgibt. Da Lambdas kein this binden, ist this in click2 das gleiche wie im Testrahmen. Was das ist - keine Ahnung. Hängt vom Rahmen ab :)

                    • click5 ist böse Veräppelung, da wird ein Function-Objekt namens click2 in einer Variablen namens click5 gespeichert.

                    Also:
                    1: element
                    2: Rahmen-this
                    3: element
                    4: vermutlich sinnlos, click2 dürfte keinen Eventhandler zurückgeben
                    5: element
                    6: element
                    7: Rahmen-this
                    8: element. NICHT das Objekt, da sind schon viele drüber gestolpert
                    9: dito
                    10: Da musste ich doch überlegen. Ich denke, es ist das Rahmen-this, weil keine Objektmethode aufgerufen wird.

                    Der hier:

                    element.addEventListener('click', ({getclick: function() { return () => { /* */ }}}).getclick());
                    

                    wäre was anderes, da während des getclick-Aufrufs this das literale Objekt ist und das Lambda deshalb daran gebunden ist.

                    Rolf

                    --
                    sumpsi - posui - clusi
                3. hallo

                  Aber Du hast recht, detail wird auch funktionieren, weil die Registrierung mit einer forEach Schleife erfolgt und daher eine Closure existiert. Ich persönlich vermeide allerdings die Nutzung von Closure-Daten, wenn es sich irgendwie vermeiden lässt.

                  Bin voll bei dir.

                  Ich habe jetzt etwas refactoriert. Das aktuell funktionierende Script:

                  function detailsInit(){
                  	document.body.classList.remove("noscript");
                  	document.querySelectorAll("details").forEach( function(detail){
                  		detail.querySelector("summary").setAttribute("aria-expanded","false");
                  		detail.querySelector("summary").setAttribute("role","button");
                  		detail.addEventListener( "click", detailsInitSub );
                  	});
                  }
                  
                  function detailsInitSub(ev){
                  	if( ev.target.nodeName !== "SUMMARY") return;
                  	ev.stopPropagation();
                  	let thisEl=this;
                  	thisEl.querySelector("summary").setAttribute("aria-expanded","true");
                  	this.parentElement.querySelectorAll("details").forEach( function(item){
                  		if(item === thisEl){
                  			if( item.open == ""){
                  				item.removeAttribute("open");
                  				item.querySelector("summary").setAttribute("aria-expanded","true");
                  			}
                  			else{
                  				item.querySelector("summary").setAttribute("aria-expanded","false");
                  			}
                  		}
                  		else{
                  			item.removeAttribute("open");
                  			item.querySelector("summary").setAttribute("aria-expanded","false");
                  		}
                  	} );
                  }
                  document.addEventListener('DOMContentLoaded', detailsInit, false);
                  
                  
                  --
                  Neu im Forum! Signaturen kann man ausblenden!
  2. Das ist die kritische Zeile:

    item.removeAttribute("open");
    

    Den Effekt wirst du nicht zu sehen bekommen, weil der Browser danach gleich wieder das open-Attribut hinzufügt. Außerdem werden deine if-Bedingungen nicht das tun, was du vermutest. Es gibt in JavaScript einen Unterschied zwischen HTML-Attributen und DOM-Properties, der hier eine Rolle spielt Und der Vergleich mit == verschlimmert die Sache noch. Bastle mal ein reduziertes Beispiel mit nur einem <details>-Element. Und anschließend verfeinere das Beispiel, achte auf den Unterschied zwischen Attribut- und Properties, vermeide this im Event-Handler und benutze nur den strikten Vergleich ===. Dann solltest du schnell darauf kommen, was da schief läuft.

    1. Hallo 1unitedpower,

      weiß nicht, bei mir funktioniert ein item.open = false genausogut wie ein item.removeAttribute("open"). Der DOM Inspektor in Chrome zeigt mir, dass auch ohne „störendes“ JS ein geöffnetes details-Element ein open Attribut bekommt, das beim Schließen wieder verschwindet.

      Das open Property am Elementobjekt, das bleibt stehen. Aber das wird von removeAttribute nicht entfernt. Denke ich, glaube ich, hoffe ich…

      Rolf

      --
      sumpsi - posui - clusi
      1. weiß nicht, bei mir funktioniert ein item.open = false genausogut wie ein item.removeAttribute("open").

        Nicht im click-Handler. Das toggle-Event wird erst danach ausgeführt, vermutlich hast du damit getestet. Aber so oder so, muss man weder Attribut noch Property setzen, das macht der Browser von alleine. Außer man hat einen Button außerhalb des details-Elements, der den zusätzlichen Inhalt togglen können soll. Das ist hier aber nicht der Fall.

        1. Hallo 1unitedpower,

          ich hab's im click und im toggle verwendet. Hat funktioniert. Die Spec sagt:

          The open IDL attribute must reflect the open content attribute.

          Und ich meine, „IDL attribute“ ist spec-Sprech für Properties der DOM-Objekte.

          SETZEN (im Sinne von „auf true setzen“ muss man es als Reaktion auf click tatsächlich nicht, aber für den gewünschten Effekt muss man es in den Geschwistern LÖSCHEN, damit die Nachbar-Äste im Baum zuklappen.

          Das toggle-Event wird erst danach ausgeführt

          Ja. Weil die Spec sagt, dass das Ändern des open-Zustandes einen Task in die Mikroqueue stellen muss, der die Notification übernimmt. Und zwar genau einen Task, egal wie oft open sich ändert.

          Rolf

          --
          sumpsi - posui - clusi
        2. hallo

          weiß nicht, bei mir funktioniert ein item.open = false genausogut wie ein item.removeAttribute("open").

          Nicht im click-Handler. Das toggle-Event wird erst danach ausgeführt, vermutlich hast du damit getestet. Aber so oder so, muss man weder Attribut noch Property setzen, das macht der Browser von alleine. Außer man hat einen Button außerhalb des details-Elements, der den zusätzlichen Inhalt togglen können soll. Das ist hier aber nicht der Fall.

          Ihr habt natürlich nicht meinen konkreten Fall (html) getestst

          Die Absicht ist:

          • Bei sibling details darf nur einer sichtbar sein.
          • ein offenes details muss auch geschlossen werden können.
          • bei schliessen sind alle child details auch zu schliessen.

          Meine Version gewährleistet das.

          Aber eventuell tritt das ein, was 1unitedpower sagte, dass nämlich click vor dem toggle gefeuert wird. Es arbeiten also 2 events nacheinander, aber nur einer wird benutzt, um die aria Attribute umzuschreiben.

          Damit würde für mich die scheinbare unlogik im Code verständlich.

          --
          Neu im Forum! Signaturen kann man ausblenden!
          1. Hallo beatovich,

            ich habe hiermit getestet: https://jsfiddle.net/Rolf_b/o1w4dzLr/

            Rolf

            --
            sumpsi - posui - clusi