@zhencai/vue-focus-scope 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/{readme.md → README.md} +49 -4
- package/index.d.ts +1 -0
- package/index.js +144 -52
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT
|
package/{readme.md → README.md}
RENAMED
|
@@ -19,9 +19,14 @@
|
|
|
19
19
|
- **`loop` (默认)**:强循环模式。焦点被严格限制在组件内部,无法通过 Tab 键移出
|
|
20
20
|
- **`soft-loop`**:软循环模式。在首尾之间提供缓冲,允许特定的交互逻辑(防止单元素死循环)
|
|
21
21
|
|
|
22
|
+
- **自动聚焦**
|
|
23
|
+
- 通过 `autofocus` 属性控制组件挂载时是否自动聚焦第一个可聚焦元素
|
|
24
|
+
- 支持动态切换,可在组件挂载后通过改变属性值触发聚焦
|
|
25
|
+
- 适用于多作用域场景,避免后挂载的组件抢占焦点
|
|
26
|
+
|
|
22
27
|
- **无障碍友好**:原生支持键盘导航,无需额外配置
|
|
23
28
|
|
|
24
|
-
##
|
|
29
|
+
## 安装
|
|
25
30
|
|
|
26
31
|
```vue
|
|
27
32
|
npm install @zhencai/vue-focus-scope
|
|
@@ -63,13 +68,53 @@ import FocusScope from "@zhencai/vue-focus-scope";
|
|
|
63
68
|
</template>
|
|
64
69
|
```
|
|
65
70
|
|
|
71
|
+
### 自动聚焦控制
|
|
72
|
+
|
|
73
|
+
通过 `autofocus` 属性控制焦点行为:
|
|
74
|
+
|
|
75
|
+
```vue
|
|
76
|
+
<template>
|
|
77
|
+
<div>
|
|
78
|
+
<!-- 场景1:默认不自动聚焦 -->
|
|
79
|
+
<FocusScope :autofocus="false">
|
|
80
|
+
<input type="text" placeholder="不会自动聚焦" />
|
|
81
|
+
</FocusScope>
|
|
82
|
+
|
|
83
|
+
<!-- 场景2:挂载时自动聚焦 -->
|
|
84
|
+
<FocusScope :autofocus="true">
|
|
85
|
+
<input type="text" placeholder="会自动聚焦到这里" />
|
|
86
|
+
</FocusScope>
|
|
87
|
+
|
|
88
|
+
<!-- 场景3:动态控制聚焦(多作用域场景) -->
|
|
89
|
+
<button @click="showDialog = true">打开对话框</button>
|
|
90
|
+
<FocusScope v-if="showDialog" :autofocus="true">
|
|
91
|
+
<input type="text" placeholder="对话框打开时自动聚焦" />
|
|
92
|
+
<button @click="showDialog = false">关闭</button>
|
|
93
|
+
</FocusScope>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<script setup lang="ts">
|
|
98
|
+
import { ref } from "vue";
|
|
99
|
+
|
|
100
|
+
const showDialog = ref(false);
|
|
101
|
+
</script>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**多作用域场景说明**:
|
|
105
|
+
|
|
106
|
+
- 当页面有多个 `FocusScope` 组件时,为了避免后挂载的组件抢占焦点
|
|
107
|
+
- 可以先将所有组件的 `autofocus` 设为 `false`
|
|
108
|
+
- 在需要聚焦时(如对话框显示、标签页切换),动态将对应组件的 `autofocus` 设为 `true`
|
|
109
|
+
|
|
66
110
|
## API 文档
|
|
67
111
|
|
|
68
112
|
### Props
|
|
69
113
|
|
|
70
|
-
| 属性名
|
|
71
|
-
|
|
|
72
|
-
| `tabMode`
|
|
114
|
+
| 属性名 | 类型 | 默认值 | 说明 |
|
|
115
|
+
| :---------- | :---------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------- |
|
|
116
|
+
| `tabMode` | `'loop' \| 'soft-loop'` | `'loop'` | 循环模式:<br>- `loop`: 严格限制焦点在插槽内<br>- `soft-loop`: 允许一定程度的外部交互,主要用于防止单元素死循环 |
|
|
117
|
+
| `autofocus` | `boolean` | `false` | 是否自动聚焦第一个可聚焦元素:<br>- `true`: 组件挂载时或属性变为 true 时自动聚焦<br>- `false`: 不自动聚焦,需手动控制 |
|
|
73
118
|
|
|
74
119
|
### Slots
|
|
75
120
|
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,71 +1,163 @@
|
|
|
1
|
-
|
|
1
|
+
/* My Library v1.0.0 */
|
|
2
|
+
import { createCommentVNode, createElementBlock, createElementVNode, defineComponent, openBlock, renderSlot, useTemplateRef, watch } from "vue";
|
|
2
3
|
//#region src/components/FocusScope.vue?vue&type=script&setup=true&lang.ts
|
|
3
|
-
var
|
|
4
|
+
var _hoisted_1 = {
|
|
4
5
|
key: 0,
|
|
5
6
|
tabindex: "0"
|
|
6
|
-
}
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/components/FocusScope.vue
|
|
10
|
+
var FocusScope_default = /* @__PURE__ */ defineComponent({
|
|
7
11
|
__name: "FocusScope",
|
|
8
|
-
props: {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
props: {
|
|
13
|
+
tabMode: { default: "loop" },
|
|
14
|
+
autofocus: {
|
|
15
|
+
type: Boolean,
|
|
16
|
+
default: true
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
setup(__props) {
|
|
20
|
+
const props = __props;
|
|
21
|
+
/**
|
|
22
|
+
* wrapper 容器引用
|
|
23
|
+
* 用于限定焦点作用域(Focus Scope)
|
|
24
|
+
*/
|
|
25
|
+
const wrapper = useTemplateRef("wrapper");
|
|
26
|
+
/**
|
|
27
|
+
* 获取当前作用域内所有“可聚焦元素”
|
|
28
|
+
*
|
|
29
|
+
* 规则:
|
|
30
|
+
* 1. 排除 tabindex = -1(不可通过 Tab 聚焦)
|
|
31
|
+
* 2. 排除 disabled 元素
|
|
32
|
+
* 3. 排除哨兵节点(sentinel-start / sentinel-end)
|
|
33
|
+
* 4. 排除被 disabled 的 fieldset 包裹的元素
|
|
34
|
+
*/
|
|
35
|
+
function getAllTabObjects(scope) {
|
|
36
|
+
if (!scope) return [];
|
|
37
|
+
return Array.from(scope.querySelectorAll("a[href]:not([tabindex=\"-1\"]):not(.sentinel-start):not(.sentinel-end), button:not([tabindex=\"-1\"]):not([disabled]):not(.sentinel-start):not(.sentinel-end), input:not([tabindex=\"-1\"]):not([disabled]):not(.sentinel-start):not(.sentinel-end), select:not([tabindex=\"-1\"]):not([disabled]):not(.sentinel-start):not(.sentinel-end), textarea:not([tabindex=\"-1\"]):not([disabled]):not(.sentinel-start):not(.sentinel-end), [tabindex]:not([tabindex=\"-1\"]):not(.sentinel-start):not(.sentinel-end)")).filter((el) => {
|
|
38
|
+
/**
|
|
39
|
+
* 向上查找父级
|
|
40
|
+
* 如果在 disabled 的 fieldset 内,则不可聚焦
|
|
41
|
+
*/
|
|
42
|
+
let parent = el.parentElement;
|
|
43
|
+
while (parent) {
|
|
44
|
+
if (parent.tagName === "FIELDSET") {
|
|
45
|
+
if (parent.disabled) return false;
|
|
46
|
+
}
|
|
47
|
+
parent = parent.parentElement;
|
|
17
48
|
}
|
|
18
|
-
return
|
|
19
|
-
})
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
20
51
|
}
|
|
21
|
-
|
|
52
|
+
/**
|
|
53
|
+
* 聚焦第一个可聚焦元素
|
|
54
|
+
*/
|
|
55
|
+
function focusFirst() {
|
|
22
56
|
try {
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
57
|
+
if (wrapper.value) {
|
|
58
|
+
const objects = getAllTabObjects(wrapper.value);
|
|
59
|
+
if (objects.length === 0) return;
|
|
60
|
+
objects[0].focus();
|
|
27
61
|
}
|
|
28
|
-
} catch (
|
|
29
|
-
console.error("focusFirst error:",
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("focusFirst error:", error);
|
|
30
64
|
}
|
|
31
65
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
/**
|
|
67
|
+
* 聚焦最后一个可聚焦元素
|
|
68
|
+
*/
|
|
69
|
+
function focusLast() {
|
|
70
|
+
if (wrapper.value) {
|
|
71
|
+
const objects = getAllTabObjects(wrapper.value);
|
|
72
|
+
if (objects.length === 0) return;
|
|
73
|
+
objects[objects.length - 1].focus();
|
|
37
74
|
}
|
|
38
75
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
76
|
+
/**
|
|
77
|
+
* 当焦点进入“起始哨兵”时触发
|
|
78
|
+
*
|
|
79
|
+
* 关键逻辑:
|
|
80
|
+
* 1. 判断焦点来源(relatedTarget)
|
|
81
|
+
* 2. 区分是:
|
|
82
|
+
* - 从外部进入
|
|
83
|
+
* - 从内部 Shift+Tab 回退
|
|
84
|
+
*/
|
|
85
|
+
function onFocusStart(e) {
|
|
86
|
+
if (!wrapper.value) return;
|
|
87
|
+
const from = e.relatedTarget;
|
|
88
|
+
/**
|
|
89
|
+
* 情况1:没有来源(例如浏览器初始化 / JS focus)
|
|
90
|
+
* → 默认聚焦第一个
|
|
91
|
+
*/
|
|
92
|
+
if (!(from instanceof Node)) {
|
|
93
|
+
focusFirst();
|
|
44
94
|
return;
|
|
45
95
|
}
|
|
46
|
-
|
|
96
|
+
/**
|
|
97
|
+
* 情况2:从作用域外进入
|
|
98
|
+
* → 正常进入,聚焦第一个
|
|
99
|
+
*/
|
|
100
|
+
if (!wrapper.value.contains(from)) focusFirst();
|
|
101
|
+
else
|
|
102
|
+
/**
|
|
103
|
+
* 情况3:从作用域内部 Shift+Tab 回来
|
|
104
|
+
* → 说明用户想“往前”,应跳到最后一个(循环)
|
|
105
|
+
*/
|
|
106
|
+
focusLast();
|
|
47
107
|
}
|
|
48
|
-
|
|
49
|
-
|
|
108
|
+
/**
|
|
109
|
+
* 当焦点进入“结束哨兵”时触发
|
|
110
|
+
*
|
|
111
|
+
* 场景:
|
|
112
|
+
* 用户在最后一个元素按 Tab
|
|
113
|
+
*
|
|
114
|
+
* 行为:
|
|
115
|
+
* → 跳回第一个(形成循环)
|
|
116
|
+
*/
|
|
117
|
+
function onFocusEnd() {
|
|
118
|
+
focusFirst();
|
|
50
119
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
120
|
+
/**
|
|
121
|
+
* 监听 autofocus 属性的变化
|
|
122
|
+
*
|
|
123
|
+
* 作用:
|
|
124
|
+
* 1. 当 autofocus 从 false 变为 true 时,自动聚焦第一个可聚焦元素
|
|
125
|
+
* 2. 支持动态控制自动聚焦行为(例如在模态框打开时启用)
|
|
126
|
+
* 3. 该监听的主要作用是,当一个页面有多个作用域时,为了不让后挂载的元素抢占焦点,可以先把autofocus设置为false,在挂载后有自动聚焦需求,再设置为true。比如挂载后的第二作用域可以显示或隐藏。若不是该需求,挂载自动聚焦就足够了
|
|
127
|
+
*
|
|
128
|
+
* immediate: true 的意义:
|
|
129
|
+
* - 组件创建时立即执行一次
|
|
130
|
+
* - 如果初始 autofocus 为 true,会在挂载后立即聚焦第一个元素
|
|
131
|
+
* - 避免需要手动调用 focusFirst()
|
|
132
|
+
*
|
|
133
|
+
* 使用场景:
|
|
134
|
+
* - 模态框/对话框打开时自动聚焦第一个输入框
|
|
135
|
+
* - 表单显示时自动聚焦第一个字段
|
|
136
|
+
* - 动态切换焦点作用域时重新聚焦
|
|
137
|
+
*/
|
|
138
|
+
watch(() => props.autofocus, (newValue) => {
|
|
139
|
+
if (newValue === true) focusFirst();
|
|
140
|
+
}, { immediate: true });
|
|
141
|
+
return (_ctx, _cache) => {
|
|
142
|
+
return openBlock(), createElementBlock("div", {
|
|
143
|
+
ref_key: "wrapper",
|
|
144
|
+
ref: wrapper
|
|
145
|
+
}, [
|
|
146
|
+
createElementVNode("div", {
|
|
147
|
+
tabindex: "0",
|
|
148
|
+
class: "sentinel-start",
|
|
149
|
+
onFocus: onFocusStart
|
|
150
|
+
}, null, 32),
|
|
151
|
+
renderSlot(_ctx.$slots, "default"),
|
|
152
|
+
__props.tabMode === "soft-loop" ? (openBlock(), createElementBlock("div", _hoisted_1)) : createCommentVNode("", true),
|
|
153
|
+
createElementVNode("div", {
|
|
154
|
+
tabindex: "0",
|
|
155
|
+
class: "sentinel-end",
|
|
156
|
+
onFocus: onFocusEnd
|
|
157
|
+
}, null, 32)
|
|
158
|
+
], 512);
|
|
159
|
+
};
|
|
68
160
|
}
|
|
69
161
|
});
|
|
70
162
|
//#endregion
|
|
71
|
-
export {
|
|
163
|
+
export { FocusScope_default as FocusScope, FocusScope_default as default };
|