Jak wykryć kliknięcie poza elementem?
To pytanie jest tak popularne i zawiera tak wiele odpowiedzi, że jest zwodniczo złożone. Po prawie ośmiu latach i dziesiątkach odpowiedzi jestem naprawdę zaskoczony, widząc, jak mało uwagi poświęcono dostępności.
Chciałbym ukryć te elementy, gdy użytkownik kliknie poza obszarem menu.
To szlachetna przyczyna i faktyczny problem. Tytuł pytania - na które większość odpowiedzi wydaje się próbować odpowiedzieć - zawiera niefortunny czerwony śledź.
Wskazówka: to słowo „kliknij” !
W rzeczywistości nie chcesz wiązać programów obsługi kliknięć.
Jeśli wiążesz procedury obsługi kliknięć w celu zamknięcia okna, już nie powiodło się. Powodem niepowodzenia jest to, że nie wszyscy wyzwalają click
zdarzenia. Użytkownicy nieużywający myszy będą mogli wyjść z twojego okna dialogowego (a twoje menu podręczne jest prawdopodobnie rodzajem okna dialogowego) przez naciśnięcie Tab, a następnie nie będą mogli odczytać treści za oknem dialogowym bez wywołania click
zdarzenia.
Przeformułujmy więc pytanie.
Jak zamknąć okno dialogowe, gdy użytkownik go zakończy?
To jest cel. Niestety, teraz musimy powiązać userisfinishedwiththedialog
wydarzenie, a wiązanie to nie jest takie proste.
Jak więc wykryć, że użytkownik zakończył korzystanie z okna dialogowego?
focusout
zdarzenie
Dobrym początkiem jest ustalenie, czy fokus opuścił okno dialogowe.
Wskazówka: bądź ostrożny ze blur
zdarzeniem, blur
nie rozprzestrzenia się, jeśli wydarzenie było związane z fazą bulgotania!
jQuery's focusout
będzie w porządku. Jeśli nie możesz użyć jQuery, możesz użyć blur
podczas fazy przechwytywania:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
Ponadto w wielu oknach dialogowych musisz pozwolić na skupienie się kontenera. Dodaj, tabindex="-1"
aby okno dialogowe mogło dynamicznie otrzymywać fokus, nie zakłócając w inny sposób operacji tabulacji.
$('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>
Jeśli grasz w tę wersję demo przez ponad minutę, powinieneś szybko zacząć widzieć problemy.
Po pierwsze, link w oknie dialogowym nie jest klikalny. Próba kliknięcia na nią lub karty spowoduje zamknięcie okna dialogowego przed interakcją. Wynika to z tego, że skupienie elementu wewnętrznego wyzwala focusout
zdarzenie przed ponownym wywołaniem focusin
zdarzenia.
Rozwiązaniem jest kolejkowanie zmiany stanu w pętli zdarzeń. Można to zrobić za pomocą setImmediate(...)
lub setTimeout(..., 0)
w przeglądarkach, które nie obsługują setImmediate
. Po umieszczeniu w kolejce można go anulować poprzez 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>
Drugi problem polega na tym, że okno dialogowe nie zamyka się po ponownym naciśnięciu łącza. Wynika to z faktu, że okno dialogowe traci fokus, wyzwalając zachowanie zamknięcia, po czym kliknięcie łącza powoduje ponowne otwarcie okna dialogowego.
Podobnie jak w poprzednim wydaniu, stanem skupienia należy zarządzać. Biorąc pod uwagę, że zmiana stanu została już umieszczona w kolejce, jest to tylko kwestia obsługi zdarzeń skupienia w wyzwalaczach dialogu:
To powinno wyglądać znajomo
$('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 klucz
Jeśli uważasz, że skończyłeś przez obsługę stanów skupienia, możesz zrobić więcej, aby uprościć obsługę.
Jest to często funkcja „miło mieć”, ale często, gdy masz modalne lub wyskakujące okienko, Escklucz go zamyka.
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>
Jeśli wiesz, że w oknie dialogowym znajdują się elementy, które można aktywować, nie trzeba bezpośrednio ustawiać ostrości w oknie dialogowym. Jeśli budujesz menu, możesz zamiast tego skoncentrować pierwszy element 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.
WAI-ARIA Obsługa ról i inne ułatwienia dostępu
Mam nadzieję, że ta odpowiedź obejmuje podstawy dostępnej obsługi klawiatury i myszy dla tej funkcji, ale ponieważ jest już dość spora, uniknę dyskusji o rolach i atrybutach WAI-ARIA , jednak gorąco polecam, aby implementatorzy odnieśli się do specyfikacji w celu uzyskania szczegółowych informacji na temat ról, których powinni używać i wszelkich innych odpowiednich atrybutów.