UNPKG

veui

Version:

Baidu Enterprise UI for Vue.js.

189 lines (181 loc) 5.25 kB
import { findIndex, find, get } from 'lodash' import { getFocusable, toggleClass, matches, scrollIntoView, isOverflow } from '../utils/dom' function isActive (elem, focusSelector) { return focusSelector ? matches(elem, focusSelector) : elem === document.activeElement } function toggleSelector (elem, selector, force) { if (selector.charAt(0) === '.') { toggleClass(elem, selector.slice(1), force) return } let [, attr] = selector.match(/^\[([^\]]+)\]$/) if (attr) { if (elem.hasAttribute(attr) || force === false) { elem.removeAttribute(attr) } else if (!elem.hasAttribute(attr) || force === true) { elem.setAttribute(attr, '') } } else { throw new Error( '[veui-key-select] `focusSelector` only accepts a simple class selector (`.foo`) or an attribute existence selector (`[foo]`).' ) } } const useKeySelect = ({ useNativeFocus, handlers, expandedKey = 'expanded' }) => ({ computed: { focusMode () { return typeof useNativeFocus === 'string' ? this[useNativeFocus] : typeof useNativeFocus === 'function' ? useNativeFocus(this) : useNativeFocus }, focusSelector () { return this.focusMode ? null : '[data-focus-visible-added]' } }, created () { this.$watch(expandedKey, (val) => { if (!val) { if (this.focusSelector) { this.clearFocusSelector() } this.clearFocusableFilter() } }) }, methods: { // 方便覆盖 getFocusableContainer () { return this.$refs.box }, getFocusable () { let container = this.getFocusableContainer() return container ? getFocusable(container) : [] }, getFilteredFocusable () { let focusable = this.getFocusable() if (typeof this.focusableFilter === 'function') { return focusable.filter(this.focusableFilter) } return focusable }, clearFocusableFilter () { this.focusableFilter = null }, getCurrentActiveElement () { return find(this.getFocusable(), (elem) => isActive(elem, this.focusSelector) ) }, // 按 DOM 顺序线性导航 navigate (forward = true) { this.focusAt(forward) }, /** * 焦点导航到指定的层级, TODO 进一步标准化 data-kbd-xxx 来控制导航? * @param {string} levelSelector 指定层级的选择器,如 [data-kbd-level="1"] * @param {string} options.targetSelector 进一步指定指定层级中获得焦点的元素,一般用来关闭某个展开后,焦点回到该展开按钮上 * @param {boolean} options.lock 锁定之后的导航只能在该层级中进行 */ navigateToLevel (levelSelector, { targetSelector, lock } = {}) { let focusable = this.getFocusable() let filter = (elem) => matches(elem, levelSelector) let nextLevel = find( focusable, (elem) => filter(elem) && (!targetSelector || matches(elem, targetSelector)) ) if (nextLevel) { this.focusElement(focusable, nextLevel) } if (lock) { this.focusableFilter = filter } }, focusAt (indexOrDirection) { let focusable = this.getFilteredFocusable() let length = focusable.length if (!length) { return } let index = indexOrDirection if (typeof indexOrDir !== 'number') { let forward = indexOrDirection index = findIndex(focusable, (elem) => isActive(elem, this.focusSelector) ) index = index === -1 ? 0 : (index + length + (forward ? 1 : -1)) % length } this.focusAtIndex(focusable, index) }, clearFocusSelector () { return this.focusAtIndex(this.getFocusable(), -1) }, focusAtIndex (focusableList, index) { if (this.focusSelector) { focusableList.forEach((elem, idx) => { toggleSelector(elem, this.focusSelector, index === idx) }) } else { focusableList[index].focus() } if (index !== -1 && isOverflow(this.getFocusableContainer())) { scrollIntoView(focusableList[index]) } }, focusElement (focusableList, element) { focusableList.forEach((elem) => { toggleSelector(elem, this.focusSelector, element === elem) }) scrollIntoView(element) }, handleKeydown (e) { let passive = false switch (e.key) { case 'Esc': case 'Escape': case 'Left': case 'ArrowLeft': this[expandedKey] = false break case 'Up': case 'ArrowUp': this.navigate(false) break case 'Down': case 'ArrowDown': this.navigate() break default: { // 先就简单支持下,目前仅用到 tab 和 enter let handler = e.key && get(handlers, e.key.toLowerCase()) if (handler) { handler.call(this, e) } passive = true break } } if (!passive) { e.stopPropagation() e.preventDefault() } } } }) export default useKeySelect({ useNativeFocus: true }) export { useKeySelect }