การสร้างไดอะล็อกที่สามารถเข้าถึงได้ตั้งแต่เริ่มต้น

เผยแพร่แล้ว: 2022-03-10
สรุปโดยย่อ ↬ กล่อง โต้ตอบมีอยู่ทั่วไปในการออกแบบอินเทอร์เฟซที่ทันสมัย ​​(ไม่ว่าจะดีหรือไม่ดี) และยังมีอีกจำนวนมากที่ไม่สามารถเข้าถึงเทคโนโลยีอำนวยความสะดวกได้ ในบทความนี้ เราจะเจาะลึกถึงวิธีสร้างสคริปต์สั้น ๆ เพื่อสร้างกล่องโต้ตอบที่สามารถเข้าถึงได้

ก่อนอื่นอย่าทำที่บ้าน อย่าเขียนบทสนทนาหรือห้องสมุดของคุณเอง มีผลิตภัณฑ์มากมายที่ผ่านการทดสอบ ตรวจสอบ ใช้และนำกลับมาใช้ใหม่ และคุณควรเลือกใช้สิ่งเหล่านี้มากกว่าของคุณเอง a11y-dialog เป็นหนึ่งในนั้น แต่มีมากกว่านั้น (ระบุไว้ในตอนท้ายของบทความนี้)

ผมขอถือเอาโพสต์นี้เป็นโอกาสในการเตือนทุกท่านให้ ระมัดระวังในการใช้กล่องโต้ตอบ กำลังทดสอบเพื่อแก้ไขปัญหาการออกแบบทั้งหมดโดยเฉพาะบนอุปกรณ์เคลื่อนที่ แต่มักมีวิธีอื่นในการเอาชนะปัญหาการออกแบบ เรามักจะตกหลุมรักการใช้ไดอะล็อกอย่างรวดเร็ว ไม่ใช่เพราะจำเป็นต้องเป็นทางเลือกที่ถูกต้อง แต่เพราะว่าง่าย พวกเขาแยกปัญหาเรื่องอสังหาริมทรัพย์โดยการแลกเปลี่ยนเพื่อเปลี่ยนบริบท ซึ่งไม่ใช่การแลกเปลี่ยนที่ถูกต้องเสมอไป ประเด็นคือ: พิจารณาว่าไดอะล็อกเป็นรูปแบบการออกแบบที่ถูกต้องหรือไม่ก่อนใช้งาน

ในบทความนี้ เราจะเขียน ไลบรารี JavaScript ขนาดเล็กสำหรับสร้างไดอะล็อกที่สามารถเข้าถึงได้ ตั้งแต่เริ่มต้น (โดยพื้นฐานแล้วคือการสร้าง a11y-dialog) เป้าหมายคือการทำความเข้าใจกับสิ่งที่เข้าไป เราจะไม่จัดการกับสไตล์มากเกินไป แค่ส่วน JavaScript เราจะใช้ JavaScript ที่ทันสมัยเพื่อความเรียบง่าย (เช่น คลาสและฟังก์ชันลูกศร) แต่โปรดทราบว่าโค้ดนี้อาจไม่ทำงานในเบราว์เซอร์รุ่นเก่า

  1. การกำหนด API
  2. การสร้างอินสแตนซ์ของกล่องโต้ตอบ
  3. แสดงและซ่อน
  4. ปิดด้วยโอเวอร์เลย์
  5. ปิดด้วยการหลบหนี
  6. โฟกัสดักจับ
  7. รักษาโฟกัส
  8. ฟื้นฟูโฟกัส
  9. ให้ชื่อที่สามารถเข้าถึงได้
  10. การจัดการเหตุการณ์ที่กำหนดเอง
  11. การทำความสะอาด
  12. เอามารวมกัน
  13. ห่อ

การกำหนด API

