¿Cómo detectar un clic fuera de un elemento?
La razón por la que esta pregunta es tan popular y tiene tantas respuestas es porque es engañosamente compleja. Después de casi ocho años y docenas de respuestas, estoy realmente sorprendido de ver cuán poco se ha prestado atención a la accesibilidad.
Me gustaría ocultar estos elementos cuando el usuario hace clic fuera del área de menús.
Esta es una causa noble y es el problema real . El título de la pregunta, que es lo que la mayoría de las respuestas parecen intentar abordar, contiene un desafortunado arenque rojo.
Sugerencia: es la palabra "clic" !
En realidad, no desea vincular los controladores de clics.
Si vincula los controladores de clic para cerrar el cuadro de diálogo, ya ha fallado. La razón por la que ha fallado es que no todos desencadenan click
eventos. Los usuarios que no usen un mouse podrán escapar de su diálogo (y su menú emergente podría decirse que es un tipo de diálogo) presionando Tab, y luego no podrán leer el contenido detrás del diálogo sin activar posteriormente un click
evento.
Así que reformulemos la pregunta.
¿Cómo se cierra un diálogo cuando un usuario termina con él?
Este es el objetivo. Desafortunadamente, ahora necesitamos vincular el userisfinishedwiththedialog
evento, y esa vinculación no es tan sencilla.
Entonces, ¿cómo podemos detectar que un usuario ha terminado de usar un diálogo?
focusout
evento
Un buen comienzo es determinar si el foco ha salido del diálogo.
Sugerencia: ¡tenga cuidado con el blur
evento, blur
no se propaga si el evento estaba vinculado a la fase de burbujeo!
jQuery focusout
lo hará bien. Si no puede usar jQuery, puede usarlo blur
durante la fase de captura:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
Además, para muchos cuadros de diálogo deberá permitir que el contenedor se enfoque. Agregar tabindex="-1"
para permitir que el cuadro de diálogo reciba el foco dinámicamente sin interrumpir el flujo de tabulación.
$('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 juegas con esa demo por más de un minuto, deberías comenzar a ver rápidamente los problemas.
El primero es que no se puede hacer clic en el enlace del cuadro de diálogo. Si intentas hacer clic en él o presionarlo, se cerrará el cuadro de diálogo antes de que tenga lugar la interacción. Esto se debe a que enfocar el elemento interno desencadena un focusout
evento antes de activarlo focusin
nuevamente.
La solución es poner en cola el cambio de estado en el bucle de eventos. Esto se puede hacer usando setImmediate(...)
, o setTimeout(..., 0)
para navegadores que no son compatibles setImmediate
. Una vez en cola, puede ser cancelado por un posterior 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>
El segundo problema es que el diálogo no se cerrará cuando se presione nuevamente el enlace. Esto se debe a que el cuadro de diálogo pierde el foco, desencadenando el comportamiento de cierre, después de lo cual el clic en el enlace hace que el cuadro de diálogo se vuelva a abrir.
Similar al problema anterior, el estado de enfoque necesita ser administrado. Dado que el cambio de estado ya se ha puesto en cola, solo es cuestión de manejar eventos de enfoque en los disparadores de diálogo:
Esto debería parecer familiar
$('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 llave
Si creía que había terminado manejando los estados de enfoque, hay más que puede hacer para simplificar la experiencia del usuario.
Esto es a menudo una característica "agradable de tener", pero es común que cuando tenga un modo o ventana emergente de cualquier tipo, la Escclave la cierre.
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 sabe que tiene elementos enfocables dentro del diálogo, no necesitará enfocar el diálogo directamente. Si está creando un menú, podría enfocar el primer elemento del menú.
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.
Roles WAI-ARIA y otro soporte de accesibilidad
Es de esperar que esta respuesta cubra los aspectos básicos del soporte accesible de teclado y mouse para esta función, pero como ya es bastante considerable, voy a evitar cualquier discusión sobre los roles y atributos de WAI-ARIA , sin embargo, recomiendo encarecidamente que los implementadores consulten la especificación para obtener más detalles. sobre qué roles deben usar y cualquier otro atributo apropiado.