Comment détecter un clic en dehors d'un élément?
La raison pour laquelle cette question est si populaire et a tant de réponses est qu'elle est d'une complexité trompeuse. Après près de huit ans et des dizaines de réponses, je suis véritablement surpris de voir le peu de soin accordé à l'accessibilité.
Je voudrais masquer ces éléments lorsque l'utilisateur clique en dehors de la zone des menus.
C'est une noble cause et c'est le vrai problème. Le titre de la question - qui est ce à quoi la plupart des réponses semblent tenter de répondre - contient un triste hareng rouge.
Astuce: c'est le mot "clic" !
Vous ne voulez pas réellement lier les gestionnaires de clic.
Si vous liez des gestionnaires de clics pour fermer la boîte de dialogue, vous avez déjà échoué. La raison pour laquelle vous avez échoué est que tout le monde ne déclenche pas d' click
événements. Les utilisateurs n'utilisant pas de souris pourront échapper à votre boîte de dialogue (et votre menu contextuel est sans doute un type de boîte de dialogue) en appuyant sur Tab, et ils ne pourront alors pas lire le contenu derrière la boîte de dialogue sans déclencher par la suite unclick
événement.
Alors reformulons la question.
Comment ferme-t-on une boîte de dialogue lorsqu'un utilisateur en a terminé avec elle?
Tel est l'objectif. Malheureusement, nous devons maintenant lieruserisfinishedwiththedialog
événement, et cette liaison n'est pas si simple.
Alors, comment pouvons-nous détecter qu'un utilisateur a fini d'utiliser une boîte de dialogue?
focusout
un événement
Un bon début consiste à déterminer si le focus a quitté la boîte de dialogue.
Astuce: soyez prudent avec l' blur
événement, blur
ne se propage pas si l'événement était lié à la phase bouillonnante!
jQuery focusout
fera très bien. Si vous ne pouvez pas utiliser jQuery, vous pouvez utiliser blur
pendant la phase de capture:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
De plus, pour de nombreuses boîtes de dialogue, vous devrez permettre au conteneur de se concentrer. Ajoutez tabindex="-1"
pour permettre à la boîte de dialogue de recevoir le focus de manière dynamique sans interrompre le flux de tabulation.
$('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>
Si vous jouez avec cette démo pendant plus d'une minute, vous devriez rapidement commencer à voir des problèmes.
Le premier est que le lien dans la boîte de dialogue n'est pas cliquable. Tenter de cliquer dessus ou sur un onglet entraînera la fermeture de la boîte de dialogue avant que l'interaction n'ait lieu. En effet, la focalisation de l'élément interne déclenche un focusout
événement avant de déclencher unefocusin
nouveau événement.
Le correctif consiste à mettre en file d'attente le changement d'état sur la boucle d'événements. Cela peut être fait en utilisant setImmediate(...)
ou setTimeout(..., 0)
pour les navigateurs qui ne prennent pas en charge setImmediate
. Une fois mis en file d'attente, il peut être annulé par un autre 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>
Le deuxième problème est que la boîte de dialogue ne se ferme pas lorsque le lien est enfoncé à nouveau. En effet, la boîte de dialogue perd le focus, déclenchant le comportement de fermeture, après quoi le clic sur le lien déclenche la réouverture de la boîte de dialogue.
Comme pour le numéro précédent, l'état de mise au point doit être géré. Étant donné que le changement d'état a déjà été mis en file d'attente, il s'agit simplement de gérer les événements de focus sur les déclencheurs de dialogue:
Cela devrait vous sembler familier
$('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 clé
Si vous pensiez avoir terminé en gérant les états de focus, vous pouvez faire plus pour simplifier l'expérience utilisateur.
C'est souvent une fonctionnalité "agréable à avoir", mais il est courant que lorsque vous avez un modal ou une popup de quelque sorte que ce soit, la Escclé la ferme.
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>
Si vous savez que la boîte de dialogue contient des éléments pouvant être mis au point, vous n'aurez pas besoin de faire la mise au point directement. Si vous créez un menu, vous pouvez plutôt concentrer le premier élément de 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.
Rôles WAI-ARIA et autres supports d'accessibilité
J'espère que cette réponse couvre les bases de la prise en charge accessible du clavier et de la souris pour cette fonctionnalité, mais comme elle est déjà assez importante, je vais éviter toute discussion sur les rôles et les attributs WAI-ARIA , mais je recommande vivement aux implémenteurs de se référer à la spécification pour plus de détails. quels rôles ils devraient utiliser et tout autre attribut approprié.