@manohub/ui 0.10.1 → 0.10.3

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.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/dist/components/badge/index.d.ts +46 -1
  3. package/dist/components/badge/index.js +42 -9
  4. package/dist/components/card/index.d.ts +1 -1
  5. package/dist/components/drawer/index.d.ts +1 -1
  6. package/dist/components/form/index.d.ts +50 -1
  7. package/dist/components/form/index.js +20 -2
  8. package/dist/components/input/index.js +1 -0
  9. package/dist/components/nav/index.d.ts +27 -0
  10. package/dist/components/nav/index.js +12 -0
  11. package/dist/components/notice/index.d.ts +1 -1
  12. package/dist/components/notification/index.d.ts +2 -2
  13. package/dist/components/select/index.d.ts +9 -0
  14. package/dist/components/select/index.js +57 -12
  15. package/dist/components/select/select-list.d.ts +42 -8
  16. package/dist/components/select/select-list.js +36 -11
  17. package/dist/components/select/select-panel.d.ts +36 -8
  18. package/dist/components/select/select-panel.js +18 -4
  19. package/dist/components/select/select-shared.d.ts +51 -10
  20. package/dist/components/select/select-shared.js +12 -2
  21. package/dist/components/select/select-tree.d.ts +25 -3
  22. package/dist/components/select/select-tree.js +17 -6
  23. package/dist/components/select/select-trigger.d.ts +21 -0
  24. package/dist/components/select/select-trigger.js +15 -1
  25. package/dist/components/tag/index.d.ts +32 -153
  26. package/dist/components/tag/index.js +102 -101
  27. package/dist/components/text/index.d.ts +1 -1
  28. package/dist/components/tooltip/index.d.ts +1 -1
  29. package/dist/components/tree/index.d.ts +65 -0
  30. package/dist/components/tree/index.js +29 -2
  31. package/dist/index.d.ts +1 -1
  32. package/dist/styles/components/avatar.css +2 -2
  33. package/dist/styles/components/badge.css +31 -4
  34. package/dist/styles/components/badge.tokens.css +24 -0
  35. package/dist/styles/components/button.css +7 -6
  36. package/dist/styles/components/button.tokens.css +5 -2
  37. package/dist/styles/components/dialog.css +13 -0
  38. package/dist/styles/components/divider.css +8 -4
  39. package/dist/styles/components/divider.tokens.css +2 -1
  40. package/dist/styles/components/form.css +51 -0
  41. package/dist/styles/components/notification.css +1 -1
  42. package/dist/styles/components/select.css +62 -39
  43. package/dist/styles/components/select.tokens.css +16 -9
  44. package/dist/styles/components/tag.css +77 -154
  45. package/dist/styles/components/tag.tokens.css +13 -19
  46. package/dist/styles/components/tree.css +37 -3
  47. package/dist/styles/components/tree.tokens.css +4 -3
  48. package/dist/styles/components/upload.tokens.css +1 -1
  49. package/dist/styles/index.css +2 -1
  50. package/dist/styles/operations/popup.css +5 -1
  51. package/package.json +2 -2
