Accessibilità: l’ingrediente mancante

Tanto tanto tempo fa, trattavo l’accessibilità web come un extra. Certo, le mie immagini avevano l’attributo alt. Sì, i miei link avevano i title. Onoravo anche la compatibilità con la 508, ma solitamente era l’ultima cosa che facevo. Per come la vedevo, la torta era pronta da mangiare: l’accessibilità era la glassa decorativa da spalmarci sopra alla fine.

L’articolo prosegue sotto

Purtroppo, non ero l’unico. Molti sviluppatori con cui ho recentemente parlato sembra che mettano l’accessibilità all’ultimo posto, sempre che la gestiscano. Perché l’accessibilità viene spesso trattata come un ripensamento? Tra i motivi principali vanno inclusi la mancanza di tool e specifiche, una bassa domanda da parte del settore e la pigrizia da parte degli sviluppatori.

Tornando alla fine degli anni ’90, l’accessibilità web era al massimo “primitiva”. JAWS era ancora nella sua infanzia e WAI-ARIA non era ancora stata concepita. Tutto quello che sembrava avere importanza era quanto rapidamente si potesse impressionare in maniera visuale un utente. DHTML e Flash dettavano legge e le camicie di flanella piacevano di più portate legate in vita.

Mentre l’accessibilità cominciava a prendere velocità nel 2004-2005, molti sviluppatori e molte aziende prestavano ancora poca attenzione alla materia. I web developer erano persi nella magia nera di CSS, nei tutorial PHP e nei libri su JavaScript. In qualità di freelancer con una certa esperienza potevo dire di non aver mai sentito menzionare l’accessibilità tra i requisiti di un progetto.

Malgrado ora sia ben pubblicizzata e supportata, WAI-ARIA, una specifica progettata specificamente per aiutare gli utenti disabili, non viene implementata spesso da noi sviluppatori. Forse stiamo aspettando che un cliente con una mente portata all’accessibilità catalizzi il processo di apprendimento. Forse ci sentiamo sopraffatti e non sappiamo da dove cominciare. Qualunque sia la scusa, non è più valida date le risorse e la tecnologia attualmente a nostra disposizione.

Dopo alcuni mesi di lavoro in IBM, io e i miei colleghi stavamo devolvendo le nostre ore settimanali ad un’applicazione web che doveva soddisfare una rigorosa checklist di accessibilità. Dovevamo testare attentamente l’intera applicazione web affinché fosse conforme a WAI-ARIA e alle WCAG. All’improvviso, mi venne dato il compito di creare un piano generale che descrivesse nel dettaglio le questioni aperte, oltre a stimare quanto ci avrebbe messo il team per sistemarle. Mettere l’accessibilità all’ultimo posto per così tanti anni mi aveva lasciato poco istruito e impreparato.

Attraverso un processo di prove ed errori, con l’aiuto dei miei colleghi più edotti e leggendo molto riguardo a questa materia, ne sono uscito come uno sviluppatore diverso. Scrivere codice accessibile non è una cosa extra da considerare alla fine di un progetto, ma semplicemente un’altra cosa da considerare fin dall’inizio. Vediamo passo a passo un esempio fatto in casa che mostra alcuni utili WAI-ARIA roles, states e properties.

Prima di iniziare#section1

Gli screen reader, così come i browser, cambiano molto a seconda di chi li produce. Non ho intenzione di fornire un tutorial esaustivo su questo vasto argomento. Sceglierò ChromeVox per questo tutorial perché è uno screen reader plugin gratuito che funziona sia su Mac sia su Windows. Comunque, esistono moltissimi altri screen reader. Per lo sviluppo web con WAI-ARIA, le scelte più popolari sono le seguenti:

Utenti Windows#section2

Utenti Mac#section3

Utenti Linux#section4

Come già detto, ogni screen reader è unico. Se seguite questa demo con uno screen reader diverso da ChromeVox, noterete delle differenze nel modo in cui la tecnologia assistiva fa il parse di ARIA.

Cosa stiamo cercando di ottenere?#section5

