@lytjs/common-a11y 6.0.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/dist/index.cjs ADDED
@@ -0,0 +1,358 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ function getTabIndex(disabled, customTabIndex) {
5
+ if (customTabIndex !== void 0) return customTabIndex;
6
+ return disabled ? -1 : 0;
7
+ }
8
+ function getButtonA11yProps(props) {
9
+ return {
10
+ role: "button",
11
+ "aria-label": props.ariaLabel,
12
+ "aria-describedby": props.ariaDescribedBy,
13
+ "aria-labelledby": props.ariaLabelledBy,
14
+ "aria-disabled": props.ariaDisabled ?? props.disabled,
15
+ "aria-pressed": props.ariaPressed,
16
+ "tabindex": getTabIndex(props.disabled ?? !!props.ariaDisabled, props.tabIndex),
17
+ id: props.id
18
+ };
19
+ }
20
+ function getFormControlA11yProps(props) {
21
+ return {
22
+ "aria-label": props.ariaLabel,
23
+ "aria-describedby": props.ariaDescribedBy,
24
+ "aria-labelledby": props.ariaLabelledBy,
25
+ "aria-required": props.ariaRequired ?? props.required,
26
+ "aria-invalid": props.ariaInvalid ?? props.invalid,
27
+ "aria-disabled": props.ariaDisabled ?? props.disabled,
28
+ tabindex: getTabIndex(props.disabled ?? !!props.ariaDisabled, props.tabIndex),
29
+ id: props.id
30
+ };
31
+ }
32
+ function getInputControlA11yProps(props) {
33
+ return {
34
+ ...getFormControlA11yProps(props),
35
+ "aria-checked": props.checked
36
+ };
37
+ }
38
+ function getSwitchA11yProps(props) {
39
+ return {
40
+ role: "switch",
41
+ "aria-checked": props.checked,
42
+ ...getFormControlA11yProps(props)
43
+ };
44
+ }
45
+ function getComboboxA11yProps(props) {
46
+ return {
47
+ role: "combobox",
48
+ "aria-expanded": props.expanded,
49
+ "aria-controls": props.ariaControls ?? props.controls,
50
+ "aria-haspopup": "listbox",
51
+ ...getFormControlA11yProps(props)
52
+ };
53
+ }
54
+ function getOptionA11yProps(props) {
55
+ return {
56
+ role: "option",
57
+ "aria-selected": props.selected,
58
+ "aria-disabled": props.disabled,
59
+ tabindex: props.disabled ? -1 : 0,
60
+ id: props.id
61
+ };
62
+ }
63
+ function getSliderA11yProps(props) {
64
+ return {
65
+ role: "slider",
66
+ "aria-valuenow": props.value,
67
+ "aria-valuemin": props.min,
68
+ "aria-valuemax": props.max,
69
+ ...getFormControlA11yProps(props)
70
+ };
71
+ }
72
+ function getSpinbuttonA11yProps(props) {
73
+ return {
74
+ role: "spinbutton",
75
+ "aria-valuenow": props.value,
76
+ "aria-valuemin": props.min,
77
+ "aria-valuemax": props.max,
78
+ ...getFormControlA11yProps(props)
79
+ };
80
+ }
81
+ function getTablistA11yProps(props) {
82
+ return {
83
+ role: "tablist",
84
+ "aria-label": props.ariaLabel ?? props.label,
85
+ "aria-describedby": props.ariaDescribedBy,
86
+ id: props.id
87
+ };
88
+ }
89
+ function getTabA11yProps(props) {
90
+ return {
91
+ role: "tab",
92
+ "aria-selected": props.selected,
93
+ "aria-disabled": props.disabled,
94
+ "aria-controls": props.ariaControls ?? props.controls,
95
+ tabindex: props.selected ? 0 : -1,
96
+ id: props.id
97
+ };
98
+ }
99
+ function getTabpanelA11yProps(props) {
100
+ return {
101
+ role: "tabpanel",
102
+ "aria-labelledby": props.ariaLabelledBy ?? props.labelledBy,
103
+ "aria-hidden": props.hidden,
104
+ id: props.id
105
+ };
106
+ }
107
+ function getDialogA11yProps(props) {
108
+ return {
109
+ role: "dialog",
110
+ "aria-modal": props.ariaModal ?? props.modal ?? true,
111
+ "aria-labelledby": props.ariaLabelledBy ?? props.labelledBy,
112
+ "aria-describedby": props.ariaDescribedBy ?? props.describedBy,
113
+ "aria-label": props.ariaLabel,
114
+ id: props.id
115
+ };
116
+ }
117
+ function getGroupA11yProps(props) {
118
+ return {
119
+ role: props.role ?? "group",
120
+ "aria-label": props.ariaLabel ?? props.label,
121
+ "aria-describedby": props.ariaDescribedBy,
122
+ "aria-required": props.ariaRequired ?? props.required,
123
+ id: props.id
124
+ };
125
+ }
126
+ function mergeA11yProps(...propsList) {
127
+ const result = {};
128
+ for (const props of propsList) {
129
+ for (const [key, value] of Object.entries(props)) {
130
+ if (value !== void 0 && value !== null) {
131
+ result[key] = value;
132
+ }
133
+ }
134
+ }
135
+ return Object.fromEntries(
136
+ Object.entries(result).filter(([_, v]) => v !== void 0 && v !== null)
137
+ );
138
+ }
139
+ var ARIA_ROLES = {
140
+ alert: ["aria-live"],
141
+ alertdialog: ["aria-labelledby", "aria-describedby"],
142
+ button: [],
143
+ checkbox: ["aria-checked"],
144
+ combobox: ["aria-expanded", "aria-controls"],
145
+ dialog: ["aria-labelledby", "aria-describedby"],
146
+ grid: [],
147
+ gridcell: [],
148
+ link: [],
149
+ listbox: ["aria-label"],
150
+ menu: ["aria-label"],
151
+ menubar: [],
152
+ menuitem: [],
153
+ option: ["aria-selected"],
154
+ progressbar: ["aria-valuenow"],
155
+ radio: ["aria-checked"],
156
+ radiogroup: ["aria-label"],
157
+ slider: ["aria-valuenow"],
158
+ spinbutton: ["aria-valuenow"],
159
+ tab: ["aria-selected"],
160
+ tablist: [],
161
+ tabpanel: ["aria-labelledby"],
162
+ textbox: [],
163
+ tree: ["aria-label"],
164
+ treeitem: ["aria-selected"]
165
+ };
166
+ var FOCUSABLE_SELECTOR = [
167
+ "a[href]",
168
+ "area[href]",
169
+ "button:not([disabled])",
170
+ "input:not([disabled])",
171
+ "select:not([disabled])",
172
+ "textarea:not([disabled])",
173
+ '[tabindex]:not([tabindex="-1"])',
174
+ '[contenteditable="true"]'
175
+ ].join(", ");
176
+ function isFocusable(element) {
177
+ if (!(element instanceof HTMLElement)) return false;
178
+ if ("disabled" in element && element.disabled) return false;
179
+ if (element.getAttribute("tabindex") === "-1") return false;
180
+ if (element.getAttribute("aria-hidden") === "true") return false;
181
+ const tag = element.tagName.toLowerCase();
182
+ const focusableTags = /* @__PURE__ */ new Set([
183
+ "a",
184
+ "button",
185
+ "input",
186
+ "select",
187
+ "textarea",
188
+ "details",
189
+ "summary"
190
+ ]);
191
+ if (focusableTags.has(tag)) return true;
192
+ if (element.getAttribute("tabindex") !== null) return true;
193
+ if (element.isContentEditable || element.getAttribute("contenteditable") === "true") return true;
194
+ return false;
195
+ }
196
+ function getFocusableElements(container) {
197
+ const elements = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
198
+ return elements.filter(
199
+ (el) => el instanceof HTMLElement && isFocusable(el)
200
+ );
201
+ }
202
+ function focusTrap(container, options) {
203
+ const { initialFocus, escapeDeactivates = true } = options || {};
204
+ const focusableElements = getFocusableElements(container);
205
+ const firstElement = focusableElements[0] || container;
206
+ const lastElement = focusableElements[focusableElements.length - 1] || container;
207
+ if (initialFocus) {
208
+ initialFocus.focus();
209
+ } else {
210
+ firstElement.focus();
211
+ }
212
+ const handleKeyDown = (event) => {
213
+ if (event.key === "Escape" && escapeDeactivates) {
214
+ cleanup();
215
+ return;
216
+ }
217
+ if (event.key !== "Tab") return;
218
+ if (focusableElements.length === 0) {
219
+ event.preventDefault();
220
+ return;
221
+ }
222
+ if (event.shiftKey) {
223
+ if (document.activeElement === firstElement) {
224
+ event.preventDefault();
225
+ lastElement.focus();
226
+ }
227
+ } else {
228
+ if (document.activeElement === lastElement) {
229
+ event.preventDefault();
230
+ firstElement.focus();
231
+ }
232
+ }
233
+ };
234
+ document.addEventListener("keydown", handleKeyDown);
235
+ const cleanup = () => {
236
+ document.removeEventListener("keydown", handleKeyDown);
237
+ };
238
+ return cleanup;
239
+ }
240
+ function manageFocus(container, triggerEl) {
241
+ const previousFocus = document.activeElement;
242
+ const focusableElements = getFocusableElements(container);
243
+ if (focusableElements.length > 0) {
244
+ focusableElements[0].focus();
245
+ } else {
246
+ container.setAttribute("tabindex", "-1");
247
+ container.focus();
248
+ }
249
+ return () => {
250
+ const target = triggerEl || previousFocus;
251
+ if (target && typeof target.focus === "function") {
252
+ target.focus();
253
+ }
254
+ };
255
+ }
256
+ function getAriaProps(element) {
257
+ const result = {};
258
+ const attrs = element.attributes;
259
+ for (let i = 0; i < attrs.length; i++) {
260
+ const attr = attrs[i];
261
+ if (attr.name.startsWith("aria-")) {
262
+ result[attr.name] = attr.value;
263
+ }
264
+ }
265
+ return result;
266
+ }
267
+ function setAriaProps(element, props) {
268
+ for (const key of Object.keys(props)) {
269
+ if (key.startsWith("aria-")) {
270
+ element.setAttribute(key, props[key]);
271
+ }
272
+ }
273
+ }
274
+ function assertActiveElement(element) {
275
+ return document.activeElement === element;
276
+ }
277
+ function getNextEnabledIndex(currentIndex, totalItems, isEnabled, direction = "forward") {
278
+ const step = direction === "forward" ? 1 : -1;
279
+ let nextIndex = (currentIndex + step + totalItems) % totalItems;
280
+ for (let i = 0; i < totalItems; i++) {
281
+ if (isEnabled(nextIndex)) {
282
+ return nextIndex;
283
+ }
284
+ nextIndex = (nextIndex + step + totalItems) % totalItems;
285
+ }
286
+ return currentIndex;
287
+ }
288
+ function handleListKeydown(event, currentIndex, totalItems, isEnabled, onSelect, onClose) {
289
+ switch (event.key) {
290
+ case "ArrowDown":
291
+ case "ArrowRight":
292
+ event.preventDefault();
293
+ onSelect(getNextEnabledIndex(currentIndex, totalItems, isEnabled, "forward"));
294
+ break;
295
+ case "ArrowUp":
296
+ case "ArrowLeft":
297
+ event.preventDefault();
298
+ onSelect(getNextEnabledIndex(currentIndex, totalItems, isEnabled, "backward"));
299
+ break;
300
+ case "Home":
301
+ event.preventDefault();
302
+ for (let i = 0; i < totalItems; i++) {
303
+ if (isEnabled(i)) {
304
+ onSelect(i);
305
+ break;
306
+ }
307
+ }
308
+ break;
309
+ case "End":
310
+ event.preventDefault();
311
+ for (let i = totalItems - 1; i >= 0; i--) {
312
+ if (isEnabled(i)) {
313
+ onSelect(i);
314
+ break;
315
+ }
316
+ }
317
+ break;
318
+ case "Enter":
319
+ case " ":
320
+ event.preventDefault();
321
+ if (isEnabled(currentIndex)) {
322
+ onSelect(currentIndex);
323
+ }
324
+ break;
325
+ case "Escape":
326
+ event.preventDefault();
327
+ onClose?.();
328
+ break;
329
+ }
330
+ }
331
+
332
+ exports.ARIA_ROLES = ARIA_ROLES;
333
+ exports.assertActiveElement = assertActiveElement;
334
+ exports.focusTrap = focusTrap;
335
+ exports.getAriaProps = getAriaProps;
336
+ exports.getButtonA11yProps = getButtonA11yProps;
337
+ exports.getComboboxA11yProps = getComboboxA11yProps;
338
+ exports.getDialogA11yProps = getDialogA11yProps;
339
+ exports.getFocusableElements = getFocusableElements;
340
+ exports.getFormControlA11yProps = getFormControlA11yProps;
341
+ exports.getGroupA11yProps = getGroupA11yProps;
342
+ exports.getInputControlA11yProps = getInputControlA11yProps;
343
+ exports.getNextEnabledIndex = getNextEnabledIndex;
344
+ exports.getOptionA11yProps = getOptionA11yProps;
345
+ exports.getSliderA11yProps = getSliderA11yProps;
346
+ exports.getSpinbuttonA11yProps = getSpinbuttonA11yProps;
347
+ exports.getSwitchA11yProps = getSwitchA11yProps;
348
+ exports.getTabA11yProps = getTabA11yProps;
349
+ exports.getTabIndex = getTabIndex;
350
+ exports.getTablistA11yProps = getTablistA11yProps;
351
+ exports.getTabpanelA11yProps = getTabpanelA11yProps;
352
+ exports.handleListKeydown = handleListKeydown;
353
+ exports.isFocusable = isFocusable;
354
+ exports.manageFocus = manageFocus;
355
+ exports.mergeA11yProps = mergeA11yProps;
356
+ exports.setAriaProps = setAriaProps;
357
+ //# sourceMappingURL=index.cjs.map
358
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * @lytjs/common-a11y\r\n * 轻量级无障碍访问工具\r\n */\r\n\r\ndeclare const __DEV__: boolean;\r\n\r\nexport interface FocusTrapOptions {\r\n initialFocus?: HTMLElement;\r\n escapeDeactivates?: boolean;\r\n}\r\n\r\n/**\r\n * 通用无障碍属性接口\r\n */\r\nexport interface A11yProps {\r\n id?: string;\r\n ariaLabel?: string;\r\n ariaDescribedBy?: string;\r\n ariaLabelledBy?: string;\r\n ariaRequired?: boolean;\r\n ariaInvalid?: boolean;\r\n ariaDisabled?: boolean;\r\n ariaHidden?: boolean;\r\n ariaExpanded?: boolean;\r\n ariaChecked?: boolean | 'mixed';\r\n ariaSelected?: boolean;\r\n ariaPressed?: boolean;\r\n ariaHasPopup?: boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';\r\n ariaControls?: string;\r\n ariaOwns?: string;\r\n ariaLive?: 'off' | 'polite' | 'assertive';\r\n ariaValuenow?: number | string;\r\n ariaValuemax?: number;\r\n ariaValuemin?: number;\r\n ariaModal?: boolean;\r\n tabIndex?: number;\r\n role?: string;\r\n}\r\n\r\n/**\r\n * 生成 tabindex 属性值\r\n */\r\nexport function getTabIndex(disabled: boolean, customTabIndex?: number): number | undefined {\r\n if (customTabIndex !== undefined) return customTabIndex;\r\n return disabled ? -1 : 0;\r\n}\r\n\r\n/**\r\n * 为按钮组件生成 a11y 属性\r\n */\r\nexport function getButtonA11yProps(props: A11yProps & { disabled?: boolean }): Record<string, any> {\r\n return {\r\n role: 'button',\r\n 'aria-label': props.ariaLabel,\r\n 'aria-describedby': props.ariaDescribedBy,\r\n 'aria-labelledby': props.ariaLabelledBy,\r\n 'aria-disabled': props.ariaDisabled ?? props.disabled,\r\n 'aria-pressed': props.ariaPressed,\r\n 'tabindex': getTabIndex(props.disabled ?? !!props.ariaDisabled, props.tabIndex),\r\n id: props.id,\r\n };\r\n}\r\n\r\n/**\r\n * 为表单控件生成 a11y 属性\r\n */\r\nexport function getFormControlA11yProps(props: A11yProps & { disabled?: boolean; required?: boolean; invalid?: boolean }): Record<string, any> {\r\n return {\r\n 'aria-label': props.ariaLabel,\r\n 'aria-describedby': props.ariaDescribedBy,\r\n 'aria-labelledby': props.ariaLabelledBy,\r\n 'aria-required': props.ariaRequired ?? props.required,\r\n 'aria-invalid': props.ariaInvalid ?? props.invalid,\r\n 'aria-disabled': props.ariaDisabled ?? props.disabled,\r\n tabindex: getTabIndex(props.disabled ?? !!props.ariaDisabled, props.tabIndex),\r\n id: props.id,\r\n };\r\n}\r\n\r\n/**\r\n * 为复选框/单选框生成 a11y 属性\r\n */\r\nexport function getInputControlA11yProps(props: A11yProps & { disabled?: boolean; checked?: boolean | 'mixed'; required?: boolean; invalid?: boolean }): Record<string, any> {\r\n return {\r\n ...getFormControlA11yProps(props),\r\n 'aria-checked': props.checked,\r\n };\r\n}\r\n\r\n/**\r\n * 为开关组件生成 a11y 属性\r\n */\r\nexport function getSwitchA11yProps(props: A11yProps & { disabled?: boolean; checked?: boolean; required?: boolean; invalid?: boolean }): Record<string, any> {\r\n return {\r\n role: 'switch',\r\n 'aria-checked': props.checked,\r\n ...getFormControlA11yProps(props),\r\n };\r\n}\r\n\r\n/**\r\n * 为下拉选择组件生成 a11y 属性\r\n */\r\nexport function getComboboxA11yProps(props: A11yProps & { disabled?: boolean; expanded?: boolean; controls?: string; required?: boolean; invalid?: boolean }): Record<string, any> {\r\n return {\r\n role: 'combobox',\r\n 'aria-expanded': props.expanded,\r\n 'aria-controls': props.ariaControls ?? props.controls,\r\n 'aria-haspopup': 'listbox',\r\n ...getFormControlA11yProps(props),\r\n };\r\n}\r\n\r\n/**\r\n * 为列表框选项生成 a11y 属性\r\n */\r\nexport function getOptionA11yProps(props: A11yProps & { selected?: boolean; disabled?: boolean }): Record<string, any> {\r\n return {\r\n role: 'option',\r\n 'aria-selected': props.selected,\r\n 'aria-disabled': props.disabled,\r\n tabindex: props.disabled ? -1 : 0,\r\n id: props.id,\r\n };\r\n}\r\n\r\n/**\r\n * 为滑块组件生成 a11y 属性\r\n */\r\nexport function getSliderA11yProps(props: A11yProps & { disabled?: boolean; value?: number | string; min?: number; max?: number }): Record<string, any> {\r\n return {\r\n role: 'slider',\r\n 'aria-valuenow': props.value,\r\n 'aria-valuemin': props.min,\r\n 'aria-valuemax': props.max,\r\n ...getFormControlA11yProps(props),\r\n };\r\n}\r\n\r\n/**\r\n * 为数字输入组件生成 a11y 属性\r\n */\r\nexport function getSpinbuttonA11yProps(props: A11yProps & { disabled?: boolean; value?: number | string; min?: number; max?: number }): Record<string, any> {\r\n return {\r\n role: 'spinbutton',\r\n 'aria-valuenow': props.value,\r\n 'aria-valuemin': props.min,\r\n 'aria-valuemax': props.max,\r\n ...getFormControlA11yProps(props),\r\n };\r\n}\r\n\r\n/**\r\n * 为标签页列表生成 a11y 属性\r\n */\r\nexport function getTablistA11yProps(props: A11yProps & { label?: string }): Record<string, any> {\r\n return {\r\n role: 'tablist',\r\n 'aria-label': props.ariaLabel ?? props.label,\r\n 'aria-describedby': props.ariaDescribedBy,\r\n id: props.id,\r\n };\r\n}\r\n\r\n/**\r\n * 为单个标签页生成 a11y 属性\r\n */\r\nexport function getTabA11yProps(props: A11yProps & { selected?: boolean; disabled?: boolean; controls?: string }): Record<string, any> {\r\n return {\r\n role: 'tab',\r\n 'aria-selected': props.selected,\r\n 'aria-disabled': props.disabled,\r\n 'aria-controls': props.ariaControls ?? props.controls,\r\n tabindex: props.selected ? 0 : -1,\r\n id: props.id,\r\n };\r\n}\r\n\r\n/**\r\n * 为标签面板生成 a11y 属性\r\n */\r\nexport function getTabpanelA11yProps(props: A11yProps & { labelledBy?: string; hidden?: boolean }): Record<string, any> {\r\n return {\r\n role: 'tabpanel',\r\n 'aria-labelledby': props.ariaLabelledBy ?? props.labelledBy,\r\n 'aria-hidden': props.hidden,\r\n id: props.id,\r\n };\r\n}\r\n\r\n/**\r\n * 为对话框/模态框生成 a11y 属性\r\n */\r\nexport function getDialogA11yProps(props: A11yProps & { labelledBy?: string; describedBy?: string; modal?: boolean }): Record<string, any> {\r\n return {\r\n role: 'dialog',\r\n 'aria-modal': props.ariaModal ?? props.modal ?? true,\r\n 'aria-labelledby': props.ariaLabelledBy ?? props.labelledBy,\r\n 'aria-describedby': props.ariaDescribedBy ?? props.describedBy,\r\n 'aria-label': props.ariaLabel,\r\n id: props.id,\r\n };\r\n}\r\n\r\n/**\r\n * 为分组组件(checkboxGroup/radioGroup)生成 a11y 属性\r\n */\r\nexport function getGroupA11yProps(props: A11yProps & { role?: 'radiogroup' | 'group' | 'listbox'; required?: boolean; label?: string }): Record<string, any> {\r\n return {\r\n role: props.role ?? 'group',\r\n 'aria-label': props.ariaLabel ?? props.label,\r\n 'aria-describedby': props.ariaDescribedBy,\r\n 'aria-required': props.ariaRequired ?? props.required,\r\n id: props.id,\r\n };\r\n}\r\n\r\n/**\r\n * 合并多个 a11y 属性对象\r\n */\r\nexport function mergeA11yProps(...propsList: Array<Record<string, any>>): Record<string, any> {\r\n const result: Record<string, any> = {};\r\n for (const props of propsList) {\r\n for (const [key, value] of Object.entries(props)) {\r\n if (value !== undefined && value !== null) {\r\n result[key] = value;\r\n }\r\n }\r\n }\r\n // 过滤 undefined 值\r\n return Object.fromEntries(\r\n Object.entries(result).filter(([_, v]) => v !== undefined && v !== null)\r\n );\r\n}\r\n\r\n/** ARIA 角色到必需属性的映射 */\r\nexport const ARIA_ROLES: Record<string, string[]> = {\r\n alert: ['aria-live'],\r\n alertdialog: ['aria-labelledby', 'aria-describedby'],\r\n button: [],\r\n checkbox: ['aria-checked'],\r\n combobox: ['aria-expanded', 'aria-controls'],\r\n dialog: ['aria-labelledby', 'aria-describedby'],\r\n grid: [],\r\n gridcell: [],\r\n link: [],\r\n listbox: ['aria-label'],\r\n menu: ['aria-label'],\r\n menubar: [],\r\n menuitem: [],\r\n option: ['aria-selected'],\r\n progressbar: ['aria-valuenow'],\r\n radio: ['aria-checked'],\r\n radiogroup: ['aria-label'],\r\n slider: ['aria-valuenow'],\r\n spinbutton: ['aria-valuenow'],\r\n tab: ['aria-selected'],\r\n tablist: [],\r\n tabpanel: ['aria-labelledby'],\r\n textbox: [],\r\n tree: ['aria-label'],\r\n treeitem: ['aria-selected'],\r\n};\r\n\r\n/** 可聚焦元素选择器 */\r\nconst FOCUSABLE_SELECTOR = [\r\n 'a[href]',\r\n 'area[href]',\r\n 'button:not([disabled])',\r\n 'input:not([disabled])',\r\n 'select:not([disabled])',\r\n 'textarea:not([disabled])',\r\n '[tabindex]:not([tabindex=\"-1\"])',\r\n '[contenteditable=\"true\"]',\r\n].join(', ');\r\n\r\n/**\r\n * 检查元素是否可聚焦\r\n */\r\nexport function isFocusable(element: Element): boolean {\r\n if (!(element instanceof HTMLElement)) return false;\r\n if ('disabled' in element && (element as { disabled: boolean }).disabled) return false;\r\n if (element.getAttribute('tabindex') === '-1') return false;\r\n if (element.getAttribute('aria-hidden') === 'true') return false;\r\n\r\n const tag = element.tagName.toLowerCase();\r\n const focusableTags = new Set([\r\n 'a', 'button', 'input', 'select', 'textarea',\r\n 'details', 'summary',\r\n ]);\r\n\r\n if (focusableTags.has(tag)) return true;\r\n if (element.getAttribute('tabindex') !== null) return true;\r\n if (element.isContentEditable || element.getAttribute('contenteditable') === 'true') return true;\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * 获取容器内所有可聚焦元素\r\n */\r\nexport function getFocusableElements(container: Element): HTMLElement[] {\r\n const elements = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));\r\n return elements.filter(\r\n (el): el is HTMLElement => el instanceof HTMLElement && isFocusable(el),\r\n );\r\n}\r\n\r\n/**\r\n * 在容器内创建焦点陷阱\r\n *\r\n * @param container - 陷阱容器\r\n * @param options - 配置选项\r\n * @returns 清理函数\r\n */\r\nexport function focusTrap(\r\n container: HTMLElement,\r\n options?: FocusTrapOptions,\r\n): () => void {\r\n const { initialFocus, escapeDeactivates = true } = options || {};\r\n\r\n const focusableElements = getFocusableElements(container);\r\n const firstElement = focusableElements[0] || container;\r\n const lastElement = focusableElements[focusableElements.length - 1] || container;\r\n\r\n // 设置初始焦点\r\n if (initialFocus) {\r\n initialFocus.focus();\r\n } else {\r\n firstElement.focus();\r\n }\r\n\r\n const handleKeyDown = (event: KeyboardEvent) => {\r\n if (event.key === 'Escape' && escapeDeactivates) {\r\n cleanup();\r\n return;\r\n }\r\n\r\n if (event.key !== 'Tab') return;\r\n\r\n if (focusableElements.length === 0) {\r\n event.preventDefault();\r\n return;\r\n }\r\n\r\n if (event.shiftKey) {\r\n if (document.activeElement === firstElement) {\r\n event.preventDefault();\r\n lastElement.focus();\r\n }\r\n } else {\r\n if (document.activeElement === lastElement) {\r\n event.preventDefault();\r\n firstElement.focus();\r\n }\r\n }\r\n };\r\n\r\n document.addEventListener('keydown', handleKeyDown);\r\n\r\n const cleanup = () => {\r\n document.removeEventListener('keydown', handleKeyDown);\r\n };\r\n\r\n return cleanup;\r\n}\r\n\r\n/**\r\n * 管理焦点:保存之前的焦点,将焦点移入容器,返回恢复函数\r\n *\r\n * @param container - 目标容器\r\n * @param triggerEl - 触发元素,恢复焦点时优先回到此元素\r\n * @returns 恢复函数\r\n */\r\nexport function manageFocus(\r\n container: HTMLElement,\r\n triggerEl?: HTMLElement,\r\n): () => void {\r\n const previousFocus = document.activeElement as HTMLElement | null;\r\n\r\n const focusableElements = getFocusableElements(container);\r\n if (focusableElements.length > 0) {\r\n focusableElements[0]!.focus();\r\n } else {\r\n container.setAttribute('tabindex', '-1');\r\n container.focus();\r\n }\r\n\r\n return () => {\r\n const target = triggerEl || previousFocus;\r\n if (target && typeof target.focus === 'function') {\r\n target.focus();\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * 获取元素上所有 aria-* 属性\r\n */\r\nexport function getAriaProps(element: Element): Record<string, string> {\r\n const result: Record<string, string> = {};\r\n const attrs = element.attributes;\r\n for (let i = 0; i < attrs.length; i++) {\r\n const attr = attrs[i]!;\r\n if (attr.name.startsWith('aria-')) {\r\n result[attr.name] = attr.value!;\r\n }\r\n }\r\n return result;\r\n}\r\n\r\n/**\r\n * 批量设置 aria-* 属性\r\n */\r\nexport function setAriaProps(\r\n element: Element,\r\n props: Record<string, string>,\r\n): void {\r\n for (const key of Object.keys(props)) {\r\n if (key.startsWith('aria-')) {\r\n element.setAttribute(key, props[key]!);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * 检查给定元素是否是当前活动元素\r\n */\r\nexport function assertActiveElement(element: Element): boolean {\r\n return document.activeElement === element;\r\n}\r\n\r\n/**\r\n * 键盘导航辅助函数 - 在启用的选项间循环\r\n */\r\nexport function getNextEnabledIndex(\r\n currentIndex: number,\r\n totalItems: number,\r\n isEnabled: (index: number) => boolean,\r\n direction: 'forward' | 'backward' = 'forward'\r\n): number {\r\n const step = direction === 'forward' ? 1 : -1;\r\n let nextIndex = (currentIndex + step + totalItems) % totalItems;\r\n \r\n for (let i = 0; i < totalItems; i++) {\r\n if (isEnabled(nextIndex)) {\r\n return nextIndex;\r\n }\r\n nextIndex = (nextIndex + step + totalItems) % totalItems;\r\n }\r\n \r\n return currentIndex;\r\n}\r\n\r\n/**\r\n * 处理列表组件的键盘导航\r\n */\r\nexport function handleListKeydown(\r\n event: KeyboardEvent,\r\n currentIndex: number,\r\n totalItems: number,\r\n isEnabled: (index: number) => boolean,\r\n onSelect: (index: number) => void,\r\n onClose?: () => void\r\n): void {\r\n switch (event.key) {\r\n case 'ArrowDown':\r\n case 'ArrowRight':\r\n event.preventDefault();\r\n onSelect(getNextEnabledIndex(currentIndex, totalItems, isEnabled, 'forward'));\r\n break;\r\n case 'ArrowUp':\r\n case 'ArrowLeft':\r\n event.preventDefault();\r\n onSelect(getNextEnabledIndex(currentIndex, totalItems, isEnabled, 'backward'));\r\n break;\r\n case 'Home':\r\n event.preventDefault();\r\n for (let i = 0; i < totalItems; i++) {\r\n if (isEnabled(i)) {\r\n onSelect(i);\r\n break;\r\n }\r\n }\r\n break;\r\n case 'End':\r\n event.preventDefault();\r\n for (let i = totalItems - 1; i >= 0; i--) {\r\n if (isEnabled(i)) {\r\n onSelect(i);\r\n break;\r\n }\r\n }\r\n break;\r\n case 'Enter':\r\n case ' ':\r\n event.preventDefault();\r\n if (isEnabled(currentIndex)) {\r\n onSelect(currentIndex);\r\n }\r\n break;\r\n case 'Escape':\r\n event.preventDefault();\r\n onClose?.();\r\n break;\r\n }\r\n}\r\n"]}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * @lytjs/common-a11y
3
+ * 轻量级无障碍访问工具
4
+ */
5
+ interface FocusTrapOptions {
6
+ initialFocus?: HTMLElement;
7
+ escapeDeactivates?: boolean;
8
+ }
9
+ /**
10
+ * 通用无障碍属性接口
11
+ */
12
+ interface A11yProps {
13
+ id?: string;
14
+ ariaLabel?: string;
15
+ ariaDescribedBy?: string;
16
+ ariaLabelledBy?: string;
17
+ ariaRequired?: boolean;
18
+ ariaInvalid?: boolean;
19
+ ariaDisabled?: boolean;
20
+ ariaHidden?: boolean;
21
+ ariaExpanded?: boolean;
22
+ ariaChecked?: boolean | 'mixed';
23
+ ariaSelected?: boolean;
24
+ ariaPressed?: boolean;
25
+ ariaHasPopup?: boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';
26
+ ariaControls?: string;
27
+ ariaOwns?: string;
28
+ ariaLive?: 'off' | 'polite' | 'assertive';
29
+ ariaValuenow?: number | string;
30
+ ariaValuemax?: number;
31
+ ariaValuemin?: number;
32
+ ariaModal?: boolean;
33
+ tabIndex?: number;
34
+ role?: string;
35
+ }
36
+ /**
37
+ * 生成 tabindex 属性值
38
+ */
39
+ declare function getTabIndex(disabled: boolean, customTabIndex?: number): number | undefined;
40
+ /**
41
+ * 为按钮组件生成 a11y 属性
42
+ */
43
+ declare function getButtonA11yProps(props: A11yProps & {
44
+ disabled?: boolean;
45
+ }): Record<string, any>;
46
+ /**
47
+ * 为表单控件生成 a11y 属性
48
+ */
49
+ declare function getFormControlA11yProps(props: A11yProps & {
50
+ disabled?: boolean;
51
+ required?: boolean;
52
+ invalid?: boolean;
53
+ }): Record<string, any>;
54
+ /**
55
+ * 为复选框/单选框生成 a11y 属性
56
+ */
57
+ declare function getInputControlA11yProps(props: A11yProps & {
58
+ disabled?: boolean;
59
+ checked?: boolean | 'mixed';
60
+ required?: boolean;
61
+ invalid?: boolean;
62
+ }): Record<string, any>;
63
+ /**
64
+ * 为开关组件生成 a11y 属性
65
+ */
66
+ declare function getSwitchA11yProps(props: A11yProps & {
67
+ disabled?: boolean;
68
+ checked?: boolean;
69
+ required?: boolean;
70
+ invalid?: boolean;
71
+ }): Record<string, any>;
72
+ /**
73
+ * 为下拉选择组件生成 a11y 属性
74
+ */
75
+ declare function getComboboxA11yProps(props: A11yProps & {
76
+ disabled?: boolean;
77
+ expanded?: boolean;
78
+ controls?: string;
79
+ required?: boolean;
80
+ invalid?: boolean;
81
+ }): Record<string, any>;
82
+ /**
83
+ * 为列表框选项生成 a11y 属性
84
+ */
85
+ declare function getOptionA11yProps(props: A11yProps & {
86
+ selected?: boolean;
87
+ disabled?: boolean;
88
+ }): Record<string, any>;
89
+ /**
90
+ * 为滑块组件生成 a11y 属性
91
+ */
92
+ declare function getSliderA11yProps(props: A11yProps & {
93
+ disabled?: boolean;
94
+ value?: number | string;
95
+ min?: number;
96
+ max?: number;
97
+ }): Record<string, any>;
98
+ /**
99
+ * 为数字输入组件生成 a11y 属性
100
+ */
101
+ declare function getSpinbuttonA11yProps(props: A11yProps & {
102
+ disabled?: boolean;
103
+ value?: number | string;
104
+ min?: number;
105
+ max?: number;
106
+ }): Record<string, any>;
107
+ /**
108
+ * 为标签页列表生成 a11y 属性
109
+ */
110
+ declare function getTablistA11yProps(props: A11yProps & {
111
+ label?: string;
112
+ }): Record<string, any>;
113
+ /**
114
+ * 为单个标签页生成 a11y 属性
115
+ */
116
+ declare function getTabA11yProps(props: A11yProps & {
117
+ selected?: boolean;
118
+ disabled?: boolean;
119
+ controls?: string;
120
+ }): Record<string, any>;
121
+ /**
122
+ * 为标签面板生成 a11y 属性
123
+ */
124
+ declare function getTabpanelA11yProps(props: A11yProps & {
125
+ labelledBy?: string;
126
+ hidden?: boolean;
127
+ }): Record<string, any>;
128
+ /**
129
+ * 为对话框/模态框生成 a11y 属性
130
+ */
131
+ declare function getDialogA11yProps(props: A11yProps & {
132
+ labelledBy?: string;
133
+ describedBy?: string;
134
+ modal?: boolean;
135
+ }): Record<string, any>;
136
+ /**
137
+ * 为分组组件(checkboxGroup/radioGroup)生成 a11y 属性
138
+ */
139
+ declare function getGroupA11yProps(props: A11yProps & {
140
+ role?: 'radiogroup' | 'group' | 'listbox';
141
+ required?: boolean;
142
+ label?: string;
143
+ }): Record<string, any>;
144
+ /**
145
+ * 合并多个 a11y 属性对象
146
+ */
147
+ declare function mergeA11yProps(...propsList: Array<Record<string, any>>): Record<string, any>;
148
+ /** ARIA 角色到必需属性的映射 */
149
+ declare const ARIA_ROLES: Record<string, string[]>;
150
+ /**
151
+ * 检查元素是否可聚焦
152
+ */
153
+ declare function isFocusable(element: Element): boolean;
154
+ /**
155
+ * 获取容器内所有可聚焦元素
156
+ */
157
+ declare function getFocusableElements(container: Element): HTMLElement[];
158
+ /**
159
+ * 在容器内创建焦点陷阱
160
+ *
161
+ * @param container - 陷阱容器
162
+ * @param options - 配置选项
163
+ * @returns 清理函数
164
+ */
165
+ declare function focusTrap(container: HTMLElement, options?: FocusTrapOptions): () => void;
166
+ /**
167
+ * 管理焦点:保存之前的焦点,将焦点移入容器,返回恢复函数
168
+ *
169
+ * @param container - 目标容器
170
+ * @param triggerEl - 触发元素,恢复焦点时优先回到此元素
171
+ * @returns 恢复函数
172
+ */
173
+ declare function manageFocus(container: HTMLElement, triggerEl?: HTMLElement): () => void;
174
+ /**
175
+ * 获取元素上所有 aria-* 属性
176
+ */
177
+ declare function getAriaProps(element: Element): Record<string, string>;
178
+ /**
179
+ * 批量设置 aria-* 属性
180
+ */
181
+ declare function setAriaProps(element: Element, props: Record<string, string>): void;
182
+ /**
183
+ * 检查给定元素是否是当前活动元素
184
+ */
185
+ declare function assertActiveElement(element: Element): boolean;
186
+ /**
187
+ * 键盘导航辅助函数 - 在启用的选项间循环
188
+ */
189
+ declare function getNextEnabledIndex(currentIndex: number, totalItems: number, isEnabled: (index: number) => boolean, direction?: 'forward' | 'backward'): number;
190
+ /**
191
+ * 处理列表组件的键盘导航
192
+ */
193
+ declare function handleListKeydown(event: KeyboardEvent, currentIndex: number, totalItems: number, isEnabled: (index: number) => boolean, onSelect: (index: number) => void, onClose?: () => void): void;
194
+
195
+ export { type A11yProps, ARIA_ROLES, type FocusTrapOptions, assertActiveElement, focusTrap, getAriaProps, getButtonA11yProps, getComboboxA11yProps, getDialogA11yProps, getFocusableElements, getFormControlA11yProps, getGroupA11yProps, getInputControlA11yProps, getNextEnabledIndex, getOptionA11yProps, getSliderA11yProps, getSpinbuttonA11yProps, getSwitchA11yProps, getTabA11yProps, getTabIndex, getTablistA11yProps, getTabpanelA11yProps, handleListKeydown, isFocusable, manageFocus, mergeA11yProps, setAriaProps };