agentcss 0.0.0-stage → 0.11.2
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/01-tokens/README.md +236 -0
- package/01-tokens/behavior/dismissable.js +132 -0
- package/01-tokens/behavior/emit.js +76 -0
- package/01-tokens/behavior/focus-return.js +81 -0
- package/01-tokens/behavior/roving.js +152 -0
- package/01-tokens/behavior/typeahead.js +124 -0
- package/01-tokens/focus-ring-demo.html +133 -0
- package/01-tokens/focus-ring.css +102 -0
- package/01-tokens/page.css +244 -0
- package/01-tokens/polyfill.js +116 -0
- package/01-tokens/responsive-demo.html +171 -0
- package/01-tokens/scrollbar-demo.html +194 -0
- package/01-tokens/scrollbar.css +179 -0
- package/01-tokens/theme-toggle.js +103 -0
- package/01-tokens/tokens.css +972 -0
- package/01-tokens/typography.css +156 -0
- package/02-primitives/badge/README.md +71 -0
- package/02-primitives/badge/badge.css +141 -0
- package/02-primitives/badge/demo.html +101 -0
- package/02-primitives/button/README.md +99 -0
- package/02-primitives/button/button.css +353 -0
- package/02-primitives/button/demo.html +200 -0
- package/02-primitives/card/README.md +145 -0
- package/02-primitives/card/card.css +246 -0
- package/02-primitives/card/demo.html +170 -0
- package/02-primitives/choice/README.md +161 -0
- package/02-primitives/choice/choice.css +395 -0
- package/02-primitives/choice/demo.html +277 -0
- package/02-primitives/combobox/README.md +79 -0
- package/02-primitives/combobox/combobox.css +223 -0
- package/02-primitives/combobox/combobox.js +485 -0
- package/02-primitives/combobox/demo.html +109 -0
- package/02-primitives/date-range/README.md +100 -0
- package/02-primitives/date-range/date-range.css +206 -0
- package/02-primitives/date-range/date-range.js +301 -0
- package/02-primitives/date-range/demo.html +104 -0
- package/02-primitives/input/README.md +170 -0
- package/02-primitives/input/demo.html +246 -0
- package/02-primitives/input/input.css +239 -0
- package/02-primitives/popover/README.md +80 -0
- package/02-primitives/popover/demo.html +150 -0
- package/02-primitives/popover/popover.css +187 -0
- package/02-primitives/popover/popover.js +331 -0
- package/02-primitives/progress/README.md +82 -0
- package/02-primitives/progress/demo.html +154 -0
- package/02-primitives/progress/progress.css +172 -0
- package/02-primitives/select/README.md +95 -0
- package/02-primitives/select/demo.html +121 -0
- package/02-primitives/select/select.css +151 -0
- package/02-primitives/select/select.js +549 -0
- package/02-primitives/separator/README.md +63 -0
- package/02-primitives/separator/demo.html +84 -0
- package/02-primitives/separator/separator.css +73 -0
- package/02-primitives/skeleton/README.md +69 -0
- package/02-primitives/skeleton/demo.html +110 -0
- package/02-primitives/skeleton/skeleton.css +96 -0
- package/02-primitives/switch/README.md +65 -0
- package/02-primitives/switch/demo.html +115 -0
- package/02-primitives/switch/switch.css +370 -0
- package/03-patterns/accordion/README.md +121 -0
- package/03-patterns/accordion/accordion.css +453 -0
- package/03-patterns/accordion/accordion.js +369 -0
- package/03-patterns/accordion/demo.html +223 -0
- package/03-patterns/content/README.md +103 -0
- package/03-patterns/content/content.css +286 -0
- package/03-patterns/content/demo.html +88 -0
- package/03-patterns/drawer/README.md +79 -0
- package/03-patterns/drawer/demo.html +101 -0
- package/03-patterns/drawer/drawer.css +129 -0
- package/03-patterns/drawer/drawer.js +258 -0
- package/03-patterns/dropdown/README.md +78 -0
- package/03-patterns/dropdown/demo.html +101 -0
- package/03-patterns/dropdown/dropdown.css +135 -0
- package/03-patterns/dropdown/dropdown.js +451 -0
- package/03-patterns/form-validation/README.md +147 -0
- package/03-patterns/form-validation/demo.html +280 -0
- package/03-patterns/form-validation/form-validation.css +263 -0
- package/03-patterns/list/README.md +150 -0
- package/03-patterns/list/demo.html +225 -0
- package/03-patterns/list/flip.js +201 -0
- package/03-patterns/list/list.css +162 -0
- package/03-patterns/nav/README.md +225 -0
- package/03-patterns/nav/demo.html +131 -0
- package/03-patterns/nav/nav.css +314 -0
- package/03-patterns/nav/nav.js +430 -0
- package/03-patterns/nav/toc.js +111 -0
- package/03-patterns/overlay/README.md +196 -0
- package/03-patterns/overlay/demo.html +241 -0
- package/03-patterns/overlay/overlay.css +319 -0
- package/03-patterns/overlay/overlay.js +563 -0
- package/03-patterns/pagination/README.md +114 -0
- package/03-patterns/pagination/demo.html +165 -0
- package/03-patterns/pagination/pagination.css +195 -0
- package/03-patterns/pagination/pagination.js +395 -0
- package/03-patterns/reveal/README.md +91 -0
- package/03-patterns/reveal/demo.html +106 -0
- package/03-patterns/reveal/reveal.css +93 -0
- package/03-patterns/reveal/reveal.js +172 -0
- package/03-patterns/states/README.md +141 -0
- package/03-patterns/states/demo.html +240 -0
- package/03-patterns/states/states.css +252 -0
- package/03-patterns/tabs/README.md +84 -0
- package/03-patterns/tabs/demo.html +139 -0
- package/03-patterns/tabs/tabs.css +172 -0
- package/03-patterns/tabs/tabs.js +307 -0
- package/03-patterns/tooltip/README.md +96 -0
- package/03-patterns/tooltip/demo.html +78 -0
- package/03-patterns/tooltip/tooltip.css +89 -0
- package/03-patterns/tooltip/tooltip.js +288 -0
- package/03-patterns/tree/README.md +94 -0
- package/03-patterns/tree/demo.html +138 -0
- package/03-patterns/tree/tree.css +113 -0
- package/03-patterns/tree/tree.js +456 -0
- package/04-recipes/analysis-report/README.md +80 -0
- package/04-recipes/analysis-report/analysis-report.css +187 -0
- package/04-recipes/analysis-report/demo.html +274 -0
- package/04-recipes/data-showcase/README.md +45 -0
- package/04-recipes/data-showcase/demo.html +247 -0
- package/04-recipes/detail-page/README.md +93 -0
- package/04-recipes/detail-page/demo.html +55 -0
- package/04-recipes/detail-page/detail-page.css +190 -0
- package/04-recipes/detail-page/full.html +60 -0
- package/04-recipes/detail-page/null.html +60 -0
- package/04-recipes/detail-page/partial.html +60 -0
- package/04-recipes/detail-page/verify-skeleton.py +19 -0
- package/04-recipes/longform/README.md +93 -0
- package/04-recipes/longform/agc-memory.svg +4 -0
- package/04-recipes/longform/content.html +610 -0
- package/04-recipes/longform/content.md +51 -0
- package/04-recipes/longform/longform.html +627 -0
- package/04-recipes/report/README.md +176 -0
- package/04-recipes/report/demo.html +355 -0
- package/04-recipes/report/report.css +364 -0
- package/04-recipes/table/README.md +103 -0
- package/04-recipes/table/demo.html +455 -0
- package/04-recipes/table/table.css +372 -0
- package/04-recipes/tierA-tokens.css +189 -0
- package/09-assets/README.md +74 -0
- package/09-assets/bar/README.md +94 -0
- package/09-assets/bar/bar.css +166 -0
- package/09-assets/bar/bar.js +320 -0
- package/09-assets/bar/demo.html +176 -0
- package/09-assets/echarts-adapter/demo.html +192 -0
- package/09-assets/echarts-adapter/echarts-adapter.js +277 -0
- package/09-assets/model-viewer/README.md +155 -0
- package/09-assets/model-viewer/demo.html +105 -0
- package/09-assets/model-viewer/model-viewer.css +143 -0
- package/09-assets/model-viewer/model-viewer.js +423 -0
- package/09-assets/scientific-plot/README.md +94 -0
- package/09-assets/scientific-plot/demo.html +252 -0
- package/09-assets/scientific-plot/scientific-plot.css +141 -0
- package/09-assets/scientific-plot/scientific-plot.js +610 -0
- package/09-assets/sparkline/README.md +153 -0
- package/09-assets/sparkline/demo.html +163 -0
- package/09-assets/sparkline/sparkline.css +158 -0
- package/09-assets/sparkline/sparkline.js +508 -0
- package/API.md +332 -0
- package/CHANGELOG.md +3116 -0
- package/CODE_OF_CONDUCT.md +64 -0
- package/CONTRIBUTING.md +89 -0
- package/ERRORS.md +1658 -0
- package/LICENSE +21 -0
- package/README.md +491 -2
- package/SECURITY.md +72 -0
- package/START-HERE.md +164 -0
- package/SUPPORT.md +109 -0
- package/adapters/presentation/README.md +113 -0
- package/adapters/presentation/check.js +101 -0
- package/adapters/presentation/deck.schema.json +74 -0
- package/adapters/presentation/to-doc.js +138 -0
- package/ai/START-HERE.md +104 -0
- package/ai/behaviors.json +380 -0
- package/ai/behaviors.schema.json +61 -0
- package/ai/capabilities.json +56 -0
- package/ai/cli.js +387 -0
- package/ai/components.json +1944 -0
- package/ai/components.meta.json +168 -0
- package/ai/components.schema.json +220 -0
- package/ai/contract.schema.json +147 -0
- package/ai/deprecations.json +55 -0
- package/ai/deprecations.schema.json +85 -0
- package/ai/motion-policy.json +22 -0
- package/ai/patch.js +323 -0
- package/ai/patch.schema.json +63 -0
- package/ai/profiles.json +92 -0
- package/ai/tokens.audience.json +55 -0
- package/ai/tokens.audience.schema.json +73 -0
- package/ai/tokens.json +762 -0
- package/ai/tokens.schema.json +58 -0
- package/ai/tokens.tree.json +752 -0
- package/ai/tokens.tree.schema.json +56 -0
- package/ai/validate.js +383 -0
- package/dist/01-tokens/behavior/dismissable.js +60 -0
- package/dist/01-tokens/behavior/emit.js +21 -0
- package/dist/01-tokens/behavior/focus-return.js +31 -0
- package/dist/01-tokens/behavior/roving.js +77 -0
- package/dist/01-tokens/behavior/typeahead.js +61 -0
- package/dist/01-tokens/focus-ring.css +1 -0
- package/dist/01-tokens/page.css +1 -0
- package/dist/01-tokens/polyfill.js +78 -0
- package/dist/01-tokens/scrollbar.css +1 -0
- package/dist/01-tokens/theme-toggle.js +61 -0
- package/dist/01-tokens/tokens.css +1 -0
- package/dist/01-tokens/typography.css +1 -0
- package/dist/02-primitives/badge/badge.css +1 -0
- package/dist/02-primitives/button/button.css +1 -0
- package/dist/02-primitives/card/card.css +1 -0
- package/dist/02-primitives/choice/choice.css +1 -0
- package/dist/02-primitives/combobox/combobox.css +1 -0
- package/dist/02-primitives/combobox/combobox.js +439 -0
- package/dist/02-primitives/date-range/date-range.css +1 -0
- package/dist/02-primitives/date-range/date-range.js +267 -0
- package/dist/02-primitives/input/input.css +1 -0
- package/dist/02-primitives/popover/popover.css +1 -0
- package/dist/02-primitives/popover/popover.js +257 -0
- package/dist/02-primitives/progress/progress.css +1 -0
- package/dist/02-primitives/select/select.css +1 -0
- package/dist/02-primitives/select/select.js +449 -0
- package/dist/02-primitives/separator/separator.css +1 -0
- package/dist/02-primitives/skeleton/skeleton.css +1 -0
- package/dist/02-primitives/switch/switch.css +1 -0
- package/dist/03-patterns/accordion/accordion.css +1 -0
- package/dist/03-patterns/accordion/accordion.js +219 -0
- package/dist/03-patterns/content/content.css +1 -0
- package/dist/03-patterns/drawer/drawer.css +1 -0
- package/dist/03-patterns/drawer/drawer.js +229 -0
- package/dist/03-patterns/dropdown/dropdown.css +1 -0
- package/dist/03-patterns/dropdown/dropdown.js +365 -0
- package/dist/03-patterns/form-validation/form-validation.css +1 -0
- package/dist/03-patterns/list/flip.js +117 -0
- package/dist/03-patterns/list/list.css +1 -0
- package/dist/03-patterns/nav/nav.css +1 -0
- package/dist/03-patterns/nav/nav.js +322 -0
- package/dist/03-patterns/nav/toc.js +76 -0
- package/dist/03-patterns/overlay/overlay.css +1 -0
- package/dist/03-patterns/overlay/overlay.js +398 -0
- package/dist/03-patterns/pagination/pagination.css +1 -0
- package/dist/03-patterns/pagination/pagination.js +273 -0
- package/dist/03-patterns/reveal/reveal.css +1 -0
- package/dist/03-patterns/reveal/reveal.js +138 -0
- package/dist/03-patterns/states/states.css +1 -0
- package/dist/03-patterns/tabs/tabs.css +1 -0
- package/dist/03-patterns/tabs/tabs.js +242 -0
- package/dist/03-patterns/tooltip/tooltip.css +1 -0
- package/dist/03-patterns/tooltip/tooltip.js +213 -0
- package/dist/03-patterns/tree/tree.css +1 -0
- package/dist/03-patterns/tree/tree.js +383 -0
- package/dist/04-recipes/analysis-report/analysis-report.css +1 -0
- package/dist/04-recipes/detail-page/detail-page.css +1 -0
- package/dist/04-recipes/report/report.css +1 -0
- package/dist/04-recipes/table/table.css +1 -0
- package/dist/04-recipes/tierA-tokens.css +1 -0
- package/dist/09-assets/bar/bar.css +1 -0
- package/dist/09-assets/bar/bar.js +222 -0
- package/dist/09-assets/echarts-adapter/echarts-adapter.js +185 -0
- package/dist/09-assets/model-viewer/model-viewer.css +1 -0
- package/dist/09-assets/model-viewer/model-viewer.js +320 -0
- package/dist/09-assets/scientific-plot/scientific-plot.css +1 -0
- package/dist/09-assets/scientific-plot/scientific-plot.js +568 -0
- package/dist/09-assets/sparkline/sparkline.css +1 -0
- package/dist/09-assets/sparkline/sparkline.js +265 -0
- package/dist/COSTS.md +56 -0
- package/docs/AI-CONTRACT.md +197 -0
- package/docs/BACKLOG.md +127 -0
- package/docs/BENCHMARK.md +252 -0
- package/docs/GUIDANCE.md +154 -0
- package/docs/INVARIANT.md +372 -0
- package/docs/LAYER-BASELINE.md +158 -0
- package/docs/RELEASING.md +275 -0
- package/docs/RFC-THEME-SCOPE.md +141 -0
- package/docs/ROADMAP.md +182 -0
- package/index.html +82 -0
- package/package.json +107 -4
- package/types/index.d.ts +310 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* select.js — 自建下拉选择(E1 /
|
|
3
|
+
*
|
|
4
|
+
* 依据:APG Select-Only Combobox 模式
|
|
5
|
+
* ============================================================================
|
|
6
|
+
* ⭐ 四条最容易漏的键盘契约(逐条实现并在注释里标出):
|
|
7
|
+
*
|
|
8
|
+
* ① **打开后焦点落在"选中项"**(不是第一项,也不是按钮)
|
|
9
|
+
* ⇒ 用户按 Alt+↓ 打开后,直接 Enter 就能确认当前值。
|
|
10
|
+
* ⇒ 若落在第一项,一开就选错了 —— 这是自建 select 最常见的错。
|
|
11
|
+
*
|
|
12
|
+
* ② **↑↓ 循环 · Home/End 首末 · Enter/Space 选中并关闭**
|
|
13
|
+
*
|
|
14
|
+
* ③ **Esc 关闭并把焦点还给按钮**(不是"选中项")
|
|
15
|
+
*
|
|
16
|
+
* ④ **类型搜索**:连续打字跳到匹配项(原生 select 有,必须做)
|
|
17
|
+
* ⇒ 缓冲 500ms,超时清空(否则 "abc" 永远搜不到)。
|
|
18
|
+
*
|
|
19
|
+
* 无依赖 · ES5
|
|
20
|
+
*/
|
|
21
|
+
(function (global) {
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
/* ==== BEHAVIOR INJECT BEGIN: dismissable ==== */
|
|
25
|
+
var flDismissable = function (opts) {
|
|
26
|
+
var opt = opts || {};
|
|
27
|
+
var escOn = opt.escOn || [];
|
|
28
|
+
var inside = opt.inside || [];
|
|
29
|
+
var when = opt.when || function () { return true; };
|
|
30
|
+
var onDismiss = opt.onDismiss || function () {};
|
|
31
|
+
|
|
32
|
+
var shared = window.__flDismiss || (window.__flDismiss = { stack: [], bound: false });
|
|
33
|
+
|
|
34
|
+
function isInside(node) {
|
|
35
|
+
if (!node) return false;
|
|
36
|
+
for (var i = 0; i < inside.length; i++) {
|
|
37
|
+
var el = inside[i];
|
|
38
|
+
if (!el || !el.contains) continue;
|
|
39
|
+
if (el === node || el.contains(node)) return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function onKey(e) {
|
|
45
|
+
if (!when()) return;
|
|
46
|
+
var k = e.key;
|
|
47
|
+
if (k !== 'Escape' && k !== 'Esc' && e.keyCode !== 27) return;
|
|
48
|
+
/* 🔴 为什么 onDismiss 在前、preventDefault/stopPropagation 在后
|
|
49
|
+
(0.8.8 改):**分级 Esc** 需要核让组件有机会说"这次我没关"。
|
|
50
|
+
combobox 的 Esc 是三级的:
|
|
51
|
+
① 列表开着 → 关列表
|
|
52
|
+
② 列表关了、输入有字 → 清空输入
|
|
53
|
+
③ 都没了 → 什么都不做(此时**不该**阻止默认行为,
|
|
54
|
+
让 Esc 能继续传给外层浮层)
|
|
55
|
+
而原实现是「无条件 preventDefault + stopPropagation,再调 onDismiss」
|
|
56
|
+
⇒ 组件根本无法表达"这次我没关"。
|
|
57
|
+
⇒ 改成:先问onDismiss,它返回 false 就**不干预**默认行为。
|
|
58
|
+
⚠️ 顺序安全性:stopPropagation 只需在**本次监听器执行期间**被调用即可
|
|
59
|
+
阻止冒泡到外层,放在 onDismiss 之后仍然有效。
|
|
60
|
+
*/
|
|
61
|
+
var closed = onDismiss(e, 'esc');
|
|
62
|
+
if (closed === false) return;
|
|
63
|
+
if (e.preventDefault) e.preventDefault();
|
|
64
|
+
if (e.stopPropagation) e.stopPropagation();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onDocClick(e) {
|
|
68
|
+
var top = null;
|
|
69
|
+
for (var i = shared.stack.length - 1; i >= 0; i--) {
|
|
70
|
+
if (shared.stack[i].when()) { top = shared.stack[i]; break; }
|
|
71
|
+
}
|
|
72
|
+
if (!top || top.isInside(e.target)) return;
|
|
73
|
+
top.onDismiss(e, 'outside');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
77
|
+
|
|
78
|
+
for (var j = 0; j < escOn.length; j++) {
|
|
79
|
+
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
80
|
+
}
|
|
81
|
+
shared.stack.push(rec);
|
|
82
|
+
if (!shared.bound) {
|
|
83
|
+
shared.bound = true;
|
|
84
|
+
document.addEventListener('click', onDocClick, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
destroy: function () {
|
|
89
|
+
for (var j = 0; j < escOn.length; j++) {
|
|
90
|
+
if (escOn[j]) escOn[j].removeEventListener('keydown', onKey);
|
|
91
|
+
}
|
|
92
|
+
var k = shared.stack.indexOf(rec);
|
|
93
|
+
if (k >= 0) shared.stack.splice(k, 1);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
/* ==== BEHAVIOR INJECT END: dismissable ==== */
|
|
98
|
+
/* ==== BEHAVIOR INJECT BEGIN: focus-return ==== */
|
|
99
|
+
var flFocusReturn = function (opts) {
|
|
100
|
+
var opt = opts || {};
|
|
101
|
+
var saved = null;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
save: function () {
|
|
105
|
+
var a = document.activeElement;
|
|
106
|
+
saved = (a && a !== document.body && a.focus) ? a : null;
|
|
107
|
+
return saved;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
restore: function () {
|
|
111
|
+
var target = saved;
|
|
112
|
+
/* ⚠️ 见文档 ②:不判 contains 的话,元素被移除后 focus() 不报错
|
|
113
|
+
但焦点不动 ⇒ 用户以为还回去了,其实掉在 body 上。 */
|
|
114
|
+
if (!target || !document.contains(target)) target = opt.fallback || null;
|
|
115
|
+
if (!target) { saved = null; return false; }
|
|
116
|
+
try {
|
|
117
|
+
target.focus();
|
|
118
|
+
} catch (e) {
|
|
119
|
+
saved = null;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
var ok = document.activeElement === target;
|
|
123
|
+
saved = null; /* 见文档 ③ */
|
|
124
|
+
return ok;
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
target: function () { return saved; },
|
|
128
|
+
clear: function () { saved = null; },
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
/* ==== BEHAVIOR INJECT END: focus-return ==== */
|
|
132
|
+
/* ==== BEHAVIOR INJECT BEGIN: typeahead ==== */
|
|
133
|
+
var flTypeahead = function (opts) {
|
|
134
|
+
var opt = opts || {};
|
|
135
|
+
var items = opt.items || function () { return []; };
|
|
136
|
+
var textOf = opt.textOf || function (x) { return String((x && x.textContent) || ''); };
|
|
137
|
+
var usable = opt.usable || function () { return true; };
|
|
138
|
+
var timeout = opt.timeout || 500;
|
|
139
|
+
var onHit = opt.onHit || function () {};
|
|
140
|
+
var buf = '';
|
|
141
|
+
var timer = null;
|
|
142
|
+
var last = -1;
|
|
143
|
+
|
|
144
|
+
function stop() {
|
|
145
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* 连打同一字符 ⇒ 只按首字符匹配(理由见文件头 ③④)*/
|
|
149
|
+
function repeated(s) {
|
|
150
|
+
if (s.length < 2) return false;
|
|
151
|
+
for (var i = 1; i < s.length; i++) {
|
|
152
|
+
if (s.charAt(i) !== s.charAt(0)) return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function find(needle) {
|
|
158
|
+
var l = items() || [];
|
|
159
|
+
var n = l.length;
|
|
160
|
+
if (!n) return -1;
|
|
161
|
+
for (var k = 0; k < n; k++) {
|
|
162
|
+
var i = (last + 1 + k) % n; /* ① 从当前项之后开始 ⇒ 会绕回 */
|
|
163
|
+
if (!usable(l[i])) continue; /* ⑤ */
|
|
164
|
+
if (textOf(l[i]).toLowerCase().indexOf(needle) === 0) return i;
|
|
165
|
+
}
|
|
166
|
+
return -1; /* ⑥ */
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function type(ch) {
|
|
170
|
+
if (!ch) return -1;
|
|
171
|
+
buf += String(ch).toLowerCase(); /* ② */
|
|
172
|
+
stop();
|
|
173
|
+
timer = setTimeout(clear, timeout); /* ④ */
|
|
174
|
+
var needle = repeated(buf) ? buf.charAt(0) : buf; /* ③ */
|
|
175
|
+
var i = find(needle);
|
|
176
|
+
if (i >= 0) {
|
|
177
|
+
last = i;
|
|
178
|
+
onHit(i, (items() || [])[i], buf);
|
|
179
|
+
}
|
|
180
|
+
return i;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* 只清缓冲,不清 last(理由见文件头)*/
|
|
184
|
+
function clear() {
|
|
185
|
+
buf = '';
|
|
186
|
+
stop();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
type: type,
|
|
191
|
+
clear: clear,
|
|
192
|
+
buffer: function () { return buf; },
|
|
193
|
+
destroy: function () { stop(); },
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
/* ==== BEHAVIOR INJECT END: typeahead ==== */
|
|
197
|
+
/* ==== BEHAVIOR INJECT BEGIN: emit ==== */
|
|
198
|
+
var flEmit = function (el, name, detail) {
|
|
199
|
+
if (!el || !el.dispatchEvent) return null;
|
|
200
|
+
var type = name.indexOf('fl-') === 0 ? name : 'fl-' + name;
|
|
201
|
+
var ev = null;
|
|
202
|
+
/* 老 WebView 没有 CustomEvent 构造函数 ⇒ 兜底走 createEvent(理由见文件头)*/
|
|
203
|
+
if (typeof window.CustomEvent === 'function') {
|
|
204
|
+
try {
|
|
205
|
+
ev = new window.CustomEvent(type, {
|
|
206
|
+
detail: detail || null, bubbles: true, cancelable: false
|
|
207
|
+
});
|
|
208
|
+
} catch (e) { ev = null; }
|
|
209
|
+
}
|
|
210
|
+
if (!ev) {
|
|
211
|
+
try {
|
|
212
|
+
ev = document.createEvent('CustomEvent');
|
|
213
|
+
ev.initCustomEvent(type, true, false, detail || null);
|
|
214
|
+
} catch (e2) { return null; }
|
|
215
|
+
}
|
|
216
|
+
el.dispatchEvent(ev);
|
|
217
|
+
return ev;
|
|
218
|
+
};
|
|
219
|
+
/* ==== BEHAVIOR INJECT END: emit ==== */
|
|
220
|
+
/* ---- 旧环境兼容:Element.closest 在老 WebView 上不存在 ----
|
|
221
|
+
直接调用会在真机上抛 `is not a function`(本地完全正常,
|
|
222
|
+
只在老环境炸 —— 属静默失效)。这里做模块内兜底,
|
|
223
|
+
不污染全局,也不需要使用者额外引入 polyfill。 */
|
|
224
|
+
function closest(el, sel) {
|
|
225
|
+
if (!el) return null;
|
|
226
|
+
if (el.closest) return el.closest(sel);
|
|
227
|
+
while (el && el.nodeType === 1) {
|
|
228
|
+
if (elMatches(el, sel)) return el;
|
|
229
|
+
el = el.parentElement;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
function elMatches(el, sel) {
|
|
234
|
+
var m = sel.match(/^([a-zA-Z][\w-]*)/);
|
|
235
|
+
if (m && el.tagName.toLowerCase() !== m[1].toLowerCase()) return false;
|
|
236
|
+
var cls = sel.match(/\.([\w-]+)/g);
|
|
237
|
+
if (cls) {
|
|
238
|
+
for (var i = 0; i < cls.length; i++) {
|
|
239
|
+
var c = cls[i].slice(1);
|
|
240
|
+
if ((' ' + (el.className || '') + ' ').indexOf(' ' + c + ' ') < 0) return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
var id = sel.match(/#([\w-]+)/);
|
|
244
|
+
if (id && el.id !== id[1]) return false;
|
|
245
|
+
var at = sel.match(/\[([\w-]+)(?:=["']?([^\]"']*)["']?)?\]/);
|
|
246
|
+
if (at) {
|
|
247
|
+
var v = el.getAttribute(at[1]);
|
|
248
|
+
if (v === null) return false;
|
|
249
|
+
if (at[2] !== undefined && v !== at[2]) return false;
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
var uid = 0;
|
|
255
|
+
|
|
256
|
+
function create(root, opt) {
|
|
257
|
+
opt = opt || {};
|
|
258
|
+
var btn = root.querySelector('[data-select-btn]');
|
|
259
|
+
var list = root.querySelector('[data-select-list]');
|
|
260
|
+
if (!btn || !list) throw new Error('Select: 缺少 [data-select-btn] 或 [data-select-list]');
|
|
261
|
+
|
|
262
|
+
/* 焦点归位核:open() 时save() 记住焦点、close() 时按参数 restore()。
|
|
263
|
+
原先是 close() 里直接 btn.focus() —— 那是**无条件**归位,
|
|
264
|
+
而 close(false) 的调用场景(选完就走的场景)本意就是不夺焦点。
|
|
265
|
+
核的语义是「记了才还」,正好把「打开时记、关闭时还」讲清楚。 */
|
|
266
|
+
var fr = flFocusReturn({ fallback: btn });
|
|
267
|
+
|
|
268
|
+
/* 「关得掉」核:Esc + 点外部。收编解决两个实际问题:
|
|
269
|
+
① 原来的 document click 是**冒泡阶段**判定 ⇒ 点在按钮上但事件仍会
|
|
270
|
+
继续冒泡,容易出现「刚打开就被自己那一下点关掉」;
|
|
271
|
+
② Esc 的 stopPropagation(只关本层、不带崩外层)由核统一承担,
|
|
272
|
+
组件里不再各写一份。
|
|
273
|
+
⚠️ close(false) 的场景(Tab 走开)**不夺焦点**,
|
|
274
|
+
所以 onDismiss 用 close(false)、Esc 那条单独 close(true)。 */
|
|
275
|
+
var dismiss = flDismissable({
|
|
276
|
+
escOn: [btn],
|
|
277
|
+
inside: [root],
|
|
278
|
+
when: function () { return !list.hidden; },
|
|
279
|
+
onDismiss: function (ev, reason) {
|
|
280
|
+
close(reason === 'esc');
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
if (!list.id) list.id = 'select-list-' + (++uid);
|
|
284
|
+
|
|
285
|
+
var options = [].slice.call(list.querySelectorAll('[role="option"]'));
|
|
286
|
+
var isMulti = !!opt.multiple;
|
|
287
|
+
var selected = opt.value != null ? String(opt.value) : null;
|
|
288
|
+
var activeIdx = -1;
|
|
289
|
+
|
|
290
|
+
list.setAttribute('role', 'listbox');
|
|
291
|
+
if (isMulti) list.setAttribute('aria-multiselectable', 'true');
|
|
292
|
+
list.setAttribute('aria-label', opt.label || '选项');
|
|
293
|
+
btn.setAttribute('aria-haspopup', 'listbox');
|
|
294
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
295
|
+
btn.setAttribute('aria-controls', list.id);
|
|
296
|
+
|
|
297
|
+
/* 保证每个 option 都有 id(aria-activedescendant 需要) */
|
|
298
|
+
options.forEach(function (o, i) {
|
|
299
|
+
o.setAttribute('role', 'option');
|
|
300
|
+
if (!o.id) o.id = list.id + '-o' + i;
|
|
301
|
+
o.setAttribute('aria-selected', 'false');
|
|
302
|
+
if (o.hasAttribute('disabled') || o.getAttribute('aria-disabled') === 'true') {
|
|
303
|
+
o.setAttribute('aria-disabled', 'true');
|
|
304
|
+
}
|
|
305
|
+
o.setAttribute('tabindex', '-1'); /* 焦点在按钮上,选项不占 Tab */
|
|
306
|
+
if (o.className.indexOf('select__opt') < 0) {
|
|
307
|
+
o.className = (o.className ? o.className + ' ' : '') + 'select__opt';
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
function enabled() {
|
|
312
|
+
return options.filter(function (o) { return o.getAttribute('aria-disabled') !== 'true'; });
|
|
313
|
+
}
|
|
314
|
+
/* 类型搜索(连续打字定位)走 typeahead 核(01-tokens/behavior/typeahead.js)。
|
|
315
|
+
⭐ 之前这里只实现了"累积前缀",**没有连打同一字符时循环** ⇒
|
|
316
|
+
连按两次同一个字母,游标不动(用户以为卡了)。
|
|
317
|
+
⭐ 而 tree 里那一份只实现了循环、没有累积 —— 两份各缺一半。
|
|
318
|
+
⇒ 核一次把两半都补齐(APG 的 typeahead 约定),两处共用一份规则。 */
|
|
319
|
+
var typeahead = flTypeahead({
|
|
320
|
+
items: function () { return options; },
|
|
321
|
+
textOf: optText,
|
|
322
|
+
usable: function (o) { return o.getAttribute('aria-disabled') !== 'true'; },
|
|
323
|
+
onHit: function (i) { setActive(i, 1); },
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
function selectedIdx() {
|
|
327
|
+
for (var i = 0; i < options.length; i++) {
|
|
328
|
+
if (options[i].getAttribute('data-value') === selected) return i;
|
|
329
|
+
}
|
|
330
|
+
return -1;
|
|
331
|
+
}
|
|
332
|
+
function valueText() {
|
|
333
|
+
var i = selectedIdx();
|
|
334
|
+
return i < 0 ? (opt.placeholder || '请选择') : optText(options[i]);
|
|
335
|
+
}
|
|
336
|
+
function optText(o) {
|
|
337
|
+
var t = o.querySelector('.select__opt-text');
|
|
338
|
+
return ((t || o).textContent || '').replace(/[✓\s]/g, '').trim();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function paint() {
|
|
342
|
+
var v = valueText();
|
|
343
|
+
var vEl = btn.querySelector('.select__value');
|
|
344
|
+
if (vEl) {
|
|
345
|
+
vEl.textContent = v;
|
|
346
|
+
vEl.classList.toggle('select__value--placeholder', selectedIdx() < 0);
|
|
347
|
+
}
|
|
348
|
+
options.forEach(function (o) {
|
|
349
|
+
var on = o.getAttribute('data-value') === selected;
|
|
350
|
+
o.setAttribute('aria-selected', on ? 'true' : 'false');
|
|
351
|
+
var ck = o.querySelector('.select__check');
|
|
352
|
+
if (ck) ck.textContent = on ? '✓' : '';
|
|
353
|
+
});
|
|
354
|
+
/* hidden input 承接表单值(自建组件最容易漏的)*/
|
|
355
|
+
var hidden = root.querySelector('input[type="hidden"]');
|
|
356
|
+
if (hidden) hidden.value = selected == null ? '' : selected;
|
|
357
|
+
flEmit(root, 'fl-change',
|
|
358
|
+
{ value: selected, text: optText(options[selectedIdx()] || options[0]) });
|
|
359
|
+
if (opt.onChange) opt.onChange(selected, optText(options[selectedIdx()] || options[0]));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 🔴 修一个**索引错位导致 ↑↓ 卡死**的 bug。
|
|
364
|
+
*
|
|
365
|
+
* 原来:`setActive(i)` 里的 i 被调用方按 **enabled() 过滤后数组**的索引传,
|
|
366
|
+
* 而函数内部又当成 **options 原数组**的索引用
|
|
367
|
+
* ⇒ 两者含义不同;末项之后算出越界,被 Math.min 夹回末项
|
|
368
|
+
* ⇒ **连按 ↓ 永远停在最后一项**。
|
|
369
|
+
* ⚠️ 症状极具欺骗性:前几项能正常走,最后一项之后就"卡住"了。
|
|
370
|
+
*
|
|
371
|
+
* ⭐ 正解:**对外统一用 options 的索引**,
|
|
372
|
+
* 跳过 disabled 的逻辑放在函数内部做(步进 + 环绕)。
|
|
373
|
+
*
|
|
374
|
+
* @param {number} i options 数组里的索引(可越界,会自动环绕)
|
|
375
|
+
* @param {number} [dir=1] 步进方向(-1 用于 ↑)
|
|
376
|
+
*/
|
|
377
|
+
function setActive(i, dir) {
|
|
378
|
+
if (!options.length) return;
|
|
379
|
+
dir = dir || 1;
|
|
380
|
+
/* 从 i 开始按 dir 找第一个未禁用的(最多转一圈)*/
|
|
381
|
+
var n = options.length;
|
|
382
|
+
var idx = ((i % n) + n) % n;
|
|
383
|
+
for (var k = 0; k < n; k++) {
|
|
384
|
+
if (options[idx].getAttribute('aria-disabled') !== 'true') break;
|
|
385
|
+
idx = ((idx + dir) % n + n) % n;
|
|
386
|
+
}
|
|
387
|
+
activeIdx = idx;
|
|
388
|
+
options.forEach(function (o) { o.removeAttribute('data-state'); });
|
|
389
|
+
options[idx].setAttribute('data-state', 'active');
|
|
390
|
+
/* ⭐ aria-activedescendant:焦点始终在按钮上,靠它告诉读屏"当前在第几项" */
|
|
391
|
+
btn.setAttribute('aria-activedescendant', options[idx].id);
|
|
392
|
+
if (options[idx].scrollIntoView) {
|
|
393
|
+
options[idx].scrollIntoView({ block: 'nearest' });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** 当前游标所在项在 options 里的索引(没打开时用选中项)*/
|
|
398
|
+
function activeIndex() {
|
|
399
|
+
if (activeIdx >= 0) return activeIdx;
|
|
400
|
+
var i = selectedIdx();
|
|
401
|
+
if (i >= 0) return i;
|
|
402
|
+
var e2 = enabled();
|
|
403
|
+
return e2.length ? options.indexOf(e2[0]) : 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function open() {
|
|
407
|
+
/* 打开这一刻先记焦点,close() 才能还回去(要在改焦点之前做)。 */
|
|
408
|
+
fr.save();
|
|
409
|
+
list.hidden = false;
|
|
410
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
411
|
+
/* 🔴 修:必须**先把焦点放到按钮上**。
|
|
412
|
+
本组件用 aria-activedescendant 模式(焦点常驻按钮,靠它告知读屏
|
|
413
|
+
"当前在第几项")⇒ 焦点必须始终在按钮,事件也必须由按钮处理。
|
|
414
|
+
原来只设了 aria-activedescendant 就 open()
|
|
415
|
+
⇒ 用鼠标点开时焦点可能停在 body
|
|
416
|
+
⇒ 挂在 list 上的 keydown(Esc / 类型搜索)**收不到事件**。
|
|
417
|
+
⚠️ 症状:鼠标能选、↑↓ 能走、Esc 与字母键完全无效。 */
|
|
418
|
+
btn.focus();
|
|
419
|
+
/* ① 焦点落在**选中项**(没有选中则落在第一个未禁用的项)*/
|
|
420
|
+
setActive(activeIndex());
|
|
421
|
+
flEmit(root, 'fl-open', null);
|
|
422
|
+
if (opt.onOpen) opt.onOpen();
|
|
423
|
+
}
|
|
424
|
+
function close(restoreFocus) {
|
|
425
|
+
list.hidden = true;
|
|
426
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
427
|
+
btn.removeAttribute('aria-activedescendant');
|
|
428
|
+
options.forEach(function (o) { o.removeAttribute('data-state'); });
|
|
429
|
+
activeIdx = -1;
|
|
430
|
+
typeahead.clear(); /* 关闭即清空缓冲:下次打开不该接着上次的半个词 */
|
|
431
|
+
/* ③ Esc 关闭时焦点**还给按钮**(不是选中项)*/
|
|
432
|
+
if (restoreFocus !== false) fr.restore();
|
|
433
|
+
flEmit(root, 'fl-close', null);
|
|
434
|
+
if (opt.onClose) opt.onClose();
|
|
435
|
+
}
|
|
436
|
+
function commit(i) {
|
|
437
|
+
var o = options[i];
|
|
438
|
+
if (!o || o.getAttribute('aria-disabled') === 'true') return;
|
|
439
|
+
if (isMulti) {
|
|
440
|
+
/* 多选:再点一次取消 */
|
|
441
|
+
selected = o.getAttribute('data-value') === selected ? null : o.getAttribute('data-value');
|
|
442
|
+
} else {
|
|
443
|
+
selected = o.getAttribute('data-value');
|
|
444
|
+
close(true); /* 选中并关闭 + 归位 */
|
|
445
|
+
}
|
|
446
|
+
paint();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* ---------- 键盘(APG Select)---------- */
|
|
450
|
+
btn.addEventListener('keydown', function (e) {
|
|
451
|
+
var k = e.key;
|
|
452
|
+
if (k === 'ArrowDown' || k === 'ArrowUp' || k === 'Enter' || k === ' ' ||
|
|
453
|
+
k === 'Spacebar' || (k === 'Alt' && false)) {
|
|
454
|
+
if (k === 'ArrowDown' || k === 'ArrowUp') {
|
|
455
|
+
e.preventDefault();
|
|
456
|
+
if (list.hidden) { open(); return; }
|
|
457
|
+
var dir = k === 'ArrowDown' ? 1 : -1;
|
|
458
|
+
setActive(activeIndex() + dir, dir);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
if (list.hidden) open();
|
|
463
|
+
else if (activeIdx >= 0) commit(activeIdx);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
/* Alt+↓ 直接打开(不改值)*/
|
|
467
|
+
if (k === 'ArrowDown' && e.altKey) { e.preventDefault(); open(); return; }
|
|
468
|
+
/* Home/End */
|
|
469
|
+
if (list.hidden && (k === 'Home' || k === 'End')) {
|
|
470
|
+
e.preventDefault(); open();
|
|
471
|
+
if (k === 'Home') setActive(0);
|
|
472
|
+
else setActive(options.length - 1);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
/* 🔴 焦点常驻按钮 ⇒ 键盘事件主要落在 btn 上。
|
|
478
|
+
这里把 btn 的 handler 也挂一份完整逻辑(Esc/类型搜索/Home/End),
|
|
479
|
+
否则它们永远收不到事件。 */
|
|
480
|
+
function onListKey(e) {
|
|
481
|
+
var k = e.key;
|
|
482
|
+
if (k === 'ArrowDown') { e.preventDefault(); setActive(activeIndex() + 1, 1); return; }
|
|
483
|
+
if (k === 'ArrowUp') { e.preventDefault(); setActive(activeIndex() - 1, -1); return; }
|
|
484
|
+
if (k === 'Home') { e.preventDefault(); setActive(0, 1); return; }
|
|
485
|
+
if (k === 'End') { e.preventDefault(); setActive(options.length - 1, -1); return; }
|
|
486
|
+
/* ③ Esc 关闭 + 焦点还按钮
|
|
487
|
+
🔴 加 stopPropagation(**组合契约**抓到的真问题):
|
|
488
|
+
本组件常被放进 drawer / dialog 里。
|
|
489
|
+
原来只 preventDefault ⇒ 事件**继续冒泡**到外层弹层
|
|
490
|
+
⇒ **按一次 Esc 把内外两层一起关掉**。
|
|
491
|
+
⚠️ 单组件契约**测不到这个**(它没有外层)——
|
|
492
|
+
所以必须有"组合契约"这种真嵌套的测试。
|
|
493
|
+
⇒ 正解:内层弹层"吃掉"这个键时必须 stopPropagation。
|
|
494
|
+
层级弹层的 Esc 语义本来就是"只关最上面那层"。 */
|
|
495
|
+
/* Esc 已由 dismissable 核接管(见上面的 flDismissable):
|
|
496
|
+
核把它挂在 btn 上冒泡、并 stopPropagation ⇒ 只关最内层,
|
|
497
|
+
组件里不再各写一份(原先手写 10 处、分布 7 个文件)。 */
|
|
498
|
+
if (k === 'Tab') { close(false); return; }
|
|
499
|
+
if (k === 'Enter' || k === ' ' || k === 'Spacebar') {
|
|
500
|
+
e.preventDefault();
|
|
501
|
+
if (activeIdx >= 0) commit(activeIdx);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
/* ④ 类型搜索 —— 已移到 typeahead 核(见上面的 flTypeahead)。
|
|
505
|
+
⚠️ 核不监听键盘:Enter / Space / Tab 在本组件各有语义,
|
|
506
|
+
只有"单个可打印字符"才算打字。 */
|
|
507
|
+
if (k.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
508
|
+
e.preventDefault();
|
|
509
|
+
typeahead.type(k);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/* 挂在按钮上(焦点常驻处)*/
|
|
513
|
+
btn.addEventListener('keydown', function (e) {
|
|
514
|
+
/* 列表没开时,btn 自己的 handler 已经处理了开合;开了之后交给 onListKey */
|
|
515
|
+
if (list.hidden) return;
|
|
516
|
+
var k = e.key;
|
|
517
|
+
/* 这些键在"已打开"状态下由 onListKey 统一处理 */
|
|
518
|
+
if (k === 'Escape' || k === 'Esc' || k === 'ArrowDown' || k === 'ArrowUp' ||
|
|
519
|
+
k === 'Home' || k === 'End' || k === 'Enter' || k === ' ' ||
|
|
520
|
+
k === 'Spacebar' || k === 'Tab') {
|
|
521
|
+
/* Enter/Space 在关闭态要"打开",交给 btn 原 handler */
|
|
522
|
+
if (list.hidden) return;
|
|
523
|
+
onListKey(e);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (k.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) onListKey(e);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
/* ---------- 点击 ---------- */
|
|
530
|
+
btn.addEventListener('click', function () {
|
|
531
|
+
if (list.hidden) open(); else close();
|
|
532
|
+
});
|
|
533
|
+
list.addEventListener('click', function (e) {
|
|
534
|
+
var o = e.target.closest ? closest(e.target, '[role="option"]') : null;
|
|
535
|
+
if (!o) return;
|
|
536
|
+
commit(options.indexOf(o));
|
|
537
|
+
});
|
|
538
|
+
/* 点外部关闭已由 dismissable 核接管(document 捕获阶段判定)。 */
|
|
539
|
+
|
|
540
|
+
paint();
|
|
541
|
+
return {
|
|
542
|
+
open: open, close: close,
|
|
543
|
+
get value() { return selected; },
|
|
544
|
+
set value(v) { selected = v == null ? null : String(v); paint(); },
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
global.Select = { create: create };
|
|
549
|
+
})(window);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# separator · 分隔线
|
|
2
|
+
|
|
3
|
+
## 两种,必须分清
|
|
4
|
+
|
|
5
|
+
| | 类 | 用途 | 读屏 |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| **装饰性** | `.separator` | 只把内容分成块 | **不播报** |
|
|
8
|
+
| **语义性** | `.separator--label` | 分开**不同的东西** | **播报"分隔符"** |
|
|
9
|
+
|
|
10
|
+
**为什么必须分**:两者视觉上都是一条线,但**对辅助技术的意义相反** ——
|
|
11
|
+
装饰性的播报是噪音,语义性的不播报则丢信息。
|
|
12
|
+
|
|
13
|
+
## 类
|
|
14
|
+
|
|
15
|
+
| 类 | 作用 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `.separator` | 装饰性(`<hr>`)|
|
|
18
|
+
| `.separator--tight` | 间距更小(同组内部)|
|
|
19
|
+
| `.separator--label` | 语义性,文字**前**画线 |
|
|
20
|
+
| `.separator--both` | 两侧都画线 |
|
|
21
|
+
|
|
22
|
+
## 用法
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<!-- 装饰性 -->
|
|
26
|
+
<hr class="separator">
|
|
27
|
+
|
|
28
|
+
<!-- 语义性:必须有名字 -->
|
|
29
|
+
<hr class="separator separator--label" role="separator" aria-label="高级设置">
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 为什么用两档颜色(实测,不是拍脑袋)
|
|
33
|
+
|
|
34
|
+
本库实测(vs `--surface`,WCAG 1.4.11 非文字对比要 ≥ 3:1):
|
|
35
|
+
|
|
36
|
+
| 令牌 | 浅色 | 暗色 | 判定 |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| `--border-decor` | 1.26 | 1.23 | < 3 |
|
|
39
|
+
| `--border-decor-str` | 1.60 | 1.61 | < 3 |
|
|
40
|
+
| `--border-control` | **3.42** | **4.34** | ✅ ≥ 3 |
|
|
41
|
+
|
|
42
|
+
**差 2.7 倍。** 所以:
|
|
43
|
+
|
|
44
|
+
- **装饰性**用 `--border-decor` —— 1.4.11 **不适用于它**(不承担语义)
|
|
45
|
+
- **语义性**用 `--border-control` —— 它是读屏用户**唯一能看到的线索**,
|
|
46
|
+
视觉上必须够明显
|
|
47
|
+
|
|
48
|
+
**这就是"不要一档打天下"的实测依据。**
|
|
49
|
+
|
|
50
|
+
## 两个反例
|
|
51
|
+
|
|
52
|
+
**① 语义分隔用装饰色** —— 分隔"基本信息"与"高级设置"是两类不同的东西,
|
|
53
|
+
但 1.26:1 的线在低视力与暗色下几乎看不见,而读屏只念"分隔符"、
|
|
54
|
+
不解释两边是什么 ⇒ 该线是唯一线索时必须用达标的档。
|
|
55
|
+
|
|
56
|
+
**② 用 div + border-bottom 代替 hr** —— `<div class="line">` 对读屏
|
|
57
|
+
**什么都没有**。语义分隔必须用 `<hr>` 或 `role="separator"`,
|
|
58
|
+
装饰性的才可以用纯 div。
|
|
59
|
+
|
|
60
|
+
## 细节
|
|
61
|
+
|
|
62
|
+
用 `border` 而不是 `box-shadow` 画线 —— 1px 的线用 box-shadow
|
|
63
|
+
在某些浏览器上会渲染成 2px(亚像素取整)。
|