Questa traduzione è incompleta. Collabora alla traduzione di questo articolo dall’originale in lingua inglese.

Ci sono molti casi in cui i widget del modulo html disponibili non sono abbastanza. Se vuoi eseguire uno styling avanzato su alcuni widget come l'elemento <select>  o sei vuoi fornirli di comportamenti personalizzati, l'unica scelta che hai è quella di costruirti il tuo widget personale.

In quest'articolo vedremo come costruire questo tipo di widget. A tal fine lavoreremo con un esempio: ricostruire l'elemento <select> .

Nota: ci concentreremo sulla costruzione del widget, non su come rendere il codice generico e riutilizzabile; questo comporterebbe del codice JavaScript and la manipolazione del DOM in un contesto sconosciuto, uscendo fuori dallo scopo di quest'articolo.

Design, strutture, e semantica

Prima di costruire un widget personalizzato, dovresti cominciare a pensare esattamente cosa vuoi. Questo ti aiuterà a evitare perdite di tempo. In particolare, e' importante definire chiaramente tutti gli stati del tuo widget. Per fare ciò, è bene cominciare da un widget esistente che ha stati e comportamenti ben noti.

Nel nostro esempio ricostruiremo l'elemento <select> . Ecco il risultato che vogliamo raggiungere:

The three states of a select box

Questo screenshot ci illustra i tre stati principali del nostro widget: lo stato normale(a sinistra); lo stato di attivazione ( al centro) and lo stato aperto (a destra).

In termini di compartamento vogliamo che il nostro widget sia utilizabile con il mouse e con la tastiera, proprio come un ogni widget non modificato. Cominciamo definendo come il widget raggiungerà uno stato:

Il widget è nel suo stato normale quando:
  • la pagina carica
  • Il widget era attivo e l'utente fa clico ovunque al di fuori del widget
  • il widget era attivo e l'utente sposta lo stato attivo su un altro widget usando la tastiera

Nota: Moving the focus around the page is usually done through hitting the tab key, but this is not standard everywhere. For example cycling through links on a page is done in Safari by default using the Option+Tab combination.

Il widget è nel suo stato attivo quando:
  • L'user lo clicca
  • L'user clicca il tasto tab e lo evidenzia
  • il widget era aperto and l'user clicca sul widget.
Il widget è nello stato di apertura quando:
  • il widget è in uno stato diverso da quello aperto e l'utente fa clic su di esso

Una volta compreso come cambiare gli stati, e importante definire come cambiare i valori del widget:

I valori cambiano quando:
  • l'user clicca su un'opzione quando il widget è nello stato di apertura
  • l'user preme dalla tastiera la frecciasu o giù quando il widget è aperto 

Finalmente, possiamo definire come le opzioni del widget dovranno comportarsi:

  • Quando il widget è aperto, l'opzione selezionata è evidenziata
  • Quando il mouse si trova su un'opzione, l'opzione viene evidenziata e l'opzione evidenziata in precedenza viene riportata al suo stato normale

Ai fini del nostro esempio, ci fermeremo qui; comunque, se sei un lettore attendo avrai notato che mancano dei comportamenti. Per esempio: Cosa accadrà se l'user preme tab MENTRE il widget è aperto?La risposta è... Nulla. OK, il comportamento corretto sembra ovvio ma il fatto è che, poiché non è definito nelle nostre specifiche, è molto facile trascurare questo comportamento. Ciò è particolarmente vero in un ambiente di squadra quando le persone che progettano il comportamento del widget sono diverse da quelle che lo implementano.

Un altro esempio divertente: Cosa succederà se l'user premerà il tasto su o giù mentre il widget è aperto? Questo è un po' più complicato. Se consideri che lo stato attivo e lo stato aperto sono totalmente diversi, la risposta è di nuovo " non succederà niente"!
Perchè non abbiamo definito nessuna interazione con la tastiera quando il widget è aperto.D'altronde, se si considera che lo stato attivo e lo stato aperto si sovrappongono un po', il valore potrebbe cambiare ma l'opzione ma l'opzione non sarà sicuramente evidenziata di conseguenza, ancora una volta perchè non abbiamo definito interazioni con la tastiera con le opzioni quando il widget si trova nel suo stato aperto (abbiamo solo definito cosa dovrebbe accadere quando il widget è aperto, ma nulla dopo).