@@ -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
- * 选项是真实 `<button role="option">`(不是 div + JS 高亮):键盘可达、`:focus-visible` 可直接
11
- * 承载「高亮行」这层语义,于是不需要再维护一份 JS 高亮下标 —— **焦点即高亮**,
12
- * 状态只有 DOM 焦点一个来源。四条按钮重置(`border` / `background` / `font-family` /
13
- * `text-align`)在面上自带(reset 层清了前三条、`text-align` 属列表行语义故面里显式写),
14
- * 否则选项会长成原生按钮的灰底立体框 —— 见 `select.css` 的 `.mh-select__option` 一节。
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
- return createVNode("button", {
72
+ const slot = activatable.value.indexOf(option);
73
+ return createVNode("div", {
58
74
  "key": String(option.value),
59
- "type": "button",
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 ? createVNode("button", {
86
- "type": "button",
87
- "class": "mh-select__option mh-select__add",
88
- "onClick": () => props.onCreate?.()
89
- }, [createTextVNode("创建「"), props.keyword.trim(), createTextVNode("」")]) : null]);
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
- * 以及列表形态的 `.mh-select__option` 焦点遍历都要在父层拿到这个 DOM 元素。
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
- * 列表形态给 `↑↓` 移动高亮行(选项是真实 `<button>`,焦点即高亮);树形态不给 ——
57
- * 树的行是 `div`、没有 tabindex,焦点无处可落(与 `Tree` 自身不做键盘导航同口径)。
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
- * 列表形态给 `↑↓` 移动高亮行(选项是真实 `<button>`,焦点即高亮);树形态不给 ——
96
- * 树的行是 `div`、没有 tabindex,焦点无处可落(与 `Tree` 自身不做键盘导航同口径)。
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
- * 列表形态给 `↑↓` 移动高亮行(选项是真实 `<button>`,焦点即高亮);树形态不给 ——
38
- * 树的行是 `div`、没有 tabindex,焦点无处可落(与 `Tree` 自身不做键盘导航同口径)。
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
- * 列表形态给上它(选项是真实 `<button>`,焦点即高亮);树形态不给 ——
51
- * 树的行是 `div`、没有 tabindex,焦点无处可落。
90
+ * 有高亮才返回 `true`(此时键被接管:`preventDefault`,不再切换面板);
91
+ * 没有高亮就返回 `false`,`Enter` / `Space` 回落到按钮的原生切换行为。
52
92
  */
53
- focusFirst?: (direction: 1 | -1) => void;
93
+ commitActive?: () => boolean;
54
94
  }
55
95
  /**
56
96
  * 触发器上的键盘契约(`Select` 与 `SelectTree` 共用一份)。
57
97
  *
58
- * `Delete` 清值、`↑` / `↓` 开面板(并可选聚焦首 / 末项)—— 这一对是**触发器形态**的通用操作,
59
- * 与「面板里装的是列表还是树」无关,故两件同源。`focusFirst` 的入参是**方向**而不是下标:
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
- const direction = event.key === "ArrowDown" ? 1 : -1;
14
- if (options.focusFirst) requestAnimationFrame(() => options.focusFirst?.(direction));
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
  };
@@ -11,15 +11,15 @@
11
11
  * **整支保留**,并强制全展到命中层(算法与切分都是纯函数,见 `select-tree-filter.ts`);
12
12
  * 命中文本按面层的 `.mh-select__hit` 高亮(靠 `Tree` 的可选 `renderLabel` 钩子落进标签槽)。
13
13
  * 3. **面板内限高滚动**:限高归面板(`.mh-select__panel` 的 `--ui-select-panel-max-height`)、
14
- * 溢出由面板内容区(`.mh-select__body`)承担;本件只把树包在 `.mh-select__tree` 里给内距
15
- * —— 限高不由形态自持,列表形态共用同一份。
14
+ * 溢出与内容内距都由面板内容区(`.mh-select__body`)承担 —— 两形态共用同一份,本件因此
15
+ * 把树**直接**装进内容区,不再自持一层只给内距的包裹(限高与内距都不由形态自持)。
16
16
  *
17
17
  * ## 与 `Select` 的三处有意差异(都是「树不是列表」决定的)
18
18
  *
19
19
  * | 事项 | `Select` | `SelectTree` |
20
20
  * | --- | --- | --- |
21
21
  * | 值 | `string \| number \| Array<…>` | 只单选、只 `string`(`Tree` 的主键本就 `String()` 化) |
22
- * | 面板内 ↑↓ 行导航 | 有(选项是真实 `<button>`,焦点即高亮) | **无**(树的行是 `div`、没有 tabindex;`Tree` 自身也不做键盘导航) |
22
+ * | 面板内 ↑↓ 行导航 | 有(高亮位由 `data-active` 表达,焦点留在触发器上) | **无**(树的行不做键盘导航,与 `Tree` 自身同口径) |
23
23
  * | 空 / 加载态 | 列表体的内建行 | 面板内同样出一行文字(**不复用**整件三态容器:下拉里出空态插画太重) |
24
24
  *
25
25
  * @example 基础单选
@@ -139,6 +139,16 @@ export declare const SelectTree: import("vue").DefineComponent<import("vue").Ext
139
139
  type: PropType<(keyword: string) => void>;
140
140
  default: undefined;
141
141
  };
142
+ /** 面板里树行的节点图标(标签前的一位图标):透传给 `Tree.renderIcon` */
143
+ renderIcon: {
144
+ type: PropType<(node: TreeNode) => unknown>;
145
+ default: undefined;
146
+ };
147
+ /** 面板里树行的展开 / 收起箭头字形:透传给 `Tree.renderCaret` */
148
+ renderCaret: {
149
+ type: PropType<(node: TreeNode, expanded: boolean) => unknown>;
150
+ default: undefined;
151
+ };
142
152
  }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
