La navigazione con selezione direzionale (utilizzando i tasti freccia o i D-pad) consente agli utenti che utilizzano solo la tastiera e l'input alternativo di navigare tra gli elementi interattivi in base al loro layout 2D visivo. È molto utile per le interfacce utente spaziali (come i caroselli multimediali, i pannelli della dashboard o i cataloghi nidificati), ma deve essere implementato con attenzione per evitare di interrompere i comportamenti nativi del browser, come lo scorrimento della viewport.
Questa guida illustra come creare una navigazione direzionale 2D accessibile e ad alte prestazioni all'interno di widget compositi.
Principi architettonici fondamentali
Quando implementi lo stato attivo spaziale personalizzato con i tasti Freccia, segui queste regole di base:
- Limita ai widget compositi: non eseguire l'override dei tasti Freccia sul normale flusso di testo del documento o sui layout a elenco verticale semplici in cui è prevista la navigazione sequenziale predefinita con il tasto Tab. Consulta le linee guida per la programmazione dell'accessibilità per indicazioni generali sul flusso dei documenti.
- Preserva lo scorrimento dei limiti nativi: se non è presente alcun elemento attivabile nella direzione premuta, non impedire il comportamento predefinito del browser. Consenti l'evento bubble in modo che il browser possa scorrere l'area visibile in modo nativo. L'override dei tasti Freccia interrompe incondizionatamente l'accessibilità (WCAG 1.4.10).
- Garantisci la visibilità e l'allineamento dello stato attivo: quando lo stato attivo cambia, assicurati che l'elemento appena attivato venga visualizzato (ad esempio utilizzando
scrollIntoView({ block: 'nearest', inline: 'nearest' })) in modo che rimanga completamente visibile. - Coordinamento con il sistema di layout: determina le posizioni dei candidati per lo stato attivo dalle geometrie di delimitazione attive (
getBoundingClientRect()) durante la gestione di ogni pressione dei tasti. Evita di presumere che l'ordine del DOM o una struttura a griglia fissa rappresentino il layout grafico. Consulta Layout CSS e responsive design per suggerimenti sui layout moderni.
Gestione del focus accessibile
Il contenitore spaziale deve esporre un singolo punto di tabulazione per la navigazione sequenziale tra le schede della pagina. All'interno del widget, gestisci la selezione della tastiera utilizzando il pattern Roving Tabindex:
- L'elemento attivo ha
tabindex="0". - Tutti gli elementi non attivi hanno
tabindex="-1". - Durante la navigazione, sposta lo stato attivo in modo programmatico modificando l'attributo
tabindex="0"e chiamandofocus().
Selezione visiva dei candidati
Nei layout asimmetrici, con ritorno a capo o a griglia, la mappatura basata su indice non riesce. Invece, ricava i candidati dalla geometria visiva attiva per la pressione del tasto corrente:
- Candidati: utilizza i discendenti attivabili del contenitore del widget che superano
checkVisibility(). Gli elementi nascosti possono avere un rettangolo di dimensioni pari a zero in(0, 0)e non devono diventare target accidentali diupoleft. - Geometria: leggi
getBoundingClientRect()per l'elemento attivo e ogni candidato durante la gestione della pressione dei tasti. Non memorizzare nella cache questi rettangoli a livello globale: il layout può cambiare per molti motivi e un numero ridotto di letture della geometria è economico quando si gestisce un evento chiave con ritmo dell'utente. - Filtro di direzione: un candidato si trova nella direzione premuta solo quando il suo bordo vicino si trova in corrispondenza o oltre il bordo lontano dell'elemento corrente su quell'asse, consentendo una piccola tolleranza per l'arrotondamento dei sub-pixel. Ad esempio, un candidato per
rightdeve averecandidate.left >= current.right - tolerance. Non confrontare i centri per determinare se un candidato si trova nella direzione. - Candidati allineati per primi: tra i candidati che superano il filtro di direzione, preferisci quelli la cui estensione sull'asse perpendicolare si sovrappone all'estensione dell'elemento corrente. Scegli l'intervallo più piccolo sull'asse di navigazione, interrompendo i collegamenti con l'offset centrale perpendicolare più piccolo. In questo modo, la navigazione rimane nella riga o nella colonna visiva adiacente anche quando un elemento è insolitamente largo.
- Punteggio di riserva: se nessun candidato si sovrappone sull'asse perpendicolare, assegna un punteggio di
gap + w * perpendicularCentreOffsetai candidati rimanenti e scegli il valore minimo. Utilizza un pesow >= 2come punto di partenza, quindi ottimizzalo per il layout; un peso maggiore favorisce i candidati più allineati. - Nessun candidato: non restituisce nulla. Il gestore di eventi non deve chiamare
preventDefault(), consentendo al browser di scorrere in modo nativo.
L'implementazione esatta può variare, ma mantieni il test di direzione basato sui bordi e applica la preferenza di sovrapposizione prima del fallback ponderato. Questo è il fulcro dell'algoritmo; evita di presentare una cache o un peso fisso come universalmente corretti.
Integrazione della pressione dei tasti e cooperazione con lo scorrimento
Il router di eventi deve impedire lo scorrimento standard dell'area visibile solo quando si verifica una transizione di messa a fuoco spaziale valida:
- Non intercettare l'evento quando viene premuto un tasto di modifica, quando
e.defaultPreventedè già true o quando il target dell'evento più interno utilizza i tasti Freccia. Utilizzae.composedPath()[0]anziché soloe.target, perchée.targetpuò essere reindirizzato a un host shadow. Un controllo pratico di modificabilità/controllo ètarget.matches(':read-write, select, input[type="range"], [role="slider"]'); gli shadow root chiusi non possono essere ispezionati. - Definisci l'ambito del listener nel contenitore del widget, in modo che più widget spaziali possano coesistere in una pagina.
- Agisci solo quando l'elemento attivo è un candidato. Se gli elementi contengono i propri controlli attivabili, rendi anche questi controlli candidati o lasciali invariati; non risolvere un elemento principale con
closest(). - Quando viene trovato un candidato, chiama
preventDefault(), aggiorna il rovingtabindex, chiamafocus()e utilizzascrollIntoView({ block: 'nearest', inline: 'nearest' }). - Se non viene trovato alcun candidato, restituisci il risultato senza chiamare
preventDefault()in modo che lo scorrimento nativo continui.
Evita di aggiungere scorciatoie di un solo carattere come h/j/k/l o W/A/S/D per la navigazione spaziale. I tasti Freccia non sono tasti di caratteri e la conservazione dello scorrimento nativo quando non esiste un candidato evita di bloccare inutilmente il movimento della visualizzazione.