@manohub/ui 0.10.2 → 0.10.4
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/README.md +1 -1
- package/dist/components/badge/index.d.ts +46 -1
- package/dist/components/badge/index.js +42 -9
- package/dist/components/card/index.d.ts +1 -1
- package/dist/components/drawer/index.d.ts +1 -1
- package/dist/components/filter/index.d.ts +74 -6
- package/dist/components/filter/index.js +38 -10
- package/dist/components/form/index.d.ts +50 -1
- package/dist/components/form/index.js +20 -2
- package/dist/components/input/index.js +1 -0
- package/dist/components/nav/index.d.ts +27 -0
- package/dist/components/nav/index.js +12 -0
- package/dist/components/notice/index.d.ts +1 -1
- package/dist/components/notification/index.d.ts +2 -2
- package/dist/components/select/index.d.ts +9 -0
- package/dist/components/select/index.js +57 -12
- package/dist/components/select/select-list.d.ts +42 -8
- package/dist/components/select/select-list.js +36 -11
- package/dist/components/select/select-panel.d.ts +36 -8
- package/dist/components/select/select-panel.js +18 -4
- package/dist/components/select/select-shared.d.ts +51 -10
- package/dist/components/select/select-shared.js +12 -2
- package/dist/components/select/select-tree.d.ts +25 -3
- package/dist/components/select/select-tree.js +17 -6
- package/dist/components/select/select-trigger.d.ts +21 -0
- package/dist/components/select/select-trigger.js +15 -1
- package/dist/components/tabs/index.d.ts +22 -0
- package/dist/components/tabs/index.js +6 -0
- package/dist/components/tag/index.d.ts +32 -153
- package/dist/components/tag/index.js +102 -101
- package/dist/components/text/index.d.ts +1 -1
- package/dist/components/tooltip/index.d.ts +1 -1
- package/dist/components/tree/index.d.ts +65 -0
- package/dist/components/tree/index.js +29 -2
- package/dist/index.d.ts +2 -2
- package/dist/styles/components/avatar.css +2 -2
- package/dist/styles/components/badge.css +21 -1
- package/dist/styles/components/badge.tokens.css +24 -0
- package/dist/styles/components/button.css +7 -6
- package/dist/styles/components/button.tokens.css +5 -2
- package/dist/styles/components/capsule.css +8 -0
- package/dist/styles/components/divider.css +8 -4
- package/dist/styles/components/divider.tokens.css +2 -1
- package/dist/styles/components/form.css +51 -0
- package/dist/styles/components/notification.css +1 -1
- package/dist/styles/components/select.css +62 -39
- package/dist/styles/components/select.tokens.css +15 -8
- package/dist/styles/components/tag.css +77 -154
- package/dist/styles/components/tag.tokens.css +13 -22
- package/dist/styles/components/tree.css +37 -3
- package/dist/styles/components/tree.tokens.css +4 -3
- package/dist/styles/components/upload.tokens.css +1 -1
- package/dist/styles/index.css +2 -1
- package/package.json +2 -2
|
@@ -58,6 +58,16 @@ const Nav = /* @__PURE__ */ defineComponent({
|
|
|
58
58
|
type: Function,
|
|
59
59
|
default: void 0
|
|
60
60
|
},
|
|
61
|
+
/** 导航项图标(标签前的一位图标):见 `Tree.renderIcon`(侧栏图标走这一位,不要塞进 `renderLabel`) */
|
|
62
|
+
renderIcon: {
|
|
63
|
+
type: Function,
|
|
64
|
+
default: void 0
|
|
65
|
+
},
|
|
66
|
+
/** 展开 / 收起箭头字形:见 `Tree.renderCaret`(`list` 态箭头位被隐藏,传了也不显形) */
|
|
67
|
+
renderCaret: {
|
|
68
|
+
type: Function,
|
|
69
|
+
default: void 0
|
|
70
|
+
},
|
|
61
71
|
/** 侧栏标题(可选;不传则只有导航体) */
|
|
62
72
|
title: {
|
|
63
73
|
type: String,
|
|
@@ -146,6 +156,8 @@ const Nav = /* @__PURE__ */ defineComponent({
|
|
|
146
156
|
"defaultExpandedKeys": props.defaultExpandedKeys,
|
|
147
157
|
"onExpandChange": props.onExpandChange,
|
|
148
158
|
"renderActions": props.renderActions,
|
|
159
|
+
"renderIcon": props.renderIcon,
|
|
160
|
+
"renderCaret": props.renderCaret,
|
|
149
161
|
"size": props.size,
|
|
150
162
|
"loading": props.loading,
|
|
151
163
|
"empty": props.empty,
|
|
@@ -143,8 +143,8 @@ export declare const Notice: import("vue").DefineComponent<import("vue").Extract
|
|
|
143
143
|
tone: NoticeTone;
|
|
144
144
|
description: string;
|
|
145
145
|
action: string;
|
|
146
|
-
closeText: string;
|
|
147
146
|
onAction: () => void;
|
|
147
|
+
closeText: string;
|
|
148
148
|
}, SlotsType<{
|
|
149
149
|
default?: () => VNodeChild;
|
|
150
150
|
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -16,7 +16,7 @@ export type NotificationTone = 'info' | 'success' | 'warning' | 'error' | 'submi
|
|
|
16
16
|
*
|
|
17
17
|
* - 图标:`icon` 布尔(按 `tone` 映射)或直接给图标名;
|
|
18
18
|
* - 头像:给 `avatar` 插槽 → 原样渲染(推荐塞 `<Avatar>`);给 `avatar` 字符串 → 渲染字标兜底盒;
|
|
19
|
-
* - 标签:`tags`
|
|
19
|
+
* - 标签:`tags` 插槽里放标记族(只读 `<Badge shape="soft" tone="error">`,要可关闭用 `<Tag>`);
|
|
20
20
|
* - 操作:`actions` 插槽里放按钮族的 `<Button size="sm" variant="primary">`。
|
|
21
21
|
*
|
|
22
22
|
* 关闭是**受控**的:本件只回调 `onClose`,不自己隐藏(可见性归消费方,避免「点了没反应」与
|
|
@@ -27,7 +27,7 @@ export type NotificationTone = 'info' | 'success' | 'warning' | 'error' | 'submi
|
|
|
27
27
|
*
|
|
28
28
|
* @example 图标 + 自动消失 + 头像 + 标签
|
|
29
29
|
* <Notification icon duration={4000} tone="info" title="审批通知" avatar="张" @close="dismiss">
|
|
30
|
-
* <
|
|
30
|
+
* <Badge shape="soft" tone="error">逾期</Badge>
|
|
31
31
|
* {{ actions: () => <Button size="sm" variant="primary">去处理</Button> }}
|
|
32
32
|
* </Notification>
|
|
33
33
|
*/
|
|
@@ -35,6 +35,15 @@
|
|
|
35
35
|
* **面板有高度上限**:`--ui-select-panel-max-height`(默认 320px,钉在 `select.css` 的面板根上)——
|
|
36
36
|
* 超出即面板内容区(`.mh-select__body`)滚动、搜索行常驻;列表与 `SelectTree` 两形态共用这一份。
|
|
37
37
|
*
|
|
38
|
+
* ## 键盘与高亮(焦点**留在触发器上**)
|
|
39
|
+
*
|
|
40
|
+
* 面板里的选项是 `div[role='option']`、**不是 button**、也不可聚焦:高亮位由本件持有
|
|
41
|
+
* (可激活项里的格号),列表体据此下发 `data-active`,触发器上的 `aria-activedescendant`
|
|
42
|
+
* 指到那一行的 id —— 原生 popover 下的「虚拟焦点」标准表达。两条入口共用同一份模型:
|
|
43
|
+
* 触发器(`↑` / `↓` 开面板并平移、`Enter` / `Space` 提交高亮项)与面板内的搜索行
|
|
44
|
+
* (`↑` / `↓` / `Enter` 冒泡到面板)。可激活项 = 摊平 + 跳过禁用项 + (可创建时)末尾那一格
|
|
45
|
+
* 「创建 xxx」行,顺序由 `activatableOptions()` 一处给。
|
|
46
|
+
*
|
|
38
47
|
* ## 状态三源等价
|
|
39
48
|
*
|
|
40
49
|
* 选中行 = `[data-state='selected']`(组件下发)/ 原生 `input:checked`(静态页零 JS 路径)/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { defineComponent, ref, computed, createVNode, Fragment } from "vue";
|
|
1
|
+
import { defineComponent, ref, computed, watch, createVNode, Fragment } from "vue";
|
|
2
2
|
import { SelectList } from "./select-list.js";
|
|
3
3
|
import { SelectPanel } from "./select-panel.js";
|
|
4
4
|
import { SelectTrigger } from "./select-trigger.js";
|
|
5
|
-
import { createSelectPanelId, isGroup, createTriggerKeydown, showPanel, hidePanel } from "./select-shared.js";
|
|
5
|
+
import { createSelectPanelId, isGroup, activatableOptions, activatableCount, createTriggerKeydown, optionDomId, showPanel, hidePanel } from "./select-shared.js";
|
|
6
6
|
const Select = /* @__PURE__ */ defineComponent({
|
|
7
7
|
name: "Select",
|
|
8
8
|
inheritAttrs: false,
|
|
@@ -167,27 +167,68 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
167
167
|
keyword.value = "";
|
|
168
168
|
};
|
|
169
169
|
const clear = () => commit(props.multiple ? [] : "");
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
const activatable = computed(() => activatableOptions(visibleOptions.value));
|
|
171
|
+
const slotCount = computed(() => activatableCount(visibleOptions.value, creatable.value));
|
|
172
|
+
const activeIndex = ref(void 0);
|
|
173
|
+
const activeEl = () => panelRef.value?.querySelector('[data-active="true"]') ?? null;
|
|
174
|
+
const scrollActiveIntoView = () => {
|
|
175
|
+
requestAnimationFrame(() => activeEl()?.scrollIntoView?.({
|
|
176
|
+
block: "nearest"
|
|
177
|
+
}));
|
|
178
|
+
};
|
|
179
|
+
const moveActive = (direction) => {
|
|
180
|
+
const count = slotCount.value;
|
|
181
|
+
if (!count) return;
|
|
182
|
+
const at = activeIndex.value ?? -1;
|
|
183
|
+
activeIndex.value = at < 0 ? direction > 0 ? 0 : count - 1 : (at + direction + count) % count;
|
|
184
|
+
scrollActiveIntoView();
|
|
185
|
+
};
|
|
186
|
+
const commitActive = () => {
|
|
187
|
+
const slot = activeIndex.value;
|
|
188
|
+
if (slot === void 0) return false;
|
|
189
|
+
if (creatable.value && slot === activatable.value.length) {
|
|
190
|
+
createFromKeyword();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
const option = activatable.value[slot];
|
|
194
|
+
if (!option) return false;
|
|
195
|
+
pick(option);
|
|
196
|
+
if (!props.multiple) activeIndex.value = void 0;
|
|
197
|
+
return true;
|
|
198
|
+
};
|
|
199
|
+
watch(keyword, () => {
|
|
200
|
+
activeIndex.value = void 0;
|
|
201
|
+
});
|
|
202
|
+
const panelOpen = ref(false);
|
|
203
|
+
const isPanelOpen = () => panelOpen.value;
|
|
204
|
+
const openPanel = () => {
|
|
205
|
+
if (!panelOpen.value) activeIndex.value = void 0;
|
|
206
|
+
panelOpen.value = true;
|
|
207
|
+
showPanel(panelRef.value);
|
|
208
|
+
};
|
|
209
|
+
const onPanelToggle = (event) => {
|
|
210
|
+
if (event.newState === "open") return;
|
|
211
|
+
panelOpen.value = false;
|
|
212
|
+
activeIndex.value = void 0;
|
|
177
213
|
};
|
|
178
214
|
const onTriggerKeydown = createTriggerKeydown({
|
|
179
215
|
clearable: props.clearable,
|
|
180
216
|
disabled: props.disabled,
|
|
181
217
|
clear,
|
|
182
|
-
openPanel
|
|
183
|
-
|
|
218
|
+
openPanel,
|
|
219
|
+
isPanelOpen,
|
|
220
|
+
moveActive,
|
|
221
|
+
commitActive
|
|
184
222
|
});
|
|
185
223
|
const onPanelKeydown = (event) => {
|
|
186
224
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
187
225
|
event.preventDefault();
|
|
188
|
-
|
|
226
|
+
moveActive(event.key === "ArrowDown" ? 1 : -1);
|
|
227
|
+
return;
|
|
189
228
|
}
|
|
229
|
+
if (event.key === "Enter" && commitActive()) event.preventDefault();
|
|
190
230
|
};
|
|
231
|
+
const activeDescendant = computed(() => activeIndex.value === void 0 ? void 0 : optionDomId(panelId, activeIndex.value));
|
|
191
232
|
return () => {
|
|
192
233
|
const hasValue = selectedValues.value.length > 0;
|
|
193
234
|
return createVNode("div", {
|
|
@@ -203,6 +244,7 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
203
244
|
"multiple": props.multiple,
|
|
204
245
|
"placeholder": props.placeholder,
|
|
205
246
|
"label": selectedOptions.value[0]?.label,
|
|
247
|
+
"activeDescendant": activeDescendant.value,
|
|
206
248
|
"onKeydown": onTriggerKeydown,
|
|
207
249
|
"onClear": clear,
|
|
208
250
|
"renderValue": props.multiple ? () => createVNode(Fragment, null, [visibleTags.value.map((option) => createVNode("span", {
|
|
@@ -215,6 +257,7 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
215
257
|
"id": panelId,
|
|
216
258
|
"elementRef": panelRef,
|
|
217
259
|
"searchable": props.searchable,
|
|
260
|
+
"onToggle": onPanelToggle,
|
|
218
261
|
"keyword": keyword.value,
|
|
219
262
|
"onKeyword": (value) => {
|
|
220
263
|
keyword.value = value;
|
|
@@ -225,6 +268,8 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
225
268
|
default: () => createVNode(SelectList, {
|
|
226
269
|
"options": visibleOptions.value,
|
|
227
270
|
"selectedValues": selectedValues.value,
|
|
271
|
+
"activeIndex": activeIndex.value,
|
|
272
|
+
"idBase": panelId,
|
|
228
273
|
"loading": props.loading,
|
|
229
274
|
"emptyText": props.emptyText,
|
|
230
275
|
"creatable": creatable.value,
|
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 它拥有「面板里那一撮选项」的全部形态与状态行:平铺选项 / 分组(组标题不可点)/ 「创建」行 /
|
|
5
5
|
* 空态行 / 加载行。**不含面板壳与搜索行**(那是 `SelectPanel` 的事),也不持有任何状态 ——
|
|
6
|
-
*
|
|
7
|
-
* 行由面板壳装在**内容区**(`.mh-select__body
|
|
8
|
-
*
|
|
6
|
+
* 过滤结果、选中值、高亮格号、是否可创建统统由消费方算好喂进来,本件只渲染。
|
|
7
|
+
* 行由面板壳装在**内容区**(`.mh-select__body`,面板里唯一的滚动容器)里:限高、滚动与内容内距
|
|
8
|
+
* 都归那一层(与树形态共用一份),本件不写第二个滚动容器、也不自己包一层给内距。
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* ## 选项是 `div[role='option']`,**不是 button**
|
|
11
|
+
*
|
|
12
|
+
* listbox 的 option 不是交互控件:真实按钮会带来第二套语义(可激活、可聚焦、`:focus-visible`
|
|
13
|
+
* 的 UA 描边)与四件套 UA 外观重置(见 `select.css` 的选项行一节 —— 那几条已随本改动退场)。
|
|
14
|
+
* 键盘因此换一套模型:**焦点留在触发器上**,高亮由 `data-active` 表达,并由
|
|
15
|
+
* `aria-activedescendant` 指到这一行的 `id`(`optionDomId()`,格号 = 可激活项里的顺序)。
|
|
16
|
+
* 点击仍是真实 `onClick`(`div` 一样收得到),禁用项靠 `aria-disabled` + 消费方的 `pick()` 兜住
|
|
17
|
+
* —— 没有 `disabled` 属性可用,这一点在 `Select` 的选中逻辑里成对处理。
|
|
18
|
+
*
|
|
19
|
+
* 空 / 加载行仍是 `span`(不进键盘顺序:`role` 不给,消费方的条数只数 `role='option'`)。
|
|
15
20
|
*
|
|
16
21
|
* 选中态**三源等价**(面层只写一份声明):组件下发 `[data-state='selected']` /
|
|
17
22
|
* 原生 `:has(input:checked)`(静态页零 JS 单选路径)/ `.is-selected`(规格冻结档)。
|
|
@@ -29,6 +34,20 @@ export declare const SelectList: import("vue").DefineComponent<import("vue").Ext
|
|
|
29
34
|
type: PropType<Array<string | number>>;
|
|
30
35
|
default: () => never[];
|
|
31
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* 键盘高亮位(可激活项里的格号;`undefined` = 没有高亮)。
|
|
39
|
+
*
|
|
40
|
+
* 格号由 `activatableOptions()` 的顺序给,「创建」行占末尾那一格(= 可激活项总数)。
|
|
41
|
+
*/
|
|
42
|
+
activeIndex: {
|
|
43
|
+
type: NumberConstructor;
|
|
44
|
+
default: undefined;
|
|
45
|
+
};
|
|
46
|
+
/** 面板 id:选项行的 `id` 由它 + 格号拼出(`aria-activedescendant` 要指过来) */
|
|
47
|
+
idBase: {
|
|
48
|
+
type: StringConstructor;
|
|
49
|
+
required: true;
|
|
50
|
+
};
|
|
32
51
|
loading: {
|
|
33
52
|
type: BooleanConstructor;
|
|
34
53
|
default: boolean;
|
|
@@ -78,6 +97,20 @@ export declare const SelectList: import("vue").DefineComponent<import("vue").Ext
|
|
|
78
97
|
type: PropType<Array<string | number>>;
|
|
79
98
|
default: () => never[];
|
|
80
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* 键盘高亮位(可激活项里的格号;`undefined` = 没有高亮)。
|
|
102
|
+
*
|
|
103
|
+
* 格号由 `activatableOptions()` 的顺序给,「创建」行占末尾那一格(= 可激活项总数)。
|
|
104
|
+
*/
|
|
105
|
+
activeIndex: {
|
|
106
|
+
type: NumberConstructor;
|
|
107
|
+
default: undefined;
|
|
108
|
+
};
|
|
109
|
+
/** 面板 id:选项行的 `id` 由它 + 格号拼出(`aria-activedescendant` 要指过来) */
|
|
110
|
+
idBase: {
|
|
111
|
+
type: StringConstructor;
|
|
112
|
+
required: true;
|
|
113
|
+
};
|
|
81
114
|
loading: {
|
|
82
115
|
type: BooleanConstructor;
|
|
83
116
|
default: boolean;
|
|
@@ -118,6 +151,7 @@ export declare const SelectList: import("vue").DefineComponent<import("vue").Ext
|
|
|
118
151
|
loading: boolean;
|
|
119
152
|
options: SelectItem[];
|
|
120
153
|
selectedValues: (string | number)[];
|
|
154
|
+
activeIndex: number;
|
|
121
155
|
emptyText: string;
|
|
122
156
|
creatable: boolean;
|
|
123
157
|
keyword: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { defineComponent, createVNode, createTextVNode, Fragment } from "vue";
|
|
2
|
-
import { isGroup } from "./select-shared.js";
|
|
1
|
+
import { defineComponent, computed, createVNode, createTextVNode, Fragment } from "vue";
|
|
2
|
+
import { activatableOptions, isGroup, optionDomId } from "./select-shared.js";
|
|
3
3
|
const SelectList = /* @__PURE__ */ defineComponent({
|
|
4
4
|
name: "SelectList",
|
|
5
5
|
inheritAttrs: false,
|
|
@@ -14,6 +14,20 @@ const SelectList = /* @__PURE__ */ defineComponent({
|
|
|
14
14
|
type: Array,
|
|
15
15
|
default: () => []
|
|
16
16
|
},
|
|
17
|
+
/**
|
|
18
|
+
* 键盘高亮位(可激活项里的格号;`undefined` = 没有高亮)。
|
|
19
|
+
*
|
|
20
|
+
* 格号由 `activatableOptions()` 的顺序给,「创建」行占末尾那一格(= 可激活项总数)。
|
|
21
|
+
*/
|
|
22
|
+
activeIndex: {
|
|
23
|
+
type: Number,
|
|
24
|
+
default: void 0
|
|
25
|
+
},
|
|
26
|
+
/** 面板 id:选项行的 `id` 由它 + 格号拼出(`aria-activedescendant` 要指过来) */
|
|
27
|
+
idBase: {
|
|
28
|
+
type: String,
|
|
29
|
+
required: true
|
|
30
|
+
},
|
|
17
31
|
loading: {
|
|
18
32
|
type: Boolean,
|
|
19
33
|
default: false
|
|
@@ -52,16 +66,19 @@ const SelectList = /* @__PURE__ */ defineComponent({
|
|
|
52
66
|
}
|
|
53
67
|
},
|
|
54
68
|
setup(props) {
|
|
69
|
+
const activatable = computed(() => activatableOptions(props.options));
|
|
55
70
|
const renderOption = (option) => {
|
|
56
71
|
const selected = props.selectedValues.includes(option.value);
|
|
57
|
-
|
|
72
|
+
const slot = activatable.value.indexOf(option);
|
|
73
|
+
return createVNode("div", {
|
|
58
74
|
"key": String(option.value),
|
|
59
|
-
"
|
|
75
|
+
"id": optionDomId(props.idBase, slot),
|
|
60
76
|
"class": ["mh-select__option", option.disabled ? "is-disabled" : ""].filter(Boolean).join(" "),
|
|
61
|
-
"data-state": selected ? "selected" : "unselected",
|
|
62
|
-
"disabled": option.disabled,
|
|
63
77
|
"role": "option",
|
|
78
|
+
"data-state": selected ? "selected" : "unselected",
|
|
79
|
+
"data-active": props.activeIndex === slot ? "true" : void 0,
|
|
64
80
|
"aria-selected": selected ? "true" : "false",
|
|
81
|
+
"aria-disabled": option.disabled ? "true" : void 0,
|
|
65
82
|
"onClick": () => props.onPick?.(option)
|
|
66
83
|
}, [option.label]);
|
|
67
84
|
};
|
|
@@ -82,11 +99,19 @@ const SelectList = /* @__PURE__ */ defineComponent({
|
|
|
82
99
|
"class": "mh-select__group"
|
|
83
100
|
}, [createVNode("span", {
|
|
84
101
|
"class": "mh-select__group-label"
|
|
85
|
-
}, [item.label]), item.options.map((option) => renderOption(option))]) : renderOption(item)), props.creatable ?
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"
|
|
89
|
-
|
|
102
|
+
}, [item.label]), item.options.map((option) => renderOption(option))]) : renderOption(item)), props.creatable ? (
|
|
103
|
+
/* 「创建」行也占一个可激活格(末尾那格):箭头键能走到它、回车即创建 ——
|
|
104
|
+
它同样写 `.mh-select__option` 基类(行几何与三态全沿用),`.mh-select__add` 只改字色 */
|
|
105
|
+
createVNode("div", {
|
|
106
|
+
"id": optionDomId(props.idBase, activatable.value.length),
|
|
107
|
+
"class": "mh-select__option mh-select__add",
|
|
108
|
+
"role": "option",
|
|
109
|
+
"data-state": "unselected",
|
|
110
|
+
"data-active": props.activeIndex === activatable.value.length ? "true" : void 0,
|
|
111
|
+
"aria-selected": "false",
|
|
112
|
+
"onClick": () => props.onCreate?.()
|
|
113
|
+
}, [createTextVNode("创建「"), props.keyword.trim(), createTextVNode("」")])
|
|
114
|
+
) : null]);
|
|
90
115
|
};
|
|
91
116
|
}
|
|
92
117
|
});
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* SelectPanel —— 选择器族的**面板壳内件**(`Select` 与 `SelectTree` 共用,不对外导出)。
|
|
3
3
|
*
|
|
4
4
|
* 它只拥有四样东西:**原生 popover 元素**(开合 / Esc / 点外部关闭 / 顶层渲染 / 定位全归浏览器)、
|
|
5
|
-
* **搜索行**(面板顶部那一个输入框)、**内容区**(`.mh-select__body
|
|
5
|
+
* **搜索行**(面板顶部那一个输入框)、**内容区**(`.mh-select__body`,面板里唯一的滚动容器,
|
|
6
|
+
* 也是**两形态共用的一处内容内距** —— 列表的选项与树的树都直接装在它里面,形态里不再各包一层给内距)
|
|
6
7
|
* 与**内容槽**(列表体或树体)。
|
|
7
8
|
* 「面板里装什么」不在本件 —— 这正是让列表与树共用同一份面板皮肤的原因。
|
|
8
9
|
*
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
* 锚是挂在**表壳**上的 `.mh-popup-anchor`(触发器进壳后隐式锚会缩成壳内控件,故走显式锚档)。
|
|
18
19
|
*
|
|
19
20
|
* `elementRef` 是**由消费方持有的 ref**(不是本件自己的):开面板(触发器 ↑↓)、收起面板(选中后)、
|
|
20
|
-
*
|
|
21
|
+
* 以及列表形态按 `[data-active]` 取高亮行(滚动入视)都要在父层拿到这个 DOM 元素。
|
|
21
22
|
*/
|
|
22
23
|
import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
23
24
|
import type { ElementRef } from './select-shared';
|
|
@@ -51,15 +52,28 @@ export declare const SelectPanel: import("vue").DefineComponent<import("vue").Ex
|
|
|
51
52
|
default: undefined;
|
|
52
53
|
};
|
|
53
54
|
/**
|
|
54
|
-
*
|
|
55
|
+
* 面板上的键盘契约(列表形态给,树形态不给)。
|
|
55
56
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
57
|
+
* 列表形态接 `↑` / `↓`(平移高亮)与 `Enter`(提交高亮项)。这些事件**来自面板内的搜索行**
|
|
58
|
+
* —— 选项是 `div[role='option']`、不可聚焦,焦点留在触发器上(触发器那条路走
|
|
59
|
+
* `createTriggerKeydown()`,两条路共用同一份高亮模型)。树形态不给:树的行不做键盘导航
|
|
60
|
+
* (与 `Tree` 自身同口径)。
|
|
58
61
|
*/
|
|
59
62
|
onKeydown: {
|
|
60
63
|
type: PropType<(event: KeyboardEvent) => void>;
|
|
61
64
|
default: undefined;
|
|
62
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* 面板的原生 `toggle` 事件(开合态变化,含 Esc 与点外部关闭)。
|
|
68
|
+
*
|
|
69
|
+
* 声明成本件的 prop 而不是靠 `attrs`:本件的 `attrs` 是关掉的(`inheritAttrs: false`),
|
|
70
|
+
* 且这个事件要落到**面板元素**上(`toggle` 只有它派发)。消费方据此回写「面板开着吗」
|
|
71
|
+
* —— 那是 `Enter` 接管的判定依据(见 `Select` 的 `isPanelOpen`)。
|
|
72
|
+
*/
|
|
73
|
+
onToggle: {
|
|
74
|
+
type: PropType<(event: Event) => void>;
|
|
75
|
+
default: undefined;
|
|
76
|
+
};
|
|
63
77
|
}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
64
78
|
/** 面板 id(必须与触发器的 `popovertarget` 成对) */
|
|
65
79
|
id: {
|
|
@@ -90,17 +104,31 @@ export declare const SelectPanel: import("vue").DefineComponent<import("vue").Ex
|
|
|
90
104
|
default: undefined;
|
|
91
105
|
};
|
|
92
106
|
/**
|
|
93
|
-
*
|
|
107
|
+
* 面板上的键盘契约(列表形态给,树形态不给)。
|
|
94
108
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
109
|
+
* 列表形态接 `↑` / `↓`(平移高亮)与 `Enter`(提交高亮项)。这些事件**来自面板内的搜索行**
|
|
110
|
+
* —— 选项是 `div[role='option']`、不可聚焦,焦点留在触发器上(触发器那条路走
|
|
111
|
+
* `createTriggerKeydown()`,两条路共用同一份高亮模型)。树形态不给:树的行不做键盘导航
|
|
112
|
+
* (与 `Tree` 自身同口径)。
|
|
97
113
|
*/
|
|
98
114
|
onKeydown: {
|
|
99
115
|
type: PropType<(event: KeyboardEvent) => void>;
|
|
100
116
|
default: undefined;
|
|
101
117
|
};
|
|
118
|
+
/**
|
|
119
|
+
* 面板的原生 `toggle` 事件(开合态变化,含 Esc 与点外部关闭)。
|
|
120
|
+
*
|
|
121
|
+
* 声明成本件的 prop 而不是靠 `attrs`:本件的 `attrs` 是关掉的(`inheritAttrs: false`),
|
|
122
|
+
* 且这个事件要落到**面板元素**上(`toggle` 只有它派发)。消费方据此回写「面板开着吗」
|
|
123
|
+
* —— 那是 `Enter` 接管的判定依据(见 `Select` 的 `isPanelOpen`)。
|
|
124
|
+
*/
|
|
125
|
+
onToggle: {
|
|
126
|
+
type: PropType<(event: Event) => void>;
|
|
127
|
+
default: undefined;
|
|
128
|
+
};
|
|
102
129
|
}>> & Readonly<{}>, {
|
|
103
130
|
onKeydown: (event: KeyboardEvent) => void;
|
|
131
|
+
onToggle: (event: Event) => void;
|
|
104
132
|
keyword: string;
|
|
105
133
|
elementRef: ElementRef;
|
|
106
134
|
searchable: boolean;
|
|
@@ -32,14 +32,27 @@ const SelectPanel = /* @__PURE__ */ defineComponent({
|
|
|
32
32
|
default: void 0
|
|
33
33
|
},
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* 面板上的键盘契约(列表形态给,树形态不给)。
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* 列表形态接 `↑` / `↓`(平移高亮)与 `Enter`(提交高亮项)。这些事件**来自面板内的搜索行**
|
|
38
|
+
* —— 选项是 `div[role='option']`、不可聚焦,焦点留在触发器上(触发器那条路走
|
|
39
|
+
* `createTriggerKeydown()`,两条路共用同一份高亮模型)。树形态不给:树的行不做键盘导航
|
|
40
|
+
* (与 `Tree` 自身同口径)。
|
|
39
41
|
*/
|
|
40
42
|
onKeydown: {
|
|
41
43
|
type: Function,
|
|
42
44
|
default: void 0
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* 面板的原生 `toggle` 事件(开合态变化,含 Esc 与点外部关闭)。
|
|
48
|
+
*
|
|
49
|
+
* 声明成本件的 prop 而不是靠 `attrs`:本件的 `attrs` 是关掉的(`inheritAttrs: false`),
|
|
50
|
+
* 且这个事件要落到**面板元素**上(`toggle` 只有它派发)。消费方据此回写「面板开着吗」
|
|
51
|
+
* —— 那是 `Enter` 接管的判定依据(见 `Select` 的 `isPanelOpen`)。
|
|
52
|
+
*/
|
|
53
|
+
onToggle: {
|
|
54
|
+
type: Function,
|
|
55
|
+
default: void 0
|
|
43
56
|
}
|
|
44
57
|
},
|
|
45
58
|
slots: Object,
|
|
@@ -56,7 +69,8 @@ const SelectPanel = /* @__PURE__ */ defineComponent({
|
|
|
56
69
|
"data-side": "bottom",
|
|
57
70
|
"data-align": "start",
|
|
58
71
|
"role": "listbox",
|
|
59
|
-
"onKeydown": props.onKeydown
|
|
72
|
+
"onKeydown": props.onKeydown,
|
|
73
|
+
"onToggle": props.onToggle
|
|
60
74
|
}), [props.searchable ? createVNode("div", {
|
|
61
75
|
"class": "mh-select__search"
|
|
62
76
|
}, [createVNode("input", {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 选择器族的**共用原子**(类型、面板 id、popover
|
|
2
|
+
* 选择器族的**共用原子**(类型、面板 id、popover 开合、触发器键盘契约、键位模型的可激活项摊平)。
|
|
3
3
|
*
|
|
4
4
|
* 这是 `select/` 目录里的**叶子模块**:它不 import 同目录任何其它文件,因此触发器 / 面板壳 /
|
|
5
5
|
* 列表体 / 树体四件可以自由地互相组合而不产生环。内件一律**不对外导出**(`src/index.ts` 只出
|
|
6
6
|
* `Select` 与 `SelectTree`),故本文件的导出面是「族内契约」,改它要同时看那四件。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 为什么单独一份而不是各自写一遍:这些都是**跨两件以上**的东西 ——
|
|
9
9
|
* 类型(列表体与树体都要)、面板 id 生成器(两件的面板都得全页唯一)、
|
|
10
|
-
* popover
|
|
10
|
+
* popover 开合(两件都要收起面板)、触发器键盘契约(两件的触发器是同一个形态)、
|
|
11
|
+
* 可激活项的摊平顺序(`Select` 算下一个高亮位、`SelectList` 算行 id 与高亮态,必须同一份)。
|
|
11
12
|
*/
|
|
12
13
|
import type { Ref } from 'vue';
|
|
13
14
|
/** 选项(平铺项与分组内选项同构) */
|
|
@@ -27,6 +28,29 @@ export type SelectItem = SelectOption | SelectOptionGroup;
|
|
|
27
28
|
export type SelectValue = string | number | Array<string | number>;
|
|
28
29
|
/** 判别:带 `options` 数组的是分组 */
|
|
29
30
|
export declare const isGroup: (item: SelectItem) => item is SelectOptionGroup;
|
|
31
|
+
/**
|
|
32
|
+
* **键盘可激活项**:摊平成一行行(分组只影响渲染、不影响键盘顺序)并跳过禁用项。
|
|
33
|
+
*
|
|
34
|
+
* 摊平这件事**只在键位模型里**需要(`Select` 算下一个高亮位、`SelectList` 算「这一行是第几格」),
|
|
35
|
+
* 两处都调本函数 ⇒ 顺序只有一个来源,不会各自摊一遍。返回的是**原对象引用**
|
|
36
|
+
* (选项对象来自消费方传入的同一批数据),故列表侧可以直接用 `indexOf` 反查格号。
|
|
37
|
+
*/
|
|
38
|
+
export declare const activatableOptions: (items: SelectItem[]) => SelectOption[];
|
|
39
|
+
/**
|
|
40
|
+
* 可激活格总数。
|
|
41
|
+
*
|
|
42
|
+
* `creatable` 时**末尾多一格**「创建 xxx」行 —— 它也是可激活的一格(回车即创建),
|
|
43
|
+
* 键位模型与 id 生成都按「选项格 + 尾格」这同一条约定走。
|
|
44
|
+
*/
|
|
45
|
+
export declare const activatableCount: (items: SelectItem[], creatable: boolean) => number;
|
|
46
|
+
/**
|
|
47
|
+
* 选项行 / 「创建」行的元素 id。
|
|
48
|
+
*
|
|
49
|
+
* 两处用途:`aria-activedescendant` 指过来(原生 popover 下的虚拟焦点,见 `Select`),
|
|
50
|
+
* 以及消费方与用例的稳定锚点。格号由 `activatableOptions` 的顺序给,与值本身无关
|
|
51
|
+
* (值可能含空格,直接拼进 id 会破坏 `aria-activedescendant` 的引用)。
|
|
52
|
+
*/
|
|
53
|
+
export declare const optionDomId: (panelId: string, index: number) => string;
|
|
30
54
|
/** 生成全页唯一的面板 id(随机段与全库其它随机 id 同法) */
|
|
31
55
|
export declare const createSelectPanelId: () => string;
|
|
32
56
|
/**
|
|
@@ -45,19 +69,36 @@ export interface TriggerKeydownOptions {
|
|
|
45
69
|
clear: () => void;
|
|
46
70
|
openPanel: () => void;
|
|
47
71
|
/**
|
|
48
|
-
*
|
|
72
|
+
* 面板当前是否开着(**只服务 `Enter` / `Space` 的接管判定**,可选)。
|
|
73
|
+
*
|
|
74
|
+
* 开着才把这两个键从触发器的原生行为(开 / 收面板)里抢过来交给高亮项;
|
|
75
|
+
* 关着时必须原样放过 —— 否则「关掉面板后再按回车」不会重开面板。
|
|
76
|
+
*/
|
|
77
|
+
isPanelOpen?: () => boolean;
|
|
78
|
+
/**
|
|
79
|
+
* 面板打开后把**键盘高亮**平移一格(**可选**)。
|
|
80
|
+
*
|
|
81
|
+
* 注意它**不移动焦点**:选项行不是可聚焦元素(`div[role='option']`),高亮由 `data-active`
|
|
82
|
+
* 与 `aria-activedescendant` 表达(见 `Select`)。入参是**方向**而不是下标:
|
|
83
|
+
* 尚无高亮时落到首项(向下)或末项(向上)。
|
|
84
|
+
* 列表形态给上它;树形态不给 —— 树的行既不可聚焦也不做键盘导航(与 `Tree` 自身同口径)。
|
|
85
|
+
*/
|
|
86
|
+
moveActive?: (direction: 1 | -1) => void;
|
|
87
|
+
/**
|
|
88
|
+
* 提交当前高亮项(**可选**),返回**是否真的提交了**。
|
|
49
89
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
90
|
+
* 有高亮才返回 `true`(此时键被接管:`preventDefault`,不再切换面板);
|
|
91
|
+
* 没有高亮就返回 `false`,`Enter` / `Space` 回落到按钮的原生切换行为。
|
|
52
92
|
*/
|
|
53
|
-
|
|
93
|
+
commitActive?: () => boolean;
|
|
54
94
|
}
|
|
55
95
|
/**
|
|
56
96
|
* 触发器上的键盘契约(`Select` 与 `SelectTree` 共用一份)。
|
|
57
97
|
*
|
|
58
|
-
* `Delete` 清值、`↑` / `↓`
|
|
59
|
-
*
|
|
60
|
-
*
|
|
98
|
+
* `Delete` 清值、`↑` / `↓` 开面板并可选平移高亮、`Enter` / `Space` 提交高亮项
|
|
99
|
+
* —— 这几条是**触发器形态**的通用操作,与「面板里装的是列表还是树」无关,故两件同源。
|
|
100
|
+
* 与改前的一处差别:不再 `requestAnimationFrame` 等一帧等面板可见(那时是为了 `focus()`),
|
|
101
|
+
* 现在只改状态、不碰焦点;滚动入视由列表侧自己延后一帧(见 `Select` 的 `scrollActiveIntoView`)。
|
|
61
102
|
*/
|
|
62
103
|
export declare function createTriggerKeydown(options: TriggerKeydownOptions): (event: KeyboardEvent) => void;
|
|
63
104
|
/** 供内件声明 ref 属性用的类型别名(面板壳的 elementRef 入参) */
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const isGroup = (item) => Array.isArray(item.options);
|
|
2
|
+
const activatableOptions = (items) => items.flatMap((item) => isGroup(item) ? item.options : [item]).filter((option) => !option.disabled);
|
|
3
|
+
const activatableCount = (items, creatable) => activatableOptions(items).length + (creatable ? 1 : 0);
|
|
4
|
+
const optionDomId = (panelId, index) => `${panelId}-opt-${index}`;
|
|
2
5
|
const PANEL_ID_PREFIX = "mh-select-";
|
|
3
6
|
const createSelectPanelId = () => `${PANEL_ID_PREFIX}${Math.random().toString(36).slice(2, 10)}`;
|
|
4
7
|
const showPanel = (element) => element?.showPopover?.();
|
|
@@ -10,15 +13,22 @@ function createTriggerKeydown(options) {
|
|
|
10
13
|
event.preventDefault();
|
|
11
14
|
if (options.disabled) return;
|
|
12
15
|
options.openPanel();
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
options.moveActive?.(event.key === "ArrowDown" ? 1 : -1);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
20
|
+
if (!options.isPanelOpen?.()) return;
|
|
21
|
+
if (options.commitActive?.()) event.preventDefault();
|
|
15
22
|
}
|
|
16
23
|
};
|
|
17
24
|
}
|
|
18
25
|
export {
|
|
26
|
+
activatableCount,
|
|
27
|
+
activatableOptions,
|
|
19
28
|
createSelectPanelId,
|
|
20
29
|
createTriggerKeydown,
|
|
21
30
|
hidePanel,
|
|
22
31
|
isGroup,
|
|
32
|
+
optionDomId,
|
|
23
33
|
showPanel
|
|
24
34
|
};
|