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 clickeventos. 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 clickevento 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 userisfinishedwiththedialogevento, 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 blurevento, blurnão se propaga se o evento foi vinculado à fase de bolhas!
jQuery focusoutvai fazer muito bem. Se você não pode usar o jQuery, pode usar blurdurante 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 focusoutevento antes de acioná-lo focusinnovamente.
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.