Accessibility ใน Custom Element
ทำไม custom element จึงต้องทำงานด้าน accessibility
หัวข้อที่มีชื่อว่า “ทำไม custom element จึงต้องทำงานด้าน accessibility”element แบบเนทีฟอย่าง <button> มี ARIA role, การจัดการ focus และการจัดการคีย์บอร์ดในตัวอยู่แล้ว แต่ custom element เริ่มต้นโดยไม่มีสิ่งเหล่านี้เลย คุณต้องเพิ่มสิ่งที่ browser มักจัดให้ฟรีเข้าไปอย่างชัดเจน
เมื่อผู้ใช้นำทางด้วยคีย์บอร์ดหรือใช้ screen reader browser จะเปิดเผย accessibility tree ที่เป็นภาพแทนของ DOM ที่ขนานกันไป สำหรับให้เทคโนโลยีสิ่งอำนวยความสะดวก (assistive technology) อ่าน element แบบเนทีฟจะเติมข้อมูลลงใน tree นี้โดยอัตโนมัติ แต่ <div> หรือ custom element จะไม่ทำ ซึ่งหมายความว่าผู้ใช้ screen reader อาจไม่ได้รับข้อมูลใด ๆ เกี่ยวกับว่า element นั้นคืออะไรหรือต้องโต้ตอบอย่างไร
ARIA role และ attribute บน host
หัวข้อที่มีชื่อว่า “ARIA role และ attribute บน host”วิธีที่ง่ายที่สุดในการอธิบายจุดประสงค์ของ custom element คือการตั้งค่า attribute role และ aria-* ลงบน host element โดยตรง
class AccessibleCard extends HTMLElement { connectedCallback() { this.setAttribute('role', 'region'); this.setAttribute('aria-label', this.getAttribute('label') || 'Card'); }}การตั้งค่า role="region" บอก assistive technology ว่า element นี้คือ landmark region ส่วน aria-label ให้ชื่อที่เข้าถึงได้ (accessible name) กับ element attribute ทั้งสองจะถูกสะท้อนเข้าไปใน accessibility tree ทันที
delegatesFocus ใน attachShadow
หัวข้อที่มีชื่อว่า “delegatesFocus ใน attachShadow”เมื่อส่ง delegatesFocus: true ให้ attachShadow การคลิกบน shadow host (หรือส่วนใดก็ตามที่ focus ไม่ได้ภายในนั้น) จะย้าย focus ไปยัง element แรกที่ focus ได้ภายใน shadow root สิ่งนี้มีประโยชน์อย่างยิ่งสำหรับ custom input
this.shadow = this.attachShadow({ mode: 'open', delegatesFocus: true });หากไม่มี delegatesFocus การคลิก host element จะไม่ย้าย focus ของคีย์บอร์ดเข้าไปใน shadow DOM ลักษณะที่มองเห็นอาจดูเหมือนถูก focus แต่ accessibility tree ไม่เห็นด้วย และผู้ใช้คีย์บอร์ดจะไม่สามารถโต้ตอบกับ element ได้
การจัดการคีย์บอร์ด
หัวข้อที่มีชื่อว่า “การจัดการคีย์บอร์ด”เพื่อให้ทำงานเหมือนปุ่ม ให้จัดการ keydown สำหรับ Enter และ Space ใน connectedCallback และเรียก action เดียวกันกับ click handler
ปุ่มแบบเนทีฟจะยิง event click เมื่อกด Enter หรือ Space แต่ custom element ไม่ได้รับสิ่งนี้มาฟรี ๆ คุณต้องคอยฟัง keydown และตรวจสอบคีย์เหล่านั้นอย่างชัดเจน ให้เรียก e.preventDefault() กับ Space เสมอ เพื่อป้องกันไม่ให้หน้าเว็บเลื่อน (scroll)
this.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); // same action as the click handler }});internals.role และ ARIA reflection
หัวข้อที่มีชื่อว่า “internals.role และ ARIA reflection”ผ่าน ElementInternals คุณสามารถตั้งค่า property ของ ARIA ได้โดยตรงโดยไม่ทำให้รายการ attribute ของ element เลอะเทอะ ตัวอย่างเช่น internals.role = 'button' และ internals.ariaLabel = 'Close' property ของ ARIA เหล่านี้จะสะท้อนไปยัง accessibility tree โดยไม่ปรากฏให้เห็นเป็น HTML attribute วิธีนี้ทำให้ attribute ของ element ไม่รกเกินจำเป็น และหลีกเลี่ยงความขัดแย้งกับ attribute ที่ผู้เขียนตั้งค่าไว้
ARIA reflection ผ่าน ElementInternals ต้องใช้ attachInternals() ซึ่งในทางกลับกันก็ต้องใช้ static formAssociated = true เฉพาะเมื่อคุณต้องการการเข้าร่วม form ด้วยเท่านั้น สำหรับ ARIA reflection ล้วน ๆ คุณสามารถเรียก attachInternals() ได้โดยไม่ต้องมี formAssociated
เดโมที่รันได้
หัวข้อที่มีชื่อว่า “เดโมที่รันได้”เดโมด้านล่างนิยาม element <focus-btn> ที่แนบ shadow root ด้วย delegatesFocus: true วาง <button> ไว้ภายใน ตั้งค่า role="button" บน host และจัดการคีย์บอร์ด Enter กับ Space
คลิกปุ่มด้วยเมาส์ของคุณ จากนั้นลองกด Tab เพื่อ focus ไปที่และเรียกใช้งานด้วย Enter หรือ Space ทั้งสองเส้นทางควรอัปเดตย่อหน้าด้านล่าง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
ตั้งค่า ARIA ผ่าน setAttribute('role', ...) บน host | เขียนง่าย ตรวจสอบผ่าน DOM inspector ได้ตรง ๆ | attribute อาจถูก override โดยผู้เขียน HTML ที่เรียกใช้ element ของคุณโดยไม่ตั้งใจ |
ตั้งค่า ARIA ผ่าน internals.role / internals.ariaLabel (ElementInternals) | ไม่ทำให้ attribute list ของ element รกและไม่ชนกับค่าที่ผู้เขียนตั้งเอง | ต้องเรียก attachInternals() ก่อน และ browser รุ่นเก่ากว่าสนับสนุนได้จำกัดกว่า attribute ธรรมดา |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ลืม
delegatesFocus— เมื่อสร้าง custom input ที่มี focusable element อยู่ใน shadow DOM แต่ไม่ส่งdelegatesFocus: trueให้attachShadowการคลิก host จะไม่ย้าย focus เข้าไปข้างใน ทำให้ผู้ใช้คีย์บอร์ดงงว่าทำไม tab ไม่ไปที่ element ที่คาดไว้ - ข้าม keyboard handling บน interactive element — การทำให้ element ทำหน้าที่เหมือนปุ่มด้วย
role="button"แต่ไม่จัดการkeydownสำหรับ Enter/Space ทำให้ element นั้นใช้ไม่ได้เลยสำหรับผู้ใช้คีย์บอร์ดและ screen reader แม้ดูเหมือนกดได้ด้วยเมาส์ก็ตาม - ตั้ง ARIA role โดยไม่ตั้ง
tabindex— การใส่role="button"เฉยๆ ไม่ได้ทำให้ element focus ได้อัตโนมัติเหมือน<button>เนทีฟ ต้องเพิ่มtabindex="0"เองด้วย ไม่งั้น element จะถูกข้ามไปตอนกด Tab
💡 ตัวอย่างจากของจริง
GitHub — ใช้ custom element (
<relative-time>,<clipboard-copy>เป็นต้น) ที่ผ่านการทดสอบ accessibility อย่างเข้มงวด เพื่อให้ผู้ใช้ screen reader ใช้งานฟีเจอร์ทั้งหมดได้เท่าเทียมกับผู้ใช้เมาส์Adobe Spectrum Web Components — ออกแบบทุก component ให้รองรับ
delegatesFocusและ ARIA reflection ผ่านElementInternalsเพื่อให้ใช้งานได้ในระบบ design system ระดับ enterprise ที่ต้องผ่านมาตรฐาน accessibility