Özet
<howto-tabs>
, görünür içeriği birden fazla panele ayırarak sınırlandırabilir. Aynı anda yalnızca bir panel görünür. İlgili sekmelerin tümü ise her zaman görünür durumdadır. Bir panelden diğerine geçmek için ilgili sekmenin seçilmesi gerekir.
Kullanıcı, tıklayarak veya ok tuşlarını kullanarak etkin sekmenin seçimini değiştirebilir.
JavaScript devre dışıysa tüm paneller ilgili sekmelerle birlikte gösterilir. Sekmeler artık başlık olarak işlev görür.
Referans
Demo
Canlı demoyu GitHub'da görüntüleme
Örnek kullanım
<style>
howto-tab {
border: 1px solid black;
padding: 20px;
}
howto-panel {
padding: 20px;
background-color: lightgray;
}
howto-tab[selected] {
background-color: bisque;
}
JavaScript çalışmıyorsa öğe :defined
ile eşleşmez. Bu durumda bu stil, sekmeler ile önceki panel arasına boşluk ekler.
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() {
Klavye etkinliklerini işlemeye yardımcı olması için tuş kodlarını tanımlayın.
const KEYCODE = {
DOWN: 40,
LEFT: 37,
RIGHT: 39,
UP: 38,
HOME: 36,
END: 35,
};
Her yeni örnek için ayrıştırıcıyı .innerHTML
ile çağırmamak amacıyla, gölge DOM'un içeriği için bir şablon tüm <howto-tabs>
örnekleri tarafından paylaşılır.
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, sekmeler ve paneller için bir kapsayıcı öğesidir.
<howto-tabs>
öğesinin tüm alt öğeleri <howto-tab>
veya <howto-tabpanel>
olmalıdır. Bu öğe durum bilgisi içermez. Yani hiçbir değer önbelleğe alınmaz ve bu nedenle çalışma zamanında değişiklikler yapılır.
class HowtoTabs extends HTMLElement {
constructor() {
super();
Bu öğeye eklenmemiş etkinlik işleyicilerin, this
öğesine erişmesi gerekiyorsa bağlanmaları gerekir.
this._onSlotChange = this._onSlotChange.bind(this);
Artımlı geliştirme için işaretleme, sekmeler ve paneller arasında değişmelidir. Alt öğelerini yeniden düzenleyen öğeler, çerçevelerle iyi çalışmaz. Bunun yerine, öğeleri aralıkları kullanarak yeniden sıralamak için gölge DOM kullanılır.
this.attachShadow({ mode: 'open' });
Sekme ve panel yuvaları oluşturmak için paylaşılan şablonu içe aktarın.
this.shadowRoot.appendChild(template.content.cloneNode(true));
this._tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
this._panelSlot = this.shadowRoot.querySelector('slot[name=panel]');
Bu öğe, aria-labelledby
ve aria-controls
'ü kullanarak sekmeleri ve paneli anlamsal olarak bağladığı için yeni alt öğelere tepki vermelidir. Yeni çocuklar otomatik olarak alana eklenir ve slotchange etkinleşmesine neden olur. Bu nedenle MutationObserver'a gerek yoktur.
this._tabSlot.addEventListener('slotchange', this._onSlotChange);
this._panelSlot.addEventListener('slotchange', this._onSlotChange);
}
connectedCallback()
, sekmeleri ve panelleri yeniden düzenleyerek gruplandırır ve tam olarak bir sekmenin etkin olduğundan emin olur.
connectedCallback() {
Ok tuşları ve Home / End ile geçiş yapılmasına izin vermek için öğenin bazı manuel giriş etkinliklerini işlemesi gerekir.
this.addEventListener('keydown', this._onKeyDown);
this.addEventListener('click', this._onClick);
if (!this.hasAttribute('role'))
this.setAttribute('role', 'tablist');
Yakın zamana kadar, bir öğe ayrıştırıcı tarafından yükseltildiğinde slotchange
etkinlikleri tetiklenmiyordu. Bu nedenle öğe, işleyiciyi manuel olarak çağırır. Yeni davranış tüm tarayıcılara uygulandıktan sonra aşağıdaki kod kaldırılabilir.
Promise.all([
customElements.whenDefined('howto-tab'),
customElements.whenDefined('howto-panel'),
])
.then(() => this._linkPanels());
}
disconnectedCallback()
, connectedCallback()
tarafından eklenen etkinlik işleyicileri kaldırır.
disconnectedCallback() {
this.removeEventListener('keydown', this._onKeyDown);
this.removeEventListener('click', this._onClick);
}
_onSlotChange()
, gölge DOM yuvalarından birine öğe eklendiğinde veya bir öğe bu yuvalardan kaldırıldığında çağrılır.
_onSlotChange() {
this._linkPanels();
}
_linkPanels()
, aria-controls ve aria-labelledby
kullanarak sekmeleri bitişik panelleriyle bağlar. Ayrıca bu yöntem, yalnızca bir sekmenin etkin olmasını sağlar.
_linkPanels() {
const tabs = this._allTabs();
Her panele, onu kontrol eden sekmeyi belirten bir aria-labelledby
özelliği verin.
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);
});
Öğe, sekmelerden herhangi birinin seçili olarak işaretlenip işaretlenmediğini kontrol eder. Aksi takdirde ilk sekme seçilir.
const selectedTab =
tabs.find((tab) => tab.selected) || tabs[0];
Ardından, seçili sekmeye geçin. _selectTab()
, diğer tüm sekmelerin seçimini kaldırır ve diğer tüm panelleri gizler.
this._selectTab(selectedTab);
}
_allPanels()
, sekme panelindeki tüm panelleri döndürür. DOM sorguları performans sorunu haline gelirse bu işlev sonucu ezberleyebilir. Hatırlama yönteminin dezavantajı, dinamik olarak eklenen sekmelerin ve panellerin işlenmemesidir.
Geter, okumanın ucuz olduğunu ima ettiği için bu bir alıcı değil, bir yöntemdir.
_allPanels() {
return Array.from(this.querySelectorAll('howto-panel'));
}
_allTabs()
, sekme panelindeki tüm sekmeleri döndürür.
_allTabs() {
return Array.from(this.querySelectorAll('howto-tab'));
}
_panelForTab()
, belirtilen sekmenin kontrol ettiği paneli döndürür.
_panelForTab(tab) {
const panelId = tab.getAttribute('aria-controls');
return this.querySelector(`#${panelId}`);
}
_prevTab()
, seçili sekmeden önceki sekmeyi döndürür ve ilk sekmeye ulaştığında döngüye girer.
_prevTab() {
const tabs = this._allTabs();
findIndex()
, geçerli olarak seçili öğenin dizesini bulmak için kullanılır ve önceki öğenin dizesini almak için bir çıkarılır.
let newIdx = tabs.findIndex((tab) => tab.selected) - 1;
Dizin pozitif bir sayı olduğundan emin olmak ve gerekirse modülün sarmalanması için tabs.length
ekleyin.
return tabs[(newIdx + tabs.length) % tabs.length];
}
_firstTab()
ilk sekmeyi döndürür.
_firstTab() {
const tabs = this._allTabs();
return tabs[0];
}
_lastTab()
, son sekmeyi döndürür.
_lastTab() {
const tabs = this._allTabs();
return tabs[tabs.length - 1];
}
_nextTab()
, seçili sekmenin ardından gelen sekmeyi alır ve son sekmeye ulaştığında tekrar başa döner.
_nextTab() {
const tabs = this._allTabs();
let newIdx = tabs.findIndex((tab) => tab.selected) + 1;
return tabs[newIdx % tabs.length];
}
reset()
, tüm sekmeleri "seçili değil" olarak işaretler ve tüm panelleri gizler.
reset() {
const tabs = this._allTabs();
const panels = this._allPanels();
tabs.forEach((tab) => tab.selected = false);
panels.forEach((panel) => panel.hidden = true);
}
_selectTab()
, belirtilen sekmeyi seçili olarak işaretler. Ayrıca, belirtilen sekmeye karşılık gelen paneli gösterir.
_selectTab(newTab) {
Tüm sekmelerin seçimini kaldırın ve tüm panelleri gizleyin.
this.reset();
newTab
öğesinin ilişkili olduğu paneli alın.
const newPanel = this._panelForTab(newTab);
Bu panel yoksa işlemi iptal edin.
if (!newPanel)
throw new Error(`No panel with id ${newPanelId}`);
newTab.selected = true;
newPanel.hidden = false;
newTab.focus();
}
_onKeyDown()
, sekme panelindeki tuş basma işlemlerini yönetir.
_onKeyDown(event) {
Tuş basışı bir sekme öğesinden kaynaklanmadıysa panel içinde veya boş alanda bir tuş basışıydı. Yapılacak bir şey yok.
if (event.target.getAttribute('role') !== 'tab')
return;
Genellikle yardımcı teknolojiler tarafından kullanılan değiştirici kısayolları işleme alınmayın.
if (event.altKey)
return;
Switch-case, basılan tuşa bağlı olarak hangi sekmenin etkin olarak işaretlenmesi gerektiğini belirler.
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;
Diğer tüm tuş basımları yoksayılır ve tarayıcıya geri iletilir.
default:
return;
}
Tarayıcının ok tuşlarına, ana sayfaya veya sona bağlı bazı yerel işlevleri olabilir. Öğe, tarayıcının herhangi bir işlem yapmasını engellemek için preventDefault()
işlevini çağırır.
event.preventDefault();
Switch-case'te belirlenen yeni sekmeyi seçin.
this._selectTab(newTab);
}
_onClick()
, sekme panelindeki tıklamaları işler.
_onClick(event) {
Tıklama, sekme öğesinin kendisinde hedeflenmemişse panelin içinde veya boş alanda bir tıklamadır. Yapılacak bir şey yok.
if (event.target.getAttribute('role') !== 'tab')
return;
Ancak, sekme öğesindeyse ilgili sekmeyi seçin.
this._selectTab(event.target);
}
}
customElements.define('howto-tabs', HowtoTabs);
howtoTabCounter
, oluşturulan <howto-tab>
örneklerinin sayısını sayar. Bu numara, yeni ve benzersiz kimlikler oluşturmak için kullanılır.
let howtoTabCounter = 0;
HowtoTab
, <howto-tabs>
sekme paneli için bir sekmedir. JavaScript'te hata oluştuğunda anlamların kullanılabilir kalması için <howto-tab>
, işaretlemede her zaman role="heading"
ile birlikte kullanılmalıdır.
<howto-tab>
, aria-controls özelliğinin değeri olarak ilgili panelin kimliğini kullanarak hangi <howto-panel>
'ye ait olduğunu belirtir.
<howto-tab>
, belirtilmemişse otomatik olarak benzersiz bir kimlik oluşturur.
class HowtoTab extends HTMLElement {
static get observedAttributes() {
return ['selected'];
}
constructor() {
super();
}
connectedCallback() {
Bu komut yürütülürse JavaScript çalışıyordur ve öğenin rolü tab
olarak değişir.
this.setAttribute('role', 'tab');
if (!this.id)
this.id = `howto-tab-generated-${howtoTabCounter++}`;
İyi tanımlanmış bir ilk durum ayarlayın.
this.setAttribute('aria-selected', 'false');
this.setAttribute('tabindex', -1);
this._upgradeProperty('selected');
}
Bir mülkün örnek değeri olup olmadığını kontrol edin. Bu durumda, sınıf özelliği ayarlayıcısını gölgelememesi için değeri kopyalayın ve örnek özelliğini silin. Son olarak, herhangi bir yan etkiyi tetikleyebilmesi için değeri sınıf mülk ayarlayıcısına iletin. Bu, örneğin bir çerçevenin öğeyi sayfaya eklemiş ve özelliklerinden birinde bir değer ayarlamış ancak tanımını yavaş yüklemiş olabileceği durumlara karşı koruma sağlamak içindir. Bu koruma olmadan, yükseltilen öğe bu mülkü kaçırırdı ve örnek mülkü, sınıf mülk ayarlayıcısının çağrılmasını engellerdi.
_upgradeProperty(prop) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
Tesisler ve ilgili özellikleri birbirini yansıtmalıdır. Bu nedenle, selected
özelliğinin mülk ayarlayıcısı doğru/yanlış değerleri işler ve bunları özelliğin durumuna yansıtır. Mülk ayarlayıcıda herhangi bir yan etki olmadığını unutmayın. Örneğin, ayarlayıcı aria-selected
değerini ayarlamaz. Bunun yerine, bu işlem attributeChangedCallback
'te gerçekleşir. Genel kural olarak, mülk ayarlayıcıları çok hale getirin ve bir mülkün veya özelliğin ayarlanması yan etkiye neden oluyorsa (ör. ilgili bir ARIA özelliğinin ayarlanması) bu işlemi attributeChangedCallback()
içinde yapın. Bu sayede, karmaşık özellik/mülk yeniden giriş senaryolarını yönetmek zorunda kalmazsınız.
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
, <howto-tabs>
sekme paneli için bir paneldir.
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);
})();