le specifiche mancanti sono ovvie, quindi le gestiremo, ma potrebbe rivelare dei problemi in widget nuovi ed esotici, fper cui nessuno ha la minima idea di quale sia il comportamento giusto. E' sempre opportuno utilizzare bene il proprio tempo in questa fase di desgin: se definisci malamente un comportamento, o dimentichi di definirne uno, sarà molto difficile ridefinirlo una volta che gli utenti si saranno abituati. Se hai dubbi, chiedi l'opinione altrui , e se disponi di un budget non esitare in un user tests. Questo processo è chiamato UX design. Se vuoi conoscere di più a proposito di questo argomento, dovresti controllare le seguenti risorse:

Nota: In molti sistemi c'è un modo per aprire l'elemento <select> per esaminare tutte le scelte disponibili (lo stesso elemento <select> ). This is achieved with Alt+Down arrow under Windows and was not implemented in our example —but it would be easy to do so, as the mechanism has already been implemented for the click event.

Definire la struttura e la semantica HTML

Ora che le funzionalità basi del nostro widget sono state deficse, e' tempo di cominciare a costruire il nostro widget. Il primo passo è di definire la struttura HTML . Questo è ciò che dobbiamo ricostruire <select>:

<!-- Questo è il nostro container 
     principale per il nostro widget.
     l'attributo tabindex permette 
     all'utente di concentrarsi sul widget. 
     Vedremo dopo cos'è meglio
     utilizzare con javascript. -->

<div class="select" tabindex="0">
  
  <!-- Questo container verrà usato per stampare il valore corrente del widget -->
  <span class="value">Cherry</span>
  
  <!-- Questo container conterrà tutte le opzioni disponibili per il nostro widget.
       dato che sarà una lista, utilizzare l'opzione ul è valido. -->
  <ul class="optList">
    <!-- Ogni opzione contiene solo il valore da visualizzare, vedremo dopo come 
         manipolare il reale valore che vogliamo inviare col form -->
    <li class="option">Cherry</li>
    <li class="option">Lemon</li>
    <li class="option">Banana</li>
    <li class="option">Strawberry</li>
    <li class="option">Apple</li>
  </ul>

</div>

Nota l'uso del nome delle classi; questi identificano ciascuna parte rilevante indipendentemente dagli effettivi elementi HTML sottostanti utilizzati. Questo è importante per essere sicuri che non legheremo il nostro CSS e JavaScript ad una forte struttura HTML, in modo da poter apportare modifiche all'implementazione in un secondo momento senza rompere il codice che utilizza il widget. Per esempio se desideri implementare l'equivalente dell'elemento <optgroup>.

Creare l'aspetto grafico utilizzando i CSS

Ora che abbiamo una struttura HTML possiamo cominciare a disegnare il nostro widget. L'intero punto di costruzione di questo widget personalizzato è di essere in grado di modellare questo widget esattamente come vogliamo. Al fine di ciò, divideremo i nostri CSS in due parti: la prima parte sarà necessaria per avere un widget simile a <select> ,la seconda parte consisterà nella parte grafica in modo che appaia come noi vogliamo.

Stili richiesti

Gli stili richiesti sono quelli necessari per gestire i tre stati del nostro widget.

.select {
  /* Questo creerà un contesto di posizionamento per l'elenco di opzioni */
  position: relative;
 
  /*Questo renderà il nostro widget parte del flusso di testo e allo stesso tempo considerevole */
  display : inline-block;
}

Abbiamo bisogno di una classe extra "active" per definire il look del nostro widget quando è nello stato di attivazione. Poichè il nostro widget è selezionabile,dobbiamo raddoppiare questo stile personale con la pseudo-classe :focus In modo da essere sicuri che abbiano lo stesso comportamento.

.select.active,
.select:focus {
  outline: none;
 
  /* Questo box-shadow non è richiesto al corretto funzionamento , 
     tuttavia è importante per essere sicuro che lo stato di attivazione
     sia visibile e che lo utilizziamo come valore di defaul, 
     sentiti libero di modificarlo. */
  box-shadow: 0 0 3px 1px #227755;
}

Ora manipoliamo la lista delle opzioni:

/* il selettore .select è zucchero sintattico ( traduzione letterale, 
   concettualmente vuol dire " rendere del codice più dolce, più umano " 
   per essere sicuri che le classi che definiamo siano quelli all'interno del nostro widget.
 */

.select .optList {
  /* Ciò assicurerà che il nostro elenco di opzioni sia visualizzato sotto il valore
      e fuori dal flusso HTML */
  position : absolute;
  top      : 100%;
  left     : 0;
}

