Як виявити клацання поза елементом?
Причина того, що це питання настільки популярне і на нього так багато відповідей, це те, що воно оманливо складне. Після майже восьми років і десятків відповідей я щиро здивований, побачивши, як мало уваги приділяється доступності.
Я хотів би приховати ці елементи, коли користувач клацає за межі області меню.
Це благородна справа і є актуальним питанням. Назва питання - саме так, як видається, більшість відповідей намагається вирішити - містить нещасну червону оселедець.
Підказка: це слово "натиснути" !
Ви насправді не хочете зв’язувати обробники кліків.
Якщо ви прив'язуєте обробники кліків, щоб закрити діалогове вікно, ви вже не виконали помилку. Причина невдачі полягає в тому, що не всі викликають 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 , проте настійно рекомендую, щоб реалізатори зверталися до специфікації для деталей про те, які ролі вони повинні використовувати та які інші відповідні атрибути.