Meowsalot: jQuery slideToggle - Bereich schließen

Hallo,

habe ich die Möglichkeit einen Bereich wieder zu schließen wenn ich einen anderen öffne?

	$(document).ready(function(){

		$('.testDiv').hide();
		
		$('.test').click(function(){
						
			$(this).parent().children('.testDiv').slideToggle();
			
		});

	});       

Bis bald!
Meowsalot (Bernd)

  1. Hallo Meowsalot

    habe ich die Möglichkeit einen Bereich wieder zu schließen wenn ich einen anderen öffne?

    Die Möglichkeit hast du, aber warum solltest du das tun?

    Angenommen du hast mehrere Bereiche, die durch Nutzerinteraktion ein- und ausgeblendet werden können. Dann gibst du damit dem Benutzer die Freiheit zu entscheiden, welche der Inhalte er oder sie gerne sehen möchte. Wird nun ein Bereich eingeblendet, dann bringt der Benutzer damit zum Ausdruck: „Ich möchte diese Inhalte sehen!“ Wenn der Benutzer danach einen anderen Bereich einblendet, dann heißt das: „Ich möchte auch diese Inhalte sehen!“ Es heißt aber nicht zwangsläufig: „Ich möchte die Inhalte, die ich zuvor eingeblendet habe, jetzt nicht mehr sehen!“ Die Inhalte entgegen dem Wunsch der Benutzer deiner Seite trotzdem auszublenden, ist vermutlich nicht die allerbeste Idee. Ich kann mir nur sehr wenige Anwendungen vorstellen, wo so etwas tatsächlich sinnvoll wäre.

    	$(document).ready(function(){
    
    		$('.testDiv').hide();
    		
    		$('.test').click(function(){
    						
    			$(this).parent().children('.testDiv').slideToggle();
    			
    		});
    
    	});       
    

    Wo ist das dazugehörige Markup?

    Wenn du Programmcode postest, der auf der DOM-Repräsentation eines Dokuments operiert, dann bitte auch immer das dazugehörige Markup beifügen, selbst dann, wenn es wie hier nur ein Abschnitt aus einer Testseite ist. Eine sinnvolle Hilfestellung ist ansonsten kaum möglich: Das HTML bzw. das daraus erzeugte DOM ist die Grundlage des Programms. Das heißt, das Programm muss sich dem Markup anpassen, nicht umgekehrt. Wird dieser Grundsatz nicht beachtet, führt dies zu schlecht bedienbaren Seiten – oder gar zum Ausschluss ganzer Benutzergruppen. Niemand hier möchte, dass ihre oder seine Ratschläge in Unkenntnis der zugrundeliegenden Dokumentstruktur zu so etwas führen.

    Schauen wir uns deinen Code dennoch einmal an:

    $(document).ready(function() { /* ... */ });
    

    Diese Form der Eventhandler-Registrierung – für den Einstieg in das Programm, wenn das Markup der Seite fertig geparst wurde – ebenso wie ihre diversen Varianten, sollten nicht mehr verwendet werden. Die empfohlene Syntax ist die Kurzschreibweise $(handler). Der Eventhandler wird hierbei direkt an die jQuery-Funktion übergeben.

    $('.testDiv').hide();
    

    Alle Elemente mit der Klasse testDiv sollen ausgeblendet werden. Abgesehen davon, dass der Klassenbezeichner schlecht gewählt ist, da er absolut nichts über den beabsichtigten Zweck dieser Zusammenfassung verrät, ist das Vorgehen, die Inhalte mit JavaScript auszublenden, richtig: Wird das Skript aus irgendeinem Grund nicht geladen oder nicht ausgeführt, sind die Inhalte für die Benutzer der Seite trotzdem zugänglich.

    Es wäre allerdings wünschenswert, dass die Information über den Status dieser Elemente durch das für diesen Zweck eingeführte hidden-Attribut transportiert wird. Das wird durch die jQuery-Methode hide aber nicht gesetzt. So funktioniert jQuery leider nicht. Durch den Methodenaufruf wird stattdessen, als Inline-Style, nur display: none hinzugefügt.

    $('.test').click(function() { /* ... */ });
    

    Hier registrierst du nun für alle Elemente mit der Klasse test einen Click-Handler. Abgesehen davon, dass auch dieser Klassenbezeichner schlecht gewählt ist, da er absolut nichts über den beabsichtigten Zweck der Zusammenfassung verrät, stellt sich mir die Frage, um welche Elemente es sich hier handelt: Sind es Buttons, Links, oder irgendwelche Elemente, die kein interaktiver Inhalt sind? Das ist relevant. Deswegen wäre es gut gewesen, wenn du auch dein Markup gepostet hättest.

    Bei den Elementen zur Steuerung der Sichtbarkeit der anderen Elemente sollte es sich um Buttons handeln. Hast du hier Links gewählt, wäre das falsch, weil nicht zu einer anderen Ressource, beziehungsweise zu einem anderen Element auf derselben Seite gesprungen werden soll. Nicht-interaktive Elemente, wie zum Beispiel Divs, wären ebenfalls falsch. Solche Elemente können ohne Weiteres nicht mit der Tastatur fokussiert werden. Benutzer, die auf Tastaturbedienbarkeit angewiesen sind, wären vom Zugang zu den Inhalten, die du am Anfang deines Programms ausgeblendet hast, ausgeschlossen. Das möchtest du bestimmt nicht.

    Schauen wir uns an, wie das Markup etwa aussehen sollte:

    <button type="button" aria-expanded="true" class="summary">
        Eine kurze Zusammenfassung des Inhalts
    </button>
    <!-- Hier folgt der im Moment eingeblendete Inhalt -->
    

    Der Standardwert für das Attribut type bei Buttons ist submit. Das ist im vorliegenden Fall natürlich Unsinn, weshalb wir als erstes den korrekten Wert button setzen. Buttons dieses Typs stehen allerdings für irgendeine Aktion, die auf der Seite ausgeführt werden kann. – Die Rolle des Elements verrät noch nichts über den konkrenten Zweck, Inhalte ein- und auszublenden. Deswegen fügen wir das aria-expanded-Attribut hinzu. Dadurch wird zum einen vermittelt, dass der Button die Sichtbarkeit von Inhalten kontrolliert, und zum anderen wird über den Attributwert bekanntgegeben, ob der Inhalt gerade sichtbar ist.

    Diese Informationen stehen allerdings noch nicht allen Benutzern zur Verfügung. Es muss auch visuell erkennbar sein, welchem Zweck das Element dient – und in welchem Zustand es sich gerade befindet. Wir müssen also gestalterisch eingreifen. Dabei beziehen wir uns sinnvollerweise auf das zuvor gesetzte Attribut:

    /* Inhalt ist ausgeblendet */
    
    [aria-expanded=false]::before {
      content: "▶";
    }
    
    
    /* Inhalt ist eingeblendet */
    
    [aria-expanded=true]::before {
      content: "▼";
    }
    

    Wir nutzen einen Attributselektor und ein Pseudoelement, um den Zustand des Widgets darzustellen. Hier könntest du statt Dreiecken auch Pfeile verwenden, und du wirst aller Wahrscheinlichkeit nach weitere Deklarationen vornehmen wollen. Für den Anfang sollte das aber genügen: Benutzer können nun auch visuell die Bedeutung des Bedienelementes erkennen. Natürlich muss der Attributwert auch entsprechend gesetzt werden, aber bevor wir dazu kommen, sollten wir uns noch etwas mit dem Markup beschäftigen.

    Was der Button in seiner gegenwärtigen Form nämlich nicht enthält, ist die Information, welcher konkrete Inhalt ein- und ausgeblendet werden soll. Es gibt keine maschinenlesbare Verknüpfung zwischen Button und abhängigem Inhalt, die das besondere Verhältnis dieser Elemente kenntlich machen würde. Das könnte ein Problem darstellen.

    Aus diesem Grund gibt es das Attribut aria-controls. Dieses Attribut kann auf dem Button gesetzt werden und als Wert eine Liste von ID-Referenzen auf diejenigen Elemente erhalten, deren Sichtbarkeit durch den Button kontrolliert wird. Diese Verknüpfung könnte etwa dazu genutzt werden, Benutzern von Screen-Readern mitzuteilen, auf welche Elemente sich die Aktion bezieht. Allerdings gibt es meines Wissens keine sinnvolle Implementierung dieses Features. Sprich, entweder es wird von Screen-Readern überhaupt nicht unterstützt, oder aber in einer Form, die einer guten Bedienbarkeit eher im Wege steht. Hier wäre etwas weitergehende Recherche vielleicht angebracht. Auf Basis meines derzeitigen Wissensstandes, würde ich vom Gebrauch dieses Attributes aber abraten.

    Es sollte auch nicht nötig sein. Die Erwartung der Benutzer ist, dass beim "Aufklappen" der zuvor versteckte Inhalt direkt unterhalb des Elements erscheint, das aktiviert wurde. Wäre dies nicht der Fall, so wäre das ein Verstoß gegen das Principle of Least Surprise. Hier ist also eigentlich nur zu beachten, dass der kontrollierte Inhalt nicht nur visuell dem Button nachfolgt. Es sollte also vermieden werden, zum Beispiel unter Verwendung der Eigenschaft order, Button und Inhalt wie gewünscht zu positionieren, obwohl im Markup andere Elemente dazwischenliegen. Auf diese Weise wäre der Zusammenhang zwischen Button und Inhalt zwar für sehende Benutzer erkennbar, nicht jedoch für diejenigen Benutzer, die sich die Inhalte der Seite vorlesen lassen. Die Elemente, deren Sichtbarkeit durch den Button kontrolliert wird, sollten also im Markup direkt auf diesen folgen.

    Weiter im Programm:

    $(this).parent().children('.testDiv').slideToggle();
    

    In deinem Click-Handler führst du diese Anweisung aus. Statt parent und children zu bemühen, hättest du hier auch die Methode siblings verwenden können. Allerdings wäre es wohl noch besser, die Menge der durchsuchten Elemente auf diejenigen zu reduzieren, die dem Button nachfolgen. Denn Elemente, die im Markup vor dem Button stehen, interessieren uns aus oben genannten Gründen ohnehin nicht. Die Methode nextAll ist, was wir suchen. Oder die Methode next für den Fall, dass der Inhalt in einem Element zusammengefasst wäre, welches im Markup direkt auf den Button folgt.

    Durch die obige Anweisung werden nun die selektierten Elemente ein- bzw. ausgeblendet, aber es wird nicht der Wert des aria-expanded-Attributs angepasst. Innerhalb des Click-Handlers muss also zusätzlich der aktuelle Wert dieses Attributs abgefragt und dann der jeweils andere boolesche Wert gesetzt werden. Für beides kann die Methode attr verwendet werden. Zu beachten ist dabei aber, dass der Attributwert von Typ String ist. Das heißt, es werden zwar die booleschen Werte true und false beim Setzen des Attributes implizit in Zeichenketten umgewandelt, aber beim Lesen des Attributes erhalten wir Strings. Ich habe das bisher besprochene mal in einem Live-Beispiel zusammengefasst.

    Wolltest du nun zusätzlich beim öffnen eines Bereichs den zuletzt geöffneten Bereich automatisch schließen, was du wie eingangs erwähnt wahrscheinlich nicht willst, dann gäbe es mehrere Möglichkeiten, wie das bewerkstelligt werden könnte. Eine einfache Lösung wäre, die Umschaltlogik in eine eigene Funktion zu schreiben und innerhalb des Click-Handlers zu prüfen, ob ein anderer Bereich gerade angezeigt wird. Dieser wird dann geschlossen bevor die Aktion auf dem aktuellen Button ausgeführt wird. Das könnte etwa so aussehen:

    function toggle($summary) {
      const expanded = $summary.attr('aria-expanded');
    
      $summary.nextAll('.details').slideToggle();
      $summary.attr('aria-expanded', expanded === 'true' ? 'false' : 'true');
    }
    
    
    $summaries.click(function() {
      const $summary = $(this),
            $open = $('[aria-expanded=true]');
    
      if (!$open.is($summary)) {
        toggle($open);
      }
    
      toggle($summary);
    });
    

    Eigentlich könntest du dir diese Bastelei aber auch komplett sparen, denn ein solches Widget ist auch im Sprachumfang von HTML enthalten: Die Elemente details und summary sind genau für diesen Zweck eingeführt worden. Anders als in dem Eigenbau oben, wird die Zusammenfassung in Form des summary-Elementes hier als erstes Kindelement von details notiert, und die ein- und auszublendenden Inhalte direkt darunter. Die Sichtbarkeit wird durch das open-Attribut von details gesteuert. Die Logik übernimmt der Browser.

    <details open>
        <summary>Zusammenfassung</summary>
        <p>Inhalt</p>
    </details>
    

    Die Unterstützung ist schon relativ gut. Mit prominenten Ausnahmen. Man könnte deswegen ein Polyfill einbinden, aber das halte ich eigentlich nicht für nötig. Browser ohne Support zeigen die Inhalte dann halt an. Progressive Enhancement.

    Viele Grüße,

    Orlok

    1. Hej Orlok,

      Die Unterstützung ist schon relativ gut. Mit prominenten Ausnahmen. Man könnte deswegen ein Polyfill einbinden, aber das halte ich eigentlich nicht für nötig. Browser ohne Support zeigen die Inhalte dann halt an. Progressive Enhancement.

      Ausgezeichneter Beitrag, den ich gerne mit ein paar Änderungen (damit man ihm nicht ansieht, dass er eigentlich eine Antwort auf eine Frage im Forum ist) im Blog oder Wiki sehen würde… 😉

      Noch eine winzige Ergänzung: Das Ausblenden von Inhalten macht umso mehr Sinn, je kleiner ein Bildschirm ist. In allen mobilen Browsern funktioniert details.

      Und: Der Marktanteil der prominenten Ausnahmen ist gering. Also noch zwei Argumente für details — ganz abgesehen davon, dass MS dass doch hoffentlich mal irgendwann gebacken kriegt?!?

      Marc

    2. @@Orlok

      Bei den Elementen zur Steuerung der Sichtbarkeit der anderen Elemente sollte es sich um Buttons handeln.

      Um Toggle-Buttons (Inclusive Components).

      LLAP 🖖

      --
      „Wer durch Wissen und Erfahrung der Klügere ist, der sollte nicht nachgeben. Und nicht aufgeben.“ —Kurt Weidemann
      1. Hej Gunnar,

        @@Orlok

        Bei den Elementen zur Steuerung der Sichtbarkeit der anderen Elemente sollte es sich um Buttons handeln.

        Um Toggle-Buttons (Inclusive Components).

        Ich finde die Verwendung von details und summary sinnvoller!

        Aus dem von @Orlok genannten Grund: ein Nutzer entscheidet so selber, was geöffnet sein soll und was nicht.

        Marc

        1. Hallo Marc

          Erstmal Dank an dich und Gunnar für das Feedback. (…)

          Bei den Elementen zur Steuerung der Sichtbarkeit der anderen Elemente sollte es sich um Buttons handeln.

          Um Toggle-Buttons (Inclusive Components).

          Ich finde die Verwendung von details und summary sinnvoller!

          Aus dem von @Orlok genannten Grund: ein Nutzer entscheidet so selber, was geöffnet sein soll und was nicht.

          Die Verwendung von details und summary schließt eine Implementierung ja nicht aus, bei der zu einem bestimmten Zeitpunkt immer nur ein Bereich geöffnet ist. Das wäre sogar viel einfacher zu bewerkstelligen, als wenn man alles von Hand schreiben würde, da hier der größte Teil der Umschaltlogik von Browser übernommen wird. Man muss nur einen Eventhandler registrieren und prüfen, ob es ein offenes details-Element gibt, auf das nicht geklickt wurde – und dieses dann schließen, indem man das gesetzte open-Attribut entfernt.

          Mit jQuery könnte das ungefähr so aussehen:

          $(function() {
          
              $('details').click(function() {
                  const $openDetails = $('details[open]');
          
                  if (!$(this).is($openDetails)) {
                      $openDetails.removeAttr('open');
                  }
              });
          
          });
          

          Die Verwendung von details und summary ist sinnvoller, weil es blödsinnig ist Sachen nachzubauen, für die es bereits eine native Implementierung gibt.

          Eine Ausnahme wäre, wenn man ein Polyfill schreiben wollte. In diesem Fall könnte man sich fragen, ob es sinnvoll ist, aria-pressed zu summary hinzuzufügen. Die Antwort hängt dann davon ab, ob das in der Praxis tatsächlich zu einer Verbesserung der User Experience für Nutzer assistiver Software führt. Ob das so ist, weiß ich nicht, aber ich wage es zu bezweifeln.

          Auf welche Rolle der jeweils genutzten Accessibility API summary abgebildet werden soll, dafür gibt es Vorgaben, nachzulesen in der HTML Accessibility API Mappings Spezifikation:

          Abgesehen vom Mac, wo es mit disclosure Triangle eine native Rolle für diese Bestimmung gibt, wird summary auf dieselben Rollen abgebildet, wie Elemente mit role button, welche darüber hinaus nur die Standardwerte enthalten. Wird das aria-pressed-Attribut auf einem Button definiert, dann hat das Auswirkungen darauf, auf welche Rolle der Accessibility API des Betriebssystems das Element abgebildet wird. Die Präsenz des Attributs führt dazu, dass die Rolle Toggle-Button verwendet wird, die von fast allen Schnittstellen unterstützt wird. Das wäre dann was anderes als das, was für summary spezifiziert wurde. Das sollte man in die Überlegung einbeziehen.

          Es wirft die Frage auf, aus welchem Grund in Abwesenheit einer passenderen Rolle trotz breiter Unterstützung nicht auf Toggle-Button zurückgegriffen wurde. Meine Vermutung ist, dass es in diesem speziellen Fall keinen zusätzlichen Nutzen bringt, sondern lediglich die Komplexität erhöht. Schließlich gibt es mit dem aria-expanded-Attribut bereits einen zweiwertigen Zustand, der in allen APIs abgebildet werden kann. Worin liegt der Vorteil, wenn zusätzlich zu expanded und collapsed noch pressed und not pressed angesagt wird?

          Ich halte aria-pressed in diesem speziellen Fall nicht für sinnvoll und würde darum von dessen Gebrauch abraten. Aber ich lasse mich gerne eines Besseren belehren.

          Viele Grüße,

          Orlok

          --
          „Dance like it hurts.
          Make love like you need money.
          Work when people are watching.“ — Dogbert