Zur Navigation

Zwei Navis (desktop-first), die mobile per Toggle-Klickbutton aus-/einklappen sollen [2]

11 Martin

Ausgehend von dem Code, den ich im Eingangspost gepostet habe, habe ich nun das JavaScript unten angelegt, mit dem sich leider nichts tut.
Grundsätzlich funktioniert die JS-Datei, der übliche Hello World-Test hat mit meinem Nav-Button funktioniert.

Ich habe mich am ehesten an dem freecodecamp-Beispiel orientiert, wobei dort das Ganze wegen der DD-Untermenüs komplexer ist und seltsamerweise im HTML für nav und das div, in dem die ul steckt, die gleiche Klasse "menu" vergeben wurde.

Die eigentliche Toggle-Funktion macht jedes Beispiel etwas anders. Zwei Beispiele mit "if, else", zwei andere mit einer weiteren Konstante "(is)Expanded".
Bei dem kurzen JS von https://xane514.medium.com/aria-controls-for-creating-a-mobile-navbar-6001012153a0 , der eine "const expanded" anlegt, kommt der EventListener ganz am Schluss des Codes?


Zum Einblenden des ul-Menüs habe ich eine Klasse "menue-zeigen" im MQ ergänzt, angelehnt an die show-Klasse beim freecodecamp-Beispiel. Die Toggle-Funktion "toggle-Button" nimmt das dann auf:

ul.headnav-haupt-ul.menue-zeigen {
    display: block;
}


Der JS-Code sieht wie folgt aus und ich verstehe das so einigermaßen. Was müsste man daran ändern (liegt wohl am Menü einblenden)?

// button und ul der headnav als Konstanten definiert
const headnavButton = document.querySelector('.headnav-button');
const headnavMenue = document.querySelector('.headnav-haupt-ul');

// der Button bekommt eine EventListener-Klick-Funktion; Funkt.-Name
headnavButton.addEventListener("click", toggleButton);

// Jetzt wird die Funktion "toggleButton" definiert
function toggleButton() {
    headnavMenue.toggle("menue-zeigen"); //im MQ neu angelegt
    headnavButton.setAttribute(
        "aria-expanded",
        headnavButton.getAttribute("aria-expanded") === "false" ? "true" : "false"
    );
}

18.09.2025 19:27

12 Jörg Kruse

Ausgehend von dem Code, den ich im Eingangspost gepostet habe, habe ich nun das JavaScript unten angelegt, mit dem sich leider nichts tut.

Gibt die JavaScript-Konsole irgendwelche Fehlermeldungen aus? (in Firefox über Strg Umschalt + K erreichbar)

Kannst du eine Testseite verlinken? dann könnte man diese leichter live debuggen.

    headnavMenue.toggle("menue-zeigen"); //im MQ neu angelegt

Die Funktion toggle() muss auf die classList des Elements headnavMenue angewandt werden:

headnavMenue.classList.toggle("menue-zeigen");

Bei dem kurzen JS von https://xane514.medium.com/aria-controls-for-creating-a-mobile-navbar-6001012153a0 , der eine "const expanded" anlegt, kommt der EventListener ganz am Schluss des Codes?

Ja, ich würde die EventListener auch nach den Funktionsdefinitionen platzieren. In den EventListenern werden die zuvor definierten Funkionen ja aufgerufen.

19.09.2025 10:09 | geändert: 19.09.2025 10:29

13 Martin

Kannst du eine Testseite verlinken? dann könnte man diese leichter live debuggen.

Das wäre natürlich hilfreich, aber ich habe die Website in Visual Studio Code und rufe sie im FF über einen lokalen Server (extension "Live Server") auf.
Wenn es nötig ist, würde ich den Code bei codepen, fiddler oder so ähnlichen Codehilfen hochladen. Habe das aber noch nie gemacht und keinen Account dort.

Gibt die JavaScript-Konsole irgendwelche Fehlermeldungen aus? (in Firefox über Strg Umschalt + K erreichbar).

