Container-Adapting Tabs With “More” Button

Avatar of Osvaldas Valutis
Osvaldas Valutis am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Oder das Prioritätsnavigationsmuster, oder ein progressiv kollabierendes Navigationsmenü. Wir können es auf mindestens drei Arten benennen.

Es gibt mehrere UX-Lösungen für Tabs und Menüs, und jede von ihnen hat ihre eigenen Vorteile gegenüber einer anderen. Sie müssen nur die beste für den Fall auswählen, den Sie lösen möchten. In der Design- und Entwicklungsagentur Kollegorna debattierten wir über die am besten geeignete UX-Technik für Tabs für die Website unseres Kunden…

Wir einigten uns darauf, dass es eine One-Liner sein sollte, da die Anzahl der Tab-Elemente unbekannt ist, und grenzten unsere Optionen auf zwei ein: horizontales Scrollen und adaptive Anpassung mit einem "Mehr"-Button. Erstens ist das Problem bei ersterem, dass horizontales Scrollen als Funktion für Benutzer nicht immer visuell offensichtlich ist (insbesondere bei schmalen Elementen wie Tabs), während was sonst offensichtlicher sein kann als ein Button ("Mehr"), richtig? Zweitens ist das horizontale Scrollen mit einem mausgesteuerten Gerät keine sehr bequeme Sache, so dass wir möglicherweise unsere UI mit zusätzlichen Pfeiltasten komplexer gestalten müssen. Alles in allem entschieden wir uns für letztere Option

Planung

Die Hauptintrige hier ist, ob es möglich ist, das ohne JavaScript? zu erreichen. Teilweise ja, aber die Einschränkungen, die damit einhergehen, machen es wahrscheinlich nur für ein Museumskonzept gut und nicht für reale Szenarien (jedenfalls hat Kenan wirklich gute Arbeit geleistet). Trotzdem bedeutet die Abhängigkeit von JS nicht, dass wir es nicht nutzbar machen können, falls die Technologie aus irgendeinem Grund nicht verfügbar ist. Progressive Enhancement und Graceful Degradation sind die Gewinner!

Da die Anzahl der Tab-Elemente unsicher oder volatil ist, werden wir Flexbox verwenden, die sicherstellt, dass die Elemente schön im Container-Element verteilt sind, ohne die Breiten festzulegen.

Erstes Prototyp

Es gibt zwei Listen, sowohl visuell als auch technisch: eine für Elemente, die in den Container passen, und eine für Elemente, die nicht passen. Da wir uns auf JavaScript verlassen werden, ist es völlig in Ordnung, unseren anfänglichen Markup mit nur einer Liste zu haben (wir werden sie mit JS duplizieren)

<nav class="tabs">
  <ul class="-primary">
    <li><a href="...">Falkenberg</a></li>
    <li><a href="...">Braga</a></li>
    <!-- ... -->
  </ul>
</nav>

Mit einer winzigen Prise Flexbox-basiertem CSS werden die Dinge hier ernst. Ich werde die dekorativen CSS-Eigenschaften in meinen Beispielen hier überspringen und hier das platzieren, was wirklich wichtig ist

.tabs .-primary {
  display: flex;
}
.tabs .-primary > li {
  flex-grow: 1;
}

Hier ist, was wir bereits haben

Container-Adapting Tabs With More Button

Graceful Degradation

Bevor wir es mit JavaScript progressiv verbessern, stellen wir sicher, dass es gracefully abbaut, wenn kein JS verfügbar ist. Es gibt mehrere Gründe für die Abwesenheit von JS: es wird noch geladen, es gibt fatale Fehler, es konnte nicht über das Netzwerk übertragen werden.

.tabs:not(.--jsfied) {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

Und sobald JavaScript vorhanden ist, wird der Klasse --jsfied zum Container-Element hinzugefügt, was das obige CSS *neutralisiert*

container.classList.add('--jsfied')

Es stellt sich heraus, dass die horizontale Scroll-Strategie, die ich zuvor erwähnt habe, hier gut genutzt werden könnte! Wenn nicht genügend Platz für Menüelemente vorhanden ist, werden die überlaufenden Inhalte im Container abgeschnitten und Scrollbalken angezeigt. Das ist doch viel besser als leere Flächen oder etwas Kaputtes, nicht wahr?

Container-Adapting Tabs With More Button

Fehlende Teile

Zuerst fügen wir die fehlenden DOM-Teile ein

  • Sekundäre (Dropdown-)Liste, die eine Kopie der Hauptliste ist;
  • "Mehr"-Button.
const container = document.querySelector('.tabs')
const primary = container.querySelector('.-primary')
const primaryItems = container.querySelectorAll('.-primary > li:not(.-more)')
container.classList.add('--jsfied')

// insert "more" button and duplicate the list

primary.insertAdjacentHTML('beforeend', `
  <li class="-more">
    <button type="button" aria-haspopup="true" aria-expanded="false">
      More &darr;
    </button>
    <ul class="-secondary">
      ${primary.innerHTML}
    </ul>
  </li>
`)
const secondary = container.querySelector('.-secondary')
const secondaryItems = secondary.querySelectorAll('li')
const allItems = container.querySelectorAll('li')
const moreLi = primary.querySelector('.-more')
const moreBtn = moreLi.querySelector('button')
moreBtn.addEventListener('click', (e) => {
  e.preventDefault()
  container.classList.toggle('--show-secondary')
  moreBtn.setAttribute('aria-expanded', container.classList.contains('--show-secondary'))
})

Hier verschachteln wir die *sekundäre* Liste in die *primäre* und verwenden einige aria-* Eigenschaften. Wir wollen, dass unser Navigationsmenü barrierefrei ist, richtig?

Es gibt auch einen Event-Handler, der an den "Mehr"-Button gebunden ist und die Klasse --show-secondary auf dem Container-Element umschaltet. Wir werden ihn verwenden, um die sekundäre Liste ein- und auszublenden. Nun stylen wir die neuen Teile. Sie möchten den "Mehr"-Button visuell hervorheben.

.tabs {
  position: relative;
}
.tabs .-secondary {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
}
.tabs.--show-secondary .-secondary {
  display: block;
}

Hier sind wir angekommen

Container-Adapting Tabs With More Button

Offensichtlich brauchen wir Code, der die Tabs ein- und ausblendet…

Tabs in den Listen ein- und ausblenden

Aufgrund von Flexbox werden die Tab-Elemente niemals in mehrere Zeilen umbrechen und auf ihre kleinstmöglichen Breiten schrumpfen. Das bedeutet, dass wir jedes Element einzeln durchlaufen, ihre Breiten aufsummieren, sie mit der Breite des .tabs-Elements vergleichen und die Sichtbarkeit bestimmter Tabs entsprechend umschalten können. Dazu erstellen wir eine Funktion namens doAdapt und packen den folgenden Code in diesen Abschnitt.

Zuerst enthüllen wir visuell alle Elemente

allItems.forEach((item) => {
  item.classList.remove('--hidden')
})

Nebenbei bemerkt, .--hidden funktioniert so, wie Sie es wahrscheinlich erwartet haben

.tabs .--hidden {
  display: none;
}

Mathe-Zeit! Ich muss Sie enttäuschen, wenn Sie fortgeschrittene Mathematik erwartet haben. Wie bereits beschrieben, durchlaufen wir jeden *primären* Tab, indem wir ihre Breiten unter der Variablen stopWidth summieren. Wir führen auch eine Prüfung durch, ob das Element in den Container passt, blenden das Element aus, falls nicht, und speichern seinen Index für später.

let stopWidth = moreBtn.offsetWidth
let hiddenItems = []
const primaryWidth = primary.offsetWidth
primaryItems.forEach((item, i) => {
  if(primaryWidth >= stopWidth + item.offsetWidth) {
    stopWidth += item.offsetWidth
  } else {
    item.classList.add('--hidden')
    hiddenItems.push(i)
  }
})

Danach müssen wir die entsprechenden Elemente aus der *sekundären* Liste ausblenden, die in der *primären* Liste sichtbar geblieben sind. Ebenso blenden wir den "Mehr"-Button aus, wenn keine Tabs ausgeblendet wurden.

if(!hiddenItems.length) {
  moreLi.classList.add('--hidden')
  container.classList.remove('--show-secondary')
  moreBtn.setAttribute('aria-expanded', false)
}
else {  
  secondaryItems.forEach((item, i) => {
    if(!hiddenItems.includes(i)) {
      item.classList.add('--hidden')
    }
  })
}

Stellen Sie abschließend sicher, dass die Funktion doAdapt zu den richtigen Zeitpunkten ausgeführt wird

doAdapt() // adapt immediately on load
window.addEventListener('resize', doAdapt) // adapt on window resize

Idealerweise sollte der Resize-Event-Handler debounced werden, um unnötige Berechnungen zu vermeiden.

Meine Damen und Herren, hier ist das Ergebnis (spielen Sie mit der Größenänderung des Demo-Fensters)

Siehe den Pen Container-Adapting Tabs With “More” Button von Osvaldas (@osvaldas) auf CodePen.

Ich könnte meinen Artikel hier beenden, aber es gibt noch einen Schritt, den wir gehen können, um ihn besser zu machen, und einige Dinge zu beachten…

Verbesserungen

Es wurde in der obigen Demo implementiert, aber wir haben ein kleines Detail nicht besprochen, das die UX unseres Tab-Widgets verbessert. Es ist das automatische Ausblenden der Dropdown-Liste, wenn der Benutzer irgendwo außerhalb der Liste klickt. Dafür können wir einen globalen Klick-Listener binden und prüfen, ob das angeklickte Element oder eines seiner Eltern die *sekundäre* Liste oder der "Mehr"-Button ist. Wenn nicht, wird die Dropdown-Liste geschlossen.

document.addEventListener('click', (e) => {
  let el = e.target
  while(el) {
    if(el === secondary || el === moreBtn) {
      return;
    }
    el = el.parentNode
  }
  container.classList.remove('--show-secondary')
  moreBtn.setAttribute('aria-expanded', false)
})

Randfälle

Lange Tab-Titel

Sie haben sich vielleicht gefragt, wie sich das Widget bei langen Tab-Titeln verhält. Nun, dafür haben Sie mindestens zwei Möglichkeiten…

  1. Lassen Sie Titel in die nächste Zeile umbrechen, wie sie sich standardmäßig verhalten (Sie können auch den Zeilenumbruch mit word-wrap: break-word aktivieren)
Container-Adapting Tabs With More Button
  1. Oder Sie können alle Arten von Umbrüchen in der *primären* Liste mit white-space: nowrap deaktivieren. Das Skript ist flexibel genug, um die *zu langen* Elemente in das Dropdown zu verschieben (wo die Titel frei umbrechen dürfen), indem es die kürzeren Geschwister beiseite lässt
Container-Adapting Tabs With More Button

Viele Tabs

Auch wenn die *sekundäre* Liste position: absolute hat, spielt es keine Rolle, wie lang die Höhe Ihres Dokuments ist. Solange das Container-Element oder seine Eltern nicht position: fixed sind, passt sich das Dokument an und die unteren Elemente sind durch Scrollen auf der Seite erreichbar.

Eine Sache, die man beachten sollte

Die Dinge können knifflig werden, wenn die Tabs Buttons und keine semantischen Anker sind, was bedeutet, dass ihre Reaktion auf Klicks von JavaScript bestimmt wird, z. B.: dynamische Tabs. Das Problem hier ist, dass Tab-Button-Event-Handler nicht zusammen mit dem Markup dupliziert werden. Ich sehe mindestens zwei Ansätze, um das zu lösen

  • Platzieren Sie dynamische Event-Handler-Anbindungen direkt nach dem adaptiven Tab-Code;
  • Verwenden Sie stattdessen eine Event-Delegationsmethode (denken Sie an live() von jQuery).

Leider treten Events in Mengen auf: Wahrscheinlich haben Ihre Tabs einen *ausgewählten* Zustand, der den aktuellen Tab visuell anzeigt, daher ist es auch wichtig, die Zustände gleichzeitig zu verwalten. Andernfalls schalten Sie das Tablet um und Sie sind verloren.

Browserkompatibilität

Auch wenn ich in den Beispielen und der Demo ES6-Syntax verwendet habe, sollte sie von einem Compiler wie Babel in ES5 konvertiert werden, um die Browserunterstützung erheblich zu erweitern (bis einschließlich IE9).

Sie können die Flexbox-Implementierung auch mit einer älteren Version und Syntax (bis IE10) erweitern. Wenn Sie auch nicht-Flexbox-Browser unterstützen müssen, können Sie immer Feature-Erkennung mit CSS @supports durchführen, die Technik progressiv anwenden und sich für ältere Browser auf horizontales Scrollen verlassen.

Viel Spaß beim Tabben!