Il nostro cliente vorrebbe uno shopping cart per il suo negozio che vende parti di robot, Robot Shopper. Il carrello deve soddisfare le linee guida di WAI-ARIA e funzionare bene da tastiera. Per testare il nostro carrello con uno screen reader, useremo ChromeVox, che ci permetterà di fare l’esperienza uditiva della nostra pagina web. Quando avremo finito, potrete disabilitare il plugin. Per farlo, andate su chrome://extensions e togliete la spunta dal box accanto al plugin ChromeVox.

Per gestire gli oggetti nel carrello useremo HTML5 storage e useremo un sistema di templating JavaScript con Handlebars per mostrare il markup del nostro magazzino. Per quel che riguarda il design, vogliamo:

Visualizzazione iniziale con il carrello chiuso.

Visualizzazione iniziale con il carrello chiuso.

Il carrello è aperto e vuoto.

Il carrello è aperto e vuoto.

Il carrello è aperto con degli oggetti.

Il carrello è aperto con degli oggetti.

Markup iniziale#section6

first_pass/index.html#section7

<body role="application">

	<div id="container">
		<header>
			<h1>Robot Shopper</h1>

			<button
				title="Cart count"
				id="shopping_cart">
			</button>
		</header>

		<!-- dynamically loaded from our template -->

		<div
			id="main"
			tabindex="-1">
		</div>

		<footer>
			<p>Robot Shopper © 2014</p>
		</footer>
	</div>

	<div
		id="my_cart"
		tabindex="0">

		<div id="my_cart_contents"></div>

		<button
			title="Close dialog"
			id="close_cart">
			x
		</button>
	</div>

	<div id="page_mask"></div>
	
</body>

Il role application#section8

Sebbene non fosse mia intenzione buttarmi negli ARIA role al primo passo del codice, aggiungere il role application al body è obbligatorio per l’esperienza che vogliamo creare. Questo breve passaggio tratto dallo Yahoo! Accessibility blog cattura bene l’essenza di questo role:

In media, la vostra pagina web è un documento. L’utente si aspetta di leggervi del contenuto e che possa avere dei comportamenti interattivi. Le applicazioni sono più simili alle applicazioni desktop. L’utente si aspetta un insieme di strumenti, cambiamenti istantanei, interazioni dinamiche.

Una feature “must-have” della nostra applicazione Robot Shopper è che gli utenti possano navigare rapidamente tra le sezioni dei prodotti usando i tasti freccia su e giù. Per alcuni screen reader, come NVDA, rimuovere il role application impedirà al nostro JavaScript personalizzato di sovrascrivere questi eventi da tastiera. Senza un role application, quando si premono le frecce su e giù si sposterà invece il focus tra tutti gli elementi della pagina, che non è quello che vogliamo. Usando questo role, stiamo dicendo al browser: “So quello che faccio, non sovrascrivere i miei comportamenti definiti”. Tenete a mente che ciascuna regione che dichiariamo come application richiede che noi, gli sviluppatori, implementiamo una navigazione da tastiera personalizzata.

Il template#section9

Potreste aver notato nel codice precedente che il nostro div principale che era stato designato per la memorizzazione di tutti i nostri prodotti è vuoto. Il motivo è che il nostro template Handlebars carica immediatamente i dati JSON del nostro prodotto in questo elemento.

first_pass/index.html#section10