Die Funktion toggle() muss auf die classList des Elements headnavMenue angewandt werden:

Die Konsole hat nur die auch von dir angesprochene Funktion toggleButton moniert.
Es war tatsächlich das fehlende classList, ich hatte es weggelassen, weil ich dachte, es sei ein im Beispiel selbst vergebener Klassenname, den ich nicht habe.

Ich habe classList in der JS-Funktion ergänzt und jetzt klappt es.
Hurra, ein Erfolgserlebnis (ich kann's zur Zeit gebrauchen...).

Das Styling sieht zwar noch furchtbar aus, weil die unregelmäßigen Ränder der ul-Links hauptsächlich von nav kommen, löchrig und unterschiedlich breit sind (je nach anchor-Länge), aber das mache ich zum Schluss.

Woher weiß der Browser eigentlich, dass die ul-Links wie gewünscht nach unten ausklappen sollen? Ist das eingebauter Standard, den ich bei den zusätzlichen DD-Untermenüs, die die nächste Website hat, dann explizit ändern muss? Denn dort sollen die Untermenü-Links dann seitlich nach rechts ausklappen.

Ich habe das Ganze zwar halbwegs verstanden, aber die entscheidende Codestelle, die auch nur bei freecodecamp so aussieht, habe ich nur stumpf abgeschrieben. Kannst du kurz erklären, wie das "false true false" funktioniert, zwecks eines Kommentars dazu:
headnavButton.getAttribute("aria-expanded") === "false" ? "true" : "false"


Das verlinkte, kurze xane-Beispiel verwendet als Einziges zum Togglen und Einblenden des Menüs direkt den Attribut-Selektor aria-expanded true/false (unter "4. Use the aria-expanded..."). Der Selektor ist zwar seltsam kompliziert mit "Tilde" zum ul konstruiert, aber eigentlich scheint es mir naheliegend und verständlich so. Warum macht das keiner so?

Wäre das theoretisch eine Alternative für mich, wobei ich dazu aber vermutlich die toggle-Funktion ändern müsste? Nur theoretisch gedacht, ich bin ja froh, wenn es jetzt klappt mit dem freecodecamp-Bsp.
Wichtig ist nur, dass aria-expanded true/false zum Togglen verwendet wird, da es für die Acc. (v.a. Screenreader) mehr bietet als andere Wege.

Als nächstes müssen noch die Menü-Close-Gründe ergänzt werden.
freecodecamp geht zwar auf alle (zusätzlich zum erneuten Toggle-Klick auf den Button) denkbaren Gründe ein, unter "Collapse dropdown menu". Es betrifft aber nur die DD-Untermenüs, nicht den Menü-Button. Evtl. ist das nicht hilfreich für meine Nav?

Die zusätzlichen Gründe sind jedenfalls folgende:
1. Klick auf einen der ausgeklappten ul-Links
2. ESC
3. Klick irgendwo in den body, aber nicht auf das ul samt Links.

Das Letzte scheint etwas tricky umzusetzen zu sein. Ich habe aber mehrere Code-Beispiele gefunden dazu.
Interessant dazu ist wohl ein kurzes YT-Video, das mein Fall ist und auf 1:50 bis 6:00 eine offenbar recht gute, kurze JS-Lösung dazu gefunden hat. Bin nicht sicher, evtl. verbindet er Grund 1 und 3.

Im Kommentar weist dann noch jemand darauf hin, dass der Code etwas abgeändert werden muss, wenn der HTML-Button ein weiteres Element wie <i> oder <span> enthält. Was bei mir der Fall ist (Pfeil in <span>), s. Eingangspost.

Das Video ist auf
https://www.youtube.com/watch?v=w-SpaTBf-j0
"Easiest Click Outside function to close menu with HTML, CSS, JavaScript".
Vermutlich ist es die 4 Minuten wert, aber wie du meinst.

19.09.2025 14:32

14 Jörg Kruse

Woher weiß der Browser eigentlich, dass die ul-Links wie gewünscht nach unten ausklappen sollen? Ist das eingebauter Standard, den ich bei den zusätzlichen DD-Untermenüs, die die nächste Website hat, dann explizit ändern muss? Denn dort sollen die Untermenü-Links dann seitlich nach rechts ausklappen.

Da es sich bei ul und li um Blockelemente handelt, werden diese standardmäßig von oben nach unten angezeigt. Wenn ein Untermenü (oft mit einem Selektor "... ul ul") nach rechts aufklappt, so wurde dies mit CSS so definiert.

Kannst du kurz erklären, wie das "false true false" funktioniert, zwecks eines Kommentars dazu:
headnavButton.getAttribute("aria-expanded") === "false" ? "true" : "false"

Hier wird der ternary Operator verwendet. Wenn die Bedingung 'btn.getAttribute("aria-expanded") === "false"' erfüllt ist, wird "true" zurückgegeben, anderfalls "false". Zu beachten ist noch, dass es sich hierbei nicht um die boolschen Werte true und false handelt, sondern um Strings mit den Zeichenfolgen "true" und "false"

Youtube-Videos anschauen und die Codes auf den verschiedenen Beispielseiten vergleichen, ist etwas mühselig. Wenn du bestimmte Code-Abschnitte hier reinkopierst, die Fragen aufwerfen oder so nicht funktionieren, kann ich das aber gerne kommentieren. In das Youtube-Video habe ich ausnahmsweise kurz reingeschaut - die Umsetzung kannst du vom Prinzip sicher so machen.

19.09.2025 19:36 | geändert: 19.09.2025 19:42

15 Martin

Zu beachten ist noch, dass es sich hierbei nicht um die boolschen Werte true und false handelt, sondern um Strings mit den Zeichenfolgen "true" und "false"

Auf welche technische Weise das Button-aria-expanded auf true/false hin- und her getoggled wird, spielt für die screenreader vermutlich keine Rolle, oder?

Die zusätzlichen drei close-Gründe habe ich - an freecode angelehnt - jetzt wie folgt probiert, was aber noch nicht klappt.

Folgendes habe ich gemacht:
- Den Code für die drei zusätzlichen Close-Gründe von freecode übernommen.

- Alle drei Gründe greifen auf zwei weitere Funktionen zu, closeDropdownMenu und setAriaExpandedFalse. Deren Code habe ich ebenfalls übernommen.

- In diesen zwei Funktionen werden zwei Untermenü-Konstanten verwendet, dropdownBtn für den Untermenü-Button und dropdown für das Untermenü. Das habe ich mit headnavButton und headnavMenue ersetzt, s. Toggle-Funktion. Bei mir geht es ja um ein einzelnes Hauptmenü.

- Die Konsole hat dann die fehlende Definition der "links"-Konstante im 1. close-Grund moniert (die einzelnen Links des Menüs). Diese Konstante habe ich mit anderem Namen "menueLinks" angelegt und den Namen im 1. close-Grund geändert.

Jetzt kommt in der Konsole die Fehlermeldung "headnavMenue.forEach is not a function".
Alles nicht so einfach... wobei die drei close-Gründe und die zwei Funktionen schon nachvollziehbar sind. Sauber getrennt ist mir ganz recht.

Der gesamte Code sieht jetzt wie folgt aus:

// button, ul der headnav, ul-Links der headnav als Konstanten definiert, selektiert mit ihren Klassen
// menueLinks brauche ich für close-Grund 1
const headnavButton = document.querySelector('.headnav-button');
const headnavMenue = document.querySelector('.headnav-haupt-ul');
const menueLinks = document.querySelectorAll('.headnav-haupt-ul li a');


// Button bekommt eine EventListener-Klick-Funktion zugewiesen und ihr Name festgelegt
headnavButton.addEventListener("click", toggleButton);


// Jetzt wird die Funktion "toggleButton" definiert, angelehnt an freecode-Bsp.
// Ist die Bedingung für false erfüllt, wird true zurückgegeben, sonst false
function toggleButton() {
    headnavMenue.classList.toggle("menue-zeigen"); //im MQ neu angelegte Klasse, die das Menü zeigt
    headnavButton.setAttribute(
        "aria-expanded",
        headnavButton.getAttribute("aria-expanded") === "false" ? "true" : "false"
    );
}


// Zweite Funktion, angelehnt an freecode-Bsp., dropdownBtn durch headnavButton ersetzt
function setAriaExpandedFalse() {
    headnavButton.forEach((btn) => btn.setAttribute("aria-expanded", "false"));
    }


// Dritte Funktion, angelehnt an freecode-Bsp., dropdown durch headnavMenue ersetzt
function closeDropdownMenu() {
    headnavMenue.forEach((drop) => {
    drop.classList.remove("active");
    drop.addEventListener("click", (e) => e.stopPropagation());
    });
}


// close-Grund 1: close dropdown menu when the dropdown links are clicked
menueLinks.forEach((link) =>
    link.addEventListener("click", () => {
        closeDropdownMenu();
        setAriaExpandedFalse();
    })
);


// close-Grund 2: close dropdown menu when you click on the document body
document.documentElement.addEventListener("click", () => {
    closeDropdownMenu();
    setAriaExpandedFalse();
});


// close-Grund 3: close dropdown when the escape key is pressed
document.addEventListener("keydown", (e) => {
    if (e.key === "Escape") {
        closeDropdownMenu();
        setAriaExpandedFalse();
    }
});

20.09.2025 12:06 | geändert: 20.09.2025 12:08

16 Jörg Kruse

Zu beachten ist noch, dass es sich hierbei nicht um die boolschen Werte true und false handelt, sondern um Strings mit den Zeichenfolgen "true" und "false"
Auf welche technische Weise das Button-aria-expanded auf true/false hin- und her getoggled wird, spielt für die screenreader vermutlich keine Rolle, oder?

Das HTML-Attribut aria-expanded, welches von Screenreadern ausgewertet wird, kann die beiden Werte "true" und "false" enthalten. Die Werte von HTML-Attributen müssen in JavaScript immer als String übergeben werden, deswegen muss man in diesem Fall auf die (sonst unüblichen) Anführungszeichen achten.

Jetzt kommt in der Konsole die Fehlermeldung "headnavMenue.forEach is not a function".

forEach funktioniert nur mit Arrays. headnavMenue ist aber ein einzelnes Element:

const headnavMenue = document.querySelector('.headnav-haupt-ul');

Wenn du stattdessen querySelectorAll() verwendest, erhältst du ein Array mit einem Element, auf was dann auch das forEach angewendet werden kann.

20.09.2025 15:35 | geändert: 20.09.2025 15:36

17 Martin

Wenn du stattdessen querySelectorAll() verwendest, erhältst du ein Array mit einem Element, auf was dann auch das forEach angewendet werden kann.

Nachdem ich das "All" bei querySelector ergänzt habe, funktioniert das Ausklappen nicht mehr.

Die Konsole bringt jetzt mehrfach zwei Fehlermeldungen:

Uncaught TypeError: can't access property "toggle", headnavMenue.classList is undefined
Das betrifft offenbar die anfangs von mir angelegte Funktion toggleButton.

Uncaught TypeError: headnavButton.forEach is not a function
Das betrifft offenbar setAriaExpandedFalse.
Wieder "All" ergänzen?

20.09.2025 22:54

18 Jörg Kruse

Uncaught TypeError: can't access property "toggle", headnavMenue.classList is undefined

Hier muss headnavMenue ein einzelnes Element sein :-\ Irgendwo ist da vorher schon etwas durcheinander geraten.

Im Original wird zwischen dem Array dropdown und dem Element navMenu unterschieden. Du setzt an beider Stelle die Konstante headnavMenue - das kann so nicht funktionieren. Das Element navMenu referenziert das Menü als ganzes, das Array dropdown enthält die einzelnen Untermenüs, siehe das HTML auf freeCodeCamp. Du musst also noch ein Array dropdown mit den Untermenüs definieren, auf welches du dann das "dropdown.forEach((drop) => { ... })" anwenden kannst. Und headnavMenue muss doch als einzelnes Element definiert werden.

Uncaught TypeError: headnavButton.forEach is not a function
Das betrifft offenbar setAriaExpandedFalse.
Wieder "All" ergänzen?

Ja, headnavButton muss hier ein Array sein, welches durch querySelectorAll() erzeugt werden kann.

Edit:
bei headnavButton haben wir das gleiche Problem: im Original wird unterschieden zwischen dem Array dropdownBtn und dem Element hamburgerBtn. Ersteres enthält die Buttons, mit welchen die Untermenüs geöffnet werden, zweiteres referenziert den Hamburger Button für das gesamte Menü.

21.09.2025 20:48 | geändert: 21.09.2025 20:59

19 Martin

Du musst also noch ein Array dropdown mit den Untermenüs definieren, auf welches du dann das "dropdown.forEach((drop) => { ... })" anwenden kannst. Und headnavMenue muss doch als einzelnes Element definiert werden.

bei headnavButton haben wir das gleiche Problem...

Obwohl ich wie erwähnt - anders als freecode - nur einen Button und keine Untermenüs, sondern nur das eine DD-Hauptmenü habe, brauche ich also die 5 Konstanten wie im freecode-Beispiel?
Sprich, den Button zwei Mal und das Menü zwei Mal, beide mit verschiedenen Namen (headnavMenue + z.B. dropdownMenue; headnavButton und z.B. dropdownButton)?

Ich kann aber - anders als freecode - keine vier unterschiedlichen Klassen bzw. ids in Klammern angeben, weil es ja nur einen Button und ein ul-Menü gibt. Ich hätte dann vier Konstanten für nur zwei Elemente, Button und Menü.

So sieht es bei freecode aus:

const dropdownBtn = document.querySelectorAll(".dropdown-btn");
const dropdown = document.querySelectorAll(".dropdown");
const hamburgerBtn = document.getElementById("hamburger");
const navMenu = document.querySelector(".menu");
const links = document.querySelectorAll(".dropdown a");


Dann wäre es so bei mir:

const headnavButton = document.querySelector('.headnav-button');
const dropdownButton = document.querySelectorAll('.headnav-button');
const headnavMenue = document.querySelector('.headnav-haupt-ul');
const dropdownMenue = document.querySelectorAll('.headnav-haupt-ul');
const menueLinks = document.querySelectorAll('.headnav-haupt-ul li a');

22.09.2025 01:48

20 Jörg Kruse

Das ist denke ich nicht sinnvoll. Du bewirkst dadurch Effekte für das Gesamtmenü, die für Untermenüs gedacht sind. Beim Klick auf den Hamburger-Button würden dann zwei Toggle-Aktionen hintereinander ausgeführt, was nicht unbedingt zum gewünschten Resultat führt! Wenn du den JavaScript-Code, der für ein verschachteltes Menü gedacht ist, auch für ein unverschachteltes Menü verwenden möchtest (obwohl er dafür wohl überdimensioniert ist), könntest du den Dropdown-Konstanten leere Arrays zuweisen:

const dropdownButton = [];
const dropdownMenue = [];

... oder Klassen verwenden, die es im HTML nicht gibt:

const dropdownButton = document.querySelectorAll('.does-not-exist');
const dropdownMenue = document.querySelectorAll('.does-not-exist');

... was auch zur Zuweisung von leeren Arrays führen würde.

22.09.2025 18:55 | geändert: 22.09.2025 18:58