Focus Order คือลำดับที่องค์ประกอบอินเทอร์เฟสได้รับโฟกัสเมื่อนำทางด้วยแป้นพิมพ์ Switch Control VoiceOver หรือ TalkBack ในแอปพลิเคชันมือถือ ลำดับโฟกัสจะกำหนดว่าผู้ใช้เคลื่อนที่ระหว่างตัวควบคุมด้วยท่าทางหรือปุ่มต่างๆ อย่างไร ตาม W3C WCAG 2.2, Success Criterion 2.4.3, 2023 โฟกัสต้องเป็นไปตามลำดับตรรกะที่รักษาความหมายของเนื้อหา การละเมิดหลักการนี้เป็นหนึ่งในสาเหตุทั่วไปของ ความล้มเหลว ในการตรวจสอบการเข้าถึง
ประเด็นสำคัญ
Focus Order คือลำดับที่ผู้ใช้เคลื่อนที่ระหว่างองค์ประกอบแบบโต้ตอบด้วยวิธีการป้อนข้อมูลทางเลือก: แป้นพิมพ์ (Tab), Switch Control (ทีละขั้น), VoiceOver (ปัดขวา/ซ้าย) หรือ TalkBack แตกต่างจากเมาส์หรือหน้าจอสัมผัสที่ผู้ใช้เลือกองค์ประกอบโดยตรง การนำทางด้วยโฟกัสเป็นแบบเส้นตรง — แต่ละขั้นจะย้ายโฟกัสไปยังองค์ประกอบถัดไป
ตาม Apple HIG, 2024 VoiceOver ใช้ลำดับขององค์ประกอบในแผนผังการเข้าถึง ซึ่งสร้างขึ้นตามตำแหน่งภาพ: มุมซ้ายบน → มุมขวาล่าง หากหน้าจอมีเค้าโครงที่ซับซ้อน (คอลัมน์ Grid ZStack) แผนผังอาจไม่ตรงกับลำดับภาพ
หลักการ WCAG 2.4.3: “หากหน้าเว็บสามารถ นำทางตามลำดับ ผ่านส่วนต่างๆ และลำดับโฟกัสส่งผลต่อความหมาย โฟกัสต้องเป็นไปตามลำดับที่รักษาความหมายและความสามารถในการใช้งาน” ข้อยกเว้น: เนื้อหาแบบไดนามิกที่โฟกัสสามารถกระโดดเพื่อดึงดูดความสนใจ (การแจ้งเตือน หน้าต่างโมดอล)
ผู้ใช้ Switch Control (ผู้ที่มีความบกพร่องทางการเคลื่อนไหว) เคลื่อนที่ผ่านองค์ประกอบโดยอัตโนมัติ — รอบแล้วรอบเล่า หากลำดับเสียหาย ผู้ใช้ใช้เวลาในการกรอกแบบฟอร์มมากกว่า 3 เท่า ตาม Deque University, 2024 Focus Order ที่ถูกต้องจะลดเวลาในการกรอกแบบฟอร์มลง 60% สำหรับผู้ใช้เทคโนโลยีช่วยเหลือ
ให้ความสนใจเป็นพิเศษ — หน้าต่างโมดอล หลังจากเปิดโมดอล โฟกัสต้อง ย้ายทันที ไปยังองค์ประกอบแบบโต้ตอบแรกภายในโมดอล (โดยปกติคือปุ่ม “ปิด” หรือ “ยืนยัน”) หลังจากปิด — กลับไปยังองค์ประกอบที่เรียกโมดอล นี่คือข้อกำหนดของ WCAG 2.4.3 และเป็นข้อผิดพลาดทั่วไปด้วย
ใน iOS VoiceOver จะสร้างลำดับโดยอัตโนมัติตามเรขาคณิต: องค์ประกอบจะถูกจัดเรียงตาม Y จากนั้นตาม X สำหรับหน้าจอที่มีโครงสร้างซับซ้อน ลำดับนี้อาจไม่ถูกต้อง — นักพัฒนาต้องเข้ามาแทรกแซง
เครื่องมือหลัก:
ตัวอย่างการตั้งค่าลำดับแบบกำหนดเองสำหรับบัตรสินค้า:
class ProductCardView: UIView {
let titleLabel = UILabel()
let priceLabel = UILabel()
let buyButton = UIButton()
override var accessibilityElements: [Any]? {
get {
return [titleLabel!, priceLabel!, buyButton!]
}
set {}
}
}
สำหรับการย้ายโฟกัสเชิงโปรแกรมหลังการดำเนินการ:
UIAccessibility.post(
notification: .layoutChanged,
argument: newlyAddedItem
)
คุณสมบัติ shouldGroupAccessibilityElement มีประโยชน์สำหรับบัตรในคอลเล็กชัน หากตั้งค่าเป็น true ที่บัตรหลัก VoiceOver จะรับรู้ทั้งบัตรเป็นองค์ประกอบเดียว ผู้ใช้สามารถแตะสองครั้งเพื่อเปิดใช้งานทั้งบัตร หรือกำหนดค่าโรเตอร์สำหรับการนำทางภายใน แนะนำสำหรับ UICollectionViewCell และ UITableViewCell
ใน Android TalkBack ก็ใช้ลำดับเรขาคณิตเช่นกัน แต่ให้ความสำคัญกับแอตทริบิวต์ nextFocus* ที่ชัดเจน แอตทริบิวต์เหล่านี้ถูกกำหนดใน XML หรือเชิงโปรแกรม:
| แอตทริบิวต์ | วัตถุประสงค์ | ตัวอย่าง |
|---|---|---|
| nextFocusDown | องค์ประกอบเมื่อนำทางลง | @+id/field_email |
| nextFocusUp | องค์ประกอบเมื่อนำทางขึ้น | @+id/field_name |
| nextFocusLeft | องค์ประกอบทางซ้าย | @+id/btn_back |
| nextFocusRight | องค์ประกอบทางขวา | @+id/btn_next |
ตัวอย่างสำหรับแบบฟอร์มลงทะเบียน:
<EditText
android:id="@+id/field_email"
android:nextFocusDown="@+id/field_password" />
<EditText
android:id="@+id/field_password"
android:nextFocusDown="@+id/btn_submit" />
สำหรับ RecyclerView ลำดับโฟกัสเป็นแบบไดนามิก — กำหนดโดยอะแดปเตอร์ หากเซลล์มีโครงสร้างซับซ้อน ให้ตั้งค่า descendantFocusability = “beforeDescendants” และกำหนดลำดับในโหนดรายการ สำหรับ Jetpack Compose ลำดับโฟกัสถูกกำหนดผ่าน Modifier.focusOrder() และ FocusOrder ลำดับความสำคัญ: previous (ย่อย), next (ถัดไป), คีย์แบบกำหนดเอง
หากองค์ประกอบเล็กเกินไปสำหรับโฟกัส (น้อยกว่า 44pt) ให้เพิ่มพื้นที่สัมผัสผ่าน TouchDelegate ใน iOS หรือ minWidth/minHeight ใน Android ตาม Google Material Design, 2024 พื้นที่สัมผัสขั้นต่ำคือ 48×48dp VoiceOver และ TalkBack โฟกัสที่กล่องขอบเขตขององค์ประกอบ องค์ประกอบที่เล็กกว่า 30pt อาจไม่สามารถเข้าถึงโฟกัสด้วยท่าทาง — ผู้ใช้ไม่สามารถแตะองค์ประกอบเหล่านั้นได้ทางกายภาพ
โฟกัสกระโดด — เมื่อหลังจากการดำเนินการ (เช่น การลบองค์ประกอบ) โฟกัสย้ายไปยังจุดเริ่มต้นของรายการหรือปุ่ม “กลับ” ของระบบ ผู้ใช้ VoiceOver สูญเสียบริบท วิธีแก้ไข: ย้ายโฟกัสเชิงโปรแกรมไปยังองค์ประกอบที่ใกล้กับองค์ประกอบที่ถูกลบมากที่สุด
โฟกัสมองไม่เห็น — องค์ประกอบได้รับโฟกัสแต่ไม่มีตัวบ่งชี้ภาพ (ผู้ใช้แป้นพิมพ์ไม่เห็นว่าพวกเขาอยู่ที่ไหน) ใน iOS ให้ตรวจสอบ UIAccessibility.isVoiceOverRunning สำหรับตัวบ่งชี้แบบกำหนดเอง ตาม Deque University, 2024 โฟกัสมองไม่เห็นเป็นสาเหตุทั่วไปอันดับสองของความล้มเหลวในการตรวจสอบการเข้าถึง
โมดอล — โฟกัสยังคงอยู่บนเนื้อหาพื้นหลังหลังจากเปิดโมดอล ใน iOS มุมมองโมดอลจะจับโฟกัสโดยอัตโนมัติหากตั้งค่า modalPresentationStyle = .pageSheet ใน Android ให้ใช้ setFocusable(true) บนคอนเทนเนอร์ไดอะล็อก
ปัญหาย้อนกลับ: โฟกัสติดอยู่ภายในโมดอลและไม่สามารถออกไปได้ (ยกเว้นการปิด) สิ่งนี้ยอมรับได้ เฉพาะ สำหรับหน้าต่างโมดอล — ผู้ใช้ต้องปิดหน้าต่างโดยเจตนา สำหรับหน้าจอปกติ กับดักโฟกัสเป็นข้อผิดพลาดร้ายแรง วิธีแก้ไข: ตรวจสอบให้แน่ใจว่าองค์ประกอบสุดท้ายของโมดอล (ปุ่ม “ปิด”) ส่งโฟกัสกลับ
สำหรับ หน้าจอแบบกำหนดเอง (แผนที่ แคนวาส เกม) ลำดับเรขาคณิตอัตโนมัติไม่สามารถใช้ได้ นักพัฒนาต้องสร้างแผนผังการเข้าถึงด้วยตนเอง ใน iOS เมธอด UIAccessibilityContainer ถูกแทนที่เพื่อการนี้
ตัวอย่างสำหรับแคนวาสแบบกำหนดเอง:
class CanvasView: UIView {
var shapes: [ShapeView] = []
override var accessibilityElements: [Any]? {
get {
// จัดเรียงรูปร่างตาม Z-index ไม่ใช่ตามเรขาคณิต
return shapes.sorted { $0.zIndex < $1.zIndex }
}
set {}
}
}
ใน Android สำหรับ View แบบกำหนดเอง ให้แทนที่ onInitializeAccessibilityNodeInfo:
override fun onInitializeAccessibilityNodeInfo(
info: AccessibilityNodeInfo
) {
super.onInitializeAccessibilityNodeInfo(info)
info.addChild(firstElement)
info.addChild(secondElement)
info.isFocusable = true
}
สำหรับ รายการแบบไดนามิก (แชท ฟีดข่าว) หลังจากเพิ่มองค์ประกอบ ให้ย้ายโฟกัสไปยังองค์ประกอบใหม่แรก ใน iOS: UIAccessibility.post(notification: .layoutChanged, argument: newMessage) ใน Android: sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
iOS กำหนดพื้นที่โฟกัสโดยอัตโนมัติตามเฟรมขององค์ประกอบ หากองค์ประกอบมีการแปลง (transform, rotation) VoiceOver อาจโฟกัสที่พื้นที่ผิด กำหนด accessibilityFrame อย่างชัดเจนในพิกัดหน้าจอ: element.accessibilityFrame = UIAccessibility.convertToScreenCoordinates(element.bounds, in: element) สิ่งนี้ทำให้แน่ใจว่า VoiceOver เน้นพื้นที่ที่ถูกต้อง
สำหรับหน้าจอเคลื่อนไหว (UIKit Dynamics, Lottie, SpriteKit) โฟกัสเชิงโปรแกรมมีความสำคัญเป็นพิเศษ VoiceOver ไม่สามารถสร้างแผนผังการเข้าถึงสำหรับองค์ประกอบที่เคลื่อนที่แบบไดนามิก ตั้งค่า isAccessibilityElement = false บนคอนเทนเนอร์แอนิเมชันและ true เฉพาะบนองค์ประกอบแบบโต้ตอบภายใน
การทดสอบด้วยตนเอง: เปิด VoiceOver (iOS) หรือ TalkBack (Android) ปัดไปทางขวาผ่านลำดับทั้งหมด โฟกัสต้องเป็นไปตามลำดับภาพ — ซ้ายไปขวา บนลงล่าง แต่ละองค์ประกอบแบบโต้ตอบต้องได้รับโฟกัสเพียงครั้งเดียว
การทดสอบอัตโนมัติ เป็นสิ่งที่ท้าทายแต่เป็นไปได้:
func testKeyboardFocusOrder() {
let app = XCUIApplication()
app.launch()
app.textFields["Email"].tap()
// Tab — เฉพาะกับแป้นพิมพ์ฮาร์ดแวร์
}
สำหรับ Android ให้ใช้ Accessibility Testing Framework:
@Test
fun testFocusOrder() {
onView(withId(R.id.fieldEmail))
.check(matches(isFocusable()))
onView(withId(R.id.fieldEmail))
.perform(focus())
onView(withId(R.id.fieldPassword))
.check(matches(isFocused()))
}
วิธีที่เชื่อถือได้มากที่สุดคือ การทดสอบสถานการณ์ UI: กรอกแบบฟอร์มทีละขั้นตอน (อีเมล → รหัสผ่าน → ส่ง) ตรวจสอบว่าแต่ละขั้นตอนเสร็จสมบูรณ์ หากลำดับโฟกัสเสียหาย สถานการณ์จะล้มเหลวเมื่อพยายามโต้ตอบกับองค์ประกอบนอกโฟกัส
เครื่องมือ Accessibility Inspector ใน Xcode แสดงแผนผังการเข้าถึงทั้งหมด คุณสามารถเดินผ่านองค์ประกอบตามลำดับ VoiceOver และดูเส้นทางโฟกัสที่แน่นอน ใช้แท็บ “Audit” สำหรับการตรวจจับการละเมิด Focus Order โดยอัตโนมัติ
คำถามที่พบบ่อย
WCAG 2.4.3 (Focus Order) เป็นเกณฑ์ความสำเร็จระดับ A กำหนดให้ ลำดับโฟกัส รักษาความหมายของเนื้อหาระหว่างการนำทางตามลำดับ การละเมิดถือว่าร้ายแรงและขัดขวางการรับรอง
องค์ประกอบที่ซ่อนต้องมี isAccessibilityElement = false ใน iOS หรือ visibility = gone/invisible ใน Android เมื่อปรากฏ ให้ย้ายโฟกัสเชิงโปรแกรมผ่าน UIAccessibility.post(notification: .layoutChanged)
iOS จัดการผ่าน accessibilityElements และ shouldGroupAccessibilityElement, Android จัดการผ่านแอตทริบิวต์ nextFocus* และ AccessibilityNodeInfo หลักการเหมือนกัน: ลำดับเรขาคณิตโดยค่าเริ่มต้นที่สามารถแทนที่ได้
ตั้งค่า descendantFocusability = “beforeDescendants” บนองค์ประกอบรากและกำหนดค่าในอะแดปเตอร์ผ่าน onInitializeAccessibilityNodeInfo สำหรับแต่ละเซลล์
เชื่อมต่อ แป้นพิมพ์ฮาร์ดแวร์ ผ่าน Bluetooth หรือ USB บน iOS กด Tab เพื่อย้ายโฟกัส บน Android เปิด TalkBack และใช้ปุ่ม Tab และปุ่มลูกศร
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม