npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

focus-fly

v2.1.0

Published

焦点管理。Focus management.

Downloads

9

Readme

focus-fly

使用 focus-fly 管理和控制焦点,实现一个键盘可访问的用户界面,为用户带来流畅的键盘体验。focus-fly(v2.0.1)的压缩体积为 6.2kB

键盘可访问的用户界面,会在用户丧失或暂时丧失使用鼠标能力的时候,依然保有用户使用键盘的能力。对于有能力同时使用鼠标和键盘的用户,他们可以自由地切换访问界面的设备。您可以任意选择鼠标和键盘来访问这个使用 focus-fly 的范例网站

网页程序里有很多需要管理和控制焦点的场景,例如弹窗、菜单、选项卡、抽屉等等。焦点往往在多个元素之间相互关联,并且要有符合预期的移动轨迹。进行业务或组件开发的时候,如果不考虑焦点的影响,处处关联的焦点可能让程序变得混乱,如果遗漏处理某些情况,反应到用户界面上,则是不符预期的意外行为。

下面的文档会使用到几个关键词,分别是入口封面列表出口,引入项目之后可以运用这几个关键词,来描述“焦点如何通过入口进入列表,进入列表后如何移动焦点,以及如何让焦点通过出口退出列表”。

例如开发一个模态对话框,对话框的背景应该对所有用户隐藏,对于鼠标用户,鼠标不能访问背景元素,对于键盘用户,键盘不能访问背景元素,对于使用辅助设备的用户,辅助设备也不能访问背景元素。

focus-fly 可以控制从“打开”按钮开始、到对话框内导航、到“关闭”按钮结束,这个流程中焦点的路径,通过确定的焦点路径,避免聚焦到背景元素上。

安装

npm 安装(yarn 则使用 yarn add focus-fly 进行安装):

npm install focus-fly

使用

focus-fly 支持 ESM 和 CJS 导入,如果希望直接通过浏览器标签导入,可以下载本项目 output/ 文件夹内的 UMD 文件 进行 <script> 标签引入。

添加下面这两行代码后,焦点将会在一个列表之间陷入循环,这个列表的头元素是 #firstTabbableNode,尾元素是 #lastTabbableNode

import fFocus from "focus-fly"; // ESM 导入方式
// const focus = require("focus-fly"); // CJS 导入方式
fFocus(["#firstTabbableNode", "#lastTabbableNode"]);

这是最简洁的调用,以这种方式调用,焦点可以通过键盘(Tab)进入列表,但是无法通过键盘退出列表,下面对调用参数稍加修改,加入元素 #entryBtn 作为入口,元素 #lastTabbableNode 作为出口,这样焦点就能够通过键盘在入口、列表和出口间流动:

fFocus(["#firstTabbableNode", "#lastTabbableNode"], {
  entry: "#entryBtn",
  exit: "#lastTabbableNode",
  onEscape: true
});

上面的代码执行后,在浏览器中将会有这样的行为:在 #entryBtn 上按下 Enter#firstTabbableNode 成为焦点,按住 Tab,焦点在 #firstTabbableNode#lastTabbableNode 之间循环,在 #lastTabbableNode 上按下 Enter,或者在列表内的任意元素上按下 Esc#entryBtn 成为焦点。查看一个在线范例

这是一个简单,但是是比较完整的用法,解释了入口、列表和出口对焦点的管理与控制,继续阅读,查看关于延迟注册列表事件、触发焦点移动的钩子、自定义焦点矫正目标等更多特性的详细介绍。

focusFly(list[, options])

调用函数 focusFly 时可以传递 2 个参数,list 表示焦点列表,第二个入参 options 是可选的,用于设定若干选项,例如设定入口、封面、列表和出口相关的详细配置。

focusFly(root, list[, options])

调用函数 focusFly 时可以传递 3 个参数,rootlist 内各元素的公共祖先元素,将会被用来监听键盘(keydown)之类的事件,如果不提供 root,focus-fly 将会通过 list 找到最小公共祖先元素,第二个入参 list 表示列表,第三个 options 是可选的,用于设定若干选项。

import fFocus from "focus-fly"; // esm 方式引入
// const fFocus = require("focus-fly"); // cjs 方式引入

const dialog = document.getElementById("dialog");

// 循环焦点的根元素,对话框
fFocus(dialog, ["#head", "#tail"], { // 根元素 root 是 #dialog,根元素用来监听诸如 keydown 之类的事件,列表 list 的范围是从 #head 到 #tail,焦点如果进入列表,就会在这个范围循环
  // 入口配置
  entry: {
    // 入口的选择器字符串,例如“打开”按钮
    node: "#open",
    // 点击 #open 后的行为
    on: onEntry,
  },
  // 出口配置
  exit: {
    // 退出列表的出口元素,例如“关闭”按钮
    node: "#close",
    // 点击 #close 后的行为
    on: onExit,
  },
  // 按下 Esc 的行为
  onEscape: true,
});

/** 设置触发入口的行为 */
function onEntry() {
  dialog.classList.add("opened");
  dialog.classList.remove("closed");
}

/** 设置触发出口的行为 */
function onExit() {
  dialog.classList.add("closed");
  dialog.classList.remove("opened");
}

您也可以进入范例文件夹,通过运行范例文件夹,进行本地预览:

cd examples/cjs
npm i
npm run start

root

rootstring | Element,可以是一个 Element 对象,也可以是一个 DOMString(如 #container)。

根元素 root 是列表内各元素的公共祖先元素,将被用于监听键盘(keydown)事件以及和列表有关的其它事件,默认会监听按键 Tab 来控制焦点循环聚焦,如果开启了 options.onEscape,也会监听 Esc

如果不提供这个参数,focus-fly 会取得列表 list 内元素的最小公共祖先作为根元素 root

list

list(string | Element)[],是一个数组,数组内的元素可以是 Element 对象,也可以是 DOMString

这个参数表示列表,文档里提到的“列表”都是指这里的 list,默认情况下,数组 list 表示范围,只需要两个元素,一个可聚焦的头元素,一个可聚焦的尾元素,如果传入的数组长度大于 2,将只取头和尾。

设置 options.sequence 为 true 后,list 表示序列,是一个长度大于 2 的数组,这时按下 Tab 后,焦点将以 list 中元素的顺序进行导航。在设置 options.nextoptions.prev 后,原来的 Tab 被自定义导航键取代,同时 options.sequence 被默认设为 true。

通过入口进入列表,如果有封面,则入口进入封面,封面再进入列表。通过出口退出列表,回到入口,如果有封面,出口会退出列表,回到封面,再通过封面回到入口。

options

下面的选项,除了 triggerentryexitcover,其它选项基本都和列表相关。下面的每一个选项都是可选的。

| Name | Type | Default | Desc | |:--|:--|:--|:--| | sequence | boolean | false | 是否指定焦点导航的序列,设置 true 则按顺序聚焦列表内每项元素 | | loop | boolean | true | 是否循环聚焦,设置为 false,锁住焦点,焦点将停止在第一个和最后一个元素 | | next | isKey | listForward | null | 自定义前进按键,可传入函数和字符串,设置后,sequence 将默认为 true | | prev | isKey | listBackward | null | 自定义后退按键,可传入函数和字符串,设置后,sequence 将默认为 true | | trigger | element | null | 入口元素,用于退出列表时聚焦使用,如果在其它地方设置,可以忽略,例如设置 entry.node 后,不用设置 trigger | | entry | element | element[] | entry | entry[] | null | 入口相关配置,进入列表,可以直接设置为一个元素,也可以设置数组,表示多个入口 | | exit | element | element[] | exit | exit[] | null | 出口相关配置,退出列表,回到入口,如果存在封面,则是回到封面,可以直接设置为一个元素,也可以设置数组,表示多个出口 | | onEscape | false | handleKeydown | null | 按下 Esc 的行为,如果未设置,默认取第一个 options.exit.on | | onClick | handleClick | null | 点击列表里的某一项后的行为 | | onMove | handleMoveListItem | null | 移动的时候触发,包括进入列表时,移动列表时,以及退出列表时,sequence 为 true 才会触发 | | cover | boolean | cover | false | 封面相关配置,设置为 true,则是默认封面,默认把根元素 root 作为封面,当焦点在封面上,默认 Enter 进入列表,默认 Tab 聚焦列表的后一个元素 | | initialActive | number | -1 | 默认的初始的焦点在列表中的位置,可能会被用于修改当前和上一个聚焦元素的样式时使用 | | correctionTarget | boolean | getTarget | true | 焦点矫正,默认从非入口的空白区域进入列表,也将聚焦上一次退出前焦点在列表中的位置,设置为 false 则不进行矫正 | | delayToFocus | boolean | promiseDelay | callbackDelay | null | 延迟聚焦,执行完 options.entry.on 后,等待执行 delayToFocus 完成后聚焦,延迟聚焦的本意是等待列表渲染完成后再聚焦,延迟聚焦意味延迟添加列表相关的事件,也即在触发入口前,没有列表相关的事件,如果设为 true,则会在触发入口后立刻添加列表相关的事件,可用于性能优化 | | delayToBlur | promiseDelay | callbackDelay | null | 延迟失列表的焦,触发出口后等待执行 delayToBlur 完成后失焦,和 delayToFocus 类似 | | stopPropagation | boolean | false | 阻止(列表移动)冒泡或捕获 | | preventDefault | boolean | false | 阻止(列表移动)默认行为 | | manual | boolean | false | 手动添加监听事件,入口、列表、出口的监听事件,通过调用的返回值手动添加各事件 |

关于组合键的设置,上面和下面的表格中,类型 TypeisKey 的,有便捷的字符串的设置方式:

  • 直接传入字符串,例如 "Control-n",表示同时按下 Controln
  • 也可配合数组,用于多种按键组合完成同一个任务,例如 ["Control-n", 'j', "ArrowRight", "ArrowDown"],表示按下 Controln、按下 j、按下右方向键、按下向下方向键,这四种组合的功能都是一样的。

如果需要传入函数,也可将函数传入数组中,函数和字符串能够混合使用。

为了不影响排版阅读,下面 4 个名称过长的选项被单独制成一张表格:

| Name | Type | Default | Desc | |:--|:--|:--|:--| | removeListenersEachExit | boolean | true | 每次退出列表回到入口是否移除列表事件 | | removeListenersEachEnter | boolean | false | 每次进入列表后是否移除入口事件 | | addEntryListenersEachExit | boolean | true | 每次退出列表是否添加入口监听事件 | | allowSafariToFocusAfterMousedown | boolean | true | 用于抹平 Safari 不同于其它浏览器,点击后 button 之类的元素不会被聚焦的问题,设置为 true,Safari 中 将会在列表的 mousedown 事件里执行 focus() |

options.next

| Name | Type | isRequired | Default | Desc | |:--|:--|:--|:--|:--| | key | isKey | N | null | 自定义在列表前进的组合键,可传入函数和字符串,如果是函数,则返回 true 代表应用这个组合键 | | on | handleNextOrPrev | N | null | 前进时被执行,前进时的行为 |

options.prev

| Name | Type | isRequired | Default | Desc | |:--|:--|:--|:--|:--| | key | isKey | N | null | 自定义在列表后退的组合键,可传入函数和字符串,如果是函数,则返回 true 代表应用这个组合键 | | on | handleNextOrPrev | N | null | 后退时被执行,后退时的行为 |

下面的代码演示了使用 ctrl-n 完成前进焦点(字符串形式),使用 ctrl-p 完成后退焦点(函数形式):

import fFocus from "focus-fly";

const dialog = document.getElementById("dialog");

const isBackward = e => (
  (e.ctrlKey && e.key === "p") ||
  e.key === "ArrowTop" ||
  e.key === "ArrowLeft");

fFocus(dialog, ["#head", "#second", "#tail"], {
  entry: {
    node: "#open",
    on() {
      dialog.classList.add("openedDialog");
      dialog.classList.remove("closedDialog");
    },
  },
  exit: {
    node: "#close",
    on() {
      dialog.classList.remove("openedDialog");
      dialog.classList.add("closedDialog");
    },
  },
  next: ["Control-n", "ArrowRight", "ArrowDown"], // <----- 自定义*前进*焦点的配置项
  prev: isBackward, // <---- 自定义*后退*焦点的配置项
});

options.entry

这些选项和入口相关,描述了如何通过入口进入封面或列表。下面的选项可以在一个对象里,也可以在由这个对象组成的数组里。下面的每一个选项都是可选的。

如果已经通过入口进入列表,则在退出列表前,不能再次触发入口进入列表。通过直接点击列表,也被算作进入列表。

| Name | Type | Default | Desc | |:--|:--|:--|:--| | node | element | element[] | null | 入口元素,将用于监听点击事件,用于退出列表时聚焦使用 | | key | isKey | null | 自定义进入列表组合键,可传入字符串和函数 | | on | handleKeydown | null | 进入时被调用,进入列表前的行为,如果列表或封面在这里才开始渲染,需要设置 options.delayToFocus 来延迟聚焦,否则不能聚焦不存在的元素 | | type | enterType | enterType[] | null | 入口的监听方式,如果 options.entry 设置了 node 选项,则默认为 "click",如果还设置了 key 选项,则默认为 ["click", "keydown"],另外还支持 "focus" 类型用于聚焦触发入口,"invoke" 类型用于返回值 Return.enter 触发入口 | | target | boolean | element | getTarget | null | 进入到哪个元素?默认将聚焦列表第一个元素,设置为 false 将不改变焦点 | | delay | false | promiseDelay | callbackDelay | null | 延迟聚焦,触发 node 后等待执行 delay 完成后聚焦,如果没有设置,将取 options.delayToFocus | | if | ef | null | 触发入口的条件,如果不符合条件,将不被认为是进入了列表 | | stopPropagation | boolean | false | 阻止(入口)冒泡或捕获 | | preventDefault | boolean | true | 阻止(入口)默认行为 | | onExit | true | handleExit | null | 指定当前入口同时也是出口,作为出口的行为,设为 true,则行为取 options.entry.on,该选项类似表明这个元素是个开关 |

入口定义的方式非常自由,例如 entry: "#entryBtn"entry: { node: "#entryBtn", type: "click" }entry: ["#entryBtn"]entry: ["#entryBtn", { node: "#btn2", type: "click" }],这四种写法都是允许的。

options.exit

这些选项和出口相关,描述了焦点如何从列表回到封面或入口。

和入口类似,在下次进入列表前,不能够重复触发出口退出列表。通过点击非列表的空白区域,也被算作退出列表。

| Name | Type | Default | Desc | |:--|:--|:--|:--| | node | element | element[] | getExit | null | 出口元素,将用于监听点击事件,用于退出列表时聚焦使用 | | key | isKey | null | 自定义退出列表组合键,可传入字符串和函数 | | on | handleKeydown | null | 退出时被调用,退出列表前的行为,如果有封面就退出至封面,如果没有就退出至入口,设置该选项后,按键按下 esc 同样生效 | | type | exitType | exitType[] | ["keydown", "click"] | 出口的事件类型,和 options.entry.type 类似,但是多了 "outlist" 类型,用于聚焦空白区域、非列表区域时触发出口,这常用于弹窗的半透明蒙版 | | target | boolean | element | getTarget | null | 退出至哪个元素?默认将聚焦第一个入口,设置为 false 将不改变焦点 | | delay | false | promiseDelay | callbackDelay | null | 延迟失焦,触发 node 后等待执行 delay 完成后失焦,如果没有设置,将取 options.delayToBlur | | stopPropagation | boolean | false | 阻止(出口)冒泡或捕获 | | preventDefault | boolean | true | 阻止(出口)默认行为 | | if | ef | null | 触发出口的条件,如果不符合条件,将不被认为是退出了列表 |

出口定义的方式和入口一样,例如 exit: "#exitBtn"exit: { node: "#exitBtn", type: "click" }exit: ["#exitBtn"]entry: ["#exitBtn", { node: "#btn2", type: "click" }],这四种写法都是允许的。

options.cover

这些选项和封面有关,每个选项都是可选且默认值为空。

如果存在封面,焦点将通过入口进入封面,焦点又通过封面进入列表,焦点通过出口退出至封面,最后焦点通过封面退出至入口。也就是说,在进入列表的阶段时,封面在入口和列表之间,在退出列表的阶段,封面在出口和入口之间。

| Name | Type | Desc | |:--|:--|:--| | node | element | 封面元素,如果不指定,默认将取根元素 root | | exit | isKey | exitCover | exitCover[] | 退出封面,可以直接设置退出封面的组合键,如果不设置,Tab 将作为默认退出封面的按键,并且退出至列表的后一个元素 | | enterKey | isKey | 自定义进入列表的组合键,如果不设置,默认为 Enter,可传入函数和字符串 | | onEnter | handleKeydown | 进入列表时的行为 |

options.cover.exit 是一个有若干选项的对象,也可以是一个包含这类对象的数组。下面是 options.cover.exit 的所有选项,每一个选项都是可选的,且默认值为空:

| Name | Type | Desc | |:--|:--|:--| | key | isKey | 自定义退出封面的组合键,可传入函数和字符串 | | on | handleKeydown | 退出封面时的行为 | | target | element | 退出到哪个元素? |

Return

下面是调用函数 focusFly 后返回的属性。

| Name | Type | Desc | |:--|:--|:--| | enter | (entry: ReturnEntry) => Promise<void> | 进入列表,如果自己管理入口元素的点击监听器,可以使用该方法 | | exit | (exit: ReturnExit) => Promise<void> | 退出列表,如果自己管理退出入口元素的点击监听器,可以使用该方法 | | removeListeners | () => void | 移除所有的监听事件 | | addEntryListeners | () => void | 添加入口的监听事件 | | removeEntryListeners | () => void | 移除入口事件 | | addListRelatedListeners | () => void | 添加列表相关(封面、列表、出口)的监听事件 | | removeListRelatedListeners | () => void | 移除列表相关的事件 | | addForward | (id: string, forward: forward | getForward) => void | 添加转发,转发用于不涉及入口、列表、出口、封面的焦点转移 | | removeForward | (id: string) => void | 移除转发 | | updateList | (newList: element[]) => void | 更新列表 | | i | (newI?: number) => number | 获取和设置当前焦点的编号,设置新的编号之后,会聚焦对应编号的焦点,并触发 options.onMove |

import fFocus from "focus-fly";

const dialog = document.getElementById("dialog");
const openBtn = document.getElementById("#open");
const closeBtn = document.getElementById("#close");

const bagel = fFocus(dialog, ["#head", "#tail"]);

openBtn.addEventListener("click", e => {
  dialog.classList.add("openedDialog");
  dialog.classList.remove("closedDialog");
  bagel.enter(); // 聚焦 #head
})

closeBtn.addEventListener("click", e => {
  dialog.classList.remove("openedDialog");
  dialog.classList.add("closedDialog");
  bagel.exit(); // 聚焦 #dialog
})

查看使用 addForward 的一个范例,这个范例中,#grid_wrapper 是一个中转节点,通过按下 Tab 和反向 Tab,焦点中转到 #more_from

范例与项目开发

查看和运行范例:

cd examples/cjs # 进入使用 cjs 模块的范例文件夹
npm i # 安装依赖
npm run start # 本地运行

进行项目开发:

npm i
npm run start

运行之后,修改根目录的 index.js(focus-fly 主文件)和 examples/run-start 下的文件,即可在浏览器看到实时修改结果。开发后,提交时请编写相应的单元测试。

单元测试

npm i
npm run test

常见问题

focus-fly 的主要任务是管理和控制焦点,如果有钩子不能满足需求,可以考虑在业务开发中自行监听事件,处理样式的变化。

原理

查看原理

CHANGELOG

查看更新日志

版本规则

查看语义化版本 2.0.0

协议

查看 MIT License

支持和赞助

请随意 Issue、PR 和 Star,通过爱发电进行赞助。

其它

focus-fly 支持的特性:

  • 集中管理焦点;
  • 通过指定范围或序列循环焦点;
  • 按需监听、移除事件;
  • 矫正不是从入口进入列表的焦点;
  • 提供钩子函数完成诸如样式修改的任务。

假设准备开发一个弹窗,进行焦点管理,需要有下面的流程、考虑下面几种情况:

  • 在“打开”按钮上按下 Enter,弹窗内第一个元素获得焦点;
  • 在弹窗的内部按住 Tab,焦点(中幻术)不能逃出弹窗;
  • 点击弹窗的空白区域,按下反向 Tab,弹窗内的最后一个元素获得焦点;
  • 在“关闭”按钮上按下 Enter,“打开”按钮获得焦点;
  • 按下 Esc,或者点击弹窗背后的半透明蒙层,“打开”按钮获得焦点;
  • 管理弹窗、半透明蒙版、“打开”按钮、“关闭”按钮的点击和键盘事件。

相关链接: