Як виявити клацання поза елементом?
Причина того, що це питання настільки популярне і на нього так багато відповідей, це те, що воно оманливо складне. Після майже восьми років і десятків відповідей я щиро здивований, побачивши, як мало уваги приділяється доступності.
Я хотів би приховати ці елементи, коли користувач клацає за межі області меню.
Це благородна справа і є актуальним питанням. Назва питання - саме так, як видається, більшість відповідей намагається вирішити - містить нещасну червону оселедець.
Підказка: це слово "натиснути" !
Ви насправді не хочете зв’язувати обробники кліків.
Якщо ви прив'язуєте обробники кліків, щоб закрити діалогове вікно, ви вже не виконали помилку. Причина невдачі полягає в тому, що не всі викликають clickподії. Користувачі, які не користуються мишкою, зможуть вийти з діалогового вікна (а ваше спливаюче меню, можливо, є типом діалогового вікна), натиснувши Tab, і вони не зможуть читати вміст у діалоговому вікні без подальшої ініціювання clickподії.
Тож давайте перефразуємо питання.
Як закрити діалогове вікно, коли користувач закінчив його?
Це і є мета. На жаль, зараз нам потрібно прив’язати userisfinishedwiththedialogподію, і ця прив'язка не є такою простою.
Тож як ми можемо виявити, що користувач закінчив користуватися діалоговим вікном?
focusout подія
Хороший початок - визначити, чи фокус залишив діалогове вікно.
Підказка: будьте обережні з blurподією, blurне поширюйтесь, якщо подія була пов'язана з фазою барботажу!
jQuery's focusoutбуде добре. Якщо ви не можете використовувати jQuery, ви можете використовувати його blurна етапі захоплення:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
Крім того, для багатьох діалогів потрібно дозволити контейнеру отримати фокус. Додати, tabindex="-1"щоб дозволити діалоговому вікні динамічно отримувати фокус, не припиняючи потік вкладки.
$('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>
Якщо ви граєте з цією демонстрацією більше хвилини, ви повинні швидко почати бачити проблеми.
Перший полягає в тому, що посилання в діалоговому вікні не можна натискати. Спроба натиснути на неї або вкладку до неї призведе до закриття діалогового вікна до того, як відбудеться взаємодія. Це тому, що фокусування внутрішнього елемента запускає focusoutподію перед запуском afocusin знову подію.
Виправлення полягає в черзі зміни стану в циклі подій. Це може бути зроблено за допомогою setImmediate(...), або setTimeout(..., 0)для браузерів , які не підтримують setImmediate. Після черги його можна скасувати наступним 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').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 ключ
Якщо ви думали, що ви зробили обробку станів фокусування, ви можете зробити більше, щоб спростити роботу користувача.
Це часто є функцією "приємно мати", але звичайно, коли у вас є модальний або спливаючий тип будь-якого типу, Escключ закриє його.
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>
Якщо ви знаєте, що в діалоговому вікні є фокусуються елементи, вам не потрібно буде зосереджувати діалог безпосередньо. Якщо ви будуєте меню, ви можете зосередити увагу на першому пункті меню.
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 та інша підтримка доступності
Ця відповідь, сподіваємось, охоплює основи доступної підтримки клавіатури та миші для цієї функції, але оскільки це вже досить помітно, я збираюся уникати будь-яких обговорень ролей та атрибутів WAI-ARIA , проте настійно рекомендую, щоб реалізатори зверталися до специфікації для деталей про те, які ролі вони повинні використовувати та які інші відповідні атрибути.