Come rilevare un clic all'esterno di un elemento?
La ragione per cui questa domanda è così popolare e ha così tante risposte è che è ingannevolmente complessa. Dopo quasi otto anni e dozzine di risposte, sono sinceramente sorpreso di vedere quanto poca attenzione sia stata data all'accessibilità.
Vorrei nascondere questi elementi quando l'utente fa clic al di fuori dell'area dei menu.
Questa è una nobile causa ed è il vero problema. Il titolo della domanda - che è ciò che la maggior parte delle risposte sembrano tentare di rispondere - contiene una sfortunata aringa rossa.
Suggerimento: è la parola "clic" !
In realtà non si desidera associare i gestori di clic.
Se stai vincolando i gestori dei clic per chiudere la finestra di dialogo, hai già fallito. Il motivo per cui hai fallito è che non tutti attivano click
eventi. Gli utenti che non usano il mouse saranno in grado di sfuggire alla finestra di dialogo (e il menu a comparsa è probabilmente un tipo di finestra di dialogo) premendo Tab, e quindi non saranno in grado di leggere il contenuto dietro la finestra di dialogo senza attivare successivamente un click
evento.
Quindi riformuliamo la domanda.
Come si chiude una finestra di dialogo quando un utente ha finito con essa?
Questo è l'obiettivo. Sfortunatamente, ora dobbiamo legare l' userisfinishedwiththedialog
evento e quell'associazione non è così semplice.
Quindi, come possiamo rilevare che un utente ha finito di usare una finestra di dialogo?
focusout
evento
Un buon inizio è determinare se lo stato attivo ha lasciato la finestra di dialogo.
Suggerimento: stai attento con l' blur
evento, blur
non si propaga se l'evento è stato legato alla fase gorgogliante!
jQuery focusout
andrà benissimo. Se non puoi usare jQuery, puoi usarlo blur
durante la fase di acquisizione:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
Inoltre, per molte finestre di dialogo è necessario consentire al contenitore di ottenere lo stato attivo. Aggiungi tabindex="-1"
per consentire alla finestra di dialogo di ricevere lo stato attivo in modo dinamico senza interrompere altrimenti il flusso di tabulazione.
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on('focusout', function () {
$(this).removeClass('active');
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>
Se giochi con quella demo per più di un minuto, dovresti iniziare a vedere rapidamente i problemi.
Il primo è che il collegamento nella finestra di dialogo non è selezionabile. Il tentativo di fare clic su di esso o sulla scheda porterà alla chiusura della finestra di dialogo prima che avvenga l'interazione. Questo perché focalizzare l'elemento interno innesca un focusout
evento prima di innescare di focusin
nuovo un evento.
La correzione consiste nell'accodare la modifica dello stato nel loop degli eventi. Questo può essere fatto usando setImmediate(...)
o setTimeout(..., 0)
per i browser che non supportano setImmediate
. Una volta in coda può essere annullato da un successivo focusin
:
$('.submenu').on({
focusout: function (e) {
$(this).data('submenuTimer', setTimeout(function () {
$(this).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function (e) {
clearTimeout($(this).data('submenuTimer'));
}
});
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>
Il secondo problema è che la finestra di dialogo non si chiuderà quando si preme nuovamente il collegamento. Questo perché la finestra di dialogo perde lo stato attivo, innescando il comportamento di chiusura, dopodiché il clic sul collegamento avvia la finestra di dialogo per riaprirla.
Simile al problema precedente, lo stato attivo deve essere gestito. Dato che il cambio di stato è già stato messo in coda, si tratta solo di gestire gli eventi di attivazione nei trigger di dialogo:
Questo dovrebbe sembrare familiare
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
}
});
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>
Esc chiave
Se pensavi di aver finito gestendo gli stati di focus, puoi fare di più per semplificare l'esperienza dell'utente.
Questa è spesso una funzione "piacevole da avere", ma è comune che quando si dispone di un modale o di un popup di qualsiasi tipo, la Escchiave lo chiude.
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('active');
e.preventDefault();
}
}
$('a').on('click', function () {
$(this.hash).toggleClass('active').focus();
});
$('div').on({
focusout: function () {
$(this).data('timer', setTimeout(function () {
$(this).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('timer'));
},
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('active');
e.preventDefault();
}
}
});
$('a').on({
focusout: function () {
$(this.hash).data('timer', setTimeout(function () {
$(this.hash).removeClass('active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('timer'));
}
});
div {
display: none;
}
.active {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>
Se sai di avere elementi focalizzabili nella finestra di dialogo, non dovrai focalizzare direttamente la finestra di dialogo. Se stai creando un menu, puoi invece focalizzare la prima voce di menu.
click: function (e) {
$(this.hash)
.toggleClass('submenu--active')
.find('a:first')
.focus();
e.preventDefault();
}
$('.menu__link').on({
click: function (e) {
$(this.hash)
.toggleClass('submenu--active')
.find('a:first')
.focus();
e.preventDefault();
},
focusout: function () {
$(this.hash).data('submenuTimer', setTimeout(function () {
$(this.hash).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this.hash).data('submenuTimer'));
}
});
$('.submenu').on({
focusout: function () {
$(this).data('submenuTimer', setTimeout(function () {
$(this).removeClass('submenu--active');
}.bind(this), 0));
},
focusin: function () {
clearTimeout($(this).data('submenuTimer'));
},
keydown: function (e) {
if (e.which === 27) {
$(this).removeClass('submenu--active');
e.preventDefault();
}
}
});
.menu {
list-style: none;
margin: 0;
padding: 0;
}
.menu:after {
clear: both;
content: '';
display: table;
}
.menu__item {
float: left;
position: relative;
}
.menu__link {
background-color: lightblue;
color: black;
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.menu__link:hover,
.menu__link:focus {
background-color: black;
color: lightblue;
}
.submenu {
border: 1px solid black;
display: none;
left: 0;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
top: 100%;
}
.submenu--active {
display: block;
}
.submenu__item {
width: 150px;
}
.submenu__link {
background-color: lightblue;
color: black;
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.submenu__link:hover,
.submenu__link:focus {
background-color: black;
color: lightblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="menu">
<li class="menu__item">
<a class="menu__link" href="#menu-1">Menu 1</a>
<ul class="submenu" id="menu-1" tabindex="-1">
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
</ul>
</li>
<li class="menu__item">
<a class="menu__link" href="#menu-2">Menu 2</a>
<ul class="submenu" id="menu-2" tabindex="-1">
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
<li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
</ul>
</li>
</ul>
lorem ipsum <a href="http://example.com/">dolor</a> sit amet.
Ruoli WAI-ARIA e altri supporti per l'accessibilità
Si spera che questa risposta copra le basi del supporto accessibile di tastiera e mouse per questa funzione, ma poiché è già abbastanza considerevole eviterò qualsiasi discussione sui ruoli e gli attributi di WAI-ARIA , tuttavia consiglio vivamente che gli implementatori facciano riferimento alle specifiche per i dettagli su quali ruoli dovrebbero usare e qualsiasi altro attributo appropriato.