לדלג לתוכן

11.2 נגישות מתקדמת פתרון

פתרון - נגישות מתקדמת

פתרון תרגיל 1 - טופס הרשמה נגיש

'use client';

import { useState, useRef, useEffect, type FormEvent } from 'react';

interface FormErrors {
  name?: string;
  email?: string;
  password?: string;
  confirmPassword?: string;
  gender?: string;
  terms?: string;
}

function AccessibleRegistrationForm() {
  const [errors, setErrors] = useState<FormErrors>({});
  const [submitted, setSubmitted] = useState(false);
  const errorSummaryRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (Object.keys(errors).length > 0 && submitted) {
      errorSummaryRef.current?.focus();
    }
  }, [errors, submitted]);

  function validate(formData: FormData): FormErrors {
    const newErrors: FormErrors = {};
    const name = formData.get('name') as string;
    const email = formData.get('email') as string;
    const password = formData.get('password') as string;
    const confirmPassword = formData.get('confirmPassword') as string;
    const gender = formData.get('gender') as string;
    const terms = formData.get('terms');

    if (!name || name.length < 2) {
      newErrors.name = 'שם מלא חייב להכיל לפחות 2 תווים';
    }
    if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      newErrors.email = 'כתובת אימייל לא תקינה';
    }
    if (!password || password.length < 8) {
      newErrors.password = 'סיסמה חייבת להכיל לפחות 8 תווים';
    }
    if (password !== confirmPassword) {
      newErrors.confirmPassword = 'הסיסמאות לא תואמות';
    }
    if (!gender) {
      newErrors.gender = 'יש לבחור מגדר';
    }
    if (!terms) {
      newErrors.terms = 'יש לאשר את תנאי השימוש';
    }

    return newErrors;
  }

  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setSubmitted(true);
    const formData = new FormData(e.currentTarget);
    const newErrors = validate(formData);
    setErrors(newErrors);

    if (Object.keys(newErrors).length === 0) {
      // שליחת הטופס
      console.log('הטופס תקין!');
    }
  }

  const errorEntries = Object.entries(errors) as [keyof FormErrors, string][];

  return (
    <form onSubmit={handleSubmit} aria-label="טופס הרשמה" noValidate>
      {/* סיכום שגיאות */}
      {errorEntries.length > 0 && submitted && (
        <div
          ref={errorSummaryRef}
          tabIndex={-1}
          role="alert"
          aria-label="שגיאות בטופס"
          className="error-summary"
        >
          <h2>נמצאו {errorEntries.length} שגיאות:</h2>
          <ul>
            {errorEntries.map(([field, message]) => (
              <li key={field}>
                <a href={`#${field}`}>{message}</a>
              </li>
            ))}
          </ul>
        </div>
      )}

      {/* שם מלא */}
      <div>
        <label htmlFor="name">שם מלא</label>
        <input
          id="name"
          name="name"
          type="text"
          aria-required="true"
          aria-invalid={!!errors.name}
          aria-describedby={errors.name ? 'name-error' : undefined}
        />
        {errors.name && (
          <p id="name-error" className="error" role="alert">
            {errors.name}
          </p>
        )}
      </div>

      {/* אימייל */}
      <div>
        <label htmlFor="email">אימייל</label>
        <input
          id="email"
          name="email"
          type="email"
          aria-required="true"
          aria-invalid={!!errors.email}
          aria-describedby={errors.email ? 'email-error' : undefined}
        />
        {errors.email && (
          <p id="email-error" className="error" role="alert">
            {errors.email}
          </p>
        )}
      </div>

      {/* סיסמה */}
      <div>
        <label htmlFor="password">סיסמה</label>
        <input
          id="password"
          name="password"
          type="password"
          aria-required="true"
          aria-invalid={!!errors.password}
          aria-describedby="password-help password-error"
        />
        <p id="password-help" className="help-text">
          לפחות 8 תווים, כולל אות גדולה, מספר ותו מיוחד
        </p>
        {errors.password && (
          <p id="password-error" className="error" role="alert">
            {errors.password}
          </p>
        )}
      </div>

      {/* אישור סיסמה */}
      <div>
        <label htmlFor="confirmPassword">אישור סיסמה</label>
        <input
          id="confirmPassword"
          name="confirmPassword"
          type="password"
          aria-required="true"
          aria-invalid={!!errors.confirmPassword}
          aria-describedby={
            errors.confirmPassword ? 'confirm-error' : undefined
          }
        />
        {errors.confirmPassword && (
          <p id="confirm-error" className="error" role="alert">
            {errors.confirmPassword}
          </p>
        )}
      </div>

      {/* מגדר */}
      <fieldset aria-invalid={!!errors.gender}>
        <legend>מגדר</legend>
        <div>
          <input type="radio" id="gender-male" name="gender" value="male" />
          <label htmlFor="gender-male">זכר</label>
        </div>
        <div>
          <input type="radio" id="gender-female" name="gender" value="female" />
          <label htmlFor="gender-female">נקבה</label>
        </div>
        <div>
          <input type="radio" id="gender-other" name="gender" value="other" />
          <label htmlFor="gender-other">אחר</label>
        </div>
        {errors.gender && (
          <p className="error" role="alert">{errors.gender}</p>
        )}
      </fieldset>

      {/* תנאי שימוש */}
      <div>
        <input
          type="checkbox"
          id="terms"
          name="terms"
          aria-invalid={!!errors.terms}
          aria-describedby={errors.terms ? 'terms-error' : undefined}
        />
        <label htmlFor="terms">
          אני מסכים/ה <a href="/terms">לתנאי השימוש</a>
        </label>
        {errors.terms && (
          <p id="terms-error" className="error" role="alert">
            {errors.terms}
          </p>
        )}
      </div>

      <button type="submit">הרשמה</button>
    </form>
  );
}

