สรุป
<howto-tabs>
จำกัดเนื้อหาที่มองเห็นได้โดยแยกเนื้อหาออกเป็นแผงหลายแผง โดยจะแสดงทีละแผงเท่านั้น ส่วนแท็บที่เกี่ยวข้องทั้งหมดจะแสดงอยู่เสมอ หากต้องการสลับจากแผงหนึ่งไปยังอีกแผงหนึ่ง คุณต้องเลือกแท็บที่เกี่ยวข้อง
ผู้ใช้สามารถเปลี่ยนการเลือกแท็บที่ใช้งานอยู่ได้โดยคลิกหรือใช้ปุ่มลูกศร
หากปิดใช้ JavaScript แผงทั้งหมดจะแสดงแทรกสลับกับแท็บที่เกี่ยวข้อง ตอนนี้แท็บจะทำหน้าที่เป็นส่วนหัว
ข้อมูลอ้างอิง
การสาธิต
ตัวอย่างการใช้
<style>
howto-tab {
border: 1px solid black;
padding: 20px;
}
howto-panel {
padding: 20px;
background-color: lightgray;
}
howto-tab[selected] {
background-color: bisque;
}
หาก JavaScript ไม่ทำงาน องค์ประกอบจะไม่ตรงกับ :defined
ในกรณีนี้ สไตล์นี้จะเพิ่มระยะห่างระหว่างแท็บกับแผงก่อนหน้า
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>
รหัส
(function() {
กำหนดรหัสแป้นเพื่อช่วยจัดการเหตุการณ์บนแป้นพิมพ์
const KEYCODE = {
DOWN: 40,
LEFT: 37,
RIGHT: 39,
UP: 38,
HOME: 36,
END: 35,
};
อินสแตนซ์ <howto-tabs>
ทั้งหมดจะใช้เทมเพลตสำหรับเนื้อหาของ Shadow DOM ร่วมกัน เพื่อหลีกเลี่ยงการเรียกใช้โปรแกรมแยกวิเคราะห์ด้วย .innerHTML
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 คือองค์ประกอบคอนเทนเนอร์สำหรับแท็บและแผง
รายการย่อยทั้งหมดของ <howto-tabs>
ควรเป็น <howto-tab>
หรือ <howto-tabpanel>
องค์ประกอบนี้ไม่มีสถานะ ซึ่งหมายความว่าไม่มีการแคชค่า ดังนั้นจึงมีการเปลี่ยนแปลงในระหว่างที่ทำงานรันไทม์
class HowtoTabs extends HTMLElement {
constructor() {
super();
ตัวแฮนเดิลเหตุการณ์ที่ไม่ได้แนบอยู่กับองค์ประกอบนี้ต้องมีการเชื่อมโยงหากจำเป็นต้องเข้าถึง this
this._onSlotChange = this._onSlotChange.bind(this);
สําหรับการเพิ่มประสิทธิภาพแบบต่อเนื่อง มาร์กอัปควรสลับระหว่างแท็บและแผง องค์ประกอบที่จัดลำดับย่อยใหม่มักจะทำงานได้ไม่ดีกับเฟรมเวิร์ก แต่จะใช้ Shadow DOM ในการจัดลำดับองค์ประกอบใหม่โดยใช้ช่อง
this.attachShadow({ mode: 'open' });
นำเข้าเทมเพลตที่แชร์เพื่อสร้างช่องโฆษณาสำหรับแท็บและแผง
this.shadowRoot.appendChild(template.content.cloneNode(true));
this._tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
this._panelSlot = this.shadowRoot.querySelector('slot[name=panel]');
องค์ประกอบนี้ต้องตอบสนองต่อเด็กใหม่เนื่องจากเชื่อมโยงแท็บและแผงอย่างมีความหมายโดยใช้ aria-labelledby
และ aria-controls
หน่วยโฆษณาย่อยใหม่จะได้รับการสล็อตโดยอัตโนมัติและทำให้ slotchange เริ่มทำงาน ดังนั้นจึงไม่จำเป็นต้องมี MutationObserver
this._tabSlot.addEventListener('slotchange', this._onSlotChange);
this._panelSlot.addEventListener('slotchange', this._onSlotChange);
}
connectedCallback()
จัดกลุ่มแท็บและแผงโดยเรียงลำดับใหม่ และตรวจสอบว่าแท็บใดแท็บหนึ่งทำงานอยู่
connectedCallback() {
องค์ประกอบต้องจัดการเหตุการณ์การป้อนข้อมูลด้วยตนเองบางส่วนเพื่ออนุญาตให้สลับโดยใช้ปุ่มลูกศรและหน้าแรก / สิ้นสุด
this.addEventListener('keydown', this._onKeyDown);
this.addEventListener('click', this._onClick);
if (!this.hasAttribute('role'))
this.setAttribute('role', 'tablist');
ก่อนหน้านี้ slotchange
เหตุการณ์ไม่ได้เริ่มทำงานเมื่อโปรแกรมแยกวิเคราะห์อัปเกรดองค์ประกอบ ด้วยเหตุนี้ องค์ประกอบจะเรียกใช้ตัวแฮนเดิลด้วยตนเอง เมื่อลักษณะการทำงานใหม่นี้อยู่ในเบราว์เซอร์ทั้งหมดแล้ว คุณจะนำโค้ดด้านล่างนี้ออกได้
Promise.all([
customElements.whenDefined('howto-tab'),
customElements.whenDefined('howto-panel'),
])
.then(() => this._linkPanels());
}
disconnectedCallback()
นำ Listener เหตุการณ์ที่ connectedCallback()
เพิ่มออก
disconnectedCallback() {
this.removeEventListener('keydown', this._onKeyDown);
this.removeEventListener('click', this._onClick);
}
_onSlotChange()
จะมีการเรียกเมื่อมีการเพิ่มหรือนำองค์ประกอบออกจากช่อง Shadow DOM ช่องใดช่องหนึ่ง
_onSlotChange() {
this._linkPanels();
}
_linkPanels()
ลิงก์แท็บกับแผงที่อยู่ติดกันโดยใช้การควบคุม aria และ aria-labelledby
นอกจากนี้ วิธีการนี้ยังช่วยให้มีเพียงแท็บเดียวที่ใช้งานอยู่
_linkPanels() {
const tabs = this._allTabs();
กำหนดแอตทริบิวต์ aria-labelledby
ให้กับแต่ละแผงที่อ้างอิงแท็บที่ควบคุมนั้นๆ
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);
});
องค์ประกอบจะตรวจสอบว่ามีการทำเครื่องหมายแท็บใดเป็นแท็บที่เลือกหรือไม่ หากไม่ได้ตั้งค่าไว้ แสดงว่าได้เลือกแท็บแรกแล้ว
const selectedTab =
tabs.find((tab) => tab.selected) || tabs[0];
จากนั้นสลับไปที่แท็บที่เลือก _selectTab()
จะทำเครื่องหมายแท็บอื่นๆ ทั้งหมดว่ายกเลิกการเลือกแล้ว และซ่อนแผงอื่นๆ ทั้งหมด
this._selectTab(selectedTab);
}
_allPanels()
จะแสดงแผงทั้งหมดในแผงแท็บ ฟังก์ชันนี้จะจดจำผลลัพธ์หากการค้นหา DOM กลายเป็นปัญหาด้านประสิทธิภาพ ข้อเสียของการจดจําคือจะไม่มีการจัดการแท็บและแผงที่เพิ่มแบบไดนามิก
นี่เป็นวิธีการ ไม่ใช่ Getter เพราะ Getter บอกเป็นนัยว่าอ่านได้ราคาถูก
_allPanels() {
return Array.from(this.querySelectorAll('howto-panel'));
}
_allTabs()
แสดงแท็บทั้งหมดในแผงแท็บ
_allTabs() {
return Array.from(this.querySelectorAll('howto-tab'));
}
_panelForTab()
แสดงแผงที่แท็บนั้นๆ ควบคุม
_panelForTab(tab) {
const panelId = tab.getAttribute('aria-controls');
return this.querySelector(`#${panelId}`);
}
_prevTab()
แสดงแท็บที่อยู่ก่อนแท็บที่เลือกในปัจจุบัน โดยจะรวมเมื่อไปถึงแท็บแรก
_prevTab() {
const tabs = this._allTabs();
ใช้ findIndex()
เพื่อหาดัชนีขององค์ประกอบที่เลือกอยู่ในปัจจุบัน แล้วลบด้วย 1 เพื่อหาดัชนีขององค์ประกอบก่อนหน้า
let newIdx = tabs.findIndex((tab) => tab.selected) - 1;
บวก tabs.length
เพื่อให้ดัชนีเป็นจำนวนบวก และนำโมดูลัสมารวมเข้าด้วยกันหากจำเป็น
return tabs[(newIdx + tabs.length) % tabs.length];
}
_firstTab()
แสดงแท็บแรก
_firstTab() {
const tabs = this._allTabs();
return tabs[0];
}
_lastTab()
แสดงแท็บสุดท้าย
_lastTab() {
const tabs = this._allTabs();
return tabs[tabs.length - 1];
}
_nextTab()
จะได้รับแท็บที่ตามหลังแท็บที่เลือกอยู่ โดยต่อท้ายแท็บสุดท้ายด้วย
_nextTab() {
const tabs = this._allTabs();
let newIdx = tabs.findIndex((tab) => tab.selected) + 1;
return tabs[newIdx % tabs.length];
}
reset()
จะทำเครื่องหมายแท็บทั้งหมดว่ายกเลิกการเลือกแล้วและจะซ่อนแผงทั้งหมด
reset() {
const tabs = this._allTabs();
const panels = this._allPanels();
tabs.forEach((tab) => tab.selected = false);
panels.forEach((panel) => panel.hidden = true);
}
_selectTab()
จะทำเครื่องหมายแท็บที่ระบุไว้เป็นแท็บที่เลือกไว้ นอกจากนี้ ยังเป็นการเลิกซ่อนแผงที่ตรงกับแท็บที่ระบุไว้ด้วย
_selectTab(newTab) {
ยกเลิกการเลือกแท็บทั้งหมดและซ่อนแผงทั้งหมด
this.reset();
รับแผงที่เชื่อมโยงกับ newTab
const newPanel = this._panelForTab(newTab);
หากไม่มีแผงอยู่ ให้ล้มเลิก
if (!newPanel)
throw new Error(`No panel with id ${newPanelId}`);
newTab.selected = true;
newPanel.hidden = false;
newTab.focus();
}
_onKeyDown()
จะจัดการการกดแป้นในแผงแท็บ
_onKeyDown(event) {
หากการกดแป้นไม่ได้เกิดจากองค์ประกอบแท็บเอง การกดแป้นพิมพ์นั้นเป็นการกดแป้นภายในแผงหรือในพื้นที่ว่าง คุณไม่ต้องดำเนินการใดๆ
if (event.target.getAttribute('role') !== 'tab')
return;
อย่าจัดการแป้นพิมพ์ลัดที่ใช้แป้นกดร่วมที่เทคโนโลยีความช่วยเหลือพิเศษมักใช้
if (event.altKey)
return;
เคสสวิตช์จะกำหนดว่าแท็บใดควรทำเครื่องหมายว่าใช้งานอยู่โดยขึ้นอยู่กับแป้นที่กด
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;
ระบบจะไม่สนใจการกดปุ่มอื่นๆ และส่งกลับไปยังเบราว์เซอร์
default:
return;
}
เบราว์เซอร์อาจมีฟังก์ชันในระบบบางอย่างซึ่งผูกอยู่กับแป้นลูกศร หน้าแรก หรือจุดสิ้นสุด องค์ประกอบเรียกใช้ preventDefault()
เพื่อป้องกันไม่ให้เบราว์เซอร์ดำเนินการใดๆ
event.preventDefault();
เลือกแท็บใหม่ที่กำหนดไว้ในกรณีเปลี่ยน
this._selectTab(newTab);
}
_onClick()
จะจัดการการคลิกในแผงแท็บ
_onClick(event) {
หากไม่ได้กำหนดเป้าหมายการคลิกไปยังองค์ประกอบแท็บ ก็หมายความว่าการคลิกดังกล่าวเป็นการคลิกภายในแผงหรือในพื้นที่ว่าง คุณไม่ต้องดำเนินการใดๆ
if (event.target.getAttribute('role') !== 'tab')
return;
แต่หากอยู่ในองค์ประกอบแท็บ ให้เลือกแท็บนั้น
this._selectTab(event.target);
}
}
customElements.define('howto-tabs', HowtoTabs);
howtoTabCounter
จะนับจำนวนอินสแตนซ์ <howto-tab>
รายการที่สร้าง ระบบจะใช้หมายเลขดังกล่าวเพื่อสร้างรหัสที่ไม่ซ้ำกัน
let howtoTabCounter = 0;
HowtoTab
คือแท็บสำหรับแผงแท็บ <howto-tabs>
<howto-tab>
ควรใช้กับ role="heading"
ในมาร์กอัปเสมอเพื่อให้อรรถศาสตร์ยังคงใช้งานได้เมื่อ JavaScript ไม่ทำงาน
<howto-tab>
จะประกาศว่าเป็นเจ้าของ <howto-panel>
ใดโดยใช้รหัสของแผงนั้นเป็นค่าสำหรับแอตทริบิวต์ aria-controls
<howto-tab>
จะสร้างรหัสที่ไม่ซ้ำกันโดยอัตโนมัติหากไม่มีการระบุไว้
class HowtoTab extends HTMLElement {
static get observedAttributes() {
return ['selected'];
}
constructor() {
super();
}
connectedCallback() {
หากเรียกใช้ JavaScript จะทำงาน และองค์ประกอบเปลี่ยนบทบาทเป็น tab
this.setAttribute('role', 'tab');
if (!this.id)
this.id = `howto-tab-generated-${howtoTabCounter++}`;
ตั้งสถานะเริ่มต้นที่กำหนดไว้อย่างชัดเจน
this.setAttribute('aria-selected', 'false');
this.setAttribute('tabindex', -1);
this._upgradeProperty('selected');
}
ตรวจสอบว่าพร็อพเพอร์ตี้มีค่าอินสแตนซ์หรือไม่ ในกรณีนี้ ให้คัดลอกค่าและลบพร็อพเพอร์ตี้อินสแตนซ์เพื่อไม่ให้ตัวตั้งค่าพร็อพเพอร์ตี้คลาส สุดท้าย ส่งค่านี้ไปยังตัวตั้งค่าพร็อพเพอร์ตี้คลาสเพื่อให้ทริกเกอร์ผลข้างเคียงใดๆ เพื่อเป็นการป้องกันกรณีต่างๆ เช่น เฟรมเวิร์กอาจเพิ่มองค์ประกอบลงในหน้าเว็บและตั้งค่าในพร็อพเพอร์ตี้หนึ่งแล้ว แต่ Lazy โหลดคำจำกัดความ หากไม่มีการป้องกันนี้ องค์ประกอบที่อัปเกรดจะพลาดพร็อพเพอร์ตี้ดังกล่าว และพร็อพเพอร์ตี้ของอินสแตนซ์จะทำให้ไม่มีการเรียกใช้ตัวตั้งค่าพร็อพเพอร์ตี้คลาส
_upgradeProperty(prop) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
พร็อพเพอร์ตี้และแอตทริบิวต์ที่เกี่ยวข้องควรตรงกัน ในการดำเนินการนี้ ตัวตั้งค่าพร็อพเพอร์ตี้สำหรับ selected
จะจัดการค่าที่แท้จริง/ไม่ถูกต้อง และแสดงถึงสถานะของแอตทริบิวต์ โปรดทราบว่าจะไม่มีผลข้างเคียงเกิดขึ้นในเครื่องมือตั้งค่าพร็อพเพอร์ตี้ เช่น ตัวตั้งค่าไม่ได้ตั้งค่า aria-selected
งานดังกล่าวจะเกิดขึ้นใน attributeChangedCallback
แทน ตามกฎทั่วไปแล้ว ควรตั้งค่าพร็อพเพอร์ตี้หรือแอตทริบิวต์ให้เข้าใจได้ไม่ถูกต้อง และในกรณีที่การตั้งค่าพร็อพเพอร์ตี้หรือแอตทริบิวต์ควรจะทำให้เกิดผลข้างเคียง (เช่น การตั้งค่าแอตทริบิวต์ ARIA ที่เกี่ยวข้อง) ให้ทำงานได้ใน attributeChangedCallback()
วิธีนี้จะช่วยให้คุณไม่ต้องจัดการสถานการณ์มีแอตทริบิวต์/ที่พักที่ซับซ้อน
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>
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);
})();