Como detectar um clique fora de um elemento?
A razão pela qual essa pergunta é tão popular e tem tantas respostas é que ela é enganosamente complexa. Depois de quase oito anos e dezenas de respostas, estou genuinamente surpreso ao ver quão pouco cuidado foi dado à acessibilidade.
Gostaria de ocultar esses elementos quando o usuário clica fora da área de menus.
Esta é uma causa nobre e é o problema real . O título da pergunta - que é o que a maioria das respostas parece tentar resolver - contém um infeliz arenque vermelho.
Dica: é a palavra "clique" !
Na verdade, você não deseja vincular manipuladores de cliques.
Se você está vinculando manipuladores de clique para fechar a caixa de diálogo, você já falhou. O motivo pelo qual você falhou é que nem todos acionam click
eventos. Os usuários que não usarem o mouse poderão escapar da sua caixa de diálogo (e seu menu pop-up é sem dúvida um tipo de caixa de diálogo) pressionando Tabe, em seguida, não poderão ler o conteúdo por trás da caixa de diálogo sem disparar um click
evento posteriormente .
Então, vamos reformular a pergunta.
Como alguém fecha uma caixa de diálogo quando um usuário termina com ela?
Esse é o objetivo. Infelizmente, agora precisamos vincular o userisfinishedwiththedialog
evento, e essa vinculação não é tão direta.
Então, como podemos detectar que um usuário terminou de usar uma caixa de diálogo?
focusout
evento
Um bom começo é determinar se o foco saiu da caixa de diálogo.
Dica: tenha cuidado com o blur
evento, blur
não se propaga se o evento foi vinculado à fase de bolhas!
jQuery focusout
vai fazer muito bem. Se você não pode usar o jQuery, pode usar blur
durante a fase de captura:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
Além disso, em muitas caixas de diálogo, você precisará permitir que o contêiner ganhe foco. Adicionar tabindex="-1"
para permitir que o diálogo receba foco dinamicamente sem interromper o fluxo de tabulação.
$('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 você jogar com essa demonstração por mais de um minuto, deve começar a ver rapidamente os problemas.
A primeira é que o link na caixa de diálogo não é clicável. Tentar clicar nele ou na guia dele levará ao fechamento do diálogo antes que a interação ocorra. Isso ocorre porque o foco no elemento interno aciona um focusout
evento antes de acioná-lo focusin
novamente.
A correção é enfileirar a alteração de estado no loop de eventos. Isso pode ser feito usando setImmediate(...)
ou setTimeout(..., 0)
para navegadores que não suportam setImmediate
. Uma vez na fila, ele pode ser cancelado por um subseqüente 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>
A segunda questão é que a caixa de diálogo não será fechada quando o link for pressionado novamente. Isso ocorre porque a caixa de diálogo perde o foco, acionando o comportamento de fechamento, após o qual o clique no link aciona a caixa de diálogo para reabrir.
Semelhante à edição anterior, o estado do foco precisa ser gerenciado. Dado que a mudança de estado já foi enfileirada, é apenas uma questão de lidar com eventos de foco nos gatilhos da caixa de diálogo:
Isso deve 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 chave
Se você pensou que tinha terminado lidando com os estados de foco, há mais a fazer para simplificar a experiência do usuário.
Geralmente, é um recurso "bom de ter", mas é comum que, quando você tem um modal ou pop-up de qualquer tipo, a Escchave o feche.
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 você souber que possui elementos de foco na caixa de diálogo, não precisará focar a caixa de diálogo diretamente. Se você estiver criando um menu, poderá focar o primeiro item do 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.
Funções WAI-ARIA e outro suporte à acessibilidade
Esperamos que esta resposta cubra o básico do suporte acessível de teclado e mouse para esse recurso, mas como já é bastante considerável, evitarei qualquer discussão sobre as funções e atributos do WAI-ARIA , no entanto, recomendo vivamente que os implementadores consultem a especificação para obter detalhes sobre quais funções eles devem usar e quaisquer outros atributos apropriados.