@manohub/ui 0.7.1 → 0.8.0
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/button/index.d.ts +6 -0
- package/dist/components/card/index.d.ts +1 -1
- package/dist/components/form/index.d.ts +28 -46
- package/dist/components/form/index.js +3 -21
- package/dist/components/input/index.d.ts +74 -6
- package/dist/components/input/index.js +13 -1
- package/dist/components/list-view/index.d.ts +1 -1
- package/dist/components/pagination/index.d.ts +2 -2
- package/dist/components/pagination/index.js +1 -1
- package/dist/components/query-state/index.d.ts +18 -2
- package/dist/components/select/index.d.ts +32 -30
- package/dist/components/select/index.js +46 -82
- package/dist/components/select/select-list.d.ts +125 -0
- package/dist/components/select/select-list.js +95 -0
- package/dist/components/select/select-panel.d.ts +106 -0
- package/dist/components/select/select-panel.js +72 -0
- package/dist/components/select/select-shared.d.ts +64 -0
- package/dist/components/select/select-shared.js +24 -0
- package/dist/components/select/select-tree-filter.d.ts +61 -0
- package/dist/components/select/select-tree-filter.js +63 -0
- package/dist/components/select/select-tree.d.ts +274 -0
- package/dist/components/select/select-tree.js +263 -0
- package/dist/components/select/select-trigger.d.ts +132 -0
- package/dist/components/select/select-trigger.js +101 -0
- package/dist/components/steps/index.d.ts +1 -1
- package/dist/components/table/index.d.ts +25 -4
- package/dist/components/text/index.d.ts +7 -2
- package/dist/components/tree/index.d.ts +28 -2
- package/dist/components/tree/index.js +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/styles/components/form.css +0 -21
- package/dist/styles/components/input.css +25 -0
- package/dist/styles/components/select.css +21 -0
- package/dist/styles/components/select.tokens.css +3 -0
- package/package.json +2 -2
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { defineComponent, ref, computed, createVNode, Fragment
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { defineComponent, ref, computed, createVNode, Fragment } from "vue";
|
|
2
|
+
import { SelectList } from "./select-list.js";
|
|
3
|
+
import { SelectPanel } from "./select-panel.js";
|
|
4
|
+
import { SelectTrigger } from "./select-trigger.js";
|
|
5
|
+
import { createSelectPanelId, isGroup, createTriggerKeydown, showPanel, hidePanel } from "./select-shared.js";
|
|
5
6
|
const Select = /* @__PURE__ */ defineComponent({
|
|
6
7
|
name: "Select",
|
|
7
8
|
inheritAttrs: false,
|
|
@@ -102,7 +103,7 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
102
103
|
slots,
|
|
103
104
|
attrs
|
|
104
105
|
}) {
|
|
105
|
-
const panelId =
|
|
106
|
+
const panelId = createSelectPanelId();
|
|
106
107
|
const panelRef = ref(null);
|
|
107
108
|
const inner = ref(props.defaultValue);
|
|
108
109
|
const keyword = ref("");
|
|
@@ -149,7 +150,7 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
149
150
|
emit("update:modelValue", value);
|
|
150
151
|
props.onChange?.(value);
|
|
151
152
|
};
|
|
152
|
-
const
|
|
153
|
+
const hide = () => hidePanel(panelRef.value);
|
|
153
154
|
const pick = (option) => {
|
|
154
155
|
if (option.disabled) return;
|
|
155
156
|
if (props.multiple) {
|
|
@@ -158,7 +159,7 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
158
159
|
return;
|
|
159
160
|
}
|
|
160
161
|
commit(option.value);
|
|
161
|
-
|
|
162
|
+
hide();
|
|
162
163
|
};
|
|
163
164
|
const createFromKeyword = () => {
|
|
164
165
|
const value = keyword.value.trim();
|
|
@@ -175,104 +176,67 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
175
176
|
const next = at < 0 ? step > 0 ? 0 : items.length - 1 : (at + step + items.length) % items.length;
|
|
176
177
|
items[next].focus();
|
|
177
178
|
};
|
|
178
|
-
const onTriggerKeydown = (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
};
|
|
179
|
+
const onTriggerKeydown = createTriggerKeydown({
|
|
180
|
+
clearable: props.clearable,
|
|
181
|
+
disabled: props.disabled,
|
|
182
|
+
clear,
|
|
183
|
+
openPanel: () => showPanel(panelRef.value),
|
|
184
|
+
focusFirst: (direction) => moveFocus(direction)
|
|
185
|
+
});
|
|
188
186
|
const onPanelKeydown = (event) => {
|
|
189
187
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
190
188
|
event.preventDefault();
|
|
191
189
|
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
192
190
|
}
|
|
193
191
|
};
|
|
194
|
-
const renderOption = (option) => createVNode("button", {
|
|
195
|
-
"key": String(option.value),
|
|
196
|
-
"type": "button",
|
|
197
|
-
"class": ["mh-select__option", option.disabled ? "is-disabled" : ""].filter(Boolean).join(" "),
|
|
198
|
-
"data-state": selectedValues.value.includes(option.value) ? "selected" : "unselected",
|
|
199
|
-
"disabled": option.disabled,
|
|
200
|
-
"role": "option",
|
|
201
|
-
"aria-selected": selectedValues.value.includes(option.value) ? "true" : "false",
|
|
202
|
-
"onClick": () => pick(option)
|
|
203
|
-
}, [option.label]);
|
|
204
192
|
return () => {
|
|
205
193
|
const hasValue = selectedValues.value.length > 0;
|
|
206
194
|
return createVNode("div", {
|
|
207
195
|
"class": ["mh-popup-host", attrs.class].filter(Boolean).join(" "),
|
|
208
196
|
"style": attrs.style
|
|
209
|
-
}, [createVNode(
|
|
210
|
-
"
|
|
197
|
+
}, [createVNode(SelectTrigger, {
|
|
198
|
+
"panelId": panelId,
|
|
211
199
|
"size": props.size,
|
|
212
200
|
"status": props.status,
|
|
213
201
|
"disabled": props.disabled,
|
|
214
202
|
"clearable": props.clearable,
|
|
215
203
|
"hasValue": hasValue,
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
onKeydown: onTriggerKeydown
|
|
223
|
-
},
|
|
224
|
-
"onChange": clear
|
|
225
|
-
}, {
|
|
226
|
-
default: () => props.multiple ? createVNode(Fragment, null, [visibleTags.value.map((option) => createVNode("span", {
|
|
204
|
+
"multiple": props.multiple,
|
|
205
|
+
"placeholder": props.placeholder,
|
|
206
|
+
"label": selectedOptions.value[0]?.label,
|
|
207
|
+
"onKeydown": onTriggerKeydown,
|
|
208
|
+
"onClear": clear,
|
|
209
|
+
"renderValue": props.multiple ? () => createVNode(Fragment, null, [visibleTags.value.map((option) => createVNode("span", {
|
|
227
210
|
"key": String(option.value),
|
|
228
211
|
"class": "mh-select__chip"
|
|
229
212
|
}, [option.label])), foldedCount.value > 0 ? createVNode("span", {
|
|
230
213
|
"class": "mh-select__chip mh-select__chip--fold"
|
|
231
214
|
}, [foldText.value]) : null, !hasValue ? createVNode("span", {
|
|
232
215
|
"class": "mh-select__value mh-select__value--placeholder"
|
|
233
|
-
}, [props.placeholder]) : null]) :
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
"id": panelId
|
|
244
|
-
}, {
|
|
245
|
-
popover: "auto"
|
|
246
|
-
}, {
|
|
247
|
-
"class": "mh-popup mh-select__panel mh-popup--anchored",
|
|
248
|
-
"data-side": "bottom",
|
|
249
|
-
"data-align": "start",
|
|
250
|
-
"role": "listbox",
|
|
216
|
+
}, [props.placeholder]) : null]) : void 0
|
|
217
|
+
}, null), createVNode(SelectPanel, {
|
|
218
|
+
"id": panelId,
|
|
219
|
+
"elementRef": panelRef,
|
|
220
|
+
"searchable": showSearch.value,
|
|
221
|
+
"keyword": keyword.value,
|
|
222
|
+
"onKeyword": (value) => {
|
|
223
|
+
keyword.value = value;
|
|
224
|
+
props.onSearch?.(value);
|
|
225
|
+
},
|
|
251
226
|
"onKeydown": onPanelKeydown
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
keyword
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
"class": "mh-select__option mh-select__option--empty"
|
|
266
|
-
}, [props.emptyText]) : createVNode(Fragment, null, [visibleOptions.value.map((item, index) => isGroup(item) ? createVNode("div", {
|
|
267
|
-
"key": `group-${index}`,
|
|
268
|
-
"class": "mh-select__group"
|
|
269
|
-
}, [createVNode("span", {
|
|
270
|
-
"class": "mh-select__group-label"
|
|
271
|
-
}, [item.label]), item.options.map((option) => renderOption(option))]) : renderOption(item)), creatable.value ? createVNode("button", {
|
|
272
|
-
"type": "button",
|
|
273
|
-
"class": "mh-select__option mh-select__add",
|
|
274
|
-
"onClick": createFromKeyword
|
|
275
|
-
}, [createTextVNode("创建「"), keyword.value.trim(), createTextVNode("」")]) : null])])]);
|
|
227
|
+
}, {
|
|
228
|
+
default: () => createVNode(SelectList, {
|
|
229
|
+
"options": visibleOptions.value,
|
|
230
|
+
"selectedValues": selectedValues.value,
|
|
231
|
+
"loading": props.loading,
|
|
232
|
+
"emptyText": props.emptyText,
|
|
233
|
+
"creatable": creatable.value,
|
|
234
|
+
"keyword": keyword.value,
|
|
235
|
+
"onPick": pick,
|
|
236
|
+
"onCreate": createFromKeyword,
|
|
237
|
+
"renderEmpty": slots.empty
|
|
238
|
+
}, null)
|
|
239
|
+
})]);
|
|
276
240
|
};
|
|
277
241
|
}
|
|
278
242
|
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SelectList —— 选择器族的**选项列表体内件**(`Select` 用,不对外导出)。
|
|
3
|
+
*
|
|
4
|
+
* 它拥有「面板里那一撮选项」的全部形态与状态行:平铺选项 / 分组(组标题不可点)/ 「创建」行 /
|
|
5
|
+
* 空态行 / 加载行。**不含面板壳与搜索行**(那是 `SelectPanel` 的事),也不持有任何状态 ——
|
|
6
|
+
* 过滤结果、选中值、是否可创建统统由消费方算好喂进来,本件只渲染。
|
|
7
|
+
*
|
|
8
|
+
* 选项是真实 `<button role="option">`(不是 div + JS 高亮):键盘可达、`:focus-visible` 可直接
|
|
9
|
+
* 承载「高亮行」这层语义,于是不需要再维护一份 JS 高亮下标 —— **焦点即高亮**,
|
|
10
|
+
* 状态只有 DOM 焦点一个来源。四条按钮重置(`border` / `background` / `font-family` /
|
|
11
|
+
* `text-align`)在面上自带(reset 层清了前三条、`text-align` 属列表行语义故面里显式写),
|
|
12
|
+
* 否则选项会长成原生按钮的灰底立体框 —— 见 `select.css` 的 `.mh-select__option` 一节。
|
|
13
|
+
*
|
|
14
|
+
* 选中态**三源等价**(面层只写一份声明):组件下发 `[data-state='selected']` /
|
|
15
|
+
* 原生 `:has(input:checked)`(静态页零 JS 单选路径)/ `.is-selected`(规格冻结档)。
|
|
16
|
+
*/
|
|
17
|
+
import { type PropType, type VNodeChild } from 'vue';
|
|
18
|
+
import { type SelectItem, type SelectOption } from './select-shared';
|
|
19
|
+
export declare const SelectList: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
20
|
+
/** 已经过滤过的渲染树(分组内全空则由消费方整组去掉) */
|
|
21
|
+
options: {
|
|
22
|
+
type: PropType<SelectItem[]>;
|
|
23
|
+
default: () => never[];
|
|
24
|
+
};
|
|
25
|
+
/** 当前选中值(多选为数组;只用于判定选项行的选中态) */
|
|
26
|
+
selectedValues: {
|
|
27
|
+
type: PropType<Array<string | number>>;
|
|
28
|
+
default: () => never[];
|
|
29
|
+
};
|
|
30
|
+
loading: {
|
|
31
|
+
type: BooleanConstructor;
|
|
32
|
+
default: boolean;
|
|
33
|
+
};
|
|
34
|
+
emptyText: {
|
|
35
|
+
type: StringConstructor;
|
|
36
|
+
default: string;
|
|
37
|
+
};
|
|
38
|
+
/** 可创建:搜索词无命中时由消费方判定,本件只负责渲染那一行 */
|
|
39
|
+
creatable: {
|
|
40
|
+
type: BooleanConstructor;
|
|
41
|
+
default: boolean;
|
|
42
|
+
};
|
|
43
|
+
/** 当前搜索词(「创建「xxx」」那一行的文案来源) */
|
|
44
|
+
keyword: {
|
|
45
|
+
type: StringConstructor;
|
|
46
|
+
default: string;
|
|
47
|
+
};
|
|
48
|
+
onPick: {
|
|
49
|
+
type: PropType<(option: SelectOption) => void>;
|
|
50
|
+
default: undefined;
|
|
51
|
+
};
|
|
52
|
+
onCreate: {
|
|
53
|
+
type: PropType<() => void>;
|
|
54
|
+
default: undefined;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* 空态自定义内容。
|
|
58
|
+
*
|
|
59
|
+
* 用**函数 prop** 而不是插槽:内件不对外,插槽还要多一层 `SlotsType` 签名与父层的对象写法,
|
|
60
|
+
* 而这一处只有「给或是不给」两种。返回 `undefined` 即走内建的空态行。
|
|
61
|
+
*/
|
|
62
|
+
renderEmpty: {
|
|
63
|
+
type: PropType<() => VNodeChild>;
|
|
64
|
+
default: undefined;
|
|
65
|
+
};
|
|
66
|
+
}>, () => string | number | boolean | import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
67
|
+
[key: string]: any;
|
|
68
|
+
}> | import("vue").VNodeArrayChildren, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
69
|
+
/** 已经过滤过的渲染树(分组内全空则由消费方整组去掉) */
|
|
70
|
+
options: {
|
|
71
|
+
type: PropType<SelectItem[]>;
|
|
72
|
+
default: () => never[];
|
|
73
|
+
};
|
|
74
|
+
/** 当前选中值(多选为数组;只用于判定选项行的选中态) */
|
|
75
|
+
selectedValues: {
|
|
76
|
+
type: PropType<Array<string | number>>;
|
|
77
|
+
default: () => never[];
|
|
78
|
+
};
|
|
79
|
+
loading: {
|
|
80
|
+
type: BooleanConstructor;
|
|
81
|
+
default: boolean;
|
|
82
|
+
};
|
|
83
|
+
emptyText: {
|
|
84
|
+
type: StringConstructor;
|
|
85
|
+
default: string;
|
|
86
|
+
};
|
|
87
|
+
/** 可创建:搜索词无命中时由消费方判定,本件只负责渲染那一行 */
|
|
88
|
+
creatable: {
|
|
89
|
+
type: BooleanConstructor;
|
|
90
|
+
default: boolean;
|
|
91
|
+
};
|
|
92
|
+
/** 当前搜索词(「创建「xxx」」那一行的文案来源) */
|
|
93
|
+
keyword: {
|
|
94
|
+
type: StringConstructor;
|
|
95
|
+
default: string;
|
|
96
|
+
};
|
|
97
|
+
onPick: {
|
|
98
|
+
type: PropType<(option: SelectOption) => void>;
|
|
99
|
+
default: undefined;
|
|
100
|
+
};
|
|
101
|
+
onCreate: {
|
|
102
|
+
type: PropType<() => void>;
|
|
103
|
+
default: undefined;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* 空态自定义内容。
|
|
107
|
+
*
|
|
108
|
+
* 用**函数 prop** 而不是插槽:内件不对外,插槽还要多一层 `SlotsType` 签名与父层的对象写法,
|
|
109
|
+
* 而这一处只有「给或是不给」两种。返回 `undefined` 即走内建的空态行。
|
|
110
|
+
*/
|
|
111
|
+
renderEmpty: {
|
|
112
|
+
type: PropType<() => VNodeChild>;
|
|
113
|
+
default: undefined;
|
|
114
|
+
};
|
|
115
|
+
}>> & Readonly<{}>, {
|
|
116
|
+
loading: boolean;
|
|
117
|
+
options: SelectItem[];
|
|
118
|
+
selectedValues: (string | number)[];
|
|
119
|
+
emptyText: string;
|
|
120
|
+
creatable: boolean;
|
|
121
|
+
keyword: string;
|
|
122
|
+
onPick: (option: SelectOption) => void;
|
|
123
|
+
onCreate: () => void;
|
|
124
|
+
renderEmpty: () => VNodeChild;
|
|
125
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { defineComponent, createVNode, createTextVNode, Fragment } from "vue";
|
|
2
|
+
import { isGroup } from "./select-shared.js";
|
|
3
|
+
const SelectList = /* @__PURE__ */ defineComponent({
|
|
4
|
+
name: "SelectList",
|
|
5
|
+
inheritAttrs: false,
|
|
6
|
+
props: {
|
|
7
|
+
/** 已经过滤过的渲染树(分组内全空则由消费方整组去掉) */
|
|
8
|
+
options: {
|
|
9
|
+
type: Array,
|
|
10
|
+
default: () => []
|
|
11
|
+
},
|
|
12
|
+
/** 当前选中值(多选为数组;只用于判定选项行的选中态) */
|
|
13
|
+
selectedValues: {
|
|
14
|
+
type: Array,
|
|
15
|
+
default: () => []
|
|
16
|
+
},
|
|
17
|
+
loading: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: false
|
|
20
|
+
},
|
|
21
|
+
emptyText: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: "暂无数据"
|
|
24
|
+
},
|
|
25
|
+
/** 可创建:搜索词无命中时由消费方判定,本件只负责渲染那一行 */
|
|
26
|
+
creatable: {
|
|
27
|
+
type: Boolean,
|
|
28
|
+
default: false
|
|
29
|
+
},
|
|
30
|
+
/** 当前搜索词(「创建「xxx」」那一行的文案来源) */
|
|
31
|
+
keyword: {
|
|
32
|
+
type: String,
|
|
33
|
+
default: ""
|
|
34
|
+
},
|
|
35
|
+
onPick: {
|
|
36
|
+
type: Function,
|
|
37
|
+
default: void 0
|
|
38
|
+
},
|
|
39
|
+
onCreate: {
|
|
40
|
+
type: Function,
|
|
41
|
+
default: void 0
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* 空态自定义内容。
|
|
45
|
+
*
|
|
46
|
+
* 用**函数 prop** 而不是插槽:内件不对外,插槽还要多一层 `SlotsType` 签名与父层的对象写法,
|
|
47
|
+
* 而这一处只有「给或是不给」两种。返回 `undefined` 即走内建的空态行。
|
|
48
|
+
*/
|
|
49
|
+
renderEmpty: {
|
|
50
|
+
type: Function,
|
|
51
|
+
default: void 0
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
setup(props) {
|
|
55
|
+
const renderOption = (option) => {
|
|
56
|
+
const selected = props.selectedValues.includes(option.value);
|
|
57
|
+
return createVNode("button", {
|
|
58
|
+
"key": String(option.value),
|
|
59
|
+
"type": "button",
|
|
60
|
+
"class": ["mh-select__option", option.disabled ? "is-disabled" : ""].filter(Boolean).join(" "),
|
|
61
|
+
"data-state": selected ? "selected" : "unselected",
|
|
62
|
+
"disabled": option.disabled,
|
|
63
|
+
"role": "option",
|
|
64
|
+
"aria-selected": selected ? "true" : "false",
|
|
65
|
+
"onClick": () => props.onPick?.(option)
|
|
66
|
+
}, [option.label]);
|
|
67
|
+
};
|
|
68
|
+
return () => {
|
|
69
|
+
if (props.loading) {
|
|
70
|
+
return createVNode("span", {
|
|
71
|
+
"class": "mh-select__option mh-select__option--hint"
|
|
72
|
+
}, [createTextVNode("加载中…")]);
|
|
73
|
+
}
|
|
74
|
+
if (props.options.length === 0 && !props.creatable) {
|
|
75
|
+
const custom = props.renderEmpty?.();
|
|
76
|
+
return custom !== void 0 && custom !== null ? custom : createVNode("span", {
|
|
77
|
+
"class": "mh-select__option mh-select__option--empty"
|
|
78
|
+
}, [props.emptyText]);
|
|
79
|
+
}
|
|
80
|
+
return createVNode(Fragment, null, [props.options.map((item, index) => isGroup(item) ? createVNode("div", {
|
|
81
|
+
"key": `group-${index}`,
|
|
82
|
+
"class": "mh-select__group"
|
|
83
|
+
}, [createVNode("span", {
|
|
84
|
+
"class": "mh-select__group-label"
|
|
85
|
+
}, [item.label]), item.options.map((option) => renderOption(option))]) : renderOption(item)), props.creatable ? createVNode("button", {
|
|
86
|
+
"type": "button",
|
|
87
|
+
"class": "mh-select__option mh-select__add",
|
|
88
|
+
"onClick": () => props.onCreate?.()
|
|
89
|
+
}, [createTextVNode("创建「"), props.keyword.trim(), createTextVNode("」")]) : null]);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
export {
|
|
94
|
+
SelectList
|
|
95
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SelectPanel —— 选择器族的**面板壳内件**(`Select` 与 `SelectTree` 共用,不对外导出)。
|
|
3
|
+
*
|
|
4
|
+
* 它只拥有三样东西:**原生 popover 元素**(开合 / Esc / 点外部关闭 / 顶层渲染 / 定位全归浏览器)、
|
|
5
|
+
* **搜索行**(面板顶部那一个输入框)、以及**内容槽**(列表体或树体)。
|
|
6
|
+
* 「面板里装什么」不在本件 —— 这正是让列表与树共用同一份面板皮肤的原因。
|
|
7
|
+
*
|
|
8
|
+
* 组合类名与服务端三条属性都是**操作层的组合**,不是本件新造的浮层:
|
|
9
|
+
* `class="mh-popup mh-select__panel mh-popup--anchored"`(状态根 + 本族皮肤 + 显式锚档)、
|
|
10
|
+
* `data-side` / `data-align`(`popup.css` 的定位档)、`popover="auto"`(原生轻 dismiss)。
|
|
11
|
+
* 面板宽度跟触发器走:`select.css` 的 `.mh-select__panel` 用 `anchor-size(width)` 取锚点宽,
|
|
12
|
+
* 锚是挂在**表壳**上的 `.mh-popup-anchor`(触发器进壳后隐式锚会缩成壳内控件,故走显式锚档)。
|
|
13
|
+
*
|
|
14
|
+
* `elementRef` 是**由消费方持有的 ref**(不是本件自己的):开面板(触发器 ↑↓)、收起面板(选中后)、
|
|
15
|
+
* 以及列表形态的 `.mh-select__option` 焦点遍历都要在父层拿到这个 DOM 元素。
|
|
16
|
+
*/
|
|
17
|
+
import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
18
|
+
import type { ElementRef } from './select-shared';
|
|
19
|
+
export declare const SelectPanel: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
20
|
+
/** 面板 id(必须与触发器的 `popovertarget` 成对) */
|
|
21
|
+
id: {
|
|
22
|
+
type: StringConstructor;
|
|
23
|
+
required: true;
|
|
24
|
+
};
|
|
25
|
+
/** 面板元素的 ref,由消费方持有(父层要用它开合与遍历选项) */
|
|
26
|
+
elementRef: {
|
|
27
|
+
type: PropType<ElementRef>;
|
|
28
|
+
default: undefined;
|
|
29
|
+
};
|
|
30
|
+
/** 是否渲染搜索行(列表形态:`searchable` 或选项多于 8 条;树形态同口径) */
|
|
31
|
+
searchable: {
|
|
32
|
+
type: BooleanConstructor;
|
|
33
|
+
default: boolean;
|
|
34
|
+
};
|
|
35
|
+
keyword: {
|
|
36
|
+
type: StringConstructor;
|
|
37
|
+
default: string;
|
|
38
|
+
};
|
|
39
|
+
searchPlaceholder: {
|
|
40
|
+
type: StringConstructor;
|
|
41
|
+
default: string;
|
|
42
|
+
};
|
|
43
|
+
/** 搜索词变化(消费方负责写回 keyword,并自行决定要不要上抛 `onSearch`) */
|
|
44
|
+
onKeyword: {
|
|
45
|
+
type: PropType<(keyword: string) => void>;
|
|
46
|
+
default: undefined;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* 面板上的键盘契约。
|
|
50
|
+
*
|
|
51
|
+
* 列表形态给 `↑↓` 移动高亮行(选项是真实 `<button>`,焦点即高亮);树形态不给 ——
|
|
52
|
+
* 树的行是 `div`、没有 tabindex,焦点无处可落(与 `Tree` 自身不做键盘导航同口径)。
|
|
53
|
+
*/
|
|
54
|
+
onKeydown: {
|
|
55
|
+
type: PropType<(event: KeyboardEvent) => void>;
|
|
56
|
+
default: undefined;
|
|
57
|
+
};
|
|
58
|
+
}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
59
|
+
/** 面板 id(必须与触发器的 `popovertarget` 成对) */
|
|
60
|
+
id: {
|
|
61
|
+
type: StringConstructor;
|
|
62
|
+
required: true;
|
|
63
|
+
};
|
|
64
|
+
/** 面板元素的 ref,由消费方持有(父层要用它开合与遍历选项) */
|
|
65
|
+
elementRef: {
|
|
66
|
+
type: PropType<ElementRef>;
|
|
67
|
+
default: undefined;
|
|
68
|
+
};
|
|
69
|
+
/** 是否渲染搜索行(列表形态:`searchable` 或选项多于 8 条;树形态同口径) */
|
|
70
|
+
searchable: {
|
|
71
|
+
type: BooleanConstructor;
|
|
72
|
+
default: boolean;
|
|
73
|
+
};
|
|
74
|
+
keyword: {
|
|
75
|
+
type: StringConstructor;
|
|
76
|
+
default: string;
|
|
77
|
+
};
|
|
78
|
+
searchPlaceholder: {
|
|
79
|
+
type: StringConstructor;
|
|
80
|
+
default: string;
|
|
81
|
+
};
|
|
82
|
+
/** 搜索词变化(消费方负责写回 keyword,并自行决定要不要上抛 `onSearch`) */
|
|
83
|
+
onKeyword: {
|
|
84
|
+
type: PropType<(keyword: string) => void>;
|
|
85
|
+
default: undefined;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* 面板上的键盘契约。
|
|
89
|
+
*
|
|
90
|
+
* 列表形态给 `↑↓` 移动高亮行(选项是真实 `<button>`,焦点即高亮);树形态不给 ——
|
|
91
|
+
* 树的行是 `div`、没有 tabindex,焦点无处可落(与 `Tree` 自身不做键盘导航同口径)。
|
|
92
|
+
*/
|
|
93
|
+
onKeydown: {
|
|
94
|
+
type: PropType<(event: KeyboardEvent) => void>;
|
|
95
|
+
default: undefined;
|
|
96
|
+
};
|
|
97
|
+
}>> & Readonly<{}>, {
|
|
98
|
+
onKeydown: (event: KeyboardEvent) => void;
|
|
99
|
+
keyword: string;
|
|
100
|
+
elementRef: ElementRef;
|
|
101
|
+
searchable: boolean;
|
|
102
|
+
searchPlaceholder: string;
|
|
103
|
+
onKeyword: (keyword: string) => void;
|
|
104
|
+
}, SlotsType<{
|
|
105
|
+
default?: () => VNodeChild;
|
|
106
|
+
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { defineComponent, createVNode, mergeProps } from "vue";
|
|
2
|
+
const SelectPanel = /* @__PURE__ */ defineComponent({
|
|
3
|
+
name: "SelectPanel",
|
|
4
|
+
inheritAttrs: false,
|
|
5
|
+
props: {
|
|
6
|
+
/** 面板 id(必须与触发器的 `popovertarget` 成对) */
|
|
7
|
+
id: {
|
|
8
|
+
type: String,
|
|
9
|
+
required: true
|
|
10
|
+
},
|
|
11
|
+
/** 面板元素的 ref,由消费方持有(父层要用它开合与遍历选项) */
|
|
12
|
+
elementRef: {
|
|
13
|
+
type: Object,
|
|
14
|
+
default: void 0
|
|
15
|
+
},
|
|
16
|
+
/** 是否渲染搜索行(列表形态:`searchable` 或选项多于 8 条;树形态同口径) */
|
|
17
|
+
searchable: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: false
|
|
20
|
+
},
|
|
21
|
+
keyword: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: ""
|
|
24
|
+
},
|
|
25
|
+
searchPlaceholder: {
|
|
26
|
+
type: String,
|
|
27
|
+
default: "搜索"
|
|
28
|
+
},
|
|
29
|
+
/** 搜索词变化(消费方负责写回 keyword,并自行决定要不要上抛 `onSearch`) */
|
|
30
|
+
onKeyword: {
|
|
31
|
+
type: Function,
|
|
32
|
+
default: void 0
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* 面板上的键盘契约。
|
|
36
|
+
*
|
|
37
|
+
* 列表形态给 `↑↓` 移动高亮行(选项是真实 `<button>`,焦点即高亮);树形态不给 ——
|
|
38
|
+
* 树的行是 `div`、没有 tabindex,焦点无处可落(与 `Tree` 自身不做键盘导航同口径)。
|
|
39
|
+
*/
|
|
40
|
+
onKeydown: {
|
|
41
|
+
type: Function,
|
|
42
|
+
default: void 0
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
slots: Object,
|
|
46
|
+
setup(props, {
|
|
47
|
+
slots
|
|
48
|
+
}) {
|
|
49
|
+
return () => createVNode("div", mergeProps({
|
|
50
|
+
"ref": props.elementRef,
|
|
51
|
+
"id": props.id
|
|
52
|
+
}, {
|
|
53
|
+
popover: "auto"
|
|
54
|
+
}, {
|
|
55
|
+
"class": "mh-popup mh-select__panel mh-popup--anchored",
|
|
56
|
+
"data-side": "bottom",
|
|
57
|
+
"data-align": "start",
|
|
58
|
+
"role": "listbox",
|
|
59
|
+
"onKeydown": props.onKeydown
|
|
60
|
+
}), [props.searchable ? createVNode("div", {
|
|
61
|
+
"class": "mh-select__search"
|
|
62
|
+
}, [createVNode("input", {
|
|
63
|
+
"class": "mh-input",
|
|
64
|
+
"value": props.keyword,
|
|
65
|
+
"placeholder": props.searchPlaceholder,
|
|
66
|
+
"onInput": (event) => props.onKeyword?.(event.target.value)
|
|
67
|
+
}, null)]) : null, slots.default?.()]);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
export {
|
|
71
|
+
SelectPanel
|
|
72
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 选择器族的**共用原子**(类型、面板 id、popover 开合、触发器键盘契约)。
|
|
3
|
+
*
|
|
4
|
+
* 这是 `select/` 目录里的**叶子模块**:它不 import 同目录任何其它文件,因此触发器 / 面板壳 /
|
|
5
|
+
* 列表体 / 树体四件可以自由地互相组合而不产生环。内件一律**不对外导出**(`src/index.ts` 只出
|
|
6
|
+
* `Select` 与 `SelectTree`),故本文件的导出面是「族内契约」,改它要同时看那四件。
|
|
7
|
+
*
|
|
8
|
+
* 为什么单独一份而不是各自写一遍:这四样都是**跨两件以上**的东西 ——
|
|
9
|
+
* 类型(列表体与树体都要)、面板 id 生成器(两件的面板都得全页唯一)、
|
|
10
|
+
* popover 开合(两件都要收起面板)、触发器键盘契约(两件的触发器是同一个形态)。
|
|
11
|
+
*/
|
|
12
|
+
import type { Ref } from 'vue';
|
|
13
|
+
/** 选项(平铺项与分组内选项同构) */
|
|
14
|
+
export interface SelectOption {
|
|
15
|
+
label: string;
|
|
16
|
+
value: string | number;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** 分组标题用的标记(内部判别用,消费方不必写) */
|
|
19
|
+
options?: never;
|
|
20
|
+
}
|
|
21
|
+
/** 分组选项(`{ label: '进行中', options: [...] }`,组内选项与平铺选项同构) */
|
|
22
|
+
export interface SelectOptionGroup {
|
|
23
|
+
label: string;
|
|
24
|
+
options: SelectOption[];
|
|
25
|
+
}
|
|
26
|
+
export type SelectItem = SelectOption | SelectOptionGroup;
|
|
27
|
+
export type SelectValue = string | number | Array<string | number>;
|
|
28
|
+
/** 判别:带 `options` 数组的是分组 */
|
|
29
|
+
export declare const isGroup: (item: SelectItem) => item is SelectOptionGroup;
|
|
30
|
+
/** 生成全页唯一的面板 id(随机段与全库其它随机 id 同法) */
|
|
31
|
+
export declare const createSelectPanelId: () => string;
|
|
32
|
+
/**
|
|
33
|
+
* 开 / 收面板 —— 调的是**原生** `showPopover()` / `hidePopover()`,不是自建浮层逻辑
|
|
34
|
+
* (面板的开合、Esc、点外部关闭、顶层渲染全归浏览器与 `styles/operations/popup.css`)。
|
|
35
|
+
*
|
|
36
|
+
* 先探存在性再调用:jsdom 至今没实现这套 API,直接 `?.()` 会在测试里抛「不是函数」。
|
|
37
|
+
*/
|
|
38
|
+
export declare const showPanel: (element: HTMLElement | null) => void;
|
|
39
|
+
export declare const hidePanel: (element: HTMLElement | null) => void;
|
|
40
|
+
/** 触发器键盘契约的输入 */
|
|
41
|
+
export interface TriggerKeydownOptions {
|
|
42
|
+
/** 有清除位时才接 `Delete` 清值(与输入框的键盘清除同款) */
|
|
43
|
+
clearable: boolean;
|
|
44
|
+
disabled: boolean;
|
|
45
|
+
clear: () => void;
|
|
46
|
+
openPanel: () => void;
|
|
47
|
+
/**
|
|
48
|
+
* 面板打开后把焦点落到某一项上(**可选**)。
|
|
49
|
+
*
|
|
50
|
+
* 列表形态给上它(选项是真实 `<button>`,焦点即高亮);树形态不给 ——
|
|
51
|
+
* 树的行是 `div`、没有 tabindex,焦点无处可落。
|
|
52
|
+
*/
|
|
53
|
+
focusFirst?: (direction: 1 | -1) => void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 触发器上的键盘契约(`Select` 与 `SelectTree` 共用一份)。
|
|
57
|
+
*
|
|
58
|
+
* `Delete` 清值、`↑` / `↓` 开面板(并可选聚焦首 / 末项)—— 这一对是**触发器形态**的通用操作,
|
|
59
|
+
* 与「面板里装的是列表还是树」无关,故两件同源。`focusFirst` 的入参是**方向**而不是下标:
|
|
60
|
+
* 列表侧据此在「尚未聚焦任何项」时落到首项(向下)或末项(向上)。
|
|
61
|
+
*/
|
|
62
|
+
export declare function createTriggerKeydown(options: TriggerKeydownOptions): (event: KeyboardEvent) => void;
|
|
63
|
+
/** 供内件声明 ref 属性用的类型别名(面板壳的 elementRef 入参) */
|
|
64
|
+
export type ElementRef = Ref<HTMLElement | null>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const isGroup = (item) => Array.isArray(item.options);
|
|
2
|
+
const PANEL_ID_PREFIX = "mh-select-";
|
|
3
|
+
const createSelectPanelId = () => `${PANEL_ID_PREFIX}${Math.random().toString(36).slice(2, 10)}`;
|
|
4
|
+
const showPanel = (element) => element?.showPopover?.();
|
|
5
|
+
const hidePanel = (element) => element?.hidePopover?.();
|
|
6
|
+
function createTriggerKeydown(options) {
|
|
7
|
+
return (event) => {
|
|
8
|
+
if (event.key === "Delete" && options.clearable) options.clear();
|
|
9
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
10
|
+
event.preventDefault();
|
|
11
|
+
if (options.disabled) return;
|
|
12
|
+
options.openPanel();
|
|
13
|
+
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
14
|
+
if (options.focusFirst) requestAnimationFrame(() => options.focusFirst?.(direction));
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
createSelectPanelId,
|
|
20
|
+
createTriggerKeydown,
|
|
21
|
+
hidePanel,
|
|
22
|
+
isGroup,
|
|
23
|
+
showPanel
|
|
24
|
+
};
|