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,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tooltip.js — 文字提示(B3 /
|
|
3
|
+
*
|
|
4
|
+
* ⭐ 三条硬要求:
|
|
5
|
+
* ① **键盘可达**:focus 时也要出(触屏没有 hover)
|
|
6
|
+
* ② **Esc 能关**(WCAG 1.4.13 Content on Hover or Focus)
|
|
7
|
+
* ③ ⛔ **不承载必要信息** —— 触屏没有 hover,只放 tooltip 等于不存在
|
|
8
|
+
*
|
|
9
|
+
* 无依赖 · ES5 · 用法:
|
|
10
|
+
* Tooltip.attach(el, '文字') // 默认 bottom
|
|
11
|
+
* Tooltip.attach(el, '文字', 'top') // 指定方位
|
|
12
|
+
* Tooltip.attach(el, '文字', 'start') // 内联起点侧(RTL 下自动换到右边)
|
|
13
|
+
* Tooltip.attach(el, '文字', 'left') // 物理左(RTL 下**仍是物理左**)
|
|
14
|
+
*
|
|
15
|
+
* 🔴 0.4.2 修了两个实测出来的既有缺陷(都不报错,只是"看着不对"):
|
|
16
|
+
* ① `place='left'|'right'` 拼出的 `tooltip--left` / `--right` **CSS 里没有**
|
|
17
|
+
* (只有 --inset-inline-start / -end)⇒ 这两个方位**没有小三角**。
|
|
18
|
+
* ⇒ 现在按书写方向换算类名:物理左在 LTR 是内联起点、RTL 是内联终点。
|
|
19
|
+
* ② 定位前**先入 DOM 再量**:游离元素 getBoundingClientRect() 全是 0
|
|
20
|
+
* ⇒ top 方位与锚点完全重叠(实测 tip.top == anchor.top),左右上下偏半身。
|
|
21
|
+
*/
|
|
22
|
+
(function (global) {
|
|
23
|
+
|
|
24
|
+
/* ==== BEHAVIOR INJECT BEGIN: focus-return ==== */
|
|
25
|
+
var flFocusReturn = function (opts) {
|
|
26
|
+
var opt = opts || {};
|
|
27
|
+
var saved = null;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
save: function () {
|
|
31
|
+
var a = document.activeElement;
|
|
32
|
+
saved = (a && a !== document.body && a.focus) ? a : null;
|
|
33
|
+
return saved;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
restore: function () {
|
|
37
|
+
var target = saved;
|
|
38
|
+
/* ⚠️ 见文档 ②:不判 contains 的话,元素被移除后 focus() 不报错
|
|
39
|
+
但焦点不动 ⇒ 用户以为还回去了,其实掉在 body 上。 */
|
|
40
|
+
if (!target || !document.contains(target)) target = opt.fallback || null;
|
|
41
|
+
if (!target) { saved = null; return false; }
|
|
42
|
+
try {
|
|
43
|
+
target.focus();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
saved = null;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
var ok = document.activeElement === target;
|
|
49
|
+
saved = null; /* 见文档 ③ */
|
|
50
|
+
return ok;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
target: function () { return saved; },
|
|
54
|
+
clear: function () { saved = null; },
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/* ==== BEHAVIOR INJECT END: focus-return ==== */
|
|
58
|
+
/* ==== BEHAVIOR INJECT BEGIN: dismissable ==== */
|
|
59
|
+
var flDismissable = function (opts) {
|
|
60
|
+
var opt = opts || {};
|
|
61
|
+
var escOn = opt.escOn || [];
|
|
62
|
+
var inside = opt.inside || [];
|
|
63
|
+
var when = opt.when || function () { return true; };
|
|
64
|
+
var onDismiss = opt.onDismiss || function () {};
|
|
65
|
+
|
|
66
|
+
var shared = window.__flDismiss || (window.__flDismiss = { stack: [], bound: false });
|
|
67
|
+
|
|
68
|
+
function isInside(node) {
|
|
69
|
+
if (!node) return false;
|
|
70
|
+
for (var i = 0; i < inside.length; i++) {
|
|
71
|
+
var el = inside[i];
|
|
72
|
+
if (!el || !el.contains) continue;
|
|
73
|
+
if (el === node || el.contains(node)) return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onKey(e) {
|
|
79
|
+
if (!when()) return;
|
|
80
|
+
var k = e.key;
|
|
81
|
+
if (k !== 'Escape' && k !== 'Esc' && e.keyCode !== 27) return;
|
|
82
|
+
/* 🔴 为什么 onDismiss 在前、preventDefault/stopPropagation 在后
|
|
83
|
+
(0.8.8 改):**分级 Esc** 需要核让组件有机会说"这次我没关"。
|
|
84
|
+
combobox 的 Esc 是三级的:
|
|
85
|
+
① 列表开着 → 关列表
|
|
86
|
+
② 列表关了、输入有字 → 清空输入
|
|
87
|
+
③ 都没了 → 什么都不做(此时**不该**阻止默认行为,
|
|
88
|
+
让 Esc 能继续传给外层浮层)
|
|
89
|
+
而原实现是「无条件 preventDefault + stopPropagation,再调 onDismiss」
|
|
90
|
+
⇒ 组件根本无法表达"这次我没关"。
|
|
91
|
+
⇒ 改成:先问onDismiss,它返回 false 就**不干预**默认行为。
|
|
92
|
+
⚠️ 顺序安全性:stopPropagation 只需在**本次监听器执行期间**被调用即可
|
|
93
|
+
阻止冒泡到外层,放在 onDismiss 之后仍然有效。
|
|
94
|
+
*/
|
|
95
|
+
var closed = onDismiss(e, 'esc');
|
|
96
|
+
if (closed === false) return;
|
|
97
|
+
if (e.preventDefault) e.preventDefault();
|
|
98
|
+
if (e.stopPropagation) e.stopPropagation();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function onDocClick(e) {
|
|
102
|
+
var top = null;
|
|
103
|
+
for (var i = shared.stack.length - 1; i >= 0; i--) {
|
|
104
|
+
if (shared.stack[i].when()) { top = shared.stack[i]; break; }
|
|
105
|
+
}
|
|
106
|
+
if (!top || top.isInside(e.target)) return;
|
|
107
|
+
top.onDismiss(e, 'outside');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
111
|
+
|
|
112
|
+
for (var j = 0; j < escOn.length; j++) {
|
|
113
|
+
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
114
|
+
}
|
|
115
|
+
shared.stack.push(rec);
|
|
116
|
+
if (!shared.bound) {
|
|
117
|
+
shared.bound = true;
|
|
118
|
+
document.addEventListener('click', onDocClick, true);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
destroy: function () {
|
|
123
|
+
for (var j = 0; j < escOn.length; j++) {
|
|
124
|
+
if (escOn[j]) escOn[j].removeEventListener('keydown', onKey);
|
|
125
|
+
}
|
|
126
|
+
var k = shared.stack.indexOf(rec);
|
|
127
|
+
if (k >= 0) shared.stack.splice(k, 1);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
/* ==== BEHAVIOR INJECT END: dismissable ==== */
|
|
132
|
+
'use strict';
|
|
133
|
+
|
|
134
|
+
var uid = 0;
|
|
135
|
+
|
|
136
|
+
function ensureStyle() {
|
|
137
|
+
if (document.getElementById('tooltip-css')) return;
|
|
138
|
+
/* 说明:样式由页面通过 <link> 引入;这里只做"有没有"的提示,
|
|
139
|
+
避免"以为引了其实没引"的静默失效。 */
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ensureLiveRegion() {
|
|
143
|
+
var id = 'tooltip-live';
|
|
144
|
+
var r = document.getElementById(id);
|
|
145
|
+
if (r) return r;
|
|
146
|
+
r = document.createElement('div');
|
|
147
|
+
r.id = id;
|
|
148
|
+
r.setAttribute('role', 'tooltip');
|
|
149
|
+
/* ⭐ aria-live:内容变化时读屏会念出来
|
|
150
|
+
(否则键盘用户看到的提示,盲用户听不到) */
|
|
151
|
+
r.setAttribute('aria-live', 'polite');
|
|
152
|
+
r.setAttribute('aria-atomic', 'true');
|
|
153
|
+
/* 🔴 aria-label(0.8.9 补):`role="tooltip"` 的元素**必须有可访问名**
|
|
154
|
+
—— 实测 axe 报 serious:
|
|
155
|
+
`aria-tooltip-name: ARIA tooltip nodes must have an accessible name`
|
|
156
|
+
这个 live region 里的文字是**运行时塞进去的提示内容**,
|
|
157
|
+
而在读屏用户"到达"它之前它通常是空的
|
|
158
|
+
⇒ 光靠内容拿不到名字 ⇒ 必须显式给 aria-label。
|
|
159
|
+
label 用通用措辞,具体内容由 aria-live 负责播报。 */
|
|
160
|
+
r.setAttribute('aria-label', '组件提示');
|
|
161
|
+
r.style.cssText =
|
|
162
|
+
'position:absolute;width:1px;height:1px;padding:0;margin:-1px;' +
|
|
163
|
+
'overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0';
|
|
164
|
+
document.body.appendChild(r);
|
|
165
|
+
return r;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* 逻辑方位 ⇒ 类名后缀。CSS 里只有 --inset-inline-start / --inset-inline-end */
|
|
169
|
+
var CLS = {
|
|
170
|
+
top: 'top', bottom: 'bottom',
|
|
171
|
+
start: 'inset-inline-start', end: 'inset-inline-end',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 物理方位换算成逻辑方位。
|
|
176
|
+
* 物理「左」在 LTR 里是内联起点,在 RTL 里是内联终点 ——
|
|
177
|
+
* 不换算的话,RTL 下小三角会指向气泡外侧(见 tooltip.css 的注释)。
|
|
178
|
+
*/
|
|
179
|
+
function resolve(el, place) {
|
|
180
|
+
var p = place || 'bottom';
|
|
181
|
+
if (p === 'top' || p === 'bottom' || p === 'start' || p === 'end') return p;
|
|
182
|
+
if (p !== 'left' && p !== 'right') return 'bottom';
|
|
183
|
+
var rtl = getComputedStyle(el).direction === 'rtl';
|
|
184
|
+
if (p === 'left') return rtl ? 'end' : 'start';
|
|
185
|
+
return rtl ? 'start' : 'end';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 给元素挂一个 tooltip
|
|
190
|
+
* @param {Element} el 触发元素
|
|
191
|
+
* @param {string} text 提示文字
|
|
192
|
+
* @param {string} [place] top / bottom / start / end(默认 bottom);
|
|
193
|
+
* left / right 也接受(物理方位,RTL 下按方向换算类名)
|
|
194
|
+
*/
|
|
195
|
+
function attach(el, text, place) {
|
|
196
|
+
/* 焦点归位核:在 show() 之前声明,避免读代码时误以为fr 还没定义。
|
|
197
|
+
声明写在 attach 开头 ⇒ show()/hide() 用到时它一定已赋值。 */
|
|
198
|
+
var fr = flFocusReturn({ fallback: el });
|
|
199
|
+
if (!el || !text) return;
|
|
200
|
+
ensureStyle();
|
|
201
|
+
|
|
202
|
+
var pos = resolve(el, place);
|
|
203
|
+
var id = 'tt' + (++uid);
|
|
204
|
+
var tip = document.createElement('div');
|
|
205
|
+
tip.id = id;
|
|
206
|
+
tip.className = 'tooltip tooltip--' + CLS[pos];
|
|
207
|
+
tip.setAttribute('role', 'tooltip');
|
|
208
|
+
tip.textContent = text;
|
|
209
|
+
|
|
210
|
+
var live = ensureLiveRegion();
|
|
211
|
+
|
|
212
|
+
function show() {
|
|
213
|
+
/* 🔴 打开的**这一刻**先记住焦点,关闭时才能还回去。
|
|
214
|
+
必须在任何可能改变焦点的操作**之前**做(否则记到的是新元素)。 */
|
|
215
|
+
fr.save();
|
|
216
|
+
/* 🔴 定位前**必须先入 DOM**:游离元素的 getBoundingClientRect() 全是 0
|
|
217
|
+
(实测:top 方位的气泡与锚点完全重叠,就是这么来的)。
|
|
218
|
+
先挂上去、藏起来量,量完再显示。 */
|
|
219
|
+
tip.style.visibility = 'hidden';
|
|
220
|
+
tip.style.display = 'block';
|
|
221
|
+
if (!tip.parentNode) document.body.appendChild(tip);
|
|
222
|
+
|
|
223
|
+
var b = el.getBoundingClientRect();
|
|
224
|
+
var t = tip.getBoundingClientRect();
|
|
225
|
+
if (pos === 'top') {
|
|
226
|
+
tip.style.top = (b.top - t.height) + window.pageYOffset + 'px';
|
|
227
|
+
tip.style.left = (b.left + b.width / 2 - t.width / 2) + window.pageXOffset + 'px';
|
|
228
|
+
} else if (pos === 'start' || pos === 'end') {
|
|
229
|
+
var rtl = getComputedStyle(el).direction === 'rtl';
|
|
230
|
+
/* start = 气泡在锚点的内联起点侧:LTR 靠左(右缘贴锚点左缘),
|
|
231
|
+
RTL 靠右(左缘贴锚点右缘) */
|
|
232
|
+
var x = (pos === 'start')
|
|
233
|
+
? (rtl ? b.right : b.left - t.width)
|
|
234
|
+
: (rtl ? b.left - t.width : b.right);
|
|
235
|
+
tip.style.left = x + window.pageXOffset + 'px';
|
|
236
|
+
tip.style.top = (b.top + b.height / 2 - t.height / 2) + window.pageYOffset + 'px';
|
|
237
|
+
} else {
|
|
238
|
+
tip.style.top = (b.bottom + window.pageYOffset) + 'px';
|
|
239
|
+
tip.style.left = (b.left + b.width / 2 - t.width / 2) + window.pageXOffset + 'px';
|
|
240
|
+
}
|
|
241
|
+
tip.style.visibility = ''; /* 交还给 CSS(data-state 控制显隐) */
|
|
242
|
+
tip.setAttribute('data-state', 'open');
|
|
243
|
+
live.textContent = text;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hide() {
|
|
247
|
+
tip.removeAttribute('data-state');
|
|
248
|
+
live.textContent = '';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ① 键盘可达:focus 也出 */
|
|
252
|
+
el.addEventListener('focus', show);
|
|
253
|
+
el.addEventListener('blur', hide);
|
|
254
|
+
|
|
255
|
+
/* 触屏:click 也出(没有 hover 的设备唯一入口)*/
|
|
256
|
+
el.addEventListener('click', function (e) {
|
|
257
|
+
e.stopPropagation();
|
|
258
|
+
if (tip.getAttribute('data-state') === 'open') hide(); else show();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
/* ② Esc 能关(WCAG 1.4.13)+ 点击外部关闭
|
|
262
|
+
🔴 0.8.6 收编到 dismissable 核(原先是手写的):
|
|
263
|
+
手写版是 `document.addEventListener('click', hide)` ——
|
|
264
|
+
**冒泡阶段**判定"是不是外部",于是点在浮层**内部**也会关掉气泡
|
|
265
|
+
(点击应该能选中提示里的文字)。核把判定挂在 **document 捕获阶段**,
|
|
266
|
+
并且把触发元素算进 `inside`,解决"点触发器刚打开就被关"的老坑。
|
|
267
|
+
核同时负责"一次 Esc 只关最内层"(Esc 挂在元素上冒泡,不挂 document 捕获)。
|
|
268
|
+
⚠️ Esc 关闭后的焦点归位走 focus-return 核,不再手写 `el.focus()`:
|
|
269
|
+
手写版在触发元素已被移除时会focus 一个不存在的元素。 */
|
|
270
|
+
var dismiss = flDismissable({
|
|
271
|
+
escOn: [el],
|
|
272
|
+
inside: [el, tip],
|
|
273
|
+
when: function () { return tip.getAttribute('data-state') === 'open'; },
|
|
274
|
+
onDismiss: function (ev, reason) {
|
|
275
|
+
hide();
|
|
276
|
+
if (reason === 'esc') fr.restore();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
/* ③ 关联:aria-describedby(不是 aria-label ——
|
|
281
|
+
label 是名字,describedby 是补充说明,语义不同)*/
|
|
282
|
+
var cur = el.getAttribute('aria-describedby');
|
|
283
|
+
el.setAttribute('aria-describedby', cur ? (cur + ' ' + id) : id);
|
|
284
|
+
el.setAttribute('data-tooltip', text);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
global.Tooltip = { attach: attach };
|
|
288
|
+
})(window);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# tree · 树形视图
|
|
2
|
+
|
|
3
|
+
依据 **APG(ARIA Authoring Practices)Tree View** 模式。
|
|
4
|
+
|
|
5
|
+
## ⭐ 树是键盘契约最严的组件(APG 列了 12 条)
|
|
6
|
+
|
|
7
|
+
| 键 | 行为 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `↑` `↓` | 移动焦点 |
|
|
10
|
+
| `←` `→` | 收起 / 展开(并移动焦点)|
|
|
11
|
+
| `Home` / `End` | 跳首 / 末 |
|
|
12
|
+
| `*` | 展开当前层全部同级 |
|
|
13
|
+
| `Enter` | 激活 |
|
|
14
|
+
| `Space` | 选中 |
|
|
15
|
+
| `a`–`z` | 首字母跳转 |
|
|
16
|
+
|
|
17
|
+
## 🔴 roving tabindex —— 整棵树只占一个 Tab 位
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
/* 只有"当前节点" tabindex="0",其余 -1 */
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**为什么**:若每个节点都 `tabindex="0"`,**Tab 30 次才能穿过一棵树**。
|
|
24
|
+
|
|
25
|
+
⚠️ 那个 `"0"` **必须跟着焦点走**。只在初始化时设一次是最常见的 bug ——
|
|
26
|
+
用户走到第 3 层后,按 Shift+Tab 回退会回到第 1 个节点。
|
|
27
|
+
|
|
28
|
+
实现走共享微行为核 `01-tokens/behavior/roving.js`,首字母跳转接`typeahead` 核
|
|
29
|
+
—— 不是各组件各写一遍游走逻辑。
|
|
30
|
+
|
|
31
|
+
## 结构
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<ul class="tree" role="tree" aria-label="目录">
|
|
35
|
+
<li class="tree__item" role="treeitem" aria-expanded="true" tabindex="0">
|
|
36
|
+
<span class="tree__node">
|
|
37
|
+
<span class="tree__marker" aria-hidden="true"></span>
|
|
38
|
+
<span class="tree__label">…</span>
|
|
39
|
+
<span class="tree__count">12</span>
|
|
40
|
+
</span>
|
|
41
|
+
<ul role="group">
|
|
42
|
+
<li class="tree__item" role="treeitem" tabindex="-1">…</li>
|
|
43
|
+
</ul>
|
|
44
|
+
</li>
|
|
45
|
+
</ul>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| 部件 | 说明 |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `.tree` | 根,`role="tree"` |
|
|
51
|
+
| `.tree__item` | 节点,`role="treeitem"` + `aria-expanded` |
|
|
52
|
+
| `.tree__node` | 节点内层(marker + label + count)|
|
|
53
|
+
| `.tree__marker` | 展开/收起标记位(**符号由 CSS `::before` 生成**,不是文本)|
|
|
54
|
+
| `.tree__label` | 文字 |
|
|
55
|
+
| `.tree__count` | 计数徽标 |
|
|
56
|
+
| `<ul role="group">` | 子层 |
|
|
57
|
+
|
|
58
|
+
### 标记符号跟着 `aria-expanded` 走
|
|
59
|
+
|
|
60
|
+
```css
|
|
61
|
+
.tree__item[aria-expanded="true"] > .tree__node .tree__marker::before {
|
|
62
|
+
content: "\25BC"; /* ▼ */
|
|
63
|
+
}
|
|
64
|
+
/* 收起是 ▸,叶子是 · (表示"这里没有子节点")*/
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
符号是 **CSS 生成的,不是塞进 HTML 的文本** ⇒ 状态一变,符号自动跟着变,
|
|
68
|
+
不需要 JS 去同步两处。`.tree__marker` 本身固定占 `1em` 宽并居中,
|
|
69
|
+
切换时不会引起文字抖动。
|
|
70
|
+
|
|
71
|
+
## 启用
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const t = Tree.create(元素, { label: '目录' });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`create()` 会自己补齐 `role="tree"` 与 `aria-label`(**只在没写时**补,
|
|
78
|
+
不覆盖你已有的)。
|
|
79
|
+
|
|
80
|
+
## 缩进用 padding,不是 margin
|
|
81
|
+
|
|
82
|
+
```css
|
|
83
|
+
.tree ul { padding: 0; } /* 嵌套层级靠 padding 体现 */
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
用 margin 的话,嵌套结构在视觉上"飘"着,层级读不出来。
|
|
87
|
+
|
|
88
|
+
## 成熟度:stable(无行为债)
|
|
89
|
+
|
|
90
|
+
`ai/components.json` 里 `tree` 的 `behaviorDebt` 为空。
|
|
91
|
+
|
|
92
|
+
## 门禁
|
|
93
|
+
|
|
94
|
+
`05-audit/tree-check.js`
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<link rel="icon" href="data:,">
|
|
7
|
+
<title>tree · 树形视图 · frontend-lib</title>
|
|
8
|
+
<link rel="stylesheet" href="../../01-tokens/tokens.css">
|
|
9
|
+
<link rel="stylesheet" href="../../01-tokens/typography.css">
|
|
10
|
+
<link rel="stylesheet" href="../../01-tokens/focus-ring.css">
|
|
11
|
+
<link rel="stylesheet" href="../../01-tokens/page.css">
|
|
12
|
+
<link rel="stylesheet" href="tree.css">
|
|
13
|
+
<style>
|
|
14
|
+
.kbd {
|
|
15
|
+
display: inline-block; padding: 1px 6px; margin: 0 2px;
|
|
16
|
+
background: var(--surface); border: 1px solid var(--border-decor);
|
|
17
|
+
border-radius: var(--r-xs); font-family: var(--font-mono, monospace);
|
|
18
|
+
font-size: .9em;
|
|
19
|
+
}
|
|
20
|
+
.log {
|
|
21
|
+
margin-top: var(--sp-4); padding: var(--sp-3);
|
|
22
|
+
background: var(--surface-sunken); border-radius: var(--r-sm);
|
|
23
|
+
font-family: var(--font-mono, monospace); font-size: var(--fs-xs);
|
|
24
|
+
color: var(--text-secondary); min-height: 3em;
|
|
25
|
+
}
|
|
26
|
+
.frame { padding: var(--sp-3); border: 1px solid var(--border-decor); border-radius: var(--r-md); }
|
|
27
|
+
</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body class="page page">
|
|
30
|
+
<main class="wrap">
|
|
31
|
+
|
|
32
|
+
<h1>tree · 树形视图</h1>
|
|
33
|
+
<p class="note">
|
|
34
|
+
依据 <strong>APG Tree View</strong>。两条最关键的设计:
|
|
35
|
+
<br>① <strong>Roving tabindex</strong>:整棵树只占一个 Tab 位(当前节点 tabindex=0,其余 -1)
|
|
36
|
+
<br>② <span class="kbd">←</span><span class="kbd">→</span>
|
|
37
|
+
<strong>展开/收起并带着焦点走</strong>(已收起时按 ← 回到父节点)
|
|
38
|
+
<br>其余:<span class="kbd">↑</span><span class="kbd">↓</span> 移动 ·
|
|
39
|
+
<span class="kbd">Home</span><span class="kbd">End</span> 首末 ·
|
|
40
|
+
<span class="kbd">*</span> 展开本层同级 · <span class="kbd">空格</span> 选中 ·
|
|
41
|
+
<span class="kbd">a-z</span> 首字母跳转
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<h2>1 · 文件树(试试键盘)</h2>
|
|
45
|
+
<div class="frame">
|
|
46
|
+
<div id="t1"></div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="log" id="log">(焦点位置与选中项会显示在这里)</div>
|
|
49
|
+
|
|
50
|
+
<h2>2 · 默认全部收起</h2>
|
|
51
|
+
<div class="frame">
|
|
52
|
+
<div id="t2"></div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
</main>
|
|
56
|
+
|
|
57
|
+
<script src="tree.js"></script>
|
|
58
|
+
<script>
|
|
59
|
+
(function () {
|
|
60
|
+
var log = document.getElementById('log');
|
|
61
|
+
function label(it) {
|
|
62
|
+
var n = it.node;
|
|
63
|
+
return (n.textContent || '').replace(/·/g, '').trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mk(id, collapsed) {
|
|
67
|
+
document.getElementById(id).innerHTML =
|
|
68
|
+
'<ul role="group">' +
|
|
69
|
+
'<li class="tree__item">' +
|
|
70
|
+
'<div class="tree__node"><span class="tree__label">src</span>' +
|
|
71
|
+
'<span class="tree__count">12</span></div>' +
|
|
72
|
+
'<ul role="group">' +
|
|
73
|
+
'<li class="tree__item">' +
|
|
74
|
+
'<div class="tree__node"><span class="tree__label">components</span>' +
|
|
75
|
+
'<span class="tree__count">6</span></div>' +
|
|
76
|
+
'<ul role="group">' +
|
|
77
|
+
'<li class="tree__item"><div class="tree__node">' +
|
|
78
|
+
'<span class="tree__label">Button.js</span></div></li>' +
|
|
79
|
+
'<li class="tree__item"><div class="tree__node">' +
|
|
80
|
+
'<span class="tree__label">Card.js</span></div></li>' +
|
|
81
|
+
'</ul>' +
|
|
82
|
+
'</li>' +
|
|
83
|
+
'<li class="tree__item">' +
|
|
84
|
+
'<div class="tree__node"><span class="tree__label">styles</span>' +
|
|
85
|
+
'<span class="tree__count">5</span></div>' +
|
|
86
|
+
'<ul role="group">' +
|
|
87
|
+
'<li class="tree__item"><div class="tree__node">' +
|
|
88
|
+
'<span class="tree__label">tokens.css</span></div></li>' +
|
|
89
|
+
'<li class="tree__item"><div class="tree__node">' +
|
|
90
|
+
'<span class="tree__label">base.css</span></div></li>' +
|
|
91
|
+
'</ul>' +
|
|
92
|
+
'</li>' +
|
|
93
|
+
'<li class="tree__item"><div class="tree__node">' +
|
|
94
|
+
'<span class="tree__label">index.js</span></div></li>' +
|
|
95
|
+
'</ul>' +
|
|
96
|
+
'</li>' +
|
|
97
|
+
'<li class="tree__item">' +
|
|
98
|
+
'<div class="tree__node"><span class="tree__label">docs</span>' +
|
|
99
|
+
'<span class="tree__count">3</span></div>' +
|
|
100
|
+
'<ul role="group">' +
|
|
101
|
+
'<li class="tree__item"><div class="tree__node">' +
|
|
102
|
+
'<span class="tree__label">changelog.md</span></div></li>' +
|
|
103
|
+
'</ul>' +
|
|
104
|
+
'</li>' +
|
|
105
|
+
'<li class="tree__item"><div class="tree__node">' +
|
|
106
|
+
'<span class="tree__label">package.json</span></div></li>' +
|
|
107
|
+
'</ul>';
|
|
108
|
+
|
|
109
|
+
if (collapsed) {
|
|
110
|
+
document.querySelectorAll('#' + id + ' [role="group"]').forEach(function (g) {
|
|
111
|
+
if (g.parentElement.getAttribute && g.parentElement.getAttribute('role') === 'treeitem') {
|
|
112
|
+
g.hidden = true;
|
|
113
|
+
g.parentElement.setAttribute('aria-expanded', 'false');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return Tree.create(document.getElementById(id), {
|
|
119
|
+
label: '文件树',
|
|
120
|
+
onSelect: function (it) { log.textContent = '选中:' + label(it); },
|
|
121
|
+
onActivate: function (it) { log.textContent = '激活:' + label(it); },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
var t1 = mk('t1', false);
|
|
126
|
+
mk('t2', true);
|
|
127
|
+
|
|
128
|
+
/* 焦点变化时也记一笔(演示 roving tabindex)*/
|
|
129
|
+
document.addEventListener('focusin', function (e) {
|
|
130
|
+
if (e.target.classList && e.target.classList.contains('tree__node')) {
|
|
131
|
+
var t = e.target.closest('.tree__node').textContent.replace(/·/g, '').trim();
|
|
132
|
+
log.textContent = '焦点:' + t;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
})();
|
|
136
|
+
</script>
|
|
137
|
+
</body>
|
|
138
|
+
</html>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
tree.css — 树形视图(D1 /
|
|
3
|
+
============================================================================
|
|
4
|
+
依据:APG(ARIA Authoring Practices)Tree View 模式
|
|
5
|
+
|
|
6
|
+
⭐ 树是**键盘契约最严**的组件(APG 列了 12 条):
|
|
7
|
+
↑ ↓ 移动焦点 ← → 收起/展开(并移动焦点)
|
|
8
|
+
Home End 跳首/末
|
|
9
|
+
`*` 展开当前层全部同级
|
|
10
|
+
Enter 激活 / Space 选中
|
|
11
|
+
a-z 首字母跳转
|
|
12
|
+
焦点**始终只有一个**,且用 **roving tabindex**(整棵树只占一个 Tab 位)
|
|
13
|
+
|
|
14
|
+
⚠️ roving tabindex 的意义:
|
|
15
|
+
若每个节点都 tabindex=0,Tab 30 次才能穿过一棵树。
|
|
16
|
+
正确做法:只有"当前节点" tabindex=0,其余 -1。
|
|
17
|
+
============================================================================ */
|
|
18
|
+
|
|
19
|
+
.tree {
|
|
20
|
+
font-size: var(--fs-sm);
|
|
21
|
+
color: var(--text-primary);
|
|
22
|
+
/* 容器不抢焦点(焦点在节点上) */
|
|
23
|
+
outline: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.tree ul {
|
|
27
|
+
margin: 0;
|
|
28
|
+
padding: 0;
|
|
29
|
+
/* 缩进用 padding,不用 margin(嵌套层级才看得出层次)*/
|
|
30
|
+
list-style: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.tree__item { position: relative; }
|
|
34
|
+
|
|
35
|
+
/* ---------- 节点 ---------- */
|
|
36
|
+
.tree__node {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: var(--sp-2);
|
|
40
|
+
/* 🔴 命中区:树节点密集,但 WCAG 2.5.8 AA 只要 24px
|
|
41
|
+
(这里给 32px,介于"手指好点"与"树不被撑太散"之间)*/
|
|
42
|
+
min-height: 32px;
|
|
43
|
+
padding: 0 var(--sp-2);
|
|
44
|
+
border-radius: var(--r-xs);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
user-select: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.tree__node:focus-visible {
|
|
50
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
51
|
+
outline-offset: calc(-1 * var(--focus-ring-width));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* 焦点态与悬停态视觉一致(键盘/鼠标看到同一个高亮)*/
|
|
55
|
+
@media (hover: hover) and (pointer: fine) {
|
|
56
|
+
.tree__node:hover { background: var(--surface-sunken); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.tree__node[aria-selected="true"] {
|
|
60
|
+
background: var(--accent-soft);
|
|
61
|
+
color: var(--text-primary);
|
|
62
|
+
font-weight: 500;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ---------- 展开箭头 ----------
|
|
66
|
+
⭐ 展开/收起**必须有三态形状线索**,不能只靠旋转一个箭头:
|
|
67
|
+
· 收起:▶(右三角)
|
|
68
|
+
· 展开:▼(下三角)
|
|
69
|
+
· 叶子:·(点,表示"这里没有子节点") */
|
|
70
|
+
.tree__marker {
|
|
71
|
+
flex: 0 0 auto;
|
|
72
|
+
width: 1em;
|
|
73
|
+
font-size: .8em;
|
|
74
|
+
color: var(--text-tertiary);
|
|
75
|
+
text-align: center;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.tree__item[aria-expanded="true"] > .tree__node .tree__marker::before {
|
|
79
|
+
content: "\25BC"; /* ▼ */
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.tree__item[aria-expanded="false"] > .tree__node .tree__marker::before {
|
|
83
|
+
content: "\25B6"; /* ▶ */
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.tree__item:not([aria-expanded]) > .tree__node .tree__marker::before {
|
|
87
|
+
content: "\00B7"; /* · 叶子节点 */
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ---------- 层级缩进(用左边框引导线)---------- */
|
|
91
|
+
.tree ul ul {
|
|
92
|
+
padding-inline-start: var(--sp-5);
|
|
93
|
+
border-inline-start: 1px solid var(--border-decor);
|
|
94
|
+
margin-inline-start: var(--sp-4); /* 缩进线的位置 */
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ---------- 标签 ---------- */
|
|
98
|
+
.tree__label { flex: 1 1 auto; }
|
|
99
|
+
|
|
100
|
+
.tree__count {
|
|
101
|
+
flex: 0 0 auto;
|
|
102
|
+
padding: 0 var(--sp-1);
|
|
103
|
+
font-size: var(--fs-xs);
|
|
104
|
+
font-family: var(--mono);
|
|
105
|
+
font-variant-numeric: tabular-nums;
|
|
106
|
+
color: var(--text-tertiary);
|
|
107
|
+
background: var(--surface-sunken);
|
|
108
|
+
border-radius: var(--r-pill);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@media (prefers-reduced-motion: reduce) {
|
|
112
|
+
.tree__marker { transition: none; }
|
|
113
|
+
}
|