Как отследить клик вне элемента на JavaScript?
Использование метода contains()
Ваша интуиция верна: event.target !== menu проверяет только сам элемент меню, но не его дочерние элементы. Когда вы кликаете по пункту списка внутри меню, event.target указывает на этот пункт, а не на menu, поэтому условие срабатывает и меню закрывается.
Правильное решение — использовать метод Node.contains(). Он проверяет, является ли переданный узел потомком данного узла (включая сам узел).
Базовое решение
const menu = document.getElementById('myMenu');
document.addEventListener('click', function(event) {
const isClickInsideMenu = menu.contains(event.target);
if (!isClickInsideMenu) {
menu.style.display = 'none';
}
});
Теперь menu.contains(event.target) вернёт true, если кликнули по menu или любому его дочернему элементу. Меню закроется только при клике вне него.
Улучшенная версия с обработчиком на самом меню
Лучше вешать слушатель не на document, а на элемент, который содержит всё приложение (например, body или конкретный контейнер). Это уменьшает количество обрабатываемых событий и предотвращает проблемы с всплытием:
const menu = document.getElementById('myMenu');
const appContainer = document.getElementById('app'); // или document.body
appContainer.addEventListener('click', function(event) {
if (!menu.contains(event.target)) {
menu.style.display = 'none';
}
});
Предотвращение закрытия при клике на триггер
Часто меню открывается по кнопке. Если клик по этой кнопке не должен закрывать меню:
const menu = document.getElementById('myMenu');
const toggleButton = document.getElementById('toggleButton');
document.addEventListener('click', function(event) {
const isClickInsideMenu = menu.contains(event.target);
const isClickOnToggle = toggleButton.contains(event.target);
if (!isClickInsideMenu && !isClickOnToggle) {
menu.style.display = 'none';
}
});
Удаление слушателя при закрытии
Для производительности стоит удалять слушатель, когда меню скрыто:
const menu = document.getElementById('myMenu');
let clickHandler = null;
function openMenu() {
menu.style.display = 'block';
clickHandler = function(event) {
if (!menu.contains(event.target)) {
closeMenu();
}
};
// Используем setTimeout, чтобы не закрыть меню сразу после открытия
setTimeout(() => {
document.addEventListener('click', clickHandler);
}, 0);
}
function closeMenu() {
menu.style.display = 'none';
if (clickHandler) {
document.removeEventListener('click', clickHandler);
clickHandler = null;
}
}
Важные замечания
- Всплытие событий: Событие click всплывает от целевого элемента до document. Наш слушатель ловит его на этапе всплытия.
- Stop propagation: Не используйте
event.stopPropagation()внутри меню — это нарушит работу других обработчиков на document. - Альтернатива: В современных браузерах можно использовать
Pointer Eventsдля более точного отслеживания. - Доступность: Не забывайте про клавиатуру — добавьте обработчик
Escapeдля закрытия меню.
Метод contains() — это стандартный, надёжный и кросс-браузерный способ решения вашей задачи. Он работает во всех современных браузерах, включая IE9+.
Альтернативный подход: Intersection Observer
Если вам нужно отслеживать клики вне элемента в сложном SPA-приложении с динамическим содержимым, можно использовать Intersection Observer в комбинации с событием blur для фокусируемых элементов.
const menu = document.getElementById('myMenu');
const focusableElements = menu.querySelectorAll('button, a, input, [tabindex]');
// Закрытие при потере фокуса всей группой
menu.addEventListener('focusout', function(event) {
// Проверяем, не перешёл ли фокус на другой элемент меню
setTimeout(() => {
if (!menu.contains(document.activeElement)) {
menu.style.display = 'none';
}
}, 0);
});
Когда это полезно
- Для accessibility-решений (люди, использующие Tab для навигации)
- В компонентах с модальными окнами
- Когда элемент не должен закрываться при клике на другие части страницы, но должен закрываться при уходе фокуса
Этот подход не заменяет contains(), а дополняет его для улучшения пользовательского опыта. Комбинируйте оба метода для максимальной надёжности.