อันดับแรก เราต้องการกำหนดวิธีที่เราจะใช้สคริปต์โต้ตอบของเรา เราจะทำให้มันง่ายที่สุดในการเริ่มต้น เราให้องค์ประกอบ HTML รูทสำหรับกล่องโต้ตอบของเรา และอินสแตนซ์ที่เราได้รับมีเมธอด .show(.. .show(..) และ .hide(..)

 class Dialog { constructor(element) {} show() {} hide() {} }

การสร้างอินสแตนซ์ของไดอะล็อก

สมมติว่าเรามี HTML ต่อไปนี้:

 <div>This will be a dialog.</div>

และเรายกตัวอย่างบทสนทนาของเราดังนี้:

 const element = document.querySelector('#my-dialog') const dialog = new Dialog(element)

มีบางสิ่งที่เราต้องทำภายใต้ประทุนเมื่อสร้างอินสแตนซ์:

  • ซ่อนเพื่อให้ถูกซ่อนโดยค่าเริ่มต้น ( hidden )
  • ทำเครื่องหมายว่าเป็นกล่องโต้ตอบสำหรับเทคโนโลยีอำนวยความสะดวก ( role="dialog" )
  • ทำให้ส่วนที่เหลือของหน้าเฉื่อยเมื่อเปิด ( aria-modal="true" )
 constructor (element) { // Store a reference to the HTML element on the instance so it can be used // across methods. this.element = element this.element.setAttribute('hidden', true) this.element.setAttribute('role', 'dialog') this.element.setAttribute('aria-modal', true) }

โปรดทราบว่าเราสามารถเพิ่มแอตทริบิวต์ 3 เหล่านี้ใน HTML เริ่มต้นของเราโดยไม่จำเป็นต้องเพิ่มด้วย JavaScript แต่วิธีนี้ทำให้มองไม่เห็น สคริปต์ของเราทำให้แน่ใจว่าสิ่งต่างๆ จะทำงานตามที่ควรจะเป็น ไม่ว่าเราจะคิดจะเพิ่มคุณลักษณะทั้งหมดของเราหรือไม่ก็ตาม

แสดงและซ่อน

เรามีสองวิธี: วิธีหนึ่งเพื่อแสดงกล่องโต้ตอบและอีกวิธีหนึ่งเพื่อซ่อน วิธีการเหล่านี้ไม่ได้ช่วยอะไรมาก (ในตอนนี้) นอกจากการสลับแอตทริบิวต์ที่ hidden ในองค์ประกอบรูท นอกจากนี้เรายังจะรักษาบูลีนบนอินสแตนซ์เพื่อให้สามารถประเมินได้อย่างรวดเร็วว่ากล่องโต้ตอบแสดงขึ้นหรือไม่ สิ่งนี้จะมีประโยชน์ในภายหลัง

 show() { this.isShown = true this.element.removeAttribute('hidden') } hide() { this.isShown = false this.element.setAttribute('hidden', true) }

เพื่อหลีกเลี่ยงไม่ให้มองเห็นไดอะล็อกก่อนที่จาวาสคริปต์จะเริ่มทำงานและซ่อนโดยการเพิ่มแอตทริบิวต์ การเพิ่มการ hidden ไปยังไดอะล็อกโดยตรงใน HTML ตั้งแต่เริ่มต้นอาจเป็นสิ่งที่น่าสนใจ

 <div hidden>This will be a dialog.</div>

ปิดด้วยโอเวอร์เลย์

การคลิกนอกกล่องโต้ตอบควรปิด มีหลายวิธีในการทำเช่นนั้น วิธีหนึ่งอาจเป็นได้คือการฟังเหตุการณ์การคลิกทั้งหมดบนหน้าเว็บและกรองเหตุการณ์ที่เกิดขึ้นภายในกล่องโต้ตอบออก แต่ก็ค่อนข้างซับซ้อนที่ต้องทำ

อีกวิธีหนึ่งคือการฟังเหตุการณ์การคลิกบนโอเวอร์เลย์ (บางครั้งเรียกว่า “ฉากหลัง”) โอเวอร์เลย์เองก็สามารถทำได้ง่ายๆ เหมือนกับ <div> ที่มีสไตล์บางอย่าง

ดังนั้นเมื่อเปิดกล่องโต้ตอบ เราจำเป็นต้องผูกเหตุการณ์การคลิกบนโอเวอร์เลย์ เราสามารถให้ ID หรือคลาสที่แน่นอนเพื่อให้สามารถสืบค้นได้ หรือเราอาจให้แอตทริบิวต์ข้อมูลแก่มัน ฉันมักจะชอบสิ่งเหล่านี้สำหรับตะขอเกี่ยวกับพฤติกรรม มาแก้ไข HTML ของเราตามลำดับ:

 <div hidden> <div data-dialog-hide></div> <div>This will be a dialog.</div> </div>

ตอนนี้ เราสามารถสืบค้นองค์ประกอบด้วยแอตทริบิวต์ data-dialog-hide ภายในไดอะล็อก และให้ฟังการคลิกที่ซ่อนไดอะล็อก

 constructor (element) { // … rest of the code // Bind our methods so they can be used in event listeners without losing the // reference to the dialog instance this._show = this.show.bind(this) this._hide = this.hide.bind(this) const closers = [...this.element.querySelectorAll('[data-dialog-hide]')] closers.forEach(closer => closer.addEventListener('click', this._hide)) }

สิ่งที่ดีเกี่ยวกับการมีบางสิ่งที่ค่อนข้างทั่วไปแบบนี้คือ เราสามารถใช้สิ่งเดียวกันนี้กับปุ่มปิดของกล่องโต้ตอบได้เช่นกัน

 <div hidden> <div data-dialog-hide></div> <div> This will be a dialog. <button type="button" data-dialog-hide>Close</button> </div> </div>
เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

ปิดด้วยการหลบหนี

ไม่เพียงแต่กล่องโต้ตอบจะถูกซ่อนเมื่อคลิกภายนอกเท่านั้น แต่ควรซ่อนกล่องโต้ตอบเมื่อกด Esc ด้วย เมื่อเปิดกล่องโต้ตอบ เราสามารถผูกฟังคีย์บอร์ดกับเอกสาร และนำออกเมื่อปิด วิธีนี้จะฟังเฉพาะการกดปุ่มในขณะที่กล่องโต้ตอบเปิดอยู่แทนที่จะเปิดตลอดเวลา

 show() { // … rest of the code // Note: `_handleKeyDown` is the bound method, like we did for `_show`/`_hide` document.addEventListener('keydown', this._handleKeyDown) } hide() { // … rest of the code // Note: `_handleKeyDown` is the bound method, like we did for `_show`/`_hide` document.removeEventListener('keydown', this._handleKeyDown) } handleKeyDown(event) { if (event.key === 'Escape') this.hide() }

โฟกัสดักจับ

ตอนนี้เป็นสิ่งที่ดี การจับโฟกัสภายในบทสนทนานั้นเป็นแก่นแท้ของเรื่องทั้งหมด และจะต้องเป็นส่วนที่ซับซ้อนที่สุด (แม้ว่าอาจจะไม่ซับซ้อนอย่างที่คุณคิด)

แนวคิดนี้ค่อนข้างเรียบง่าย: เมื่อกล่องโต้ตอบเปิดขึ้น เราจะฟังการกด Tab หากกด Tab บนองค์ประกอบที่โฟกัสได้สุดท้ายของกล่องโต้ตอบ เราจะย้ายโฟกัสไปที่องค์ประกอบแรกโดยทางโปรแกรม หากกด Shift + Tab บนองค์ประกอบแรกที่โฟกัสได้ของกล่องโต้ตอบ เราจะย้ายไปยังองค์ประกอบสุดท้าย

ฟังก์ชันอาจมีลักษณะดังนี้:

 function trapTabKey(node, event) { const focusableChildren = getFocusableChildren(node) const focusedItemIndex = focusableChildren.indexOf(document.activeElement) const lastIndex = focusableChildren.length - 1 const withShift = event.shiftKey if (withShift && focusedItemIndex === 0) { focusableChildren[lastIndex].focus() event.preventDefault() } else if (!withShift && focusedItemIndex === lastIndex) { focusableChildren[0].focus() event.preventDefault() } }

สิ่งต่อไปที่เราต้องหาคือวิธีรับองค์ประกอบที่โฟกัสได้ทั้งหมดของกล่องโต้ตอบ ( getFocusableChildren ) เราจำเป็นต้องค้นหาองค์ประกอบทั้งหมดที่สามารถโฟกัสได้ในทางทฤษฎี จากนั้นเราต้องแน่ใจว่าองค์ประกอบเหล่านั้นมีประสิทธิผล

ส่วนแรกสามารถทำได้ด้วยตัวเลือกที่โฟกัสได้ มันเป็นแพ็คเกจเล็ก ๆ ที่ฉันเขียนซึ่งมีตัวเลือกมากมาย:

 module.exports = [ 'a[href]:not([tabindex^="-"])', 'area[href]:not([tabindex^="-"])', 'input:not([type="hidden"]):not([type="radio"]):not([disabled]):not([tabindex^="-"])', 'input[type="radio"]:not([disabled]):not([tabindex^="-"]):checked', 'select:not([disabled]):not([tabindex^="-"])', 'textarea:not([disabled]):not([tabindex^="-"])', 'button:not([disabled]):not([tabindex^="-"])', 'iframe:not([tabindex^="-"])', 'audio[controls]:not([tabindex^="-"])', 'video[controls]:not([tabindex^="-"])', '[contenteditable]:not([tabindex^="-"])', '[tabindex]:not([tabindex^="-"])', ]

และนั่นก็เพียงพอแล้วที่จะพาคุณไปถึงที่นั่น 99% เราสามารถใช้ตัวเลือกเหล่านี้เพื่อค้นหาองค์ประกอบที่โฟกัสได้ทั้งหมด จากนั้นจึงตรวจสอบองค์ประกอบทั้งหมดเพื่อให้แน่ใจว่ามองเห็นได้บนหน้าจอจริง (และไม่ได้ซ่อนหรือบางอย่าง)

 import focusableSelectors from 'focusable-selectors' function isVisible(element) { return element => element.offsetWidth || element.offsetHeight || element.getClientRects().length } function getFocusableChildren(root) { const elements = [...root.querySelectorAll(focusableSelectors.join(','))] return elements.filter(isVisible) }

ตอนนี้เราสามารถอัปเดตวิธี handleKeyDown ของเราได้แล้ว:

 handleKeyDown(event) { if (event.key === 'Escape') this.hide() else if (event.key === 'Tab') trapTabKey(this.element, event) }

รักษาโฟกัส

สิ่งหนึ่งที่มักถูกมองข้ามเมื่อสร้างกล่องโต้ตอบที่เข้าถึงได้คือการทำให้แน่ใจว่าโฟกัสยังคงอยู่ภายในกล่องโต้ตอบ แม้ว่า หน้าจะสูญเสียโฟกัสไปแล้ว ลองคิดแบบนี้: จะเกิดอะไรขึ้นหากกล่องโต้ตอบเปิดขึ้น เราเน้นที่แถบ URL ของเบราว์เซอร์ แล้วเริ่มแท็บอีกครั้ง กับดักโฟกัสของเราจะไม่ทำงาน เนื่องจากจะรักษาโฟกัสไว้ภายในกล่องโต้ตอบเมื่ออยู่ภายในกล่องโต้ตอบเพื่อเริ่มต้นเท่านั้น

ในการแก้ไขปัญหานั้น เราสามารถผูกผู้ฟังโฟกัสกับองค์ประกอบ <body> เมื่อกล่องโต้ตอบปรากฏขึ้น และย้ายโฟกัสไปยังองค์ประกอบแรกที่โฟกัสได้ภายในกล่องโต้ตอบ

 show () { // … rest of the code // Note: `_maintainFocus` is the bound method, like we did for `_show`/`_hide` document.body.addEventListener('focus', this._maintainFocus, true) } hide () { // … rest of the code // Note: `_maintainFocus` is the bound method, like we did for `_show`/`_hide` document.body.removeEventListener('focus', this._maintainFocus, true) } maintainFocus(event) { const isInDialog = event.target.closest('[aria-modal="true"]') if (!isInDialog) this.moveFocusIn() } moveFocusIn () { const target = this.element.querySelector('[autofocus]') || getFocusableChildren(this.element)[0] if (target) target.focus() }

องค์ประกอบใดที่ควรโฟกัสเมื่อเปิดกล่องโต้ตอบ และอาจขึ้นอยู่กับประเภทของเนื้อหาที่กล่องโต้ตอบจะแสดง โดยทั่วไป มีสองตัวเลือก:

  • เน้นองค์ประกอบแรก
    นี่คือสิ่งที่เราทำ เนื่องจากทำให้ง่ายขึ้นโดยที่เรามีฟังก์ชัน getFocusableChildren อยู่แล้ว
  • โฟกัสที่ปุ่มปิด
    นี่เป็นวิธีแก้ปัญหาที่ดี โดยเฉพาะอย่างยิ่งหากปุ่มอยู่ในตำแหน่งที่สัมพันธ์กับกล่องโต้ตอบ เราสามารถทำให้สิ่งนี้เกิดขึ้นได้โดยสะดวกโดยการวางปุ่มปิดเป็นองค์ประกอบแรกของกล่องโต้ตอบ หากปุ่มปิดอยู่ในโฟลว์ของเนื้อหากล่องโต้ตอบ ในตอนท้าย อาจเป็นปัญหาได้หากกล่องโต้ตอบมีเนื้อหาจำนวนมาก (ดังนั้นจึงสามารถเลื่อนได้) เนื่องจากปุ่มจะเลื่อนเนื้อหาไปจนสุดเมื่อเปิดอยู่
  • เน้นกล่องโต้ตอบเอง
    สิ่งนี้ไม่ธรรมดามากในไลบรารีไดอะล็อก แต่ก็ควรใช้งานได้ (แม้ว่าจะต้องเพิ่ม tabindex="-1" เข้าไปเพื่อให้เป็นไปได้เนื่องจากองค์ประกอบ <div> ไม่สามารถโฟกัสได้โดยค่าเริ่มต้น)

โปรดทราบว่าเราตรวจสอบว่ามีองค์ประกอบที่มีแอตทริบิวต์ autofocus HTML ภายในกล่องโต้ตอบหรือไม่ ซึ่งในกรณีนี้ เราจะย้ายโฟกัสไปที่องค์ประกอบนั้นแทนรายการแรก

ฟื้นฟูโฟกัส

เราสามารถดักจับโฟกัสภายในกล่องโต้ตอบได้สำเร็จ แต่เราลืมย้ายโฟกัสภายในกล่องโต้ตอบเมื่อเปิดขึ้น ในทำนองเดียวกัน เราจำเป็นต้องกู้คืนโฟกัสกลับไปที่องค์ประกอบที่มีอยู่ก่อนที่กล่องโต้ตอบจะเปิดขึ้น

เมื่อแสดงไดอะล็อก เราสามารถเริ่มต้นด้วยการอ้างอิงถึงองค์ประกอบที่มีโฟกัส ( document.activeElement ) โดยส่วนใหญ่ ปุ่มนี้จะเป็นปุ่มที่โต้ตอบด้วยเพื่อเปิดกล่องโต้ตอบ แต่ในกรณีที่เปิดกล่องโต้ตอบโดยทางโปรแกรม ซึ่งพบไม่บ่อยนัก อาจเป็นอย่างอื่น

 show() { this.previouslyFocused = document.activeElement // … rest of the code this.moveFocusIn() }

เมื่อซ่อนกล่องโต้ตอบ เราสามารถย้ายโฟกัสกลับไปที่องค์ประกอบนั้นได้ เราป้องกันด้วยเงื่อนไขเพื่อหลีกเลี่ยงข้อผิดพลาด JavaScript หากองค์ประกอบนั้นไม่มีอยู่แล้ว (หรือถ้าเป็น SVG):

 hide() { // … rest of the code if (this.previouslyFocused && this.previouslyFocused.focus) { this.previouslyFocused.focus() } }

ให้ชื่อที่สามารถเข้าถึงได้

เป็นสิ่งสำคัญที่กล่องโต้ตอบของเราจะต้องมีชื่อที่สามารถเข้าถึงได้ ซึ่งจะแสดงรายการในแผนผังการช่วยสำหรับการเข้าถึง มีสองวิธีในการแก้ไขปัญหา หนึ่งในนั้นคือการกำหนดชื่อในแอตทริบิวต์ aria- aria-label แต่ aria-label มีปัญหา

อีกวิธีหนึ่งคือการมีชื่อในกล่องโต้ตอบของเรา (ไม่ว่าจะซ่อนหรือไม่ก็ตาม) และเชื่อมโยงกล่องโต้ตอบของเรากับแอตทริบิวต์ aria-labelledby อาจมีลักษณะดังนี้:

 <div hidden aria-labelledby="my-dialog-title"> <div data-dialog-hide></div> <div> <h1>My dialog title</h1> This will be a dialog. <button type="button" data-dialog-hide>Close</button> </div> </div>

ฉันเดาว่าเราสามารถทำให้สคริปต์ของเราใช้แอตทริบิวต์นี้แบบไดนามิกโดยอิงจากการมีอยู่ของชื่อและอะไรก็ตาม แต่ฉันจะบอกว่าสิ่งนี้แก้ไขได้ง่ายๆ ด้วยการเขียน HTML ที่เหมาะสมในตอนแรก ไม่จำเป็นต้องเพิ่ม JavaScript สำหรับสิ่งนั้น

การจัดการเหตุการณ์ที่กำหนดเอง

จะเป็นอย่างไรถ้าเราต้องการตอบสนองต่อกล่องโต้ตอบที่เปิดอยู่ หรือปิด? ขณะนี้ยังไม่มีวิธีที่จะทำ แต่การเพิ่มระบบเหตุการณ์ขนาดเล็กไม่ควรยากเกินไป เราต้องการฟังก์ชันในการลงทะเบียนเหตุการณ์ (เรียก .on(..) ) และฟังก์ชันเพื่อยกเลิกการลงทะเบียน ( .off(..) )

 class Dialog { constructor(element) { this.events = { show: [], hide: [] } } on(type, fn) { this.events[type].push(fn) } off(type, fn) { const index = this.events[type].indexOf(fn) if (index > -1) this.events[type].splice(index, 1) } }

จากนั้นเมื่อแสดงและซ่อนเมธอด เราจะเรียกฟังก์ชันทั้งหมดที่ลงทะเบียนไว้สำหรับเหตุการณ์นั้นโดยเฉพาะ

 class Dialog { show() { // … rest of the code this.events.show.forEach(event => event()) } hide() { // … rest of the code this.events.hide.forEach(event => event()) } }

การทำความสะอาด

เราอาจต้องการให้วิธีการล้างกล่องโต้ตอบในกรณีที่เราใช้งานเสร็จแล้ว จะต้องรับผิดชอบในการยกเลิกการลงทะเบียนผู้ฟังเหตุการณ์เพื่อไม่ให้นานกว่าที่ควร

 class Dialog { destroy() { const closers = [...this.element.querySelectorAll('[data-dialog-hide]')] closers.forEach(closer => closer.removeEventListener('click', this._hide)) this.events.show.forEach(event => this.off('show', event)) this.events.hide.forEach(event => this.off('hide', event)) } }

รวมทุกอย่างไว้ด้วยกัน

 import focusableSelectors from 'focusable-selectors' class Dialog { constructor(element) { this.element = element this.events = { show: [], hide: [] } this._show = this.show.bind(this) this._hide = this.hide.bind(this) this._maintainFocus = this.maintainFocus.bind(this) this._handleKeyDown = this.handleKeyDown.bind(this) element.setAttribute('hidden', true) element.setAttribute('role', 'dialog') element.setAttribute('aria-modal', true) const closers = [...element.querySelectorAll('[data-dialog-hide]')] closers.forEach(closer => closer.addEventListener('click', this._hide)) } show() { this.isShown = true this.previouslyFocused = document.activeElement this.element.removeAttribute('hidden') this.moveFocusIn() document.addEventListener('keydown', this._handleKeyDown) document.body.addEventListener('focus', this._maintainFocus, true) this.events.show.forEach(event => event()) } hide() { if (this.previouslyFocused && this.previouslyFocused.focus) { this.previouslyFocused.focus() } this.isShown = false this.element.setAttribute('hidden', true) document.removeEventListener('keydown', this._handleKeyDown) document.body.removeEventListener('focus', this._maintainFocus, true) this.events.hide.forEach(event => event()) } destroy() { const closers = [...this.element.querySelectorAll('[data-dialog-hide]')] closers.forEach(closer => closer.removeEventListener('click', this._hide)) this.events.show.forEach(event => this.off('show', event)) this.events.hide.forEach(event => this.off('hide', event)) } on(type, fn) { this.events[type].push(fn) } off(type, fn) { const index = this.events[type].indexOf(fn) if (index > -1) this.events[type].splice(index, 1) } handleKeyDown(event) { if (event.key === 'Escape') this.hide() else if (event.key === 'Tab') trapTabKey(this.element, event) } moveFocusIn() { const target = this.element.querySelector('[autofocus]') || getFocusableChildren(this.element)[0] if (target) target.focus() } maintainFocus(event) { const isInDialog = event.target.closest('[aria-modal="true"]') if (!isInDialog) this.moveFocusIn() } } function trapTabKey(node, event) { const focusableChildren = getFocusableChildren(node) const focusedItemIndex = focusableChildren.indexOf(document.activeElement) const lastIndex = focusableChildren.length - 1 const withShift = event.shiftKey if (withShift && focusedItemIndex === 0) { focusableChildren[lastIndex].focus() event.preventDefault() } else if (!withShift && focusedItemIndex === lastIndex) { focusableChildren[0].focus() event.preventDefault() } } function isVisible(element) { return element => element.offsetWidth || element.offsetHeight || element.getClientRects().length } function getFocusableChildren(root) { const elements = [...root.querySelectorAll(focusableSelectors.join(','))] return elements.filter(isVisible) }

ห่อ

นั่นเป็นสิ่งที่ค่อนข้าง แต่ในที่สุดเราก็ไปถึงที่นั่น! เป็นอีกครั้งที่ฉันไม่แนะนำให้เปิดไลบรารีไดอะล็อกของคุณเอง เนื่องจากไม่ใช่ไลบรารีที่ตรงไปตรงมาที่สุด และข้อผิดพลาดอาจเป็นปัญหาอย่างมากสำหรับผู้ใช้เทคโนโลยีสิ่งอำนวยความสะดวก แต่อย่างน้อยตอนนี้คุณก็รู้ว่ามันทำงานอย่างไรภายใต้ประทุน!

หากคุณต้องการใช้ไดอะล็อกในโครงการของคุณ ให้ลองใช้หนึ่งในวิธีแก้ไขปัญหาต่อไปนี้ (ช่วยเตือนว่าเรามีรายการส่วนประกอบที่เข้าถึงได้ทั้งหมดของเราด้วย):

  • การใช้งาน Vanilla JavaScript: a11y-dialog โดยคุณอย่างแท้จริง หรือ aria-modal-dialog โดย Scott O'Hara
  • การใช้งาน React: react-a11y-dialog โดยคุณอีกครั้ง เข้าถึง/โต้ตอบจากกรอบงาน Reach หรือ @react-aria/dialog จาก Adobe คุณอาจสนใจในการเปรียบเทียบ 3 ห้องสมุดนี้
  • การใช้งาน Vue: vue-a11y-dialog โดย Moritz Kroger, a11y-vue-dialog โดย Renato de Leao

ต่อไปนี้คือสิ่งที่สามารถเพิ่มได้ แต่ไม่ใช่เพื่อความเรียบง่าย:

  • รองรับการโต้ตอบการแจ้งเตือนผ่านบทบาท alertdialog อ้างถึงเอกสารประกอบ a11y-dialog ในกล่องโต้ตอบการแจ้งเตือน
  • ล็อคความสามารถในการเลื่อนในขณะที่เปิดกล่องโต้ตอบ อ้างถึงเอกสารประกอบ a11y-dialog เกี่ยวกับการล็อคการเลื่อน
  • รองรับองค์ประกอบ HTML <dialog> เนทีฟเนื่องจากเป็นพาร์และไม่สอดคล้องกัน อ้างถึงเอกสารประกอบ a11y-dialog เกี่ยวกับองค์ประกอบการโต้ตอบและงานชิ้นนี้โดย Scott O'hara สำหรับข้อมูลเพิ่มเติมเกี่ยวกับสาเหตุที่ไม่คุ้มกับปัญหา
  • รองรับการซ้อนไดอะล็อกเพราะเป็นที่น่าสงสัย อ้างถึงเอกสารประกอบ a11y-dialog บนไดอะล็อกที่ซ้อนกัน
  • ข้อควรพิจารณาในการปิดกล่องโต้ตอบในการนำทางของเบราว์เซอร์ ในบางกรณี การปิดกล่องโต้ตอบเมื่อกดปุ่มย้อนกลับของเบราว์เซอร์อาจเป็นเรื่องที่สมเหตุสมผล