ניווט מרחבי (מיקוד בלוח מקשים כיווני)

תכונות באינטרנט: scrollIntoView()

ניווט ממוקד לפי כיוון (באמצעות מקשי החיצים או כפתורי החיצים) מאפשר למשתמשים שמשתמשים רק במקלדת או בקלט חלופי לנווט בין פריטים אינטראקטיביים על סמך הפריסה החזותית הדו-ממדית שלהם. הוא מועיל מאוד לממשקי משתמש מרחביים (כמו סרגלי מדיה, לוחות בקרה או קטלוגים בתצוגת עץ), אבל צריך להטמיע אותו בזהירות כדי לא לשבש את ההתנהגויות המקוריות של הדפדפן, כמו גלילה באזור התצוגה.

המדריך הזה מסביר איך ליצור ניווט דו-ממדי נגיש ובעל ביצועים גבוהים בתוך ווידג'טים מורכבים.


עקרונות הארכיטקטורה הבסיסיים

כשמטמיעים מיקוד מרחבי מותאם אישית באמצעות מקשי החיצים, צריך לפעול לפי הכללים הבסיסיים הבאים:

  1. הגבלה לווידג'טים מורכבים: אל תבטלו את מקשי החיצים בזרימת טקסט רגילה של מסמך או בפריסות רשימה אנכיות פשוטות שבהן צפוי ניווט רציף באמצעות מקש Tab כברירת מחדל. בהנחיות לכתיבת קוד נגיש מפורטות הנחיות כלליות לגבי סדר הקריאה במסמך.
  2. שמירה על גלילה מקורית בגבולות: אם אין רכיב שאפשר להתמקד בו בכיוון הלחיצה, לא למנוע את התנהגות דפדפן ברירת המחדל. מאפשרים לאירוע לעבור למעלה בהיררכיה כדי שהדפדפן יוכל לגלול את אזור התצוגה באופן מקורי. שינוי מקשי החיצים פוגע בנגישות (WCAG 1.4.10).
  3. ודאו שהמיקוד גלוי ומיושר: כשהמיקוד משתנה, ודאו שהרכיב החדש שבמיקוד נגלל לתצוגה (למשל, באמצעות scrollIntoView({ block: 'nearest', inline: 'nearest' })) כדי שיישאר גלוי לחלוטין.
  4. תיאום עם מערכת הפריסה: קובעים את המיקומים של המועמדים למיקוד מתוך גיאומטריות גבולות פעילות (getBoundingClientRect()) תוך כדי טיפול בכל לחיצה על מקש. אל תניח שסדר ה-DOM או מבנה רשת קבוע מייצגים את הפריסה החזותית. במאמר פריסות CSS ועיצוב רספונסיבי מפורטות המלצות לפריסות מודרניות.

ניהול מיקוד נגיש

הקונטיינר המרחבי צריך לחשוף עצירת טאב אחת לניווט רציף באמצעות מקש Tab בדף. בתוך הווידג'ט, מנהלים את מיקוד המקלדת באמצעות תבנית Tabindex נודדת:

  • לפריט הפעיל יש tabindex="0".
  • כל הפריטים הלא פעילים כוללים tabindex="-1".
  • במהלך הניווט, מעבירים את המיקוד באופן פרוגרמטי על ידי שינוי המאפיין tabindex="0" וקריאה ל-focus().

בחירת מועמד חזותי

