@apollo-design/a11y 0.0.0-stage → 0.1.9
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 +113 -2
- package/dist/combobox.mjs +37 -0
- package/dist/focus-restore.mjs +86 -0
- package/dist/index.d.mts +455 -0
- package/dist/index.d.ts +455 -0
- package/dist/index.mjs +8 -0
- package/dist/live-region.mjs +87 -0
- package/dist/roving.mjs +54 -0
- package/dist/use-active-descendant.mjs +24 -0
- package/dist/use-roving-focus.mjs +97 -0
- package/dist/use-typeahead.mjs +41 -0
- package/package.json +32 -3
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
export { FocusNode, InputFocusOptions, getFocusNodeList, lockFocus, resetFocusLock, triggerFocus, useLockFocus } from '@apollo-design/utils';
|
|
2
|
+
import { MaybeRefOrGetter, ComputedRef, Ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* combobox 的两类原语:`aria-activedescendant` 的 id 方案,与 typeahead 键盘搜索。
|
|
6
|
+
*
|
|
7
|
+
* 契约来源:
|
|
8
|
+
* - id 方案:`@rc-component/select@1.10.1/es/OptionList.js:237-281`
|
|
9
|
+
* 并被 antd 的 DOM 快照印证
|
|
10
|
+
* (`components/config-provider/__tests__/__snapshots__/components.test.tsx.snap:23546`:
|
|
11
|
+
* `aria-controls="test-id_list"` / `aria-activedescendant="test-id_list_0"`)
|
|
12
|
+
* - typeahead:**上游没有**,见下面的 §typeahead 说明
|
|
13
|
+
*
|
|
14
|
+
* 本文件是纯函数:不涉及 DOM。
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 列表框(listbox)自身的 id —— 同时也是输入框 `aria-controls` 的取值。
|
|
18
|
+
*
|
|
19
|
+
* `OptionList.js:280`:`id: `${id}_list``
|
|
20
|
+
*/
|
|
21
|
+
declare function getListboxId(id: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* 第 `index` 个选项的 id —— 输入框 `aria-activedescendant` 的取值。
|
|
24
|
+
*
|
|
25
|
+
* `OptionList.js:251`:`id: `${id}_list_${index}``(group 项的 role 是 `presentation`,
|
|
26
|
+
* 但 id 方案相同)。
|
|
27
|
+
*
|
|
28
|
+
* `${id}` 应来自 `useId`(已经在 `@apollo-design/utils`,本包不重复实现)。
|
|
29
|
+
*/
|
|
30
|
+
declare function getOptionId(id: string, index: number): string;
|
|
31
|
+
/**
|
|
32
|
+
* 两次按键间隔超过这个毫秒数就重新起头。
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ **上游没有对应实现**:antd 与 rc-* 全量搜索 `typeahead` 零命中,
|
|
35
|
+
* rc-select 的「搜索」是 combobox 的过滤语义,不是「敲首字母跳到匹配项」。
|
|
36
|
+
* 500ms 取自通用实践,见契约文档 §8 P1。
|
|
37
|
+
*/
|
|
38
|
+
declare const DEFAULT_TYPEAHEAD_RESET_DELAY = 500;
|
|
39
|
+
interface TypeaheadState {
|
|
40
|
+
/** 累积的搜索串 */
|
|
41
|
+
buffer: string;
|
|
42
|
+
/** 上一次按键的时间戳(与 `now` 同一时基) */
|
|
43
|
+
lastAt: number;
|
|
44
|
+
}
|
|
45
|
+
declare const INITIAL_TYPEAHEAD_STATE: TypeaheadState;
|
|
46
|
+
/**
|
|
47
|
+
* 按下一个字符后的新状态。
|
|
48
|
+
*
|
|
49
|
+
* 两条规则:
|
|
50
|
+
* 1. 距上次按键超过 `resetDelay` ⇒ **重新起头**(buffer 只保留本次字符)
|
|
51
|
+
* 2. 否则**累积**
|
|
52
|
+
*
|
|
53
|
+
* 「同字符循环」不用在这里特判:连续按同一个键会让 buffer 变成 `"a"` → `"aa"` → `"aaa"`,
|
|
54
|
+
* 而前缀匹配 `"aa"` 通常比 `"a"` 命中更少 —— 真正的循环语义由调用方在
|
|
55
|
+
* 「命中项没变时」自行决定是否复用同一个 buffer 继续 `findTypeaheadIndex`。
|
|
56
|
+
*
|
|
57
|
+
* 之所以不把它做成有状态的对象:调用方(Vue 组件)需要能把状态放进 ref 并在卸载时清掉,
|
|
58
|
+
* 纯函数 + 不可变返回值最好测也最好组合。
|
|
59
|
+
*
|
|
60
|
+
* @param now 本次按键的时间戳
|
|
61
|
+
* @param resetDelay 超时阈值(毫秒);判定用 `>`,即「刚好等于」不算超时
|
|
62
|
+
*/
|
|
63
|
+
declare function pushTypeaheadChar(state: TypeaheadState, char: string, now: number, resetDelay?: number): TypeaheadState;
|
|
64
|
+
/**
|
|
65
|
+
* 这个按键是不是「要参与 typeahead」的可打印字符。
|
|
66
|
+
*
|
|
67
|
+
* 判据:
|
|
68
|
+
* 1. 已处理或正在输入法组合的事件交还所属控件,不参与搜索。
|
|
69
|
+
* 2. 带修饰键(Ctrl / Meta / Alt)的一律不算 —— 那是快捷键,不是搜索
|
|
70
|
+
* 3. `key.length === 1` —— 可打印字符的 `key` 恰好是一个字符;
|
|
71
|
+
* 功能键(`'ArrowDown'` / `'Enter'` / `'Escape'`)长度都大于 1
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ 刻意**不排除空格**:经典 typeahead 里空格是合法搜索字符。
|
|
74
|
+
* 如果某个控件的空格另有语义(例如勾选),由调用方在派发前自行拦掉。
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ 只读 `key` 不读 `keyCode`:后者已废弃,且 antd 自己在
|
|
77
|
+
* `rc-select/utils/keyUtil.js` 的注释里也记录了它在 Edge 上的 bug
|
|
78
|
+
* (见 https://github.com/ant-design/ant-design/issues/51292)。
|
|
79
|
+
*/
|
|
80
|
+
declare function isTypeaheadKey(event: {
|
|
81
|
+
key: string;
|
|
82
|
+
ctrlKey?: boolean;
|
|
83
|
+
metaKey?: boolean;
|
|
84
|
+
altKey?: boolean;
|
|
85
|
+
isComposing?: boolean;
|
|
86
|
+
defaultPrevented?: boolean;
|
|
87
|
+
}): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* 从 `fromIndex` 的**下一项**开始全表找一圈,返回第一个以 `buffer` 开头的项。
|
|
90
|
+
*
|
|
91
|
+
* 搜索顺序:`fromIndex + 1 → … → count - 1 → 0 → … → fromIndex`(含 `fromIndex` 自身),
|
|
92
|
+
* 共 `count` 次 —— 这就是「循环」:连按同一个键会依次跳到各个匹配项。
|
|
93
|
+
*
|
|
94
|
+
* @param labels 候选项的文本
|
|
95
|
+
* @param buffer 搜索串;空串直接返回 `NO_ACTIVE_INDEX`
|
|
96
|
+
* @param fromIndex 当前项下标;`-1` 表示还没有当前项(此时从 0 开始)
|
|
97
|
+
* @returns 命中下标,未命中返回 `-1`
|
|
98
|
+
*/
|
|
99
|
+
declare function findTypeaheadIndex(labels: readonly string[], buffer: string, fromIndex: number): number;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 焦点恢复 —— 浮层打开时记住「焦点原本在哪」,关闭时还回去。
|
|
103
|
+
*
|
|
104
|
+
* 契约来源:`@rc-component/dialog@1.10.0/es/Dialog/index.js:53-95`
|
|
105
|
+
* (`saveLastOutSideActiveElementRef` / `focusDialogContent` / `doClose`)。
|
|
106
|
+
*
|
|
107
|
+
* 陷阱本身在 `@apollo-design/utils`(`lockFocus` / `useLockFocus`)—— 见契约文档 §1.1,
|
|
108
|
+
* 本文件只补上游 dialog 里那三条**恢复**的门控判据。
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
interface FocusRestoreOptions {
|
|
112
|
+
/**
|
|
113
|
+
* 对应 rc-dialog 的 `mask`。
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ 上游把「是否恢复焦点」额外门控在 `mask` 上 —— 无遮罩的 dialog **不恢复**。
|
|
116
|
+
* 这看起来像 bug,但它是可观察行为,别顺手修(契约文档 §3.5 第 2 条)。
|
|
117
|
+
* 默认 `true`。
|
|
118
|
+
*/
|
|
119
|
+
mask?: MaybeRefOrGetter<boolean>;
|
|
120
|
+
/** 对应 rc-dialog 的 `focusTriggerAfterClose`。默认 `true`。 */
|
|
121
|
+
enabled?: MaybeRefOrGetter<boolean>;
|
|
122
|
+
}
|
|
123
|
+
interface FocusRestoreHandle {
|
|
124
|
+
/** 打开前保存当前外部焦点;已知封闭根可显式传目标,null表示不保存。 */
|
|
125
|
+
save(target?: Element | null): void;
|
|
126
|
+
/** 浮层打开后调用:焦点若还在外面,就移到容器上 */
|
|
127
|
+
focusContent(): void;
|
|
128
|
+
/** 浮层关闭时调用:把焦点还给 `save()` 记住的元素(只生效一次) */
|
|
129
|
+
restore(): void;
|
|
130
|
+
}
|
|
131
|
+
declare function useFocusRestore(getContainer: () => HTMLElement | null, options?: FocusRestoreOptions): FocusRestoreHandle;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* live region —— 给读屏软件播报「页面某处变了」。
|
|
135
|
+
*
|
|
136
|
+
* 契约来源(上游有**三处**实现,形状不一致,见下方注释):
|
|
137
|
+
* - `@rc-component/select@1.10.1/es/BaseSelect/Polite.js` —— 文本截断与拼接
|
|
138
|
+
* - `components/image/Progress.tsx:7-17,125` —— 视觉隐藏样式 + `role="status"`
|
|
139
|
+
* - `components/spin/index.tsx:264` —— 直接挂在容器上(本包不采用,见 §4)
|
|
140
|
+
*
|
|
141
|
+
* ⚠️ 本包**零 CSS**:唯一的样式是隐藏 live region 的内联样式 ——
|
|
142
|
+
* 它不是装饰,而是「元素必须存在于无障碍树里、但不占视觉空间」这一语义的一部分。
|
|
143
|
+
*/
|
|
144
|
+
/**
|
|
145
|
+
* 播报文本最多的条目数。
|
|
146
|
+
*
|
|
147
|
+
* `Polite.js`:`const MAX_COUNT = 50;` —— 注释写得很直白:
|
|
148
|
+
* "Only cut part of values since it's a screen reader"(读屏而已,不用播全)。
|
|
149
|
+
*/
|
|
150
|
+
declare const LIVE_REGION_MAX_COUNT = 50;
|
|
151
|
+
/**
|
|
152
|
+
* 视觉隐藏样式。
|
|
153
|
+
*
|
|
154
|
+
* 逐条来自 `components/image/Progress.tsx:7-17`:
|
|
155
|
+
* ```js
|
|
156
|
+
* { position: 'absolute', width: 1, height: 1, padding: 0, margin: -1,
|
|
157
|
+
* overflow: 'hidden', clip: 'rect(0, 0, 0, 0)', whiteSpace: 'nowrap', border: 0 }
|
|
158
|
+
* ```
|
|
159
|
+
* (React 会把数字补成 px,这里直接写字符串,因为是往 `element.style` 上赋。)
|
|
160
|
+
*
|
|
161
|
+
* ⚠️ **另一套上游样式是 `Polite.js` 的 `{width:0, height:0, position:'absolute',
|
|
162
|
+
* overflow:'hidden', opacity:0}`** —— 两套不一样,我们选这一套,理由见契约文档 §6 第 2 条:
|
|
163
|
+
* `opacity:0` 在部分读屏实现下会被判为「真正不可见」而整块跳过。
|
|
164
|
+
*/
|
|
165
|
+
declare const VISUALLY_HIDDEN_STYLE: Readonly<Record<string, string>>;
|
|
166
|
+
interface LiveRegionValue {
|
|
167
|
+
/** 显示文本;只有 number / string 会被播报,否则退回 `value` */
|
|
168
|
+
label?: unknown;
|
|
169
|
+
value?: string | number;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 拼播报文本。
|
|
173
|
+
*
|
|
174
|
+
* `Polite.js`:
|
|
175
|
+
* ```js
|
|
176
|
+
* `${values.slice(0, MAX_COUNT).map(({label, value}) =>
|
|
177
|
+
* ['number','string'].includes(typeof label) ? label : value).join(', ')}`
|
|
178
|
+
* // 后面再拼:values.length > MAX_COUNT ? ', ...' : null
|
|
179
|
+
* ```
|
|
180
|
+
*
|
|
181
|
+
* 注意两处细节:
|
|
182
|
+
* - 分隔符是**半角逗号加空格** `', '`
|
|
183
|
+
* - 超长时追加的是 `', ...'`(半角逗号)
|
|
184
|
+
* - `join` 会把 `undefined` 变成空串 —— 这正是上游的行为,不要「过滤掉空项」
|
|
185
|
+
*/
|
|
186
|
+
declare function formatLiveRegionText(values: readonly LiveRegionValue[], maxCount?: number): string;
|
|
187
|
+
interface LiveRegionOptions {
|
|
188
|
+
/** 默认取全局 `document` */
|
|
189
|
+
doc?: Document;
|
|
190
|
+
/** 默认 `'status'`(隐含 `aria-live="polite"`,与 `Progress.tsx:125` 一致) */
|
|
191
|
+
role?: string;
|
|
192
|
+
/**
|
|
193
|
+
* 默认 `'polite'`。
|
|
194
|
+
*
|
|
195
|
+
* ⚠️ `'assertive'` 会**打断**读屏当前正在读的内容。上游只用 `polite`
|
|
196
|
+
* (见契约文档 §8 P2),所以这里也不提供默认值之外的快捷方式。
|
|
197
|
+
*/
|
|
198
|
+
live?: 'polite' | 'assertive';
|
|
199
|
+
className?: string;
|
|
200
|
+
}
|
|
201
|
+
interface LiveRegionHandle {
|
|
202
|
+
element: HTMLElement;
|
|
203
|
+
setText(text: string): void;
|
|
204
|
+
destroy(): void;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 建一个隐藏的 live region 并挂到 `document.body`。
|
|
208
|
+
*
|
|
209
|
+
* `Polite.js` 是**渲染在组件里**的,而 `Progress.tsx` 也是内联渲染 ——
|
|
210
|
+
* 两者都依赖 React 的渲染树。本包不产出组件(边界见契约文档 §4),
|
|
211
|
+
* 所以这里直接操作 DOM:调用方只需要一个「往里塞文本」的口子。
|
|
212
|
+
*
|
|
213
|
+
* @throws 没有 DOM 时抛错 —— live region 必须是真实节点才可能被读屏发现,
|
|
214
|
+
* 静默返回空壳会让「播报没生效」变成无法察觉的问题。
|
|
215
|
+
* 组件内请用 `useLiveRegion`(它在挂载后才建)。
|
|
216
|
+
*/
|
|
217
|
+
declare function createLiveRegion(options?: LiveRegionOptions): LiveRegionHandle;
|
|
218
|
+
interface UseLiveRegionReturn {
|
|
219
|
+
/** 播报当前文本;挂载前调用保留最新文本,挂载后写入本实例节点。 */
|
|
220
|
+
announce(text: string): void;
|
|
221
|
+
/** 播报一组值(走 `formatLiveRegionText` 的截断规则) */
|
|
222
|
+
announceValues(values: readonly LiveRegionValue[], maxCount?: number): void;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 播报一段文本(模块级单例,不需要组件上下文)。
|
|
226
|
+
*
|
|
227
|
+
* 适用于「没有宿主组件」的场景:`message` / `notification` 的命令式 API
|
|
228
|
+
* 就是这样被调用的 —— 它们没有可用的 setup 作用域。
|
|
229
|
+
*
|
|
230
|
+
* ⚠️ 节点建好后**不会自动销毁** —— 它是进程级的。测试里用
|
|
231
|
+
* `resetAnnounceRegion()` 清掉,否则会跨用例串台。
|
|
232
|
+
*
|
|
233
|
+
* @throws 没有 DOM 时抛错(同 `createLiveRegion`)
|
|
234
|
+
*/
|
|
235
|
+
declare function announce(text: string): void;
|
|
236
|
+
/** 播报一组值(走 `formatLiveRegionText` 的截断规则)。 */
|
|
237
|
+
declare function announceValues(values: readonly LiveRegionValue[], maxCount?: number): void;
|
|
238
|
+
/** 测试辅助:销毁共享节点。生产代码不应调用。 */
|
|
239
|
+
declare function resetAnnounceRegion(): void;
|
|
240
|
+
/**
|
|
241
|
+
* Vue 包装:挂载时建、卸载时销毁。
|
|
242
|
+
*
|
|
243
|
+
* ⚠️ 必须在 `onMounted` 里建而不是 setup 里 —— SSR 下没有 `document`,
|
|
244
|
+
* 而且 `onScopeDispose` 与 `onMounted` 配对才能保证「建了就一定会被销毁」。
|
|
245
|
+
*/
|
|
246
|
+
declare function useLiveRegion(options?: LiveRegionOptions): UseLiveRegionReturn;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* roving tabindex —— 「一组控件只占一个 Tab 停靠点,组内用方向键移动」。
|
|
250
|
+
*
|
|
251
|
+
* 契约来源:`@rc-component/menu@1.5.0/es/hooks/useAccessibility.js:113-138`
|
|
252
|
+
* (`getNextFocusElement` 的环绕算术)与 `:216-221`(HOME / END)。
|
|
253
|
+
*
|
|
254
|
+
* ⚠️ 只有**索引算术**是可移植的。上游 `getOffset(mode, isRootLevel, isRtl, which)`
|
|
255
|
+
* 那六张 inline / horizontal / vertical × root / sub 分派表是 **Menu 的私有语义**,
|
|
256
|
+
* 不属于通用原语 —— 调用方自己决定「哪个键对应哪个 offset」。
|
|
257
|
+
*
|
|
258
|
+
* 本文件是纯函数:不涉及 DOM,可以在 L1 里穷举。
|
|
259
|
+
*/
|
|
260
|
+
/** 没有可聚焦项时的哨兵。与 `Array.prototype.findIndex` 的约定一致。 */
|
|
261
|
+
declare const NO_ACTIVE_INDEX = -1;
|
|
262
|
+
/**
|
|
263
|
+
* 环绕地取下一个下标。
|
|
264
|
+
*
|
|
265
|
+
* 逐行对应 `useAccessibility.js:126-137`:
|
|
266
|
+
* ```js
|
|
267
|
+
* if (offset < 0) {
|
|
268
|
+
* if (focusIndex === -1) focusIndex = count - 1; // 没有当前项 ⇒ 从尾开始
|
|
269
|
+
* else focusIndex -= 1;
|
|
270
|
+
* } else if (offset > 0) {
|
|
271
|
+
* focusIndex += 1; // 没有当前项 ⇒ -1+1 = 0,从头开始
|
|
272
|
+
* }
|
|
273
|
+
* focusIndex = (focusIndex + count) % count;
|
|
274
|
+
* ```
|
|
275
|
+
*
|
|
276
|
+
* 两条容易写反的语义:
|
|
277
|
+
* - `offset > 0` 且 `focusIndex === -1` ⇒ 得到 `0`(**从头**)
|
|
278
|
+
* - `offset < 0` 且 `focusIndex === -1` ⇒ 得到 `count - 1`(**从尾**)
|
|
279
|
+
*
|
|
280
|
+
* @param focusIndex 当前下标;`-1` 表示「还没有当前项」
|
|
281
|
+
* @param offset `> 0` 向后、`< 0` 向前、`0` 原地
|
|
282
|
+
* @param count 总项数
|
|
283
|
+
* @returns 下一个下标;`count <= 0` 时返回 `NO_ACTIVE_INDEX`
|
|
284
|
+
*
|
|
285
|
+
* 零偏移严格保持当前值,包括未激活的 `-1`,不因归一化而跳到末项。
|
|
286
|
+
*
|
|
287
|
+
* ⚠️ 与上游的一处**有意**差异:`count <= 0` 时上游会算出 `NaN`(`(x + 0) % 0`),
|
|
288
|
+
* 然后 `list[NaN]` 得到 `undefined`。我们返回 `-1` —— 对调用方而言同样是「没有目标」,
|
|
289
|
+
* 但 `-1` 是可用下标哨兵,不会把 NaN 带进后续算术。见契约文档 §6。
|
|
290
|
+
*/
|
|
291
|
+
declare function nextRovingIndex(focusIndex: number, offset: number, count: number): number;
|
|
292
|
+
/** `HOME` —— 跳到第一项(`useAccessibility.js:217`)。 */
|
|
293
|
+
declare function resolveRovingHome(count: number): number;
|
|
294
|
+
/** `END` —— 跳到最后一项(`useAccessibility.js:219`)。 */
|
|
295
|
+
declare function resolveRovingEnd(count: number): number;
|
|
296
|
+
/**
|
|
297
|
+
* 单个项的 `tabindex`:只有「当前项」是 0,其余全 -1。
|
|
298
|
+
*
|
|
299
|
+
* 这就是 roving 的全部形态语义 —— 整组在 Tab 序列里只占一个停靠点。
|
|
300
|
+
*/
|
|
301
|
+
declare function getRovingTabIndex(activeIndex: number, itemIndex: number): 0 | -1;
|
|
302
|
+
/**
|
|
303
|
+
* 同 `nextRovingIndex`,但可以关掉环绕。
|
|
304
|
+
*
|
|
305
|
+
* ⚠️ **上游没有这个函数** —— rc-menu 永远环绕。这是给「不循环」的控件
|
|
306
|
+
* (例如某些 Toolbar / Segmented 的语义)留的口子,默认仍是环绕。
|
|
307
|
+
*
|
|
308
|
+
* 不循环时:
|
|
309
|
+
* - 已经到头 ⇒ 停在原地(`clamp`)
|
|
310
|
+
* - 还没有当前项 ⇒ 向后取第 0 项、向前取最后一项(与环绕版一致)
|
|
311
|
+
*/
|
|
312
|
+
declare function moveRovingIndex(current: number, offset: number, count: number, loop?: boolean): number;
|
|
313
|
+
/** 方向键的解释方式。 */
|
|
314
|
+
type RovingOrientation = 'vertical' | 'horizontal' | 'both';
|
|
315
|
+
/**
|
|
316
|
+
* 把方向键翻译成 offset。
|
|
317
|
+
*
|
|
318
|
+
* ⚠️ **这不是 rc-menu 的分派表**。上游的 `getOffset(mode, isRootLevel, isRtl, which)`
|
|
319
|
+
* 里 `vertical` 的 LEFT/RIGHT 映射的是「父/子菜单」(层级跳转),不是「前一项/后一项」——
|
|
320
|
+
* 那是 Menu 私有的层级语义(§3.6),不属于通用原语。
|
|
321
|
+
*
|
|
322
|
+
* 这里用的是 **APG(ARIA Authoring Practices)的通用映射**:
|
|
323
|
+
* - `vertical`:Up/Down 移动,Left/Right 不管
|
|
324
|
+
* - `horizontal`:Left/Right 移动,Up/Down 不管
|
|
325
|
+
* - `both`:四个方向都移动
|
|
326
|
+
* - RTL 时 Left/Right 互换
|
|
327
|
+
*
|
|
328
|
+
* @returns offset;该键在此方向下不参与导航时返回 `null`
|
|
329
|
+
*/
|
|
330
|
+
declare function getRovingOffset(orientation: RovingOrientation, rtl: boolean, key: string): number | null;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* `useActiveDescendant` —— combobox 的 `aria-activedescendant` 管理。
|
|
334
|
+
*
|
|
335
|
+
* 适用于「焦点留在输入框上、用 `aria-activedescendant` 指向当前选项」的控件
|
|
336
|
+
* (Select / AutoComplete / TreeSelect / Cascader)。
|
|
337
|
+
*
|
|
338
|
+
* 三个 id 的关系(`OptionList.js:237-281`):
|
|
339
|
+
* ```
|
|
340
|
+
* 输入框 aria-controls = `${baseId}_list`
|
|
341
|
+
* aria-activedescendant = `${baseId}_list_${index}`
|
|
342
|
+
* 列表框 id = `${baseId}_list`
|
|
343
|
+
* 选项 id = `${baseId}_list_${index}`
|
|
344
|
+
* ```
|
|
345
|
+
*
|
|
346
|
+
* ⚠️ 本组合式**只产出 id**,不渲染任何东西 —— DOM 结构属组件层(契约文档 §4)。
|
|
347
|
+
* `baseId` 复用 `@apollo-design/utils` 的 `useId`,本包不重复实现(`notDo` 第三条)。
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
interface UseActiveDescendantOptions {
|
|
351
|
+
/** 显式指定基 id;不传则由 `useId` 生成 */
|
|
352
|
+
id?: string;
|
|
353
|
+
/** 当前项下标;`-1` / `undefined` 表示没有活动项(此时 `aria-activedescendant` 应为 `undefined`) */
|
|
354
|
+
activeIndex?: MaybeRefOrGetter<number | undefined>;
|
|
355
|
+
}
|
|
356
|
+
interface UseActiveDescendantReturn {
|
|
357
|
+
/** 基 id */
|
|
358
|
+
baseId: string;
|
|
359
|
+
/** 列表框的 id,也是输入框 `aria-controls` 的取值 */
|
|
360
|
+
listboxId: string;
|
|
361
|
+
/** 输入框 `aria-activedescendant` 的取值;没有活动项时是 `undefined` */
|
|
362
|
+
activeDescendantId: ComputedRef<string | undefined>;
|
|
363
|
+
/** 第 `index` 个选项的 id */
|
|
364
|
+
getOptionId(index: number): string;
|
|
365
|
+
}
|
|
366
|
+
declare function useActiveDescendant(options?: UseActiveDescendantOptions): UseActiveDescendantReturn;
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* `useRovingFocus` —— roving tabindex 的组合式封装(Menu / Tabs / Radio.Group / Toolbar)。
|
|
370
|
+
*
|
|
371
|
+
* 纯逻辑在 `roving.ts`,这里只负责三件事:
|
|
372
|
+
* 1. 把「当前项」做成响应式状态(支持受控 / 非受控)
|
|
373
|
+
* 2. 把键盘事件翻译成移动
|
|
374
|
+
* 3. 移动之后真的 `focus()` 到目标元素
|
|
375
|
+
*
|
|
376
|
+
* 本文件**不注册任何全局监听** —— 键盘事件由调用方在自己的容器上绑定后转发进来,
|
|
377
|
+
* 这样「谁响应方向键」的边界是显式的,不会像全局监听那样误吞外部的方向键。
|
|
378
|
+
*/
|
|
379
|
+
|
|
380
|
+
interface UseRovingFocusOptions {
|
|
381
|
+
/** 项数。响应式 —— 列表增删后不用重建组合式 */
|
|
382
|
+
count: MaybeRefOrGetter<number>;
|
|
383
|
+
/** 受控当前项;`-1` 表示无当前项,undefined 撤控并保留最后接收的值。 */
|
|
384
|
+
activeIndex?: MaybeRefOrGetter<number | undefined>;
|
|
385
|
+
/** 默认 `'vertical'` */
|
|
386
|
+
orientation?: MaybeRefOrGetter<RovingOrientation>;
|
|
387
|
+
/** 默认 `false`;`horizontal` 下左右互换 */
|
|
388
|
+
rtl?: MaybeRefOrGetter<boolean>;
|
|
389
|
+
/** 是否环绕。默认 `true`(与 rc-menu 一致) */
|
|
390
|
+
loop?: MaybeRefOrGetter<boolean>;
|
|
391
|
+
/** 取第 `index` 个元素,用于真正移动焦点。不传则只改状态 */
|
|
392
|
+
getItem?: (index: number) => HTMLElement | null;
|
|
393
|
+
/** 提议当前项;受控用法由调用方接受,拒绝时不提前改状态或焦点。 */
|
|
394
|
+
onChange?: (index: number) => void;
|
|
395
|
+
/** 默认 `true`:移动后调 `getItem(index)?.focus()`。传 `false` 可只改状态 */
|
|
396
|
+
focusOnMove?: MaybeRefOrGetter<boolean>;
|
|
397
|
+
}
|
|
398
|
+
interface UseRovingFocusReturn {
|
|
399
|
+
/** 当前项下标;写入同 setActive,`-1` 表示还没有当前项。 */
|
|
400
|
+
activeIndex: Ref<number>;
|
|
401
|
+
/** 第 `index` 项该挂的 `tabindex` */
|
|
402
|
+
getTabIndex(index: number): 0 | -1;
|
|
403
|
+
setActive(index: number, focus?: boolean): void;
|
|
404
|
+
/** 相对移动 */
|
|
405
|
+
move(offset: number): void;
|
|
406
|
+
moveHome(): void;
|
|
407
|
+
moveEnd(): void;
|
|
408
|
+
/**
|
|
409
|
+
* 处理一个键盘事件。
|
|
410
|
+
* @returns 是否已消费并 preventDefault;调用方据此决定是否继续派发。
|
|
411
|
+
*/
|
|
412
|
+
onKeyDown(event: KeyboardEvent): boolean;
|
|
413
|
+
}
|
|
414
|
+
declare function useRovingFocus(options: UseRovingFocusOptions): UseRovingFocusReturn;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* `useTypeahead` —— 敲字符快速跳到匹配项(Select / Tree / Menu)。
|
|
418
|
+
*
|
|
419
|
+
* ⚠️ **上游没有对应实现**(antd 与 rc-* 全量搜索 `typeahead` 零命中),
|
|
420
|
+
* 语义由本项目定义,见契约文档 §3.9。因此本文件的每一条判据都写清了理由。
|
|
421
|
+
*
|
|
422
|
+
* 规则:
|
|
423
|
+
* 1. 只有可打印字符参与(`isTypeaheadKey`)
|
|
424
|
+
* 2. 两次按键间隔超过 `resetDelay` ⇒ 重新起头
|
|
425
|
+
* 3. 从当前项**下一项**开始全表找一圈
|
|
426
|
+
* 4. ⭐ 累积串没命中时**退化成单字符再找一次** —— 这就是「连按同一个键在匹配项之间循环」
|
|
427
|
+
* 的实现:按 `a` 命中第 0 项,再按 `a` 时 buffer 是 `"aa"`(通常无命中),
|
|
428
|
+
* 于是退回 `"a"` 从第 0 项之后继续找 ⇒ 第 1 项。
|
|
429
|
+
* 不做这一步的话,第二次按同一个键会「什么都不发生」。
|
|
430
|
+
*/
|
|
431
|
+
|
|
432
|
+
interface UseTypeaheadOptions {
|
|
433
|
+
/** 候选项的文本(响应式) */
|
|
434
|
+
labels: MaybeRefOrGetter<readonly string[]>;
|
|
435
|
+
/** 当前项下标;`-1` / `undefined` 表示还没有 */
|
|
436
|
+
activeIndex?: MaybeRefOrGetter<number | undefined>;
|
|
437
|
+
/** 超时阈值,默认 `DEFAULT_TYPEAHEAD_RESET_DELAY` */
|
|
438
|
+
resetDelay?: number;
|
|
439
|
+
/** 命中时通知(通常是 `roving.setActive(index)`) */
|
|
440
|
+
onMatch?: (index: number) => void;
|
|
441
|
+
/** 时间源,便于测试注入;默认 `Date.now` */
|
|
442
|
+
now?: () => number;
|
|
443
|
+
}
|
|
444
|
+
interface UseTypeaheadReturn {
|
|
445
|
+
/** 当前累积的搜索串(只读用途,例如显示在状态栏) */
|
|
446
|
+
buffer: Ref<string>;
|
|
447
|
+
/** 处理一个键盘事件;返回是否命中并消费了它 */
|
|
448
|
+
onKeyDown(event: KeyboardEvent): boolean;
|
|
449
|
+
/** 清空缓冲区(例如失焦时) */
|
|
450
|
+
reset(): void;
|
|
451
|
+
}
|
|
452
|
+
declare function useTypeahead(options: UseTypeaheadOptions): UseTypeaheadReturn;
|
|
453
|
+
|
|
454
|
+
export { DEFAULT_TYPEAHEAD_RESET_DELAY, INITIAL_TYPEAHEAD_STATE, LIVE_REGION_MAX_COUNT, NO_ACTIVE_INDEX, VISUALLY_HIDDEN_STYLE, announce, announceValues, createLiveRegion, findTypeaheadIndex, formatLiveRegionText, getListboxId, getOptionId, getRovingOffset, getRovingTabIndex, isTypeaheadKey, moveRovingIndex, nextRovingIndex, pushTypeaheadChar, resetAnnounceRegion, resolveRovingEnd, resolveRovingHome, useActiveDescendant, useFocusRestore, useLiveRegion, useRovingFocus, useTypeahead };
|
|
455
|
+
export type { FocusRestoreHandle, FocusRestoreOptions, LiveRegionHandle, LiveRegionOptions, LiveRegionValue, RovingOrientation, TypeaheadState, UseActiveDescendantOptions, UseActiveDescendantReturn, UseLiveRegionReturn, UseRovingFocusOptions, UseRovingFocusReturn, UseTypeaheadOptions, UseTypeaheadReturn };
|