export default AccessibleRegistrationForm;

פתרון תרגיל 2 - מודל נגיש עם מלכודת פוקוס

'use client';

import { useEffect, useRef, useCallback, type ReactNode } from 'react';

// קומפוננטת מלכודת פוקוס
function FocusTrap({ children, active }: { children: ReactNode; active: boolean }) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!active) return;

    const container = containerRef.current;
    if (!container) return;

    const getFocusableElements = () =>
      container.querySelectorAll<HTMLElement>(
        'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
      );

    const elements = getFocusableElements();
    const first = elements[0];
    const last = elements[elements.length - 1];

    first?.focus();

    function handleKeyDown(e: KeyboardEvent) {
      if (e.key !== 'Tab') return;

      const currentElements = getFocusableElements();
      const firstEl = currentElements[0];
      const lastEl = currentElements[currentElements.length - 1];

      if (e.shiftKey && document.activeElement === firstEl) {
        e.preventDefault();
        lastEl?.focus();
      } else if (!e.shiftKey && document.activeElement === lastEl) {
        e.preventDefault();
        firstEl?.focus();
      }
    }

    container.addEventListener('keydown', handleKeyDown);
    return () => container.removeEventListener('keydown', handleKeyDown);
  }, [active]);

  return <div ref={containerRef}>{children}</div>;
}

// קומפוננטת מודל נגיש
interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: ReactNode;
}

function AccessibleModal({ isOpen, onClose, title, children }: ModalProps) {
  const previousFocusRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      previousFocusRef.current = document.activeElement as HTMLElement;
      document.body.style.overflow = 'hidden';
    }

    return () => {
      document.body.style.overflow = '';
      previousFocusRef.current?.focus();
    };
  }, [isOpen]);

  const handleEscape = useCallback(
    (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    },
    [onClose]
  );

  useEffect(() => {
    if (!isOpen) return;
    document.addEventListener('keydown', handleEscape);
    return () => document.removeEventListener('keydown', handleEscape);
  }, [isOpen, handleEscape]);

  if (!isOpen) return null;

  return (
    <>
      {/* שכבת רקע */}
      <div
        className="modal-overlay"
        onClick={onClose}
        aria-hidden="true"
        style={{
          position: 'fixed',
          inset: 0,
          backgroundColor: 'rgba(0, 0, 0, 0.5)',
          zIndex: 999,
        }}
      />

      {/* המודל */}
      <FocusTrap active={isOpen}>
        <div
          role="dialog"
          aria-modal="true"
          aria-labelledby="modal-title"
          style={{
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            backgroundColor: 'white',
            padding: '24px',
            borderRadius: '8px',
            zIndex: 1000,
            minWidth: '300px',
          }}
        >
          <h2 id="modal-title">{title}</h2>
          {children}
          <button onClick={onClose} aria-label="סגור חלון">
            X
          </button>
        </div>
      </FocusTrap>
    </>
  );
}

