pin_drop当前位置:知识文库 ❯ 图文
JS集成:HTML onfocus与on - 详细教程与实战指南
一、教程简介
焦点(Focus)是Web无障碍和键盘导航的核心概念。当元素获得焦点时,它可以接收键盘输入和其他交互;当元素失去焦点时,用户需要重新聚焦才能操作。onfocus和onblur事件是焦点管理的基石,它们不仅影响交互逻辑,更直接关系到Web应用的无障碍可用性。本教程将全面讲解HTML焦点事件的类型、焦点管理API、tabIndex属性、焦点陷阱的实现、模态框的焦点处理,以及无障碍焦点管理的最佳实践,帮助开发者构建对所有用户友好的交互体验。
二、核心概念
四种焦点事件
关键区别:focus和blur不冒泡,只能在目标元素上监听;focusin和focusout会冒泡,可以在父元素上使用事件委托。
可获得焦点的元素
以下元素默认可以获得焦点(无需设置tabIndex):
-
表单元素:
<input>、<textarea>、<select>、<button> -
链接元素:
<a href="...">(有href属性的链接) -
区域元素:
<area href="..."> -
摘要元素:
<summary>(details的摘要) -
可编辑元素:具有
contenteditable="true"的元素 -
自定义元素:设置了
tabIndex属性的任何元素
FocusEvent对象
代码示例
element.addEventListener('focus', function(event) {
event.type; // 'focus'
event.target; // 获得焦点的元素
event.relatedTarget; // 失去焦点的元素(focus事件中)
});
element.addEventListener('blur', function(event) {
event.type; // 'blur'
event.target; // 失去焦点的元素
event.relatedTarget; // 获得焦点的元素(blur事件中)
});三、语法与用法
基本用法
代码示例
// focus/blur - 不冒泡
element.addEventListener('focus', function(event) { /* 获得焦点 */ });
element.addEventListener('blur', function(event) { /* 失去焦点 */ });
// focusin/focusout - 冒泡(推荐用于事件委托)
parent.addEventListener('focusin', function(event) { /* 子元素获得焦点 */ });
parent.addEventListener('focusout', function(event) { /* 子元素失去焦点 */ });焦点管理API
代码示例
// 设置焦点
element.focus(); // 聚焦元素
element.focus({ preventScroll: true }); // 聚焦但不滚动到元素
// 移除焦点
element.blur(); // 移除焦点
// 获取当前焦点元素
document.activeElement; // 当前获得焦点的元素
// 判断元素是否可获得焦点
element.tabIndex; // -1表示不可Tab聚焦,0+表示可聚焦tabIndex属性
四、代码示例
示例1:焦点事件类型对比
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>焦点事件类型对比</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
.demo-section {
background: white;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin: 16px 0;
}
.form-grid input {
width: 100%;
padding: 10px 14px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
outline: none;
transition: all 0.2s;
}
.form-grid input:focus {
border-color: #2196F3;
box-shadow: 0 0 0 3px rgba(33,150,243,0.15);
}
.form-grid input.focused {
border-color: #4CAF50;
background: #f0fff0;
}
.form-grid input.blurred {
border-color: #f44336;
background: #fff0f0;
}
.event-log {
background: #263238;
color: #ECEFF1;
padding: 14px;
border-radius: 8px;
font-family: 'Consolas', monospace;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
}
.log-focus { color: #66BB6A; }
.log-blur { color: #EF5350; }
.log-focusin { color: #42A5F5; }
.log-focusout { color: #FFA726; }
</style>
</head>
<body>
<h1>焦点事件类型对比</h1>
<div class="demo-section">
<h3>交互演示:使用Tab键在输入框间切换</h3>
<form id="demoForm">
<div class="form-grid">
<input type="text" name="name" placeholder="姓名" id="input1">
<input type="email" name="email" placeholder="邮箱" id="input2">
<input type="tel" name="phone" placeholder="电话" id="input3">
<input type="text" name="address" placeholder="地址" id="input4">
</div>
</form>
<p style="font-size:13px;color:#666;">点击输入框或按Tab键切换焦点,观察事件日志</p>
<div class="event-log" id="eventLog"></div>
</div>
<script>
const form = document.getElementById('demoForm');
const eventLog = document.getElementById('eventLog');
function addLog(msg, type) {
const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
eventLog.innerHTML += '<div class="' + type + '">[' + time + '] ' + msg + '</div>';
eventLog.scrollTop = eventLog.scrollHeight;
}
// 使用focusin/focusout(冒泡)在表单上监听
form.addEventListener('focusin', function(event) {
addLog('focusin: ' + (event.target.name || event.target.id) + ' 获得焦点' +
(event.relatedTarget ? ' (来自: ' + (event.relatedTarget.name || '?') + ')' : ''), 'log-focusin');
event.target.classList.add('focused');
event.target.classList.remove('blurred');
});
form.addEventListener('focusout', function(event) {
addLog('focusout: ' + (event.target.name || event.target.id) + ' 失去焦点' +
(event.relatedTarget ? ' (前往: ' + (event.relatedTarget.name || '?') + ')' : ''), 'log-focusout');
event.target.classList.remove('focused');
event.target.classList.add('blurred');
setTimeout(function() { event.target.classList.remove('blurred'); }, 500);
});
// focus/blur(不冒泡)需要直接绑定
form.querySelectorAll('input').forEach(function(input) {
input.addEventListener('focus', function() {
addLog('focus: ' + this.name + ' 获得焦点(不冒泡,直接监听)', 'log-focus');
});
input.addEventListener('blur', function() {
addLog('blur: ' + this.name + ' 失去焦点(不冒泡,直接监听)', 'log-blur');
});
});
</script>
</body>
</html>示例2:tabIndex与焦点管理
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tabIndex与焦点管理</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
.demo-card {
background: white;
border-radius: 10px;
padding: 20px;
margin: 16px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}
.tab-demo {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 16px 0;
}
.tab-item {
padding: 10px 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
user-select: none;
}
.tab-item:focus {
border-color: #2196F3;
box-shadow: 0 0 0 3px rgba(33,150,243,0.2);
outline: none;
}
.tab-item.tabindex-0 { background: #E8F5E9; border-color: #4CAF50; }
.tab-item.tabindex-neg { background: #FFEBEE; border-color: #f44336; }
.tab-item.tabindex-pos { background: #E3F2FD; border-color: #2196F3; }
.focus-info {
background: #f0f4f8;
padding: 14px;
border-radius: 8px;
margin: 12px 0;
font-size: 14px;
}
button {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
margin: 4px;
}
.btn-blue { background: #2196F3; color: white; }
.btn-green { background: #4CAF50; color: white; }
.btn-orange { background: #FF9800; color: white; }
</style>
</head>
<body>
<h1>tabIndex与焦点管理</h1>
<div class="demo-card">
<h3>tabIndex值对比</h3>
<p>按Tab键在元素间切换,观察不同tabIndex值的Tab顺序</p>
<div class="tab-demo">
<div class="tab-item tabindex-pos" tabIndex="3">tabIndex=3</div>
<div class="tab-item tabindex-pos" tabIndex="1">tabIndex=1</div>
<div class="tab-item tabindex-0" tabIndex="0">tabIndex=0</div>
<div class="tab-item tabindex-pos" tabIndex="2">tabIndex=2</div>
<div class="tab-item tabindex-neg" tabIndex="-1">tabIndex=-1</div>
<div class="tab-item tabindex-0" tabIndex="0">tabIndex=0</div>
</div>
<div class="focus-info" id="focusInfo">
当前焦点元素: <code id="currentFocus">无</code> |
document.activeElement: <code id="activeElement">body</code>
</div>
</div>
<div class="demo-card">
<h3>编程式焦点管理</h3>
<div>
<button class="btn-blue" onclick="focusElement('target1')">聚焦到目标1</button>
<button class="btn-blue" onclick="focusElement('target2')">聚焦到目标2</button>
<button class="btn-green" onclick="focusElement('target3', true)">聚焦目标3(不滚动)</button>
<button class="btn-orange" onclick="blurCurrent()">移除当前焦点</button>
</div>
<div style="margin-top:16px;">
<input type="text" id="target1" placeholder="目标1" style="padding:8px 12px;border:2px solid #e0e0e0;border-radius:6px;font-size:14px;outline:none;margin:4px;">
<input type="text" id="target2" placeholder="目标2" style="padding:8px 12px;border:2px solid #e0e0e0;border-radius:6px;font-size:14px;outline:none;margin:4px;">
</div>
<div id="target3" tabIndex="0" style="margin-top:8px;padding:12px;background:#f0f4f8;border-radius:6px;cursor:pointer;">
目标3 - div元素(tabIndex=0使其可聚焦)
</div>
</div>
<script>
function updateFocusInfo() {
const active = document.activeElement;
document.getElementById('currentFocus').textContent =
active.tagName + (active.id ? '#' + active.id : '') + (active.name ? '[name=' + active.name + ']' : '');
document.getElementById('activeElement').textContent =
active.tagName.toLowerCase() + (active.id ? '#' + active.id : '');
}
document.addEventListener('focusin', function() { updateFocusInfo(); });
document.addEventListener('focusout', function() { setTimeout(updateFocusInfo, 10); });
document.getElementById('target3').addEventListener('focus', function() {
this.style.outline = '2px solid #2196F3';
this.style.outlineOffset = '2px';
});
document.getElementById('target3').addEventListener('blur', function() {
this.style.outline = 'none';
});
function focusElement(id, preventScroll) {
const el = document.getElementById(id);
if (el) {
if (preventScroll) {
el.focus({ preventScroll: true });
} else {
el.focus();
}
}
}
function blurCurrent() {
const active = document.activeElement;
if (active && active !== document.body) {
active.blur();
}
}
</script>
</body>
</html>示例3:模态框焦点陷阱
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模态框焦点陷阱</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 20px;
}
.btn-open {
padding: 12px 24px;
background: #2196F3;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
}
.btn-open:hover { background: #1976D2; }
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.modal-overlay.visible { opacity: 1; visibility: visible; }
.modal {
background: white;
border-radius: 12px;
padding: 30px;
width: 90%;
max-width: 450px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.modal h2 { margin: 0 0 16px 0; }
.modal input {
width: 100%;
padding: 10px 14px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
outline: none;
margin-bottom: 12px;
}
.modal input:focus { border-color: #2196F3; }
.modal-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 16px;
}
.modal-actions button {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.btn-cancel { background: #e0e0e0; color: #333; }
.btn-confirm { background: #4CAF50; color: white; }
</style>
</head>
<body>
<h1>模态框焦点陷阱</h1>
<p>打开模态框后,Tab键焦点被限制在模态框内部循环</p>
<input type="text" placeholder="外部输入框1" style="padding:8px 12px;border:1px solid #ddd;border-radius:6px;margin:4px;">
<input type="text" placeholder="外部输入框2" style="padding:8px 12px;border:1px solid #ddd;border-radius:6px;margin:4px;">
<button class="btn-open" id="openModal">打开模态框</button>
<div class="modal-overlay" id="modalOverlay" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="modal">
<h2 id="modalTitle">用户信息</h2>
<label style="display:block;margin-bottom:4px;font-size:13px;color:#555;">姓名</label>
<input type="text" id="modalName" placeholder="输入姓名">
<label style="display:block;margin-bottom:4px;font-size:13px;color:#555;">邮箱</label>
<input type="email" id="modalEmail" placeholder="输入邮箱">
<div class="modal-actions">
<button class="btn-cancel" id="cancelBtn">取消</button>
<button class="btn-confirm" id="confirmBtn">确认</button>
</div>
</div>
</div>
<script>
const openBtn = document.getElementById('openModal');
const overlay = document.getElementById('modalOverlay');
const cancelBtn = document.getElementById('cancelBtn');
const confirmBtn = document.getElementById('confirmBtn');
const modalName = document.getElementById('modalName');
let lastFocusedElement = null;
openBtn.addEventListener('click', function() {
lastFocusedElement = document.activeElement;
overlay.classList.add('visible');
modalName.focus();
});
function closeModal() {
overlay.classList.remove('visible');
if (lastFocusedElement) {
lastFocusedElement.focus();
}
}
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', function() {
console.log('确认:', modalName.value);
closeModal();
});
overlay.addEventListener('click', function(e) {
if (e.target === overlay) closeModal();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && overlay.classList.contains('visible')) {
closeModal();
}
});
// 焦点陷阱:Tab键在模态框内循环
overlay.addEventListener('keydown', function(e) {
if (e.key !== 'Tab') return;
const focusableSelectors = 'input, button, select, textarea, [tabindex]:not([tabindex="-1"])';
const focusableElements = overlay.querySelectorAll(focusableSelectors);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else {
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
});
</script>
</body>
</html>示例4:无障碍焦点管理
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>无障碍焦点管理</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
.a11y-card {
background: white;
border-radius: 10px;
padding: 20px;
margin: 16px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}
.a11y-card h3 { margin-top: 0; }
.custom-widget {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #f0f4f8;
border: 2px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
user-select: none;
margin: 4px;
}
.custom-widget:focus {
border-color: #2196F3;
box-shadow: 0 0 0 3px rgba(33,150,243,0.2);
outline: none;
}
.custom-widget[aria-pressed="true"] {
background: #E3F2FD;
border-color: #2196F3;
}
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #2196F3;
color: white;
padding: 8px 16px;
z-index: 100;
transition: top 0.2s;
}
.skip-link:focus { top: 0; }
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: #333;
color: white;
padding: 12px 24px;
border-radius: 8px;
transition: transform 0.3s;
z-index: 100;
}
.toast.visible { transform: translateX(-50%) translateY(0); }
.focus-ring-demo {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin: 12px 0;
}
.focus-ring-demo button {
padding: 10px 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: white;
cursor: pointer;
font-size: 14px;
}
.focus-ring-demo button:focus { outline: none; }
.focus-ring-demo button:focus-visible {
outline: 3px solid #2196F3;
outline-offset: 2px;
}
.focus-ring-demo button:hover { background: #f5f5f5; }
.tip-box {
background: #E8F5E9;
border-left: 4px solid #4CAF50;
padding: 12px 16px;
border-radius: 0 8px 8px 0;
margin: 12px 0;
font-size: 14px;
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">跳到主要内容</a>
<h1>无障碍焦点管理</h1>
<div class="a11y-card">
<h3>1. 自定义组件的焦点管理</h3>
<div class="custom-widget" tabIndex="0" role="button" aria-pressed="false" id="toggleWidget1">
开关按钮
</div>
<div class="tip-box">
自定义组件必须设置: tabIndex="0"(可聚焦)、role属性(语义)、键盘事件处理(Enter/Space)
</div>
</div>
<div class="a11y-card" id="main-content">
<h3>2. focus-visible 焦点样式</h3>
<p>使用 :focus-visible 只在键盘导航时显示焦点环</p>
<div class="focus-ring-demo">
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
</div>
<div class="tip-box">
鼠标点击不显示焦点环,键盘Tab导航时显示焦点环。这是最佳实践!
</div>
</div>
<div class="a11y-card">
<h3>3. 动态内容的焦点管理</h3>
<button onclick="addNotification()" style="padding:8px 16px;border:none;border-radius:6px;cursor:pointer;background:#2196F3;color:white;">添加通知</button>
<div id="notificationArea" style="margin-top:12px;"></div>
</div>
<div class="toast" id="toast"></div>
<script>
document.querySelectorAll('.custom-widget').forEach(function(widget) {
widget.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleWidget(this);
}
});
widget.addEventListener('click', function() { toggleWidget(this); });
widget.addEventListener('focus', function() {
this.style.outline = '3px solid #2196F3';
this.style.outlineOffset = '2px';
});
widget.addEventListener('blur', function() { this.style.outline = 'none'; });
});
function toggleWidget(el) {
const isPressed = el.getAttribute('aria-pressed') === 'true';
el.setAttribute('aria-pressed', !isPressed);
showToast(isPressed ? '已关闭' : '已开启');
}
let notifCount = 0;
function addNotification() {
notifCount++;
const area = document.getElementById('notificationArea');
const notif = document.createElement('div');
notif.style.cssText = 'padding:12px 16px;background:#E3F2FD;border-radius:8px;margin:8px 0;display:flex;justify-content:space-between;align-items:center;';
notif.setAttribute('role', 'alert');
notif.innerHTML = '<span>通知 #' + notifCount + '</span>' +
'<button onclick="dismissNotification(this)" style="padding:4px 12px;border:none;border-radius:4px;cursor:pointer;background:#f44336;color:white;font-size:12px;">关闭</button>';
area.prepend(notif);
notif.querySelector('button').focus();
}
function dismissNotification(btn) {
const notif = btn.parentElement;
notif.style.opacity = '0';
notif.style.transition = 'opacity 0.3s';
setTimeout(function() {
notif.remove();
const nextBtn = document.querySelector('#notificationArea button');
if (nextBtn) nextBtn.focus();
}, 300);
}
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('visible');
setTimeout(function() { toast.classList.remove('visible'); }, 2000);
}
</script>
</body>
</html>五、浏览器兼容性
IE兼容性说明:IE9以下
focusin/focusout不支持冒泡;IE不支持focus({preventScroll})选项;IE不支持:focus-visible伪类。
六、注意事项与最佳实践
1. 使用focusin/focusout进行事件委托
代码示例
// 不推荐:focus/blur不冒泡,无法委托
parent.addEventListener('focus', handler); // 无效
// 推荐:使用focusin/focusout
parent.addEventListener('focusin', function(event) {
console.log('子元素获得焦点:', event.target);
});2. 模态框必须实现焦点陷阱
代码示例
// 模态框打开时:
// 1. 保存当前焦点元素
// 2. 将焦点移到模态框内第一个可聚焦元素
// 3. Tab/Shift+Tab在模态框内循环
// 4. 关闭时恢复之前的焦点3. 使用:focus-visible而非:focus设置焦点样式
代码示例
/* 不推荐:鼠标点击也显示焦点环 */
button:focus { outline: 3px solid blue; }
/* 推荐:只在键盘导航时显示焦点环 */
button:focus-visible { outline: 3px solid blue; }
/* 兼容方案 */
button:focus { outline: 3px solid blue; }
button:focus:not(:focus-visible) { outline: none; }4. 自定义交互组件必须支持键盘
代码示例
// 自定义按钮组件
customBtn.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.click();
}
});5. 动态内容需要管理焦点
代码示例
// 添加新内容后,将焦点移到新内容
function addItem() {
const item = createItem();
list.prepend(item);
item.querySelector('button').focus(); // 聚焦到新项
}
// 删除内容后,将焦点移到相邻项
function removeItem(btn) {
const item = btn.closest('.item');
const nextItem = item.nextElementSibling || item.previousElementSibling;
item.remove();
if (nextItem) nextItem.querySelector('button').focus();
}6. 避免使用正数tabIndex
代码示例
<!-- 不推荐:正数tabIndex打乱自然Tab顺序 -->
<div tabIndex="1">第一</div>
<div tabIndex="3">第三</div>
<div tabIndex="2">第二</div>
<!-- 推荐:使用0或-1,通过DOM顺序控制 -->
<div tabIndex="0">第一</div>
<div tabIndex="0">第二</div>
<div tabIndex="0">第三</div>七、代码规范示例
代码示例
/**
* 焦点陷阱管理器 - 模态框焦点管理
*/
class FocusTrap {
constructor(container, options = {}) {
this.container = container;
this.options = {
onEscape: null,
initialFocus: null,
returnFocus: true,
...options
};
this.previousFocus = null;
this.focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
this._handleKeyDown = this._handleKeyDown.bind(this);
}
activate() {
this.previousFocus = document.activeElement;
document.addEventListener('keydown', this._handleKeyDown);
const initialEl = this.options.initialFocus
? this.container.querySelector(this.options.initialFocus)
: this._getFocusableElements()[0];
if (initialEl) {
setTimeout(function() { initialEl.focus(); }, 0);
}
}
deactivate() {
document.removeEventListener('keydown', this._handleKeyDown);
if (this.options.returnFocus && this.previousFocus) {
this.previousFocus.focus();
this.previousFocus = null;
}
}
_getFocusableElements() {
return Array.from(this.container.querySelectorAll(this.focusableSelectors))
.filter(function(el) { return el.offsetParent !== null; });
}
_handleKeyDown(event) {
if (event.key === 'Escape' && this.options.onEscape) {
this.options.onEscape();
return;
}
if (event.key !== 'Tab') return;
const focusableElements = this._getFocusableElements();
if (focusableElements.length === 0) {
event.preventDefault();
return;
}
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey) {
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
}
}
// 使用示例
const modal = document.getElementById('modal');
const focusTrap = new FocusTrap(modal, {
initialFocus: 'input:first-of-type',
onEscape: closeModal
});
function openModal() {
modal.style.display = 'block';
focusTrap.activate();
}
function closeModal() {
modal.style.display = 'none';
focusTrap.deactivate();
}八、常见问题与解决方案
问题1:focus/blur不冒泡导致无法使用事件委托
代码示例
// 问题:focus/blur不冒泡
parent.addEventListener('focus', handler); // 不触发
// 解决方案1:使用focusin/focusout
parent.addEventListener('focusin', handler);
// 解决方案2:使用capture阶段
parent.addEventListener('focus', handler, true);问题2:模态框打开后焦点跑到外部
代码示例
// 问题:Tab键可以将焦点移到模态框外部
// 解决方案:实现焦点陷阱
// 关键:监听Tab键,在首尾元素间循环
overlay.addEventListener('keydown', function(e) {
if (e.key !== 'Tab') return;
const focusable = overlay.querySelectorAll(
'input, button, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});问题3:动态添加的元素无法获得焦点
代码示例
// 问题:动态创建的div默认不可聚焦
const div = document.createElement('div');
div.focus(); // 无效
// 解决方案:设置tabIndex
div.tabIndex = 0;
div.focus(); // 有效
// 或使用role属性增强语义
div.setAttribute('role', 'button');
div.setAttribute('tabIndex', '0');问题4:focus()导致页面滚动
代码示例
// 问题:element.focus()会滚动到元素位置
element.focus(); // 页面可能滚动
// 解决方案:使用preventScroll选项
element.focus({ preventScroll: true });
// 兼容方案
const x = window.scrollX, y = window.scrollY;
element.focus();
window.scrollTo(x, y);问题5:relatedTarget为null
代码示例
// 问题:某些情况下event.relatedTarget为null
// - 点击页面空白区域导致失焦
// - 焦点移出浏览器窗口
// - 某些浏览器的安全限制
// 解决方案:检查relatedTarget是否为null
element.addEventListener('blur', function(event) {
if (event.relatedTarget === null) {
// 焦点移到了未知位置或窗口外
console.log('焦点离开了当前元素,目标未知');
}
});九、总结
焦点管理是Web无障碍和键盘导航的核心,掌握其各个方面对于构建可访问的Web应用至关重要:
-
四种焦点事件:
focus/blur不冒泡,适合单个元素监听;focusin/focusout冒泡,适合事件委托和表单级焦点管理。 -
tabIndex属性:
tabIndex="0"使元素可Tab聚焦,tabIndex="-1"允许编程聚焦但跳过Tab,避免使用正数tabIndex。 -
焦点陷阱:模态框必须实现焦点陷阱,确保Tab键在模态框内循环,关闭时恢复之前的焦点。
-
无障碍焦点管理:自定义交互组件必须设置tabIndex和ARIA属性,支持键盘操作(Enter/Space),使用
:focus-visible区分鼠标和键盘焦点。 -
动态内容焦点:添加新内容后将焦点移到新内容,删除内容后将焦点移到相邻项,使用
role="alert"让屏幕阅读器播报动态变化。 -
编程式焦点管理:使用
element.focus()设置焦点,element.blur()移除焦点,document.activeElement获取当前焦点元素,注意focus({preventScroll})的兼容性。
常见问题
focus和focusin事件有什么区别?
focus和focusin都在元素获得焦点时触发,核心区别在于冒泡行为。focus不冒泡,只能在目标元素上直接监听;focusin会冒泡,可以在父元素上使用事件委托监听子元素的焦点变化。因此需要事件委托时应使用focusin/focusout。
tabIndex=-1和tabIndex=0有什么区别?
tabIndex=0使元素可以通过Tab键聚焦,按照文档顺序参与Tab导航;tabIndex=-1使元素可以通过JavaScript编程方式聚焦(如element.focus()),但Tab键会跳过该元素。通常用tabIndex=0让自定义组件可访问,用tabIndex=-1实现编程式焦点管理。
模态框为什么需要焦点陷阱?
模态框打开时,视觉焦点被限制在模态框内,如果键盘焦点可以Tab到模态框外部的元素,会造成键盘用户和屏幕阅读器用户的困惑。焦点陷阱确保Tab和Shift+Tab在模态框内的可聚焦元素间循环,关闭模态框后还需要恢复之前的焦点位置。
:focus-visible和:focus有什么区别?
:focus在元素获得焦点时始终触发,包括鼠标点击和键盘导航;:focus-visible只在用户通过键盘导航(如Tab键)获得焦点时触发,鼠标点击时不触发。使用:focus-visible可以避免鼠标点击时出现不必要的焦点环,同时保留键盘用户的焦点指示,是最佳实践。
如何避免focus()导致页面滚动?
使用focus方法的preventScroll选项:element.focus({ preventScroll: true }),这样聚焦元素时不会自动滚动页面。对于不支持该选项的旧浏览器,可以在focus后立即使用window.scrollTo恢复滚动位置。
本文涉及AI创作
内容由AI创作,请仔细甄别