<script id="all_products" type="text/x-handlebars-template">
	<div id="product_sections">
		{{#products}}
			<section tabindex="-1">
				<div class="product_information">
					<h2 class="product_title">{{title}}</h2>
					<span class="product_price">${{price}}</span>
				</div>
				<div class="product_details">
					<a href="#" title="{{title}} details">
						<img class="product_thumb" src="{{img_uri}}" alt="{{title}}">
					</a>
					<p class="product_description">{{description}}</p>
				</div>
				<button
					title="Add to Cart"
					class="button add_to_cart"
					tab-index="0"
					data-pid="{{pid}}">
					Add to Cart
				</button>
			</section>
		{{/products}}
	</div>
</script>

first_pass/products.js#section11

data = { products: [
	{
		title: "Jet Engines",
		price: "500.00",
		img_uri:	"http://goo.gl/riaO3q",
		description: "It's only a jetpack--a simple, high-thrust, fuel-thirsty jetpack. I would add this puppy to the cart. What could go wrong?",
		pid: "product_093A14"
	},
	
	…

]};
document.getElementById("main").innerHTML = template(data);

L’ora di ChromeVox (Demo)#section12

Vediamo in azione il nostro Robot Shopper. Nella demo, cliccate su “Add to Cart” e vedrete aumentare il contatore del carrello sul torace blu del robot. Semplice, vero? Ma come si traduce questa esperienza nel contesto di uno screen reader?

Per prima cosa, parliamo della navigazione della pagina. Sebbene possiamo usare il tasto “tab” per raggiungere l’anchor del dettaglio del prodotto e i pulsanti “Add to Cart”, l’utente deve premere due volte il tab per attraversare ciascun oggetto: un lavoro fastidioso se l’utente disabile vuole raggiungere un oggetto molto in basso nella lista usando la tastiera. Abbiamo risolto questo problema aggiungendo la navigazione con i tasti freccia della tastiera.

first_pass/app.js#section13

$(document.documentElement).keyup(function (event) {

	…

	if (key_pressed === UP) {
		direction = "prev";
	} else if (key_pressed === DOWN) {
		direction = "next";
	}

	…

$(".selected")[direction]()
	.addClass("selected")
	.focus()
	.siblings()
	.removeClass("selected");
	
	…
		
});

Un aspetto interessante di questo codice è che abbiamo associato i tasti freccia premuti con la direzione in cui vorremmo muoverci nella lista prodotti. La stringa direzionale ("prev" e "next") viene interpolata con la funzione jQuery che si occupa del movimento al posto nostro. Usando i tasti freccia su e giù, possiamo muoverci più rapidamente e in maniera più efficiente tra gli oggetti. Dopo essere giunti mediante frecce (o cliccando) nella sezione del prodotto a cui siamo interessati, possiamo poi usare il tasto tab per raggiungere la thumbnail ancorata a ciascun prodotto o il pulsante “Add to Cart”.

Una esperienza del carrello senza vitalità#section14

Dal carrello vuoto, clicchiamo sul pulsante “Add to Cart”. Gli utenti vedenti possono vedere l’incremento del numero sul torace blu del robot. Gli utenti non vedenti possono solo sentire “Add to Cart button”.

Cliccando sul robot blu si lancia la modale del carrello. Ovviamente, un utente non può saperlo a meno che non lo veda mentre succede. Gli utenti dipendenti da screen reader sentiranno “One button”, o il numero degli oggetti nel carrello.

Una volta che siamo nella modale visibile, possiamo aggiungere, sottrarre e rimuovere tutti gli oggetti dal carrello. Nel nostro primo passaggio, comunque, l’accesso a questi pulsanti porta ad una UX piuttosto scarsa per gli utenti con disabilità visiva. “Plus button” non è una risposta sufficientemente buona da parte del nostro screen reader quando premiamo il pulsante “+”.

In maniera simile, quando chiudiamo la modale del carrello usando il pulsante “x”, il nostro reader ci dice “x button”.

Non è nemmeno colpa dello screen reader. Il nostro reader non ha quello di cui ha bisogno per offrire una ricca esperienza agli utenti che dipendono da esso. Per fornire un’esperienza ricca agli gli utenti non vedenti, dobbiamo migliorare la nostra semantica usando i role, gli state e le property di WAI-ARIA.

Aggiustamenti al primo passaggio#section15

Il nostro approccio ai miglioramenti dell’accessibilità sarà di tipo “dal generico allo specifico”.

1. Landmark roles#section16

Sebbene sia in qualche modo semantico, il nostro markup del primo passaggio non contiene alcun landmark role. Perché usare i landmark role? Secondo il guru e sostenitore dell’accessibilità Steve Faulkner:

Aggiungendo gli ARIA landmark al proprio sito o a un sito che si sta sviluppando, forniamo delle feature di navigazione globale e degli aiuti per la comprensione della struttura del contenuto da parte degli utenti.

I landmark role permettono agli utenti di screen reader di navigare verso le maggiori sezioni del vostro sito web usando semplicemente un tasto. Al nostro markup mancano anche gli states e le properties di ARIA, che sono dei termini che, secondo il W3C “forniscono specifiche informazioni su un oggetto ed entrambe formano parte della definizione della natura dei roles”.

second_pass/index.html#section17

<!-- Header -->
<header role="banner">

<!-- Main content element replaces original div element-->
<main
	id="main"
	tabindex="-1">
</main>

Se avessimo scelto di restare con il nostro div come genitore del nostro contenuto principale, aggiungergli role="main" sarebbe accettabile per scopi legati all’accessibilità. Tuttavia, faremo a meno del role al posto del più diretto tag main.

Finestra di dialogo del carrello:#section18

second_pass/index.html#section19
<div
	id="my_cart"
	tabindex="0"
	role="dialog"
	aria-hidden="true"
	aria-labelledby="my_cart_title"></div>

Quando si aggiungono gli oggetti al carrello per la prima volta, il markup viene creato dinamicamente usando il codice nella funzione updateCartDialog. Gli utenti vedenti noteranno che il carrello non è ancora visibile. Per gli utenti non vedenti, dobbiamo assicurarci che il carrello non venga annunciato dallo screen reader. Per farlo, abbiamo aggiunto l’attributo aria-hidden che, secondo la specifica, imposta uno state su un dato elemento “indicante che l’elemento e tutti i suoi discendenti non sono visibili o percepibili da alcun utente come sono implementati dall’autore”. Prosegue dicendo: “se un elemento è visibile solo dopo una qualche azione da parte dell’utente, gli autori DEVONO impostare l’attributo aria-hidden a true“. La finestra di dialogo del carrello è etichettata dall’elemento caption di questa tabella.

second_pass/app.js#section20
$cart_contents
	.html("")
	.append("\
	<table id='cart_summary'>\
		<caption id='my_cart_title'>Cart Contents</caption>\
	…
	
	</table>\

2. L’aggiunta/rimozione degli oggetti dal carrello dovrebbe notificare gli utenti#section21

Cliccando sul pulsante “Add to Cart” si attiva ChromeVox, che dice: “Add to Cart button”, che non è di grande aiuto per un utente che dipende da segnali uditivi. Gli utenti dovrebbero sapere esattamente quello che si sta aggiungendo al carrello. Inoltre, l’azione di aggiungere e rimuovere oggetti dall’interno della modale del carrello dovrebbe dare informazioni uditive all’utente su quanti oggetti ci sono nel carrello nel momento in cui cambia il contatore degli oggetti nel carrello.

Per far sì che questo accada, modifichiamo per prima cosa il markup del pulsante “Add to Cart” nel nostro template Handlebars.

second_pass/index.html#section22
<script id="all_products" type="text/x-handlebars-template">
	
	…
	
	<button
		class="button add_to_cart"
		tabindex="0"
		aria-label="Add {{title}} to the cart"
		aria-controls="shopping_cart cart_count"
		data-pid="{{pid}}">
		Add to Cart
	</button>
	
	…

</script>

L’azione associata a questo pulsante controlla gli elementi shopping_cart e cart_count incrementando il numero totale di oggetti presenti nel carrello.

<script id="all_products" type="text/x-handlebars-template">
	
	…
	
	<button
		title="Cart Count"
		id="shopping_cart"
		aria-owns="cart_contents"
		aria-label="Cart count">
	</button>
	
	…
	
</script>

Il pulsante dello shopping cart (il robot blu) memorizza il numero attuale di oggetti nel carrello. Tuttavia, per notificare in maniera uditiva l’utente dei cambiamenti man mano che avvengono, useremo un nuovo approccio.

<div
	class="aria_counter"
	id="cart_count"
	aria-live="polite">
</div>

Quando l’utente aggiunge un oggetto al carrello, aggiorniamo il nostro contatore aria che contiene un attributo aria-live. Un elemento che usa questo attributo annuncia il suo contenuto quando qualcosa cambia.

3. I pulsanti della finestra di dialogo del carrello hanno bisogno delle proprietà ARIA#section23

Pulsanti della finestra di dialogo del carrello.

Pulsanti della finestra di dialogo del carrello.

3a. Pulsante “x” (rimuovi tutto)#section24

Come detto prima, se attualmente clicchiamo sul pulsante “x”, ChromeVox dice “x button”.

first_pass/app.js#section25
<button class='button row_button remove_row_items' title='Remove all items of this type' data-pid='" + element.pid + "'>x</button>

Il codice migliorato del nostro pulsante “x” risiede nella funzione updateCartDialog.

second_pass/app.js#section26
<button aria-controls='row_" + index  +  " cart_count item_count' class='button row_button remove_row_items' aria-label='Remove all " + element.title + "s from the cart?' data-pid='" + element.pid + "'>x</button>\

Introducendo l’attributo aria-controls al nostro pulsante forniamo più significato semantico. Stiamo dicendo agli screen reader che una data riga nel nostro carrello è controllata dal pulsante “x” all’interno di ogni riga, e che possiamo rimuovere l’intera riga cliccando il pulsante e confermando la rimozione. In aggiunta, il pulsante “x” è ora etichettato, così agli utenti di screen reader verrà detto esattamente quale oggetto verrà rimosso dal carrello.

3b. Pulsante “-” (decrementa la quantità di uno specifico oggetto)#section27

Quando lo si preme, ChromeVox legge ad alta voce “dash button”. Memorizziamo l’id del campo target della quantità, il pulsante cambia all’interno di un data attribute non semantico.

first_pass/app.js#section28
<button class='button row_button decrement_row_item'  title='Decrease quantity by one' data-pid='" + element.pid + "' data-product-quantity='product_quantity_" + index  + "'>-</button>

…

$(document).on("click", ".decrement_row_item", function() {
	app.decrementItemQuantity(this.dataset.pid, this.dataset.productQuantity);
	
	…

});

Eliminiamo il data attribute non semantico e sostituiamolo con un attributo ARIA semantico. Ecco la nostra versione migliorata:

second_pass/app.js#section29
<button aria-controls='product_quantity_" + index  + " cart_count item_count' class='button row_button decrement_row_item' aria-label='Decrease " + item_title + " quantity' data-pid='" + element.pid + "'>-</button>

$(document).on("click", ".decrement_row_item", function() {
	var aria_controls = $(this).attr("aria-controls").split(" ")[0];
	app.decrementItemQuantity(this.dataset.pid, aria_controls);
	
	…
  
});

Adesso il pulsante “-” è etichettato. Per una UX migliore, la nostra etichetta include ora il title del prodotto. Aggiungendo aria-controls al mix, dichiariamo gli elementi nel DOM che sono controllati da questo pulsante:

  1. Product quantity: il td contenente la quantità del prodotto attuale
  2. Cart count: il contatore off-screen il cui valore viene annunciato quando cambia il numero degli oggetti nel carrello
  3. Item count: un altro contatore off-screen il cui valore viene annunciato quando cambia la quantità di ciascun oggetto nel carrello

Ecco il JS che gestisce il click del pulsante:

$(document).on("click", ".decrement_row_item", function() {
	// get the product quantity id of the row
	var aria_controls = $(this).attr("aria-controls").split(" ")[0];
	app.decrementItemQuantity(this.dataset.pid, aria_controls);
	
	…
	
});

Migliorare il nostro markup ha anche il beneficio aggiunto di rendere più specifico il nostro JS. Il secondo argomento di decrementItemQuantity diventa parte del valore dell’attributo aria-controls. Quindi, non solo il pulsante è più accessibile, ma anche il nostro codice è diventato più leggibile.

4. La modale del carrello ha bisogno di ARIA state#section30

Attualmente, quando lanciamo la modale del carrello, ChromeVox non indica questo dettaglio all’utente. Questo perché aggiungiamo e togliamo solo un class name semanticamente debole sul body per far sì che questa interazione funzioni.

first_pass/app.js#section31
showModal: function() {
	$("body").addClass("show_cart");
},

removeModal: function() {
	$("body").removeClass("show_cart");
}

Per far sì che il reader funzioni meglio, gli diamo una semantica migliore. Quando il carrello è aperto, vorremmo nascondere il nostro container (tutto quello che sta dietro alla modale del carrello) dalle tecnologie assistive. Al contrario, il nostro carrello adesso viene fuori dallo stato in cui è nascosto ed è chiaramente rivelato alle tecnologie assistive.

second_pass/app.js#section32
showModal: function() {
	if (app.elements.$my_cart.attr("aria-hidden") === "true") {
		$("body").addClass("show_cart");
		
		…
		
		app.elements.$container.attr("aria-hidden", "true");
		app.elements.$my_cart.attr("aria-hidden", "false");
	}
	
	…
	
}

5. Sezioni del prodotto selezionato hanno bisogno di ARIA state#section33

Quando gli utenti premono i tasti freccia su e giù, una class “selected” viene aggiunta per rappresentare la sezione del prodotto attualmente selezionato. Sostituiamo la class con un aria-selected.

second_pass/app.js#section34
$(document.documentElement).keyup(function(event) {

	…

	if (key_pressed === UP) {
		direction = "prev";
	} else if (key_pressed === DOWN) {
		direction = "next";
	}
	
	…
	
	$selected[direction]()
		.attr("aria-selected", "true")
		.focus()
		.siblings()
		.attr("aria-selected", "false");
		
	…
		
});

Round 2 (Demo)#section35

Continuate a seguire e notate come abbiamo migliorato la UX per gli utenti disabili attraverso delle semantiche migliori.

  1. Da un carrello vuoto, aggiungi un qualunque oggetto al carrello.
    • Prima: “Add to cart button”
    • Dopo: “Add {{title of product}} to the cart button. Cart count one”
  2. Usare il tasto tab per portare il focus sul robot.

    • Prima: “One button”
    • Dopo: “Banner, cart count one”
  3. Cliccare sul robot per aprire il carrello.

    • Prima: (contents of the cart)
    • Dopo: “Enter dialog” (contents of the cart)
  4. Cliccare il pulsante “+” nel carrello aperto che contiener un oggetto.

    • Prima: “Plus button”
    • Dopo: “Increase {{title of product }} quantity button. Item quantity two. Cart count two”
  5. Cliccare la “x” per rimuovere tutti gli oggetti di un certo tipo.

    • Prima: “X button”
    • Dopo: “Remove all {{title of product}}s from the cart button”
  6. Cancellare il pop-up di conferma JavaScript ed uscire dalla “modal dialog” usando il tasto “esc” o il pulsante “x”.

    • Prima: “”
    • Dopo: “Exited dialog”

Il nostro focus ritorna poi all’ultimo prodotto selezionato prima che il carrello fosse aperto. Se non è stata precedentemente selezionata alcuna sezione di prodotto, il focus ritorna alla prima sezione di prodotto nell’elenco.

Conclusioni#section36

Sarei un bugiardo se vi dicessi che scrivere codice accessibile non richiede lavoro extra. Quando prototipiamo e creiamo degli schizzi con il product designer, esaminiamo le nostre decisioni ancora più attentamente di prima. Ogni design deve portare ad una UX accessibile. In maniera simile, gli sviluppatori che non hanno familiarità con le pratiche di accessibilità potrebbero passare più tempo a rivedere i propri commit prima di vedere il proprio codice “merged”.

I bravi product manager realizzano che le nuove sfide generano requisiti temporali più grandi da parte dei designer e dei developer. È incoraggiante che noi si sia delle creature altamente adattabili: questi costi di lavoro anticipati diminuiranno nel tempo. Ancora più importante, questo duro lavoro porterà a un prodotto più usabile per tutti i clienti e ad una codebase più leggibile per gli sviluppatori. Dobbiamo essere d’accordo sul non trattare l’accessibilità come la glassa sulla torna, ma piuttosto come un’essenziale ingrediente.

Illustrazioni: {carlok}

Sull’autore

Andy Hoffman

Andrew Hoffman è front-end developer in IBM. Gli piace andare in bicicletta, lo humor strano e le analogie complesse. Twitta di codice e osservazioni oscure sulla vita come @antibland.

Nessun commento

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Altro da ALA

Webwaste

In questo estratto da World Wide Waste, Gerry McGovern esamina l'impatto ambientale di siti web pieni zeppi di asset inutili. Digital is physical. Sembra economico e gratis ma non lo è: ci costa la Terra.
Industry