// דוגמת שימוש - מודל אישור מחיקה
function DeleteConfirmation() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>מחק פריט</button>

      <AccessibleModal
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        title="אישור מחיקה"
      >
        <p>האם אתה בטוח שברצונך למחוק את הפריט? לא ניתן לבטל פעולה זו.</p>
        <div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
          <button
            onClick={() => {
              // ביצוע מחיקה
              setIsOpen(false);
            }}
          >
            אישור מחיקה
          </button>
          <button onClick={() => setIsOpen(false)}>ביטול</button>
        </div>
      </AccessibleModal>
    </>
  );
}

import { useState } from 'react';

export { AccessibleModal, FocusTrap, DeleteConfirmation };

פתרון תרגיל 3 - רכיב טאבים נגיש

'use client';

import { useState, useRef, useCallback, type KeyboardEvent, type ReactNode } from 'react';

interface TabItem {
  id: string;
  label: string;
  content: ReactNode;
}

function AccessibleTabs({ tabs }: { tabs: TabItem[] }) {
  const [activeTab, setActiveTab] = useState(0);
  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);

  const handleKeyDown = useCallback(
    (e: KeyboardEvent) => {
      let newIndex = activeTab;

      switch (e.key) {
        case 'ArrowRight':
          e.preventDefault();
          newIndex = (activeTab + 1) % tabs.length;
          break;
        case 'ArrowLeft':
          e.preventDefault();
          newIndex = (activeTab - 1 + tabs.length) % tabs.length;
          break;
        case 'Home':
          e.preventDefault();
          newIndex = 0;
          break;
        case 'End':
          e.preventDefault();
          newIndex = tabs.length - 1;
          break;
        default:
          return;
      }

      setActiveTab(newIndex);
      tabRefs.current[newIndex]?.focus();
    },
    [activeTab, tabs.length]
  );

  return (
    <div>
      {/* רשימת טאבים */}
      <div role="tablist" aria-label="טאבים" onKeyDown={handleKeyDown}>
        {tabs.map((tab, index) => (
          <button
            key={tab.id}
            ref={(el) => { tabRefs.current[index] = el; }}
            role="tab"
            id={`tab-${tab.id}`}
            aria-selected={activeTab === index}
            aria-controls={`panel-${tab.id}`}
            tabIndex={activeTab === index ? 0 : -1}
            onClick={() => setActiveTab(index)}
            style={{
              padding: '8px 16px',
              border: 'none',
              borderBottom: activeTab === index ? '2px solid blue' : '2px solid transparent',
              backgroundColor: 'transparent',
              cursor: 'pointer',
              fontWeight: activeTab === index ? 'bold' : 'normal',
            }}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {/* פאנלים */}
      {tabs.map((tab, index) => (
        <div
          key={tab.id}
          role="tabpanel"
          id={`panel-${tab.id}`}
          aria-labelledby={`tab-${tab.id}`}
          hidden={activeTab !== index}
          tabIndex={0}
          style={{ padding: '16px' }}
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}

// דוגמת שימוש
function TabsExample() {
  const tabs: TabItem[] = [
    { id: 'general', label: 'כללי', content: <p>הגדרות כלליות</p> },
    { id: 'security', label: 'אבטחה', content: <p>הגדרות אבטחה</p> },
    { id: 'notifications', label: 'התראות', content: <p>הגדרות התראות</p> },
  ];

  return <AccessibleTabs tabs={tabs} />;
}

export { AccessibleTabs, TabsExample };

// components/SkipLink.tsx
function SkipLink() {
  return (
    <a
      href="#main-content"
      style={{
        position: 'absolute',
        top: '-40px',
        left: 0,
        background: '#000',
        color: '#fff',
        padding: '8px 16px',
        zIndex: 100,
        transition: 'top 0.2s',
      }}
      onFocus={(e) => {
        (e.target as HTMLElement).style.top = '0';
      }}
      onBlur={(e) => {
        (e.target as HTMLElement).style.top = '-40px';
      }}
    >
      דלג לתוכן הראשי
    </a>
  );
}
// components/BackToTop.tsx
'use client';

import { useCallback } from 'react';

function BackToTop() {
  const scrollToTop = useCallback(() => {
    window.scrollTo({ top: 0, behavior: 'smooth' });
    // העברת פוקוס לתחילת הדף
    const skipLink = document.querySelector<HTMLAnchorElement>('a[href="#main-content"]');
    skipLink?.focus();
  }, []);

  return (
    <button onClick={scrollToTop} aria-label="חזרה לראש הדף">
      חזרה למעלה
    </button>
  );
}
// components/Layout.tsx
'use client';

import { usePathname } from 'next/navigation';
import { useEffect, useRef } from 'react';

function AccessibleLayout({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  const mainRef = useRef<HTMLElement>(null);

  // העברת פוקוס ל-main בניווט בין דפים
  useEffect(() => {
    mainRef.current?.focus();
  }, [pathname]);

  return (
    <>
      <SkipLink />

      <header role="banner">
        <h1>האתר שלי</h1>
        <nav aria-label="ניווט ראשי">
          <ul>
            <li><a href="/">דף הבית</a></li>
            <li><a href="/products">מוצרים</a></li>
            <li><a href="/about">אודות</a></li>
            <li><a href="/contact">יצירת קשר</a></li>
          </ul>
        </nav>
      </header>

      <div style={{ display: 'flex' }}>
        <main id="main-content" ref={mainRef} tabIndex={-1}>
          {children}
        </main>

        <aside aria-label="תוכן צדדי">
          <nav aria-label="ניווט משני">
            <h2>קטגוריות</h2>
            <ul>
              <li><a href="/cat1">קטגוריה 1</a></li>
              <li><a href="/cat2">קטגוריה 2</a></li>
            </ul>
          </nav>
        </aside>
      </div>

      <footer role="contentinfo">
        <p>כל הזכויות שמורות</p>
        <BackToTop />
      </footer>
    </>
  );
}

export default AccessibleLayout;

הסבר: ה-Layout כולל את כל אזורי ה-landmark הנדרשים. ה-Skip Link מופיע רק בפוקוס. שני אזורי הניווט מובדלים באמצעות aria-label. בניווט בין דפים, הפוקוס עובר אוטומטית ל-main.


פתרון תרגיל 5 - רשימת משימות נגישה

'use client';

import { useState, useRef, useCallback, type KeyboardEvent } from 'react';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

function AccessibleTodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [newTodo, setNewTodo] = useState('');
  const [announcement, setAnnouncement] = useState('');
  const inputRef = useRef<HTMLInputElement>(null);
  const listRef = useRef<HTMLUListElement>(null);

  const addTodo = useCallback(() => {
    if (!newTodo.trim()) return;

    const todo: Todo = {
      id: crypto.randomUUID(),
      text: newTodo.trim(),
      completed: false,
    };

    setTodos((prev) => [...prev, todo]);
    setNewTodo('');
    setAnnouncement(`משימה "${todo.text}" נוספה`);
    inputRef.current?.focus();
  }, [newTodo]);

  const toggleTodo = useCallback((id: string) => {
    setTodos((prev) =>
      prev.map((todo) => {
        if (todo.id === id) {
          const updated = { ...todo, completed: !todo.completed };
          setAnnouncement(
            updated.completed
              ? `משימה "${todo.text}" סומנה כהושלמה`
              : `משימה "${todo.text}" סומנה כלא הושלמה`
          );
          return updated;
        }
        return todo;
      })
    );
  }, []);

  const deleteTodo = useCallback(
    (id: string, index: number) => {
      const todo = todos.find((t) => t.id === id);
      setTodos((prev) => prev.filter((t) => t.id !== id));
      setAnnouncement(`משימה "${todo?.text}" נמחקה`);

      // העברת פוקוס לפריט הבא, או הקודם, או לשדה הקלט
      requestAnimationFrame(() => {
        const items = listRef.current?.querySelectorAll<HTMLElement>('[role="listitem"]');
        if (items && items.length > 0) {
          const focusIndex = Math.min(index, items.length - 1);
          items[focusIndex]?.focus();
        } else {
          inputRef.current?.focus();
        }
      });
    },
    [todos]
  );

  const handleItemKeyDown = useCallback(
    (e: KeyboardEvent, id: string, index: number) => {
      if (e.key === 'Delete' || e.key === 'Backspace') {
        e.preventDefault();
        deleteTodo(id, index);
      } else if (e.key === ' ') {
        e.preventDefault();
        toggleTodo(id);
      }
    },
    [deleteTodo, toggleTodo]
  );

  const completedCount = todos.filter((t) => t.completed).length;

  return (
    <div>
      <h2>רשימת משימות</h2>

      {/* הודעות לקורא מסך */}
      <div aria-live="polite" aria-atomic="true" className="sr-only">
        {announcement}
      </div>

      {/* הוספת משימה */}
      <div role="search">
        <label htmlFor="new-todo">הוסף משימה חדשה</label>
        <input
          ref={inputRef}
          id="new-todo"
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter') addTodo();
          }}
          aria-describedby="todo-instructions"
        />
        <button onClick={addTodo}>הוסף</button>
        <p id="todo-instructions" className="sr-only">
          הקלד שם משימה ולחץ Enter או על כפתור הוסף
        </p>
      </div>

      {/* מונה משימות */}
      <p aria-live="polite">
        {completedCount} מתוך {todos.length} משימות הושלמו
      </p>

      {/* רשימת המשימות */}
      <ul ref={listRef} role="list" aria-label="רשימת משימות">
        {todos.map((todo, index) => (
          <li
            key={todo.id}
            role="listitem"
            tabIndex={0}
            onKeyDown={(e) => handleItemKeyDown(e, todo.id, index)}
            aria-label={`${todo.text}${todo.completed ? ', הושלמה' : ''}`}
          >
            <input
              type="checkbox"
              id={`todo-${todo.id}`}
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
              aria-checked={todo.completed}
            />
            <label
              htmlFor={`todo-${todo.id}`}
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none',
              }}
            >
              {todo.text}
            </label>
            <button
              onClick={() => deleteTodo(todo.id, index)}
              aria-label={`מחק משימה: ${todo.text}`}
            >
              מחק
            </button>
          </li>
        ))}
      </ul>

      {todos.length === 0 && <p>אין משימות ברשימה</p>}
    </div>
  );
}

export default AccessibleTodoList;

פתרון תרגיל 6 - בדיקת נגישות

א. הגדרת eslint-plugin-jsx-a11y:

npm install -D eslint-plugin-jsx-a11y
// .eslintrc.json
{
  "plugins": ["jsx-a11y"],
  "extends": ["plugin:jsx-a11y/strict"],
  "rules": {
    "jsx-a11y/anchor-is-valid": "error",
    "jsx-a11y/click-events-have-key-events": "error",
    "jsx-a11y/no-static-element-interactions": "error",
    "jsx-a11y/alt-text": "error",
    "jsx-a11y/label-has-associated-control": "error",
    "jsx-a11y/no-autofocus": "warn",
    "jsx-a11y/no-noninteractive-element-interactions": "error",
    "jsx-a11y/role-has-required-aria-props": "error",
    "jsx-a11y/interactive-supports-focus": "error"
  }
}

ב. בדיקת axe בסביבת פיתוח:

// app/AxeProvider.tsx
'use client';

import { useEffect } from 'react';

function AxeProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    if (process.env.NODE_ENV === 'development') {
      import('@axe-core/react').then((axe) => {
        const React = require('react');
        const ReactDOM = require('react-dom');
        axe.default(React, ReactDOM, 1000, {
          rules: [
            { id: 'color-contrast', enabled: true },
            { id: 'label', enabled: true },
          ],
        });
      });
    }
  }, []);

  return <>{children}</>;
}

