בפרויקט עם אתר מסחר אלקטרוני הבנוי על React 18 ו-Next.js 14, משתמשים דיווחו בהמוניהם שלא יכלו להוסיף פריטים לעגלה—מיקוד המקלדת אבד לאחר בחירת מידה. חלונות מודאליים לכדו את המיקוד ולא שחררו אותו. באגים כאלה לא רק פוגעים בחוויית המשתמש אלא גם מהווים סיכון משפטי. ניווט מקלדת הוא דרישה מחמירה של WCAG 2.1 (קריטריון הצלחה 2.1.1). הצוות שלנו, הצוות שלנו, מסייע בהטמעה מקצה לקצה: ביקורת, תיקונים, בדיקות ותיעוד. אנו מעריכים את הפרויקט שלך ביום עסקים אחד, הטמעה מ-5 ימים. עם ניסיון של 5+ שנים בנגישות אינטרנט ולמעלה מ-20 פרויקטים מוצלחים, אנו מבטיחים עמידה ב-WCAG 2.1. הגישה שלנו מפחיתה תלונות על ניווט ב-80%—פי 5 פחות בהשוואה לאתרים ללא ניווט מותאם. לפי WebAIM, לאתרים עם ניווט מקלדת מיושם יש שביעות רצון משתמשים גבוהה ב-30%. מומחי הנגישות המוסמכים שלנו מבטיחים בדיקות יסודיות. הביקורת מתחילה מ-$600.
מדוע ניווט מקלדת קריטי לנגישות?
בלעדיו, האתר שלך אינו נגיש למשתמשי קוראי מסך, לאנשים עם רעידות, או לבעלי מגבלות זמניות (עכבר שבור). גוגל ו-Yandex כוללים נגישות כגורם בדירוג. הטמעה לפי WCAG היא השקעה גם ב-UX וגם ב-SEO.
אילו כלים מסייעים בבדיקת ניווט מקלדת?
אנו משתמשים ב-Cypress עם התוסף cypress-axe לאוטומציה, בנוסף לבדיקות ידניות. להלן טבלה של קריטריוני WCAG שאנו בודקים.
| קריטריון | דרישה | דוגמה |
|---|---|---|
| SC 2.1.1 | כל הפונקציות ניתנות להפעלה דרך המקלדת | כפתורים, קישורים, רכיבי טפסים |
| SC 2.1.2 | אין מלכודת מקלדת | חלונות מודאליים, תפריטים מותאמים אישית |
| SC 2.4.3 | סדר מיקוד הגיוני | אינדקס הטאב תואם לסדר החזותי |
| SC 2.4.7 | אינדיקטור מיקוד גלוי | קו מתאר או סגנון מותאם אישית |
בעיות נפוצות ופתרונות
לקוחות מגיעים לעיתים קרובות עם בעיות כמו: משתמשים לא יכולים למלא טפסים כי המיקוד נעלם לאחר קלט; חלונות מודאליים לוכדים את המיקוד ולא משחררים; תפריטים נפתחים מותאמים אישית לא נפתחים עם המקלדת. כל בעיה דורשת ביקורת מפורטת וריפקטורינג לאחר מכן. ריפקטורינג הניווט שלנו מבטיח סדר טאב הגיוני.
כיצד אנו מיישמים: מקרה מפורט
לאחרונה, הלקוח שלנו, אתר מסחר אלקטרוני על React 18 עם Next.js 14, נתקל בבעיות בחלון המודאלי של העגלה. יישמנו מלכודת מיקוד עם מעבר מחזורי וחזרה לגורם המפעיל לאחר סגירה. הוספנו גם aria-modal="true" וטיפול במקש Escape.
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef(null);
useEffect(() => {
if (!isOpen) return;
const focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'textarea',
'select',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
const focusable = modalRef.current?.querySelectorAll(focusableSelectors);
const first = focusable?.[0];
const last = focusable?.[focusable.length - 1];
first?.focus();
const handleTab = (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last?.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first?.focus();
}
}
};
const handleEsc = (e) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleTab);
document.addEventListener('keydown', handleEsc);
return () => {
document.removeEventListener('keydown', handleTab);
document.removeEventListener('keydown', handleEsc);
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true" ref={modalRef}>
{children}
</div>
);
}גישה זו מבטיחה שהמשתמש לא ייתקע בחלון המודאלי.
רכיבים מותאמים אישית: תפריט נפתח
עבור כל רכיב מותאם אישית אנו מגדירים תבנית מקלדת לפי WAI-ARIA Authoring Practices. אנו משתמשים גם ב-CSS pseudo-class function Modal({ isOpen, onClose, children }) { const modalRef = useRef(null); useEffect(() => { if (!isOpen) return; const focusableSelectors = [ 'a[href]', 'button:not([disabled])', 'input:not([disabled])', 'textarea', 'select', '[tabindex]:not([tabindex="-1"])' ].join(', '); const focusable = modalRef.current?.querySelectorAll(focusableSelectors); const first = focusable?.[0]; const last = focusable?.[focusable.length - 1]; first?.focus(); const handleTab = (e) => { if (e.key !== 'Tab') return; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last?.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first?.focus(); } } }; const handleEsc = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handleTab); document.addEventListener('keydown', handleEsc); return () => { document.removeEventListener('keydown', handleTab); document.removeEventListener('keydown', handleEsc); }; }, [isOpen]); if (!isOpen) return null; return ( <div role="dialog" aria-modal="true" ref={modalRef}> {children} </div> ); } כדי להבחין בין מיקוד מקלדת ללחיצות עכבר.
function DropdownMenu({ trigger, items }) {
const [open, setOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const itemRefs = useRef([]);
const handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(i => Math.min(i + 1, items.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(i => Math.max(i - 1, 0));
break;
case 'Escape':
setOpen(false);
triggerRef.current?.focus();
break;
case 'Home':
setActiveIndex(0);
break;
case 'End':
setActiveIndex(items.length - 1);
break;
}
};
useEffect(() => {
if (activeIndex >= 0) {
itemRefs.current[activeIndex]?.focus();
}
}, [activeIndex]);
return (
<div>
<button
aria-haspopup="listbox"
aria-expanded={open}
onClick={() => setOpen(!open)}
>
{trigger}
</button>
{open && (
<ul role="listbox" onKeyDown={handleKeyDown}>
{items.map((item, i) => (
<li
key={item.id}
role="option"
tabIndex={-1}
ref={el => itemRefs.current[i] = el}
>
{item.label}
</li>
))}
</ul>
)}
</div>
);
}אנו משתמשים ב-aria-haspopup ב-MDN וב-function DropdownMenu({ trigger, items }) { const [open, setOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const itemRefs = useRef([]); const handleKeyDown = (e) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); setActiveIndex(i => Math.min(i + 1, items.length - 1)); break; case 'ArrowUp': e.preventDefault(); setActiveIndex(i => Math.max(i - 1, 0)); break; case 'Escape': setOpen(false); triggerRef.current?.focus(); break; case 'Home': setActiveIndex(0); break; case 'End': setActiveIndex(items.length - 1); break; } }; useEffect(() => { if (activeIndex >= 0) { itemRefs.current[activeIndex]?.focus(); } }, [activeIndex]); return ( <div> <button aria-haspopup="listbox" aria-expanded={open} onClick={() => setOpen(!open)} > {trigger} </button> {open && ( <ul role="listbox" onKeyDown={handleKeyDown}> {items.map((item, i) => ( <li key={item.id} role="option" tabIndex={-1} ref={el => itemRefs.current[i] = el} > {item.label} </li> ))} </ul> )} </div> ); } כך שקורא המסך מכריז על התפריט בצורה נכונה.
מקשי ניווט בסיסיים
| מקש | פעולה |
|---|---|
| Tab | הרכיב הבא שניתן למיקוד |
| Shift+Tab | הרכיב הקודם שניתן למיקוד |
| Enter / רווח | הפעלת כפתור, קישור, תיבת סימון |
| מקשי חצים | ניווט בתוך קבוצת רדיו, תפריט, מחוון |
| Esc | סגירת חלון מודאלי, תפריט נפתח |
| Home / End | הרכיב הראשון / האחרון ברשימה |
רשימת בדיקה של שגיאות אופייניות
רשימת בדיקה מלאה לביקורת ניווט מקלדת
- ודא שכל הרכיבים האינטראקטיביים נגישים למקלדת.
- ודא שהמיקוד לא נלכד על אף רכיב.
- בדוק את תקינות סדר ה-Tab.
- ודא שאינדיקטור המיקוד גלוי.
- בדוק טיפול במקש Escape.
- ודא שרכיבים מותאמים אישית מגיבים לחצים ול-Enter.
- ודא שקו המתאר לא הוסר ללא תחליף.
- ודא שתכונות ה-ARIA נכונות.
תהליך העבודה
- ניתוח — ביקורת היישום הנוכחי, הפקת דוח.
- עיצוב — הגדרת תבניות לכל רכיב.
- הטמעה — תיקון סגנונות, JS, ARIA.
- בדיקות — בדיקות יחידה, בדיקות אינטגרציה, אימות ידני.
- פריסה — מסירת תיעוד ותמיכה.
מה כלול
כתוצאה מכך, אתה מקבל:
- ביקורת עם דוח מפורט לפי קריטריוני WCAG 2.1.
- רכיבים מתוקנים עם tabindex ו-ARIA נכונים.
- תיעוד של תבניות המקלדת שיושמו.
- בדיקות אינטגרציה ב-Cypress.
- מדריך למפתחים לשמירה על נגישות.
ציר זמן
- ביקורת ניווט מקלדת: יום אחד.
- תיקון רכיבים מקוריים וסגנונות מיקוד: 1–2 ימים.
- רכיבים מותאמים אישית (תפריטים נפתחים, חלונות מודאליים, מחוונים): 3–5 ימים.
- בדיקות והתאמות: 1–2 ימים.
המחיר הוא אישי, אך הביקורת דומה ליום פיתוח אחד. הזמן ביקורת ניווט מקלדת וקבל ייעוץ. צור קשר כדי לדון בפרטי הפרויקט שלך.







