Làm thế nào để phát hiện một nhấp chuột bên ngoài một yếu tố?
Lý do mà câu hỏi này rất phổ biến và có rất nhiều câu trả lời là nó phức tạp. Sau gần tám năm và hàng tá câu trả lời, tôi thực sự ngạc nhiên khi thấy sự chăm sóc ít được dành cho khả năng tiếp cận.
Tôi muốn ẩn các yếu tố này khi người dùng nhấp bên ngoài khu vực menu.
Đây là một nguyên nhân cao quý và là vấn đề thực tế . Tiêu đề của câu hỏi, đó là những gì mà hầu hết các câu trả lời xuất hiện để cố gắng giải quyết vấn đề này có chứa một cá trích đỏ không may.
Gợi ý: đó là từ "nhấp chuột" !
Bạn không thực sự muốn ràng buộc xử lý nhấp chuột.
Nếu bạn ràng buộc bấm trình xử lý để đóng hộp thoại, bạn đã thất bại. Lý do bạn thất bại là không phải ai cũng kích hoạt click
các sự kiện. Người dùng không sử dụng chuột sẽ có thể thoát khỏi hộp thoại của bạn (và menu bật lên của bạn được cho là một loại hộp thoại) bằng cách nhấn Tabvà sau đó họ sẽ không thể đọc nội dung đằng sau hộp thoại mà không kích hoạt click
sự kiện.
Vì vậy, hãy viết lại câu hỏi.
Làm thế nào để đóng một hộp thoại khi người dùng kết thúc với nó?
Đây là mục tiêu. Thật không may, bây giờ chúng ta cần liên kết userisfinishedwiththedialog
sự kiện và ràng buộc đó không đơn giản như vậy.
Vậy làm thế nào chúng ta có thể phát hiện ra rằng người dùng đã kết thúc bằng hộp thoại?
focusout
biến cố
Một khởi đầu tốt là xác định xem tiêu điểm có rời khỏi hộp thoại không.
Gợi ý: hãy cẩn thận với blur
sự kiện, blur
không tuyên truyền nếu sự kiện bị ràng buộc với giai đoạn sủi bọt!
jQuery focusout
sẽ làm tốt. Nếu bạn không thể sử dụng jQuery, thì bạn có thể sử dụng blur
trong giai đoạn chụp:
element.addEventListener('blur', ..., true);
// use capture: ^^^^
Ngoài ra, đối với nhiều hộp thoại, bạn sẽ cần cho phép vùng chứa lấy nét. Thêm tabindex="-1"
để cho phép hộp thoại nhận tiêu điểm động mà không làm gián đoạn luồng tab.
$('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>
Nếu bạn chơi với bản demo đó trong hơn một phút, bạn sẽ nhanh chóng bắt đầu thấy các vấn đề.
Đầu tiên là liên kết trong hộp thoại không thể nhấp được. Cố gắng nhấp vào nó hoặc tab vào nó sẽ dẫn đến việc đóng hộp thoại trước khi tương tác diễn ra. Điều này là do tập trung vào phần tử bên trong sẽ kích hoạt một focusout
sự kiện trước khi kích hoạt lại một focusin
sự kiện.
Cách khắc phục là xếp hàng thay đổi trạng thái trên vòng lặp sự kiện. Điều này có thể được thực hiện bằng cách sử dụng setImmediate(...)
hoặc setTimeout(..., 0)
cho các trình duyệt không hỗ trợ setImmediate
. Sau khi xếp hàng, nó có thể bị hủy bởi sau đó 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>
Vấn đề thứ hai là hộp thoại sẽ không đóng khi liên kết được nhấn lại. Điều này là do hộp thoại mất tiêu điểm, kích hoạt hành vi đóng, sau đó nhấp vào liên kết sẽ kích hoạt hộp thoại để mở lại.
Tương tự như vấn đề trước, trạng thái tập trung cần phải được quản lý. Cho rằng thay đổi trạng thái đã được xếp hàng, đó chỉ là vấn đề xử lý các sự kiện tập trung vào trình kích hoạt hộp thoại:
Cái này nhìn quen quá
$('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 Chìa khóa
Nếu bạn nghĩ rằng bạn đã hoàn thành bằng cách xử lý các trạng thái tiêu điểm, bạn sẽ có thể làm nhiều hơn để đơn giản hóa trải nghiệm người dùng.
Đây thường là một tính năng "tốt để có", nhưng thông thường là khi bạn có một chế độ hoặc cửa sổ bật lên của bất kỳ loại nào mà Esckhóa sẽ đóng nó.
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>
Nếu bạn biết bạn có các yếu tố có thể tập trung trong hộp thoại, bạn sẽ không cần phải tập trung trực tiếp vào hộp thoại. Nếu bạn đang xây dựng một menu, bạn có thể tập trung vào mục menu đầu tiên.
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.
Vai trò WAI-ARIA và hỗ trợ tiếp cận khác
Câu trả lời này hy vọng bao gồm những điều cơ bản về hỗ trợ bàn phím và chuột có thể truy cập được cho tính năng này, nhưng vì nó khá dễ hiểu nên tôi sẽ tránh mọi cuộc thảo luận về vai trò và thuộc tính của WAI-ARIA , tuy nhiên tôi rất khuyến nghị người thực hiện tham khảo thông số kỹ thuật để biết chi tiết về vai trò nào họ nên sử dụng và bất kỳ thuộc tính thích hợp nào khác.