בפריסות אסימטריות, פריסות עם גלישת טקסט או פריסות רשת, המיפוי שמבוסס על אינדקס נכשל. במקום זאת, צריך להפיק מועמדים מהגיאומטריה החזותית בזמן אמת של הלחיצה הנוכחית על המקש:

  • מועמדים: משתמשים בצאצאים של מאגר הווידג'טים שאפשר להתמקד בהם, שעוברים את checkVisibility(). לרכיבים מוסתרים יכול להיות מלבן בגודל אפס ב-(0, 0), והם לא יכולים להפוך בטעות ליעדים של up או left.
  • גיאומטריה: קריאת getBoundingClientRect() של הרכיב הממוקד וכל מועמד בזמן הטיפול בלחיצה על המקש. אל תשמרו את המלבנים האלה במטמון באופן גלובלי: הפריסה יכולה להשתנות מסיבות רבות, וקריאה של מספר קטן של נתונים גיאומטריים היא זולה כשמטפלים באירוע מרכזי בקצב של המשתמש.
  • מסנן כיוון: מועמד נמצא בכיוון הלחיצה רק כשהקצה הקרוב שלו נמצא בקצה הרחוק של הרכיב הנוכחי בציר הזה או מעבר לו, מה שמאפשר סבילות קטנה לעיגול של תת-פיקסלים. לדוגמה, מועמד לתפקיד right צריך להיות בעל candidate.left >= current.right - tolerance. אל תשוו בין מרכזים כדי לקבוע אם מועמד נמצא בכיוון.
  • מועמדים מיושרים קודם: מבין המועמדים שעוברים את מסנן הכיוון, מעדיפים מועמדים שההיקף שלהם בציר המאונך חופף להיקף של הרכיב הנוכחי. בוחרים את המרווח הקטן ביותר בציר הניווט, ושוברים את השוויון באמצעות ההיסט המרכזי המאונך הקטן ביותר. כך הניווט נשאר בשורה או בעמודה החזותית הסמוכה, גם אם פריט מסוים רחב באופן חריג.
  • ניקוד חלופי: אם אין מועמד שחופף בציר האנכי, נותנים למועמדים הנותרים את הציון gap + w * perpendicularCentreOffset ובוחרים את המינימום. כדאי להשתמש במשקל w >= 2 כנקודת התחלה, ואז להתאים אותו לפריסה. משקל גדול יותר נותן עדיפות למועמדים שמתאימים יותר.
  • No candidate: לא להחזיר כלום. הגורם המטפל באירועים לא יכול לקרוא ל-preventDefault(), כדי לאפשר לדפדפן לגלול באופן מקורי.

ההטמעה המדויקת יכולה להשתנות, אבל צריך להקפיד שהבדיקה של הכיוון תהיה מבוססת-קצוות, ולהחיל את העדפת החפיפה לפני ברירת המחדל המשוקללת. זהו לב האלגוריתם. אל תציגו מטמון או משקל קבוע כנכונים באופן אוניברסלי.


שילוב של לחיצות מקשים ושיתוף פעולה בגלילה

נתב האירועים צריך למנוע גלילה רגילה של אזור התצוגה רק כשמתרחש מעבר תקין של המיקוד המרחבי:

  • אל תיירט את האירוע כשמקש צירוף לחוץ, כש-e.defaultPrevented כבר נכון, או כשמקשי החצים משמשים את יעד האירוע הפנימי ביותר. צריך להשתמש ב-e.composedPath()[0] במקום רק ב-e.target, כי אפשר להגדיר מחדש את הטירגוט של e.target למארח צללים. בדיקה מעשית של יכולת העריכה/השליטה היא target.matches(':read-write, select, input[type="range"], [role="slider"]'); לא ניתן לבדוק שורשי צל סגורים.
  • הגדירו את היקף המאזין למאגר הווידג'טים, כדי שכמה ווידג'טים מרחביים יוכלו להתקיים יחד בדף.
  • הפעולה מתבצעת רק כשהרכיב שהמיקוד עליו הוא בעצמו מועמד. אם הפריטים מכילים פקדים משלהם שניתן להתמקד בהם, צריך להפוך גם את הפקדים האלה למועמדים או להשאיר אותם כמו שהם. אל תפתרו הורה באמצעות closest().
  • כשמוצאים מועמד, מפעילים את preventDefault(), מעדכנים את ה-tabindex הנודד, מפעילים את focus() ומשתמשים ב-scrollIntoView({ block: 'nearest', inline: 'nearest' }).
  • אם לא נמצא מועמד, צריך להחזיר בלי לקרוא ל-preventDefault() כדי שהגלילה המקורית תימשך.

כדי לנווט במרחב, אל תוסיפו מקשי קיצור עם תו בודד כמו h/j/k/l או W/A/S/D. מקשי החיצים הם לא מקשי תווים, והשמירה על הגלילה המקורית כשאין מועמד מונעת חסימה מיותרת של תנועת אזור התצוגה.