Podsumowanie
<howto-tabs>
ograniczanie widocznych treści przez rozdzielenie ich na kilka paneli. W danym momencie widoczny jest tylko jeden panel, ale wszystkie odpowiednie karty są zawsze widoczne. Aby przełączyć się z jednego panelu na inny, należy wybrać odpowiednią kartę.
Użytkownik może zmienić aktywną kartę, klikając lub używając klawiszy strzałek.
Jeśli JavaScript jest wyłączony, wszystkie panele są wyświetlane przeplatane z odpowiednimi kartami. Karty pełnią teraz funkcję nagłówków.
Dokumentacja
Prezentacja
Wyświetl prezentację na żywo w GitHubie
Przykład użycia
<style>
howto-tab {
border: 1px solid black;
padding: 20px;
}
howto-panel {
padding: 20px;
background-color: lightgray;
}
howto-tab[selected] {
background-color: bisque;
}
Jeśli kod JavaScript nie zostanie uruchomiony, element nie będzie pasować do :defined
. W takim przypadku ten styl dodaje odstęp między kartami a poprzednim panelem.
howto-tabs:not(:defined), howto-tab:not(:defined), howto-panel:not(:defined) {
display: block;
}
</style>
<howto-tabs>
<howto-tab role="heading" slot="tab">Tab 1</howto-tab>
<howto-panel role="region" slot="panel">Content 1</howto-panel>
<howto-tab role="heading" slot="tab">Tab 2</howto-tab>
<howto-panel role="region" slot="panel">Content 2</howto-panel>
<howto-tab role="heading" slot="tab">Tab 3</howto-tab>
<howto-panel role="region" slot="panel">Content 3</howto-panel>
</howto-tabs>
Kod
(function() {
Definiowanie kodów klawiszy, aby ułatwić obsługę zdarzeń związanych z klawiaturą.
const KEYCODE = {
DOWN: 40,
LEFT: 37,
RIGHT: 39,
UP: 38,
HOME: 36,
END: 35,
};
Aby uniknąć wywoływania parsowania za pomocą .innerHTML
w przypadku każdego nowego wystąpienia, wszystkie instancje .innerHTML
udostępniają szablon treści DOM-u cieni.<howto-tabs>
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: flex;
flex-wrap: wrap;
}
::slotted(howto-panel) {
flex-basis: 100%;
}
</style>
<slot name="tab"></slot>
<slot name="panel"></slot>
`;
HowtoTabs to element kontenera dla kart i paneli.
Wszystkie elementy podrzędne elementu <howto-tabs>
powinny mieć wartość <howto-tab>
lub <howto-tabpanel>
. Ten element jest stanów, co oznacza, że żadne wartości nie są przechowywane w pamięci podręcznej, a zatem zmiany są wprowadzane w czasie działania.
class HowtoTabs extends HTMLElement {
constructor() {
super();
Jeśli metody obsługi zdarzeń nie są przypisane do tego elementu, muszą zostać powiązane, jeśli mają mieć dostęp do this
.
this._onSlotChange = this._onSlotChange.bind(this);
W przypadku stopniowego ulepszania oznaczenia powinny się naprzemiennie pojawiać w kartach i panelach. Elementy, które zmieniają kolejność elementów podrzędnych, zwykle nie działają dobrze w ramkach. Zamiast tego do zmiany kolejności elementów za pomocą slotów używany jest model shadow DOM.
this.attachShadow({ mode: 'open' });
Zaimportuj udostępniony szablon, aby utworzyć sloty dla kart i paneli.
this.shadowRoot.appendChild(template.content.cloneNode(true));
this._tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
this._panelSlot = this.shadowRoot.querySelector('slot[name=panel]');
Ten element musi reagować na nowe elementy podrzędne, ponieważ łączy karty i panele semantycznie za pomocą elementów aria-labelledby
i aria-controls
. Nowe elementy potomne zostaną automatycznie umieszczone w slotach i spowoduje to wywołanie slotchange, więc nie jest potrzebny obiekt MutationObserver.
this._tabSlot.addEventListener('slotchange', this._onSlotChange);
this._panelSlot.addEventListener('slotchange', this._onSlotChange);
}
connectedCallback()
grupuje karty i panele, zmieniając ich kolejność, i zapewnia, że aktywna jest dokładnie 1 karta.
connectedCallback() {
Element musi obsługiwać ręczne zdarzenia wprowadzania, aby umożliwić przełączanie za pomocą klawiszy strzałek i Home / End.
this.addEventListener('keydown', this._onKeyDown);
this.addEventListener('click', this._onClick);
if (!this.hasAttribute('role'))
this.setAttribute('role', 'tablist');
Do niedawna zdarzenia slotchange
nie były wywoływane, gdy element został uaktualniony przez parsowanie. Z tego powodu element wywołuje ręcznie metodę obsługi. Gdy nowe zachowanie zostanie wprowadzone we wszystkich przeglądarkach, kod poniżej można usunąć.
Promise.all([
customElements.whenDefined('howto-tab'),
customElements.whenDefined('howto-panel'),
])
.then(() => this._linkPanels());
}
disconnectedCallback()
usuwa detektory zdarzeń dodane przez connectedCallback()
.
disconnectedCallback() {
this.removeEventListener('keydown', this._onKeyDown);
this.removeEventListener('click', this._onClick);
}
_onSlotChange()
jest wywoływany za każdym razem, gdy element zostanie dodany lub usunięty z jednego z cienia okienek DOM.
_onSlotChange() {
this._linkPanels();
}
_linkPanels()
łączy karty z sąsiednimi panelami za pomocą atrybutów aria-controls i aria-labelledby
. Dodatkowo metoda zapewnia, że tylko jedna karta jest aktywna.
_linkPanels() {
const tabs = this._allTabs();
Przypisz do każdego panelu atrybut aria-labelledby
, który odwołuje się do karty sterującej.
tabs.forEach((tab) => {
const panel = tab.nextElementSibling;
if (panel.tagName.toLowerCase() !== 'howto-panel') {
console.error(`Tab #${tab.id} is not a` +
`sibling of a <howto-panel>`);
return;
}
tab.setAttribute('aria-controls', panel.id);
panel.setAttribute('aria-labelledby', tab.id);
});
Element sprawdza, czy któraś z kart jest zaznaczona. Jeśli nie, pierwsza karta jest wybrana.
const selectedTab =
tabs.find((tab) => tab.selected) || tabs[0];
Następnie przejdź do wybranej karty. _selectTab()
odznacza wszystkie pozostałe karty i ukrywanie pozostałych paneli.
this._selectTab(selectedTab);
}
_allPanels()
zwraca wszystkie panele w panelu kart. Ta funkcja może zapamiętać wynik, jeśli zapytania do DOM-u staną się problemem z wydajnością. Minusem zapamiętywania jest to, że dynamicznie dodane karty i panele nie będą obsługiwane.
Jest to metoda, a nie metoda dostępu, ponieważ metoda dostępu zakłada, że jest to tania operacja odczytu.
_allPanels() {
return Array.from(this.querySelectorAll('howto-panel'));
}
_allTabs()
zwraca wszystkie karty w panelu kart.
_allTabs() {
return Array.from(this.querySelectorAll('howto-tab'));
}
_panelForTab()
zwraca panel, którym steruje dana karta.
_panelForTab(tab) {
const panelId = tab.getAttribute('aria-controls');
return this.querySelector(`#${panelId}`);
}
_prevTab()
zwraca kartę, która znajduje się przed aktualnie wybraną, a po osiągnięciu pierwszej karty wraca do pierwszej.
_prevTab() {
const tabs = this._allTabs();
Funkcja findIndex()
zwraca indeks aktualnie wybranego elementu i odejmuje 1, aby uzyskać indeks poprzedniego elementu.
let newIdx = tabs.findIndex((tab) => tab.selected) - 1;
Dodaj tabs.length
, aby mieć pewność, że indeks jest liczbą dodatnią, i w razie potrzeby uzyskać moduł do zaokrąglania.
return tabs[(newIdx + tabs.length) % tabs.length];
}
_firstTab()
zwraca pierwszą kartę.
_firstTab() {
const tabs = this._allTabs();
return tabs[0];
}
_lastTab()
zwraca ostatnią kartę.
_lastTab() {
const tabs = this._allTabs();
return tabs[tabs.length - 1];
}
_nextTab()
zwraca kartę, która znajduje się po bieżąco wybranej karcie, a po dotarciu do ostatniej karty wraca na początek.
_nextTab() {
const tabs = this._allTabs();
let newIdx = tabs.findIndex((tab) => tab.selected) + 1;
return tabs[newIdx % tabs.length];
}
reset()
odznacza wszystkie karty i ukrywanie wszystkich paneli.
reset() {
const tabs = this._allTabs();
const panels = this._allPanels();
tabs.forEach((tab) => tab.selected = false);
panels.forEach((panel) => panel.hidden = true);
}
_selectTab()
oznacza wybraną kartę. Dodatkowo odkrywa panel odpowiadający danej karcie.
_selectTab(newTab) {
Odznacz wszystkie karty i ukryj wszystkie panele.
this.reset();
Pobieranie panelu, z którym powiązany jest element newTab
.
const newPanel = this._panelForTab(newTab);
Jeśli panel nie istnieje, przerwij działanie.
if (!newPanel)
throw new Error(`No panel with id ${newPanelId}`);
newTab.selected = true;
newPanel.hidden = false;
newTab.focus();
}
_onKeyDown()
obsługuje naciśnięcia klawiszy w panelu kart.
_onKeyDown(event) {
Jeśli naciśnięcie klawisza nie pochodziło z elementu karty, było to naciśnięcie klawisza w panelu lub pustej przestrzeni. Nie ma nic do zrobienia.
if (event.target.getAttribute('role') !== 'tab')
return;
Nie obsługuj skrótów klawiszowych, które są zwykle używane przez technologie wspomagające.
if (event.altKey)
return;
W zależności od wciśniętego klawisza switch-case określi, która karta powinna zostać oznaczona jako aktywna.
let newTab;
switch (event.keyCode) {
case KEYCODE.LEFT:
case KEYCODE.UP:
newTab = this._prevTab();
break;
case KEYCODE.RIGHT:
case KEYCODE.DOWN:
newTab = this._nextTab();
break;
case KEYCODE.HOME:
newTab = this._firstTab();
break;
case KEYCODE.END:
newTab = this._lastTab();
break;
Każde inne naciśnięcie klawisza jest ignorowane i przekazywane z powrotem do przeglądarki.
default:
return;
}
Przeglądarka może mieć niektóre funkcje natywne powiązane z klawiszami strzałek, Home lub End. Element wywołuje funkcję preventDefault()
, aby uniemożliwić przeglądarce podejmowanie jakichkolwiek działań.
event.preventDefault();
Wybierz nową kartę, która została określona w sekcji switch-case.
this._selectTab(newTab);
}
_onClick()
obsługuje kliknięcia w panelu kart.
_onClick(event) {
Jeśli kliknięcie nie było skierowane na element karty, było to kliknięcie w panelu lub puste miejsce. Nie ma nic do zrobienia.
if (event.target.getAttribute('role') !== 'tab')
return;
Jeśli jednak znajdował się on na elemencie karty, wybierz tę kartę.
this._selectTab(event.target);
}
}
customElements.define('howto-tabs', HowtoTabs);
howtoTabCounter
zlicza liczbę utworzonych instancji <howto-tab>
. Numer jest używany do generowania nowych, unikalnych identyfikatorów.
let howtoTabCounter = 0;
HowtoTab
to karta panelu kart <howto-tabs>
. W znacznikach <howto-tab>
należy zawsze używać znacznika role="heading"
, aby semantyka była dostępna, gdy JavaScript nie działa.
Element <howto-tab>
określa, do którego elementu <howto-panel>
należy, używając identyfikatora tego panelu jako wartości atrybutu aria-controls.
Jeśli nie podasz identyfikatora, <howto-tab>
automatycznie wygeneruje unikalny identyfikator.
class HowtoTab extends HTMLElement {
static get observedAttributes() {
return ['selected'];
}
constructor() {
super();
}
connectedCallback() {
Jeśli to zostanie wykonane, JavaScript zacznie działać, a element zmieni swoją rolę na tab
.
this.setAttribute('role', 'tab');
if (!this.id)
this.id = `howto-tab-generated-${howtoTabCounter++}`;
Ustaw dobrze zdefiniowany stan początkowy.
this.setAttribute('aria-selected', 'false');
this.setAttribute('tabindex', -1);
this._upgradeProperty('selected');
}
Sprawdź, czy dana właściwość ma wartość instancji. Jeśli tak, skopiuj wartość i usuń właściwość instancji, aby nie przyćmiewała ona metody settera właściwości klasy. Na koniec prześlij wartość do metody inicjującej właściwości klasy, aby mogła wywołać efekty uboczne. Ma to na celu ochronę przed sytuacjami, w których framework mógłby na przykład dodać element do strony i ustawić wartość jednej z jego właściwości, ale zlazy loadem zdefiniowałby jego definicję. Bez tej ochrony zaktualizowany element nie miałby tej właściwości, a właściwość instancji uniemożliwiałaby wywołanie metody setera właściwości klasy.
_upgradeProperty(prop) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
Właściwości i odpowiadające im atrybuty powinny być lustrzanym odbiciem siebie nawzajem. W tym celu metoda selected
obsługuje wartości prawda/fałsz i odzwierciedla je w stanie atrybutu. Pamiętaj, że w setterze właściwości nie występują żadne skutki uboczne. Na przykład metoda setter nie ustawia wartości aria-selected
. Zamiast tego te działania są wykonywane w attributeChangedCallback
. Ogólnie rzecz biorąc, uczyń metody ustawiania właściwości bardzo prostymi, a jeśli ustawienie właściwości lub atrybutu ma wywołać efekt uboczny (np. ustawienie odpowiedniego atrybutu ARIA), zrób to w metodie attributeChangedCallback()
. Dzięki temu unikniesz konieczności zarządzania złożonymi scenariuszami ponownego korzystania z atrybutów i właściwości.
attributeChangedCallback() {
const value = this.hasAttribute('selected');
this.setAttribute('aria-selected', value);
this.setAttribute('tabindex', value ? 0 : -1);
}
set selected(value) {
value = Boolean(value);
if (value)
this.setAttribute('selected', '');
else
this.removeAttribute('selected');
}
get selected() {
return this.hasAttribute('selected');
}
}
customElements.define('howto-tab', HowtoTab);
let howtoPanelCounter = 0;
HowtoPanel
to panel na karcie <howto-tabs>
.
class HowtoPanel extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.setAttribute('role', 'tabpanel');
if (!this.id)
this.id = `howto-panel-generated-${howtoPanelCounter++}`;
}
}
customElements.define('howto-panel', HowtoPanel);
})();