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 ją 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 demonstrację 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 wykonany, 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 stanem bezstanowym, 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 moduły obsługi zdarzeń nie są przypisane do tego elementu, muszą być powiązane, jeśli mają uzyskiwać 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ść swoich elementów podrzędnych, zwykle nie współpracują dobrze z ramami. 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()
pogrupuje karty i panele, zmieniając ich kolejność, i zapewni, że aktywna będzie tylko 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ł ulepszony przez parsownik. 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ź na wybraną kartę. _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 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 tanio odczytywać.
_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 dana karta steruje.
_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();
Użyj funkcji findIndex()
, aby znaleźć indeks aktualnie wybranego elementu, a następnie odejmij 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 na 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 wykonywanie 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 kierowane 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 służy 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ć tagu 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 polecenie 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 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 settera 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ł dodać element do strony i ustawić wartość jednej z jego właściwości, ale zdefiniował jego definicję z opóźnieniem. 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 efekty 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);
})();