如何实现Web应用上下键聚焦前后可聚焦元素?现有方案存缺陷
优化上下键焦点导航方案(解决不可聚焦元素问题)
核心问题分析
原方案直接聚焦相邻兄弟元素,未判断元素是否具备可聚焦能力,导致会选中<div>这类不可交互元素。解决关键是先识别可聚焦元素,再调整导航逻辑。
优化实现方案
1. 可聚焦元素判断函数
先实现工具函数,精准判断元素是否可聚焦:
function isFocusable(element) { // 元素不存在或隐藏直接排除 if (!element || element.hidden) return false; // 原生可聚焦元素(需未禁用,a标签需带href) const nativeFocusableTags = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA', 'A', 'AREA', 'IFRAME', 'OBJECT', 'EMBED']; if (nativeFocusableTags.includes(element.tagName)) { if (element.tagName === 'A' && !element.href) return false; return !element.disabled; } // 自定义tabindex的元素(tabindex >=0 且未禁用) const tabIndex = parseInt(element.getAttribute('tabindex'), 10); return !isNaN(tabIndex) && tabIndex >= 0 && !element.disabled; }
2. 优化后的焦点导航函数
下一个焦点元素导航
export function focusNext(focused, lockParent = false) { // 递归查找下一个可聚焦元素 function findNext(current) { // 先在当前元素的兄弟节点中查找 let nextSibling = current.nextElementSibling; while (nextSibling) { if (isFocusable(nextSibling)) return nextSibling; nextSibling = nextSibling.nextElementSibling; } // 兄弟节点无匹配,处理父元素逻辑 if (!lockParent) { const parent = current.parentElement; if (!parent) return null; // 查找父元素的下一个兄弟节点,再递归找其内部第一个可聚焦元素 const parentNext = parent.nextElementSibling; if (parentNext) { const firstFocusable = findFirstFocusable(parentNext); if (firstFocusable) return firstFocusable; // 父元素兄弟无匹配,继续向上查找 return findNext(parent); } else { // 到达根节点末尾,循环回到页面第一个可聚焦元素 return findFirstFocusable(document.body); } } else { // 锁定父元素,回到父元素内第一个可聚焦元素 return findFirstFocusable(current.parentElement); } } // 查找元素内第一个可聚焦子元素(深度优先) function findFirstFocusable(element) { if (isFocusable(element)) return element; let child = element.firstElementChild; while (child) { const found = findFirstFocusable(child); if (found) return found; child = child.nextElementSibling; } return null; } const target = findNext(focused); target && target.focus(); }
上一个焦点元素导航
export function focusPrevious(focused, lockParent = false) { // 递归查找上一个可聚焦元素 function findPrevious(current) { // 先在当前元素的兄弟节点中查找 let prevSibling = current.previousElementSibling; while (prevSibling) { if (isFocusable(prevSibling)) return prevSibling; prevSibling = prevSibling.previousElementSibling; } // 兄弟节点无匹配,处理父元素逻辑 if (!lockParent) { const parent = current.parentElement; if (!parent) return null; // 查找父元素的上一个兄弟节点,再递归找其内部最后一个可聚焦元素 const parentPrev = parent.previousElementSibling; if (parentPrev) { const lastFocusable = findLastFocusable(parentPrev); if (lastFocusable) return lastFocusable; // 父元素兄弟无匹配,继续向上查找 return findPrevious(parent); } else { // 到达根节点开头,循环回到页面最后一个可聚焦元素 return findLastFocusable(document.body); } } else { // 锁定父元素,回到父元素内最后一个可聚焦元素 return findLastFocusable(current.parentElement); } } // 查找元素内最后一个可聚焦子元素(深度优先) function findLastFocusable(element) { if (isFocusable(element)) return element; let child = element.lastElementChild; while (child) { const found = findLastFocusable(child); if (found) return found; child = child.previousElementSibling; } return null; } const target = findPrevious(focused); target && target.focus(); }
3. 事件绑定示例
在页面中监听上下键事件,触发导航逻辑:
document.addEventListener('keydown', (e) => { // 阻止默认滚动行为 if (e.key === 'ArrowDown') { e.preventDefault(); focusNext(document.activeElement); } else if (e.key === 'ArrowUp') { e.preventDefault(); focusPrevious(document.activeElement); } });
优化亮点
- 精准过滤:通过
isFocusable排除所有不可交互、隐藏的元素 - 智能导航:循环查找兄弟元素,跨父层级自动检索,支持父元素锁定
- 边界处理:到达页面首尾时自动循环,避免焦点丢失
- 兼容性:覆盖原生可聚焦元素和自定义tabindex元素的场景
内容的提问来源于stack exchange,提问作者Hydroper
相关产品推荐
相关产品推荐