143
153
  'update:modelValue': (v: string) => true;
144
154
  }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -248,6 +258,16 @@ export declare const SelectTree: import("vue").DefineComponent<import("vue").Ext
248
258
  type: PropType<(keyword: string) => void>;
249
259
  default: undefined;
250
260
  };
261
+ /** 面板里树行的节点图标(标签前的一位图标):透传给 `Tree.renderIcon` */
262
+ renderIcon: {
263
+ type: PropType<(node: TreeNode) => unknown>;
264
+ default: undefined;
265
+ };
266
+ /** 面板里树行的展开 / 收起箭头字形:透传给 `Tree.renderCaret` */
267
+ renderCaret: {
268
+ type: PropType<(node: TreeNode, expanded: boolean) => unknown>;
269
+ default: undefined;
270
+ };
251
271
  }>> & Readonly<{
252
272
  "onUpdate:modelValue"?: ((v: string) => any) | undefined;
253
273
  }>, {
@@ -257,6 +277,8 @@ export declare const SelectTree: import("vue").DefineComponent<import("vue").Ext
257
277
  loading: boolean;
258
278
  labelKey: string;
259
279
  childrenKey: string;
280
+ renderCaret: (node: TreeNode, expanded: boolean) => unknown;
281
+ renderIcon: (node: TreeNode) => unknown;
260
282
  expandedKeys: string[];
261
283
  defaultExpandedKeys: string[];
262
284
  defaultExpandAll: boolean;
@@ -113,6 +113,16 @@ const SelectTree = /* @__PURE__ */ defineComponent({
113
113
  onSearch: {
114
114
  type: Function,
115
115
  default: void 0
116
+ },
117
+ /** 面板里树行的节点图标(标签前的一位图标):透传给 `Tree.renderIcon` */
118
+ renderIcon: {
119
+ type: Function,
120
+ default: void 0
121
+ },
122
+ /** 面板里树行的展开 / 收起箭头字形:透传给 `Tree.renderCaret` */
123
+ renderCaret: {
124
+ type: Function,
125
+ default: void 0
116
126
  }
117
127
  },
118
128
  emits: {
@@ -184,7 +194,8 @@ const SelectTree = /* @__PURE__ */ defineComponent({
184
194
  disabled: props.disabled,
185
195
  clear,
186
196
  openPanel: () => showPanel(panelRef.value)
187
- /* 有意不给 `focusFirst`:树的行是 `div`、没有 tabindex,面板内不做 ↑↓ 行导航 */
197
+ /* 有意不给 `moveActive` / `commitActive`:树不做面板内键盘导航(行不可聚焦、与 `Tree` 自身
198
+ 同口径),故 `↑` / `↓` 只开面板、`Enter` / `Space` 仍归按钮的原生切换行为 */
188
199
  });
189
200
  const renderHitLabel = (node) => createVNode(Fragment, null, [splitHighlight(treeLabel(node, props.labelKey), keyword.value).map((segment, index) => segment.hit ? createVNode("span", {
190
201
  "key": index,
@@ -228,9 +239,7 @@ const SelectTree = /* @__PURE__ */ defineComponent({
228
239
  "class": "mh-select__option mh-select__option--empty"
229
240
  }, [props.emptyText]);
230
241
  }
231
- return createVNode("div", {
232
- "class": "mh-select__tree"
233
- }, [createVNode(Tree, {
242
+ return createVNode(Tree, {
234
243
  "nodes": visibleNodes.value,
235
244
  "rowKey": props.rowKey,
236
245
  "labelKey": props.labelKey,
@@ -239,8 +248,10 @@ const SelectTree = /* @__PURE__ */ defineComponent({
239
248
  "expandedKeys": expandedKeys.value,
240
249
  "onExpandChange": handleExpandChange,
241
250
  "onSelect": pick,
242
- "renderLabel": highlightLabel
243
- }, null)]);
251
+ "renderLabel": highlightLabel,
252
+ "renderIcon": props.renderIcon,
253
+ "renderCaret": props.renderCaret
254
+ }, null);
244
255
  }
245
256
  })]);
246
257
  };
@@ -45,6 +45,16 @@ export declare const SelectTrigger: import("vue").DefineComponent<import("vue").
45
45
  type: StringConstructor;
46
46
  default: undefined;
47
47
  };
48
+ /**
49
+ * 键盘高亮行的 id(`aria-activedescendant`,**列表形态专用**)。
50
+ *
51
+ * 选项是 `div[role='option']`、不可聚焦,焦点留在本按钮上 ⇒ 高亮项要靠这个属性告诉读屏。
52
+ * 树形态不给(树的行不做键盘导航,见 `SelectTree`)。
53
+ */
54
+ activeDescendant: {
55
+ type: StringConstructor;
56
+ default: undefined;
57
+ };
48
58
  /** 触发器上的键盘契约(`createTriggerKeydown()` 产出,两件同源) */
49
59
  onKeydown: {
50
60
  type: PropType<(event: KeyboardEvent) => void>;
@@ -104,6 +114,16 @@ export declare const SelectTrigger: import("vue").DefineComponent<import("vue").
104
114
  type: StringConstructor;
105
115
  default: undefined;
106
116
  };
117
+ /**
118
+ * 键盘高亮行的 id(`aria-activedescendant`,**列表形态专用**)。
119
+ *
120
+ * 选项是 `div[role='option']`、不可聚焦,焦点留在本按钮上 ⇒ 高亮项要靠这个属性告诉读屏。
121
+ * 树形态不给(树的行不做键盘导航,见 `SelectTree`)。
122
+ */
123
+ activeDescendant: {
124
+ type: StringConstructor;
125
+ default: undefined;
126
+ };
107
127
  /** 触发器上的键盘契约(`createTriggerKeydown()` 产出,两件同源) */
108
128
  onKeydown: {
109
129
  type: PropType<(event: KeyboardEvent) => void>;
@@ -130,5 +150,6 @@ export declare const SelectTrigger: import("vue").DefineComponent<import("vue").
130
150
  clearable: boolean;
131
151
  hasValue: boolean;
132
152
  onClear: () => void;
153
+ activeDescendant: string;
133
154
  renderValue: () => VNodeChild;
134
155
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -48,6 +48,16 @@ const SelectTrigger = /* @__PURE__ */ defineComponent({
48
48
  type: String,
49
49
  default: void 0
50
50
  },
51
+ /**
52
+ * 键盘高亮行的 id(`aria-activedescendant`,**列表形态专用**)。
53
+ *
54
+ * 选项是 `div[role='option']`、不可聚焦,焦点留在本按钮上 ⇒ 高亮项要靠这个属性告诉读屏。
55
+ * 树形态不给(树的行不做键盘导航,见 `SelectTree`)。
56
+ */
57
+ activeDescendant: {
58
+ type: String,
59
+ default: void 0
60
+ },
51
61
  /** 触发器上的键盘契约(`createTriggerKeydown()` 产出,两件同源) */
52
62
  onKeydown: {
53
63
  type: Function,
@@ -80,8 +90,12 @@ const SelectTrigger = /* @__PURE__ */ defineComponent({
80
90
  popovertarget: props.panelId,
81
91
  "aria-haspopup": "listbox",
82
92
  /* 开合态由原生 popover 与 popup.css 的箭头转向承担,本件不维护第二份;
83
- 恒定 'false' 是既有口径(输入面的触发器形态不跟踪 popover 的开关状态) */
93
+ 恒定 'false' 是既有口径(输入面的触发器形态不跟踪 popover 的开关状态)。
94
+ ⚠️ 这与 `aria-activedescendant`(下面那条)**都是焦点留在触发器上的必然结果**:
95
+ 真的做完整要连 `aria-expanded` 一起跟踪开合态,那是另一次改动(见 select.md 的注意事项)。 */
84
96
  "aria-expanded": "false",
97
+ /* 高亮行引用(列表形态给、树形态不给):值 `undefined` 时 Vue 会整个摘掉这个属性 */
98
+ "aria-activedescendant": props.activeDescendant,
85
99
  onKeydown: props.onKeydown
86
100
  },
87
101
  "onChange": () => props.onClear?.()