Abbiamo bisogno di una classe extra per manipolare la lista quando le opzioni sono nascoste. Questo è necessario per gestire le differenze tra lo stato attivo e lo stato aperto che non corrispondono esattamente.

.select .optList.hidden {
  /* Questo è un modo semplice per nascondere la lista in modo accessibile,
      parleremo di più sull'accessibilità alla fine */
  max-height: 0;
  visibility: hidden;
}

Abbellimento

Ora che abbiamo le funzionalità base, possiamo cominciare a divertirci. Quello seguente è solo un esempio di ciò che è possibile, e corrisponderà allo screenshot all'inizio di questo articolo. Dovresti sentirti libero di sperimentare e vedere cosa accade.

.select {
  /* Tutte le taglie saranno espresse con il valore em per motivi di accessibilità
      (per assicurarsi che il widget rimanga ridimensionabile se l'utente usa il
      zoom del browser in modalità solo testo). I calcoli sono fatti
      assumendo 1em == 16px quale è il valore predefinito nella maggior parte dei browser.
      Se ti perdi la conversione di px in em, prova http://riddle.pl/emcalc/ * /
  font-size   : 0.625em; 
  font-family : Verdana, Arial, sans-serif;

  -moz-box-sizing : border-box;
  box-sizing : border-box;

  /* Abbiamo bisogno di spazio extra per la freccia in giù che aggiungeremo
 */
  padding : .1em 2.5em .2em .5em; /* 1px 25px 2px 5px */
  width   : 10em; /* 100px */

  border        : .2em solid #000; /* 2px */
  border-radius : .4em; /* 4px */
  box-shadow    : 0 .1em .2em rgba(0,0,0,.45); /* 0 1px 2px */
  
  /* La prima dichiarazione serve per i browser che non supportano i gradienti lineari.
     La seconda dichiarazione è perché i browser basati su WebKit non hanno ancora una definizione prefissata.
     Se vuoi supportare i browser legacy prova http://www.colorzilla.com/gradient-editor/ */
  background : #F0F0F0;
  background : -webkit-linear-gradient(90deg, #E3E3E3, #fcfcfc 50%, #f0f0f0);
  background : linear-gradient(0deg, #E3E3E3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  /* poichè i valori possono essere più larghi del nostro widget, dobbiamo essere sicuri
     che il widget cambierà dimensione */
  display  : inline-block;
  width    : 100%;
  overflow : hidden;

  vertical-align: top;

  /* E se il contenuto va in overflow, è bello avere un bell'elisse . */
  white-space  : nowrap;
  text-overflow: ellipsis;
}

Non abbiamo bisogno di altri elementi per disegnare la freccia in basso; useremo invece lo pseudo-elemento :after. Comunque potrebbe anche essere implementato usando una semplice immagine  di background.

.select:after {
  content : "▼"; /* utilizziamo il carattere unicode U+25BC; vedi http://www.utf8-chartable.de */
  position: absolute;
  z-index : 1; /* importante per la posizione della freccia in modo da evitare
               sovrapposizionamenti */
  top     : 0;
  right   : 0;

  -moz-box-sizing : border-box;
  box-sizing : border-box;

  height  : 100%;
  width   : 2em;  /* 20px */
  padding-top : .1em; /* 1px */

  border-left  : .2em solid #000; /* 2px */
  border-radius: 0 .1em .1em 0;  /* 0 1px 1px 0 */

  background-color : #000;
  color : #FFF;
  text-align : center;
}

ora cominciamo il design della lista delle opzioni:

.select .optList {
  z-index : 2; /* Dichiariamo esplicitamente che la lista delle opzioni si sovraporrà 
                  alla freccia */

  /*Questo resetterà lo stile di default degli elementi <ul> */
  list-style: none;
  margin : 0;
  padding: 0;

  -moz-box-sizing : border-box;
  box-sizing : border-box;

  /* Ciò assicurerà che anche se i valori sono inferiori al widget,
      l'elenco delle opzioni sarà grande quanto il widget stesso */
  min-width : 100%;

  /*Nel caso in cui l'elenco sia troppo lungo, il suo contenuto si sovrapporrà verticalmente
      (che aggiungerà automaticamente una barra di scorrimento verticale) ma mai in orizzontale
      (poiché non abbiamo impostato una larghezza, la lista regolerà automaticamente la sua larghezza.
      Se non è possibile, il contenuto verrà troncato) */
  max-height: 10em; /* 100px */
  overflow-y: auto;
  overflow-x: hidden;

  border: .2em solid #000; /* 2px */
  border-top-width : .1em; /* 1px */
  border-radius: 0 0 .4em .4em; /* 0 0 4px 4px */

  box-shadow: 0 .2em .4em rgba(0,0,0,.4); /* 0 2px 4px */
  background: #f0f0f0;
}

Per le opzioni abbiamo bisogno di aggiungere la classe highlight in modo da identificare il valore che l'user selezionerà (o ha selezionato).

.select .option {
  padding: .2em .3em; /* 2px 3px */
}

.select .highlight {
  background: #000;
  color: #FFFFFF;
}

ecco i risultati dei nostri 3 stati:

Basic state Active state Open state
Check out the source code

Porta in vita il tuo widget con javascript

Ora che il nostro designe e la nostra struttura sono pronti possiamo scrivere il codice JavaScript che farà effettivamente lavorare il widget.

Pericolo: Il seguente codice è educativo e non dovrebbe essere usato così com'è. Tra le tante cose, come vedremo, non è a prova di futuro e non funzionerà sui browser legacy. Ha anche parti ridondanti che dovrebbero essere ottimizzate nel codice di produzione.

Nota: Creare widget riutilizzabili è qualcosa che può essere un po 'complicato. La bozza del componente Web W3C è una delle risposte a questo specifico problema. Il progetto X-Tag è un'implementazione di test di questa specifica; ti invitiamo a dare un'occhiata a questo.

Perchè non funziona?

Prima di cominciare, è importante ricordare qualcosa di fondamentale a proposito di JavaScript: dentro un browser, è una tecnologia inaffidabile. Quando stai costruendo widget personalizzati, dovrai fare affidamento su javascript perchè è un filo necessario per legare tutto insieme. Tuttavia, ci sono molti casi in cui JavaScript non può essere eseguito nel browser:

  • L'user ha bloccato l'utilizzo di javascript: Questo è il caso più insolito di sempre; veramente poche persone bloccano l'utilizzo di JavaScript oggigiorno.
  • Lo script non si carica. Questo è uno dei casi più comuni, specialmente nel mondo mobile.
  • Lo script è buggato. Dovresti sempre considerare questa possibilità.
  • Lo script entra in conflitto con uno script di terze parti. Questo può accadere con gli script di tracciamento o qualsiasi bookmarklet che l'utente utilizza.
  • Lo script è in conflitto con, o è affetto da, un'estensione del browser (come  Firefox's NoScript extension o Chrome's NotScripts ).
  • L'user sta utilizzando un browser Legacy, e una delle feature che richiedi non è supportata. Ciò si verifica frequentemente quando si utilizzano API all'avanguardia.

Per via di questi rischi, è molto importante considerare seriamente cosa accadrà se JavaScript non funziona. Trattare dettagliatamente questo problema è fuori dallo scopo di questo articolo perché è strettamente legato al modo in cui si desidera rendere generico e riutilizzabile lo script, ma nel nostro esempio considereremo le basi di ciò.

Nel nostro esempio, se il nostro codice JavaScript non è in esecuzione, ricorreremo alla visualizzazione di un elemento <select>. Per raggiungere questo abbiamo bisogno di due cose.

Per prima cosa, dobbiamo aggiungere un normale elemento <select> prima di ogni utilizzo del nostro widget personalizzato. Questo è in realtà richiesto anche per poter inviare dati dal nostro widget personalizzato insieme al resto dei dati del nostro modulo;diremo di più a proposito più tardi.

<body class="no-widget">
  <form>
    <select name="myFruit">
      <option>Cherry</option>
      <option>Lemon</option>
      <option>Banana</option>
      <option>Strawberry</option>
      <option>Apple</option>
    </select>

    <div class="select">
      <span class="value">Cherry</span>
      <ul class="optList hidden">
        <li class="option">Cherry</li>
        <li class="option">Lemon</li>
        <li class="option">Banana</li>
        <li class="option">Strawberry</li>
        <li class="option">Apple</li>
      </ul>
    </div>
  </form>

</body>

Secondo, abbiamo bisogno di due nuove classi per nascondere l'elemento non necessario (cioè, l'elemento "reale" <select> se il nostro script non è in esecuzione o il widget personalizzato è in esecuzione) . Nota che per impostazione predefinita, il nostro codice HTML nasconde il nostro widget personalizzato.

.widget select,
.no-widget .select {
  /*Questo selettore css afferma:
     - o abbiamo impostato la classe del corpo su "widget", 
       quindi nascondiamo l'effettivo elemento <select>
      - o non abbiamo cambiato la classe del corpo, quindi la classe del corpo 
         è ancora "no-widget",
        quindi gli elementi di classe "select" devono essere nascosti */
  position : absolute;
  left     : -5000em;
  height   : 0;
  overflow : hidden;
}

Ora abbiamo solo bisogno di un interruttore JavaScript per determinare se lo script è in esecuzione o meno. Questa opzione è molto semplice: se al momento del caricamento della pagina il nostro script è in esecuzione, rimuoverà la classe no-widget e aggiungerà la classe widget, scambiando così la visibilità dell'elemento <select> e del widget personalizzato.

window.addEventListener("load", function () {
  document.body.classList.remove("no-widget");
  document.body.classList.add("widget");
});
Without JS With JS
Check out the source code

Nota: Se vuoi veramente rendere il tuo codice generico e riutilizzabile, invece di fare un cambio di classe è meglio aggiungere semplicemente la classe del widget per nascondere gli elementi <select> e aggiungere dinamicamente l'albero DOM che rappresenta il widget personalizzato dopo ogni <select> elemento nella pagina.

Rendere il lavoro più facile

Nel codice che stiamo per costruire, useremo l'API DOM standard per fare tutto il lavoro di cui abbiamo bisogno. Tuttavia, sebbene il supporto dell'API DOM sia diventato molto meglio nei browser, ci sono sempre problemi con i browser legacy (specialmente con il buon vecchio Internet Explorer).

Se vuoi evitare problemi con i browser legacy, ci sono due modi per farlo: usando un framework dedicato come jQuery, $ dom, prototype, Dojo, YUI, o simili, o facendo il polyfilling della funzione mancante che vuoi usare ( che può essere fatto facilmente attraverso il caricamento condizionale, ad esempio con la libreria yepnope).

Le funzionalità che intendiamo utilizzare sono le seguenti (ordinate dal più rischioso al più sicuro):

  1. classList
  2. addEventListener
  3. forEach (This is not DOM but modern JavaScript)
  4. querySelector and querySelectorAll

Oltre alla disponibilità di tali funzionalità specifiche, rimane ancora un problema prima dell'avvio. L'oggetto restituito dalla funzione querySelectorAll () è un NodeList piuttosto che una matrice. Questo è importante perché gli oggetti Array supportano la funzione forEach, ma NodeList no. Poiché NodeList sembra davvero un array e poiché forEach è così comodo da usare, possiamo facilmente aggiungere il supporto di forEach a NodeList per rendere la nostra vita più facile, così:

NodeList.prototype.forEach = function (callback) {
  Array.prototype.forEach.call(this, callback);
}

We weren't kidding when we said it's easy to do.

Creazione di eventi Callback

Il terreno è pronto, ora possiamo iniziare a definire tutte le funzioni che verranno utilizzate ogni volta che l'utente interagisce con il nostro widget.

// Questa funzione verrà utilizzata ogni volta che si desidera disattivare un widget personalizzato
// Richiede un parametro
// seleziona: il nodo DOM con la classe `select` da disattivare
function deactivateSelect(select) {

  // Se il widget non è attivo non c'è nulla da fare
  if (!select.classList.contains('active')) return;

  // Abbiamo bisogno di ottenere l'elenco delle opzioni per il widget personalizzato
  var optList = select.querySelector('.optList');

  // Chiudiamo la lista delle opzioni
  optList.classList.add('hidden');

  // e disattiviamo il custom widget
  select.classList.remove('active');
}

// questa funzione verrà utilizzata ogni volta che l'user(dis)attiverà il widget
// prende due parametri:
// select : il nodo del DOM con le classi 'select' da attivare
// selectList :la lista di tutti i nodi dom con la la classe 'select'

function activeSelect(select, selectList) {

  // Se il widget è già attivo non c'è niente da fare
  if (select.classList.contains('active')) return;

  // Dobbiamo disattivare lo stato attivo su tutti i widget personalizzati
   // Perché la funzione deactivateSelect soddisfa tutti i requisiti di
   // per ogni funzione di callback, la usiamo direttamente senza usare un intermediario
   // funzione anonima.
  selectList.forEach(deactivateSelect);

  // E attiviamo lo stato attivo per questo specifico widget
  select.classList.add('active');
}

// Questa funzione verrà utilizzata ogni volta che l'utente desidera aprire / chiudere l'elenco di opzioni
// Richiede un parametro:
// seleziona: il nodo DOM con l'elenco da attivare
function toggleOptList(select) {

 // L'elenco è tenuto dal widget
  var optList = select.querySelector('.optList');

  // Modifichiamo la classe dell'elenco per mostrarlo / nasconderlo
  optList.classList.toggle('hidden');
}

// Questa funzione verrà utilizzata ogni volta che sarà necessario evidenziare un'opzione
// Ci vogliono due parametri:
// seleziona: il nodo DOM con la classe `select` contenente l'opzione da evidenziare
// opzione: il nodo DOM con la classe `option` da evidenziare
function highlightOption(select, option) {

  // Otteniamo l'elenco di tutte le opzioni disponibili per il nostro elemento di selezione personalizza
  var optionList = select.querySelectorAll('.option');

  // Rimuoviamo l'evidenziazione da tutte le opzioni
  optionList.forEach(function (other) {
    other.classList.remove('highlight');
  });

 // Evidenziamo l'opzione giusta
  option.classList.add('highlight');
};

 

Questo è tutto ciò che serve per gestire i vari stati del widget personalizzato.

Successivamente, associamo queste funzioni agli eventi appropriati:

 

// Gestiamo il binding di eventi quando il documento è caricato.
window.addEventListener('load', function () {
  var selectList = document.querySelectorAll('.select');

  // Each custom widget needs to be initialized
  selectList.forEach(function (select) {

    // Ogni widget personalizzato deve essere inizializzato
    var optionList = select.querySelectorAll('.option');

    // Ogni volta che un utente passa il mouse su un'opzione, evidenziamo l'opzione data
    optionList.forEach(function (option) {
      option.addEventListener('mouseover', function () {
        // Nota: le variabili `select` e` option` sono le chiusure
         // disponibile nell'ambito della nostra chiamata di funzione.
        highlightOption(select, option);
      });
    });

    // Ogni volta che l'utente fa clic su un elemento di selezione personalizzato
    select.addEventListener('click', function (event) {
     // Nota: la variabile `select` è una chiusura
       // disponibile nell'ambito della nostra chiamata di funzione.

      // Accendiamo la visibilità dell'elenco di opzioni
      toggleOptList(select);
    });

    // Nel caso in cui il widget ottenga lo stato attivo
     // Il widget ottiene l'attenzione ogni volta che l'utente fa clic su di esso o ogni volta
     // usano la chiave di tabulazione per accedere al widget
    select.addEventListener('focus', function (event) {
      // Nota: le variabili `select` e` selectList` sono le chiusure
       // disponibile nell'ambito della nostra chiamata di funzione.

      // Attiviamo il widget
      activeSelect(select, selectList);
    });

    // Nel caso in cui il widget lasci il focus
    select.addEventListener('blur', function (event) {
      // Nota: la variabile `select` è una chiusura
       // disponibile nell'ambito della nostra chiamata di funzione.

       // Disattiamo il widget
      deactivateSelect(select);
    });
  });
});

A quel punto, il nostro widget cambierà stato in base al nostro progetto, ma il suo valore non viene ancora aggiornato. Lo gestiremo dopo.

Live example
Check out the source code

Gestire il valore del Widget

 

Ora che il nostro widget funziona, dobbiamo aggiungere del codice per aggiornarne il valore in base all'input dell'utente e rendere possibile l'invio del valore insieme ai dati del modulo.

Il modo più semplice per farlo è usare un widget nativo sotto il cofano. Tale widget terrà traccia del valore con tutti i controlli integrati forniti dal browser e il valore verrà inviato normalmente al momento della presentazione di un modulo. Non ha senso reinventare la ruota quando possiamo fare tutto questo per noi.

Come visto in precedenza, utilizziamo già un widget di selezione nativo come fallback per motivi di accessibilità; possiamo semplicemente sincronizzare il suo valore con quello del nostro widget personalizzato:

 

// Questa funzione aggiorna il valore visualizzato e lo sincronizza con il widget nativo.
// Ci vogliono due parametri:
// seleziona: il nodo DOM con la classe `select` contenente il valore da aggiornare
// indice: l'indice del valore da selezionare
function updateValue(select, index) {
 // Abbiamo bisogno di ottenere il widget nativo per il widget personalizzato specificato
   // Nel nostro esempio, quel widget nativo è un fratello del widget personalizzato
  var nativeWidget = select.previousElementSibling;

  // Abbiamo anche bisogno di ottenere il valore segnaposto del nostro widget personalizzato
  var value = select.querySelector('.value');

  // E abbiamo bisogno dell'intero elenco di opzioni
  var optionList = select.querySelectorAll('.option');

  // Impostiamo l'indice selezionato sull'indice di nostra scelta
  nativeWidget.selectedIndex = index;

  // Aggiorniamo il valore placeholder di conseguenza
  value.innerHTML = optionList[index].innerHTML;

  // E evidenziamo l'opzione corrispondente del nostro widget personalizzato
  highlightOption(select, optionList[index]);
};

// Questa funzione restituisce l'indice selezionato corrente nel widget nativo
// Richiede un parametro:
// seleziona: il nodo DOM con la classe `select` relativa al widget nativo
function getIndex(select) {
 // È necessario accedere al widget nativo per il widget personalizzato specificato
   // Nel nostro esempio, quel widget nativo è un fratello del widget personalizzato
  var nativeWidget = select.previousElementSibling;

  return nativeWidget.selectedIndex;
};

Con queste due funzioni, possiamo associare i widget nativi a quelli personalizzati:

// Gestiamo il binding di eventi quando il documento è caricato.
window.addEventListener('load', function () {
  var selectList = document.querySelectorAll('.select');

 // Ogni widget personalizzato deve essere inizializzato
  selectList.forEach(function (select) {
    var optionList = select.querySelectorAll('.option'),
        selectedIndex = getIndex(select);

    // Rendiamo focalizzabile il nostro widget personalizzato
    select.tabIndex = 0;

// Facciamo in modo che il widget nativo non sia più focalizzabile
    select.previousElementSibling.tabIndex = -1;

    // Ci assicuriamo che il valore selezionato di default sia visualizzato correttamente
    updateValue(select, selectedIndex);

    // Ogni volta che un utente fa clic su un'opzione, aggiorniamo di conseguenza il valore
    optionList.forEach(function (option, index) {
      option.addEventListener('click', function (event) {
        updateValue(select, index);
      });
    });

    // Ogni volta che un utente usa la propria tastiera su un widget focalizzato, aggiorniamo di conseguenza il valore
    select.addEventListener('keyup', function (event) {
      var length = optionList.length,
          index  = getIndex(select);

     // Quando l'utente preme la freccia giù, passiamo all'opzione successiva
      if (event.keyCode === 40 && index < length - 1) { index++; }

// Quando l'utente preme la freccia su, passiamo all'opzione precedente
      if (event.keyCode === 38 && index > 0) { index--; }

      updateValue(select, index);
    });
  });
});

 

Nel codice sopra, vale la pena notare l'uso della proprietà tabIndex. L'utilizzo di questa proprietà è necessario per garantire che il widget nativo non acquisisca mai l'attenzione e per assicurarsi che il nostro widget personalizzato ottenga lo stato attivo quando l'utente utilizza la sua tastiera o il suo mouse.

Con quello, abbiamo finito! Ecco il risultato:

 

Live example
Check out the source code

Ma aspetta un secondo, abbiamo davvero finito?

Renderlo accessibile

 

Abbiamo costruito qualcosa che funziona e sebbene siamo lontani da una casella di selezione completa, funziona bene. Ma quello che abbiamo fatto non è altro che il violino con il DOM. Non ha una vera semantica e, anche se sembra una casella di selezione, dal punto di vista del browser non ne è uno, quindi le tecnologie assistive non saranno in grado di capire che è una casella di selezione. In breve, questa nuova casella di selezione non è accessibile!

Fortunatamente, esiste una soluzione e si chiama ARIA. ARIA è l'acronimo di "Accessible Rich Internet Application" ed è una specifica W3C specificamente progettata per ciò che stiamo facendo qui: rendere accessibili le applicazioni web e i widget personalizzati. È fondamentalmente un insieme di attributi che estendono l'HTML in modo da poter meglio descrivere ruoli, stati e proprietà come se l'elemento che abbiamo appena escogitato fosse l'elemento nativo per cui tentava di passare. L'utilizzo di questi attributi è estremamente semplice, quindi facciamolo.

 

L'attributo 'role'

 

L'attributo chiave utilizzato da ARIA è l'attributo 'role'. L'attributo 'role' accetta un valore che definisce per cosa viene usato un elemento. Ogni 'role' definisce i propri requisiti e comportamenti. Nel nostro esempio, useremo 'role listbox'. È un "ruolo composito", ovvero gli elementi con quel ruolo si aspettano di avere figli, ciascuno con un ruolo specifico (in questo caso, almeno un bambino con il ruolo di opzione).

Vale anche la pena notare che ARIA definisce i ruoli che vengono applicati di default al markup HTML standard. Ad esempio, l'elemento <table> corrisponde alla griglia del ruolo e l'elemento <ul> corrisponde all'elenco dei ruoli. Poiché utilizziamo un elemento <ul>, vogliamo assicurarci che il ruolo listbox del nostro widget sostituisca il ruolo di lista dell'elemento <ul>. A tal fine, useremo la presentazione del ruolo. Questo ruolo è stato progettato per farci indicare che un elemento non ha un significato speciale e viene utilizzato esclusivamente per presentare informazioni. Lo applicheremo al nostro elemento <ul>.

Per supportare il ruolo listbox, dobbiamo solo aggiornare il nostro codice HTML in questo modo:

 

<! - Aggiungiamo l'attributo role = "listbox" al nostro elemento principale -><div class="select" role="listbox">
  <span class="value">Cherry</span>
  <! - Aggiungiamo anche il role = "presentation" all'elemento ul ->
  <ul class="optList" role="presentation">
    <! - E aggiungiamo l'attributo role = "option" a tutti gli elementi li ->
    <li role="option" class="option">Cherry</li>
    <li role="option" class="option">Lemon</li>
    <li role="option" class="option">Banana</li>
    <li role="option" class="option">Strawberry</li>
    <li role="option" class="option">Apple</li>
  </ul>
</div>

Nota: Includere sia l'attributo role sia un attributo class è necessario solo se si desidera supportare i browser legacy che non supportano i selettori dell'attributo CSS. CSS attribute selectors.

L'attributo aria-selected 

Usare l'attributo role non è abbastanza. ARIA fornisce anche molti stati e attributi di proprietà. Più e meglio li usi, più il tuo widget sarà compreso dalle tecnologie assistive. Nel nostro caso, limiteremo il nostro utilizzo a un attributo: aria-selected.

l'attributo aria-selected è usato per contrassegnare quale opzione è attualmente selezionata; questo consente alle tecnologie assistive di informare l'utente su quale sia la selezione corrente. Lo useremo dinamicamente con JavaScript per contrassegnare l'opzione selezionata ogni volta che l'utente ne sceglie uno. A tal fine, abbiamo bisogno di rivedere la nostra funzione updateValue():

function updateValue(select, index) {
  var nativeWidget = select.previousElementSibling;
  var value = select.querySelector('.value');
  var optionList = select.querySelectorAll('.option');

  // Ci assicuriamo che tutte le opzioni non siano selezionate
  optionList.forEach(function (other) {
    other.setAttribute('aria-selected', 'false');
  });

  // Ci assicuriamo che l'opzione scelta sia selezionata
  optionList[index].setAttribute('aria-selected', 'true');

  nativeWidget.selectedIndex = index;
  value.innerHTML = optionList[index].innerHTML;
  highlightOption(select, optionList[index]);
};

Ecco il risultato finale di tutti questi cambiamenti (otterrai una sensazione migliore provandola con una tecnologia di assistenza come NVDA o VoiceOver):

Live example
Check out the final source code

Conclusioni

 

Abbiamo visto tutti i fondamenti della creazione di un widget di modulo personalizzato, ma come puoi vedere non è banale da fare, e spesso è meglio e più facile affidarsi a librerie di terze parti invece di codificarle da zero da soli (a meno che, ovviamente, il tuo obiettivo è costruire una tale biblioteca).

Ecco alcune librerie da prendere in considerazione prima di codificare le tue:

 

Se vuoi andare avanti, il codice in questo esempio necessita di qualche miglioramento prima che diventi generico e riutilizzabile. Questo è un esercizio che puoi provare ad esibirti. Due suggerimenti per aiutarti in questo: il primo argomento per tutte le nostre funzioni è lo stesso, il che significa che quelle funzioni necessitano dello stesso contesto. Costruire un oggetto per condividere quel contesto sarebbe saggio. Inoltre, è necessario renderlo a prova di funzionalità; cioè, deve essere in grado di funzionare meglio con una varietà di browser la cui compatibilità con gli standard Web utilizzati varia. Divertiti!

 

In questo modulo

 

Tag del documento e collaboratori

Hanno collaborato alla realizzazione di questa pagina: whiteLie
Ultima modifica di: whiteLie,