export default AxeProvider;

ג. בדיקת jest-axe:

npm install -D jest-axe @testing-library/react @testing-library/jest-dom
// __tests__/accessibility.test.tsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

describe('בדיקות נגישות', () => {
  it('טופס הרשמה ללא בעיות נגישות', async () => {
    const { container } = render(<AccessibleRegistrationForm />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  it('מודל ללא בעיות נגישות', async () => {
    const { container } = render(
      <AccessibleModal isOpen={true} onClose={() => {}} title="בדיקה">
        <p>תוכן המודל</p>
        <button>אישור</button>
      </AccessibleModal>
    );
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  it('טאבים ללא בעיות נגישות', async () => {
    const tabs = [
      { id: '1', label: 'טאב 1', content: <p>תוכן 1</p> },
      { id: '2', label: 'טאב 2', content: <p>תוכן 2</p> },
    ];
    const { container } = render(<AccessibleTabs tabs={tabs} />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  it('רשימת משימות ללא בעיות נגישות', async () => {
    const { container } = render(<AccessibleTodoList />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

ד. רשימת בדיקה ידנית:

  1. ניווט בכל האתר באמצעות מקלדת בלבד (Tab, Shift+Tab, Enter, Space, Escape, חצים)
  2. בדיקה שיש אינדיקציית פוקוס ברורה וגלויה על כל אלמנט אינטראקטיבי
  3. בדיקה עם קורא מסך (VoiceOver) שכל התוכן נקרא בסדר הגיוני
  4. בדיקה שכל התמונות המשמעותיות כוללות טקסט חלופי מתאר
  5. בדיקה שהאתר ניתן לשימוש בזום 200% ללא אובדן תוכן או פונקציונליות
  6. בדיקת ניגודיות צבעים בכל הטקסטים והאלמנטים המשמעותיים (יחס 4.5:1 לפחות)
  7. בדיקה שהודעות שגיאה מוכרזות לקוראי מסך (aria-live או role="alert")
  8. בדיקה שטפסים כוללים labels מקושרים, שגיאות ברורות, והנחיות נגישות
  9. בדיקה שמודלים וחלונות קופצים כוללים מלכודת פוקוס וסגירה ב-Escape
  10. בדיקה שסרטונים ואודיו כוללים כתוביות או תמלול

תשובות לשאלות

1. ההבדל בין aria-label, aria-labelledby ו-aria-describedby:

  • aria-label - מספק תווית טקסטואלית ישירה לאלמנט. משמש כשאין תווית גלויה. דוגמה: <button aria-label="סגור">X</button>
  • aria-labelledby - מצביע על אלמנט אחר שמשמש כתווית (באמצעות ה-id שלו). משמש כשהתווית כבר קיימת בדף. דוגמה: <div aria-labelledby="section-title"> כאשר יש <h2 id="section-title">כותרת</h2>
  • aria-describedby - מצביע על אלמנט שמספק תיאור נוסף (מעבר לתווית). משמש להנחיות, שגיאות או מידע משלים. דוגמה: שדה סיסמה עם aria-describedby="password-help" שמצביע על טקסט עם דרישות הסיסמה

2. למה לא tabIndex חיובי:

tabIndex חיובי (1, 2, 3...) שובר את סדר הטאב הטבעי של ה-DOM. אלמנטים עם tabIndex חיובי מקבלים פוקוס לפני כל שאר האלמנטים, מה שיוצר חוויה מבלבלת ולא צפויה. קשה מאוד לתחזק את הסדר כשמוסיפים או מסירים אלמנטים. השימוש ב-0 (סדר טבעי) ו-1- (רק תכנותי) מספיק לכל המקרים.

3. aria-live polite מול assertive:

  • polite - קורא המסך ימתין שיסיים את ההקראה הנוכחית לפני שיקריא את העדכון. מתאים לעדכונים שאינם דחופים. דוגמה: "הפריט נוסף לעגלת הקניות"
  • assertive - קורא המסך יפסיק את ההקראה הנוכחית ויקריא את העדכון מיד. מתאים להודעות דחופות. דוגמה: "שגיאה: החיבור לשרת נותק"

4. הכלל הראשון של ARIA:

הכלל אומר: אם אפשר להשתמש באלמנט HTML סמנטי, אל תשתמש ב-ARIA. אלמנטים סמנטיים (button, nav, input) כבר כוללים תפקידים, מצבים והתנהגויות מובנות. שימוש ב-ARIA מוסיף מורכבות ויכול לגרום לטעויות. למשל, <button> כבר תומך בפוקוס, Enter, Space ומוכרז כ-"כפתור", בעוד ש-<div role="button"> דורש הוספה ידנית של כל אלה.

5. מלכודת פוקוס (Focus Trap):

מלכודת פוקוס מגבילה את ניווט ה-Tab כך שהפוקוס נשאר בתוך אלמנט מסוים ולא יוצא ממנו. כשמגיעים לאלמנט האחרון ולוחצים Tab, הפוקוס חוזר לראשון (ולהיפך עם Shift+Tab). היא חשובה כי בלעדיה, משתמשי מקלדת עלולים "ללכת לאיבוד" מאחורי מודל פתוח. משתמשים בה במודלים (dialogs), תפריטי dropdown, ומגירות צד (drawers) - כל מקרה שבו יש שכבת UI שמכסה את שאר הדף.