@zhencai/vue-focus-scope 1.0.9 → 1.0.11
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.
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let vue = require("vue");
|
|
3
|
+
//#region src/components/FocusScope.vue?vue&type=script&setup=true&lang.ts
|
|
4
|
+
var _hoisted_1 = {
|
|
5
|
+
key: 0,
|
|
6
|
+
tabindex: "0"
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/components/FocusScope.vue
|
|
10
|
+
var FocusScope_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
11
|
+
__name: "FocusScope",
|
|
12
|
+
props: {
|
|
13
|
+
tabMode: { default: "loop" },
|
|
14
|
+
autofocus: {
|
|
15
|
+
type: Boolean,
|
|
16
|
+
default: false
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
setup(__props) {
|
|
20
|
+
const props = __props;
|
|
21
|
+
/**
|
|
22
|
+
* wrapper 容器引用
|
|
23
|
+
* 用于限定焦点作用域(Focus Scope)
|
|
24
|
+
*/
|
|
25
|
+
const wrapper = (0, vue.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;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 聚焦第一个可聚焦元素
|
|
54
|
+
*/
|
|
55
|
+
function focusFirst() {
|
|
56
|
+
try {
|
|
57
|
+
if (wrapper.value) {
|
|
58
|
+
const objects = getAllTabObjects(wrapper.value);
|
|
59
|
+
if (objects.length === 0) return;
|
|
60
|
+
objects[0].focus();
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("focusFirst error:", error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
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();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
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();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 情况2:从作用域外进入
|
|
98
|
+
* → 正常进入,聚焦第一个
|
|
99
|
+
*/
|
|
100
|
+
if (!wrapper.value.contains(from)) focusFirst();
|
|
101
|
+
else
|
|
102
|
+
/**
|
|
103
|
+
* 情况3:从作用域内部 Shift+Tab 回来
|
|
104
|
+
* → 说明用户想“往前”,应跳到最后一个(循环)
|
|
105
|
+
*/
|
|
106
|
+
focusLast();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 当焦点进入“结束哨兵”时触发
|
|
110
|
+
*
|
|
111
|
+
* 场景:
|
|
112
|
+
* 用户在最后一个元素按 Tab
|
|
113
|
+
*
|
|
114
|
+
* 行为:
|
|
115
|
+
* → 跳回第一个(形成循环)
|
|
116
|
+
*/
|
|
117
|
+
function onFocusEnd() {
|
|
118
|
+
focusFirst();
|
|
119
|
+
}
|
|
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
|
+
(0, vue.watch)(() => props.autofocus, async (newValue) => {
|
|
139
|
+
if (newValue === true) {
|
|
140
|
+
await (0, vue.nextTick)();
|
|
141
|
+
focusFirst();
|
|
142
|
+
}
|
|
143
|
+
}, { immediate: true });
|
|
144
|
+
return (_ctx, _cache) => {
|
|
145
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
146
|
+
ref_key: "wrapper",
|
|
147
|
+
ref: wrapper
|
|
148
|
+
}, [
|
|
149
|
+
(0, vue.createElementVNode)("div", {
|
|
150
|
+
tabindex: "0",
|
|
151
|
+
class: "sentinel-start",
|
|
152
|
+
onFocus: onFocusStart
|
|
153
|
+
}, null, 32),
|
|
154
|
+
(0, vue.renderSlot)(_ctx.$slots, "default"),
|
|
155
|
+
__props.tabMode === "soft-loop" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1)) : (0, vue.createCommentVNode)("", true),
|
|
156
|
+
(0, vue.createElementVNode)("div", {
|
|
157
|
+
tabindex: "0",
|
|
158
|
+
class: "sentinel-end",
|
|
159
|
+
onFocus: onFocusEnd
|
|
160
|
+
}, null, 32)
|
|
161
|
+
], 512);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
//#endregion
|
|
166
|
+
exports.FocusScope = FocusScope_default;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* My Library v1.0.0 */
|
|
2
1
|
import { createCommentVNode, createElementBlock, createElementVNode, defineComponent, nextTick, openBlock, renderSlot, useTemplateRef, watch } from "vue";
|
|
3
2
|
//#region src/components/FocusScope.vue?vue&type=script&setup=true&lang.ts
|
|
4
3
|
var _hoisted_1 = {
|
|
@@ -163,4 +162,4 @@ var FocusScope_default = /* @__PURE__ */ defineComponent({
|
|
|
163
162
|
}
|
|
164
163
|
});
|
|
165
164
|
//#endregion
|
|
166
|
-
export { FocusScope_default as FocusScope
|
|
165
|
+
export { FocusScope_default as FocusScope };
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhencai/vue-focus-scope",
|
|
3
|
-
"module": "FocusScope.
|
|
4
|
-
"
|
|
3
|
+
"module": "dist/FocusScope.mjs",
|
|
4
|
+
"main": "dist/FocusScope.cjs",
|
|
5
|
+
"version": "1.0.11",
|
|
5
6
|
"peerDependencies": {
|
|
6
7
|
"vue": "^3.0.0"
|
|
7
8
|
},
|
|
8
9
|
"files": [
|
|
9
10
|
"dist",
|
|
11
|
+
"index.d.ts",
|
|
10
12
|
"README.md",
|
|
11
13
|
"LICENSE"
|
|
12
14
|
],
|
|
15
|
+
"types": "index.d.ts",
|
|
13
16
|
"license": "MIT"
|
|
14
17
|
}
|
|
File without changes
|