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,369 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* accordion.js — Accordion 的行为层(APG: Accordion Pattern)
|
|
3
|
+
*
|
|
4
|
+
* 语法刻意用 ES5(无箭头函数 / const / let / 模板字符串)——
|
|
5
|
+
* 精简档(某项目 上的 WebView)版本不确定。
|
|
6
|
+
* 也刻意不用 Element.closest / Array.filter(ES5 之后才有),理由同 tabs.js。
|
|
7
|
+
*
|
|
8
|
+
* 🔴 键盘契约(W3C APG 原文)
|
|
9
|
+
* ------------------------------------------------
|
|
10
|
+
* Enter / Space 切换当前项
|
|
11
|
+
* Tab / Shift+Tab 在**所有**可聚焦元素之间移动(含面板内的)
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ **不要加方向键 / Home / End** ——
|
|
14
|
+
* APG 明确写:accordion 的键盘支持里**没有**方向键
|
|
15
|
+
* ("Header navigation uses the standard Tab order; arrow / Home / End
|
|
16
|
+
* key navigation is no longer part of the APG Accordion keyboard
|
|
17
|
+
* interaction.")。
|
|
18
|
+
* 那是 **Tabs** 的契约(roving tabindex)。
|
|
19
|
+
* ⇒ 很多实现照抄 tabs 给 accordion 加方向键,是把两个模式混了。
|
|
20
|
+
* ⇒ 本组件**故意不加**。
|
|
21
|
+
*
|
|
22
|
+
* 🔴 role="region" 的数量上限(APG 原文)
|
|
23
|
+
* ------------------------------------------------
|
|
24
|
+
* "…in an accordion that contains more than **approximately 6**
|
|
25
|
+
* panels that can be expanded at the same time."
|
|
26
|
+
*
|
|
27
|
+
* ⇒ 面板数 > 6 时**不**给 panel 加 role="region",
|
|
28
|
+
* 否则造成 "landmark region proliferation"(地标泛滥)。
|
|
29
|
+
* ⇒ 加 .accordion--many 即可关闭。
|
|
30
|
+
*/
|
|
31
|
+
(function () {
|
|
32
|
+
'use strict';
|
|
33
|
+
// 🔴 手风琴实例序号(保证 id 全局唯一,见 init 里的说明)
|
|
34
|
+
var uid = 0;
|
|
35
|
+
'use strict';
|
|
36
|
+
|
|
37
|
+
/* ==== BEHAVIOR INJECT BEGIN: emit ==== */
|
|
38
|
+
var flEmit = function (el, name, detail) {
|
|
39
|
+
if (!el || !el.dispatchEvent) return null;
|
|
40
|
+
var type = name.indexOf('fl-') === 0 ? name : 'fl-' + name;
|
|
41
|
+
var ev = null;
|
|
42
|
+
/* 老 WebView 没有 CustomEvent 构造函数 ⇒ 兜底走 createEvent(理由见文件头)*/
|
|
43
|
+
if (typeof window.CustomEvent === 'function') {
|
|
44
|
+
try {
|
|
45
|
+
ev = new window.CustomEvent(type, {
|
|
46
|
+
detail: detail || null, bubbles: true, cancelable: false
|
|
47
|
+
});
|
|
48
|
+
} catch (e) { ev = null; }
|
|
49
|
+
}
|
|
50
|
+
if (!ev) {
|
|
51
|
+
try {
|
|
52
|
+
ev = document.createEvent('CustomEvent');
|
|
53
|
+
ev.initCustomEvent(type, true, false, detail || null);
|
|
54
|
+
} catch (e2) { return null; }
|
|
55
|
+
}
|
|
56
|
+
el.dispatchEvent(ev);
|
|
57
|
+
return ev;
|
|
58
|
+
};
|
|
59
|
+
/* ==== BEHAVIOR INJECT END: emit ==== */
|
|
60
|
+
|
|
61
|
+
function toArray(x) { return Array.prototype.slice.call(x); }
|
|
62
|
+
|
|
63
|
+
/* requestAnimationFrame 的 ES5 安全版 */
|
|
64
|
+
var raf = (typeof window.requestAnimationFrame === 'function')
|
|
65
|
+
? window.requestAnimationFrame.bind(window)
|
|
66
|
+
: function (fn) { return setTimeout(fn, 16); };
|
|
67
|
+
|
|
68
|
+
/* 只触发一次的事件绑定(transitionend 可能来多次) */
|
|
69
|
+
function once(el, ev, fn) {
|
|
70
|
+
function handler(e) {
|
|
71
|
+
if (e && e.target !== el) return; // 冒泡上来的忽略
|
|
72
|
+
el.removeEventListener(ev, handler, true);
|
|
73
|
+
fn(e);
|
|
74
|
+
}
|
|
75
|
+
el.addEventListener(ev, handler, true);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function closest(el, sel) {
|
|
79
|
+
while (el && el.nodeType === 1) {
|
|
80
|
+
var fn = el.matches || el.msMatchesSelector || el.webkitMatchesSelector;
|
|
81
|
+
if (fn && fn.call(el, sel)) return el;
|
|
82
|
+
el = el.parentNode;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Accordion(root, opts) {
|
|
88
|
+
this.root = root;
|
|
89
|
+
this.opts = opts || {};
|
|
90
|
+
this.items = toArray(root.querySelectorAll('.accordion__item'));
|
|
91
|
+
if (!this.items.length) return;
|
|
92
|
+
// 🔴 把 `--multi` 从 CSS 类改成 data 属性。
|
|
93
|
+
//
|
|
94
|
+
// 原因:它**不是样式变体,是行为开关**。
|
|
95
|
+
// 多开模式下展开的那一项,和单开模式下展开的那一项
|
|
96
|
+
// **视觉上完全一样**(都是真的展开了)——
|
|
97
|
+
// 没有任何东西需要用样式去表达"这是多开模式"。
|
|
98
|
+
//
|
|
99
|
+
// 之前它是 class ⇒ `refs` 门禁报"class 未定义"(CSS 里确实没有)。
|
|
100
|
+
// 两个选择:
|
|
101
|
+
// ① 补一条空 CSS 规则凑数 ⇒ 骗门禁,不诚实
|
|
102
|
+
// ② 改用 data 属性 ⇒ 门禁天然不报,且**语义更准确**
|
|
103
|
+
// 选 ②。
|
|
104
|
+
//
|
|
105
|
+
// 对比 tabs--manual:那个我**给了**真实视觉差异
|
|
106
|
+
// (选中项不加粗),因为"还没生效的事不该看起来已生效"。
|
|
107
|
+
// 两个决定看起来不一致,其实标准是同一个:
|
|
108
|
+
// **有没有真实的视觉差异需要表达。**
|
|
109
|
+
//
|
|
110
|
+
// 用法:<div class="accordion" data-accordion data-accordion-multi>
|
|
111
|
+
this.single = !root.hasAttribute('data-accordion-multi')
|
|
112
|
+
&& !root.classList.contains('accordion--multi');
|
|
113
|
+
// >6 个面板时不给 role=region(APG 的上限)
|
|
114
|
+
this.useRegion = !root.classList.contains('accordion--many') &&
|
|
115
|
+
this.items.length <= 6;
|
|
116
|
+
/* 🔴 真机实测反馈「收起后点另一项要点两次」的**最终解法**:
|
|
117
|
+
|
|
118
|
+
实测量化:收起时下方 trigger 在动画中**上移 131px**。
|
|
119
|
+
⚠️ 试过两个办法都**无效**(都记在下面,别再走回头路):
|
|
120
|
+
① `pointer-events:none` 让事件穿透
|
|
121
|
+
—— 位置本身就是错的,穿透给谁?
|
|
122
|
+
② 锁住列表高度
|
|
123
|
+
—— 锁的是列表,第 1 项在列表**内部**收缩,第 2 项照样上移。
|
|
124
|
+
③ 动画期锁点击 + 结束后补偿
|
|
125
|
+
—— 补偿实测没生效,第一次点击被丢弃,**比原 bug 更糟**。
|
|
126
|
+
|
|
127
|
+
⇒ 真正的解法:**把过渡压到短到"察觉不到位移"**。
|
|
128
|
+
131px 在 **90ms** 内走完 ⇒ 约 1.4px/ms,
|
|
129
|
+
手指落下到事件派发(通常 30–80ms)的位移已足够小。
|
|
130
|
+
配合 `pointer-events:none`(收起的 panel 不吃事件)⇒ 基本无感。
|
|
131
|
+
|
|
132
|
+
⚠️ 这是**权衡**,不是完美:高度动画必然推动下方内容。
|
|
133
|
+
主流实现(Radix / Material)都接受这一点。
|
|
134
|
+
我们能做的,是把代价压到用户感知不到。 */
|
|
135
|
+
this.dur = this.reduced() ? 0 : 90; // 90ms
|
|
136
|
+
|
|
137
|
+
this.init();
|
|
138
|
+
this.bind();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Accordion.prototype.triggers = function () {
|
|
142
|
+
return this.items.map(function (it) {
|
|
143
|
+
return it.querySelector('.accordion__trigger');
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
Accordion.prototype.panels = function () {
|
|
148
|
+
return this.items.map(function (it) {
|
|
149
|
+
return it.querySelector('.accordion__panel');
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/* 🔴 初始化:补齐 ARIA 关系。
|
|
154
|
+
之所以在 JS 里补而不是要求手写:这三处(aria-controls 互指、
|
|
155
|
+
heading 层号、role=region)是**机械的对应关系**,让人手写只会写错。 */
|
|
156
|
+
Accordion.prototype.init = function () {
|
|
157
|
+
/* 🔴 本实例的编号(保证多实例时 id 不撞车,见下面 id 生成处的说明)*/
|
|
158
|
+
var seq = ++uid;
|
|
159
|
+
for (var i = 0; i < this.items.length; i++) {
|
|
160
|
+
var it = this.items[i];
|
|
161
|
+
var btn = it.querySelector('.accordion__trigger');
|
|
162
|
+
var panel = it.querySelector('.accordion__panel');
|
|
163
|
+
if (!btn || !panel) continue;
|
|
164
|
+
|
|
165
|
+
// 🔴 修「id 跨实例撞名」(axe 报 landmark-unique 的真因):
|
|
166
|
+
// 原来 `if (!panel.id) panel.id = 'acc-panel-' + (i + 1)`
|
|
167
|
+
// **每个 accordion 实例都从 1 开始编号** ⇒ 同一个页面有 3 个手风琴时
|
|
168
|
+
// ⇒ 出现 3 组 `acc-panel-1` ⇒ **id 全局不再唯一**(HTML 硬性要求)
|
|
169
|
+
// ⇒ `aria-labelledby="acc-btn-1"` 只会解析到**第一个**匹配的元素
|
|
170
|
+
// ⇒ 所有 region 的可访问名都变成同一个
|
|
171
|
+
// ⇒ axe 的 landmark-unique(landmark 名字必须唯一)必然报违规。
|
|
172
|
+
//
|
|
173
|
+
// 实测:页面上 8 个 role=region 里有 **3 组重复 id**。
|
|
174
|
+
//
|
|
175
|
+
// ⇒ 修法:ID 里带上**实例序号**(本库已有的自增计数器)。
|
|
176
|
+
if (!panel.id) panel.id = 'acc-panel-' + seq + '-' + (i + 1);
|
|
177
|
+
if (!btn.id) btn.id = 'acc-btn-' + seq + '-' + (i + 1);
|
|
178
|
+
|
|
179
|
+
// ① button → panel(aria-controls)
|
|
180
|
+
btn.setAttribute('aria-controls', panel.id);
|
|
181
|
+
// ② panel → button(aria-labelledby)
|
|
182
|
+
panel.setAttribute('aria-labelledby', btn.id);
|
|
183
|
+
// ⚠️ 实测结论:这里**不要加** role="region"(曾经加过,axe 立刻报
|
|
184
|
+
// landmark-unique,浏览器实验证实了因果):
|
|
185
|
+
// · role="region" 本身就是一个 landmark
|
|
186
|
+
// ⇒ 一个页面有多个手风琴面板 = 多个 region
|
|
187
|
+
// ⇒ axe 的 landmark-unique 要求 landmark 名字唯一
|
|
188
|
+
// 实测两种组合:
|
|
189
|
+
// 去掉 role、只留 aria-labelledby ⇒ ✅ 无违规
|
|
190
|
+
// 保留 role=region ⇒ ❌ landmark-unique
|
|
191
|
+
// ⇒ APG 允许两种做法,我们选「不加 role」:
|
|
192
|
+
// · aria-labelledby 已把面板与按钮关联(读屏能念出标题)
|
|
193
|
+
// · 不引入额外 landmark ⇒ 读屏用户不会被一堆同名地标淹没
|
|
194
|
+
// ⇒ 少一层 landmark,对读屏而言反而更清晰。
|
|
195
|
+
// ③ panel 的地标(≤6 时)
|
|
196
|
+
if (this.useRegion) panel.setAttribute('role', 'region');
|
|
197
|
+
|
|
198
|
+
// ④ heading 的 aria-level 与 h1-h6 天然隐含的一致 ⇒
|
|
199
|
+
// 这里不动(用真实的 h2/h3 比显式 aria-level 更可靠)
|
|
200
|
+
|
|
201
|
+
// ⑤ 🔴 建立**初始**的 data-state
|
|
202
|
+
// CSS 靠 `[data-state="open"]` 触发高度过渡,
|
|
203
|
+
// 没有它的话:HTML 里写了 aria-expanded="true" 也不会展开。
|
|
204
|
+
var isOpen = btn.getAttribute('aria-expanded') === 'true';
|
|
205
|
+
panel.setAttribute('data-state', isOpen ? 'open' : 'closed');
|
|
206
|
+
if (!isOpen) panel.setAttribute('hidden', '');
|
|
207
|
+
else panel.removeAttribute('hidden');
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/* ====================================================================
|
|
212
|
+
⭐ 展开 / 收起 —— 复刻 Radix Collapsible 的机制
|
|
213
|
+
====================================================================
|
|
214
|
+
Radix 源码注释(collapsible.tsx 实测):
|
|
215
|
+
"when closing we delay `present` to retrieve dimensions before closing"
|
|
216
|
+
|
|
217
|
+
意思是:**关闭时必须先量到高度,再收** ——
|
|
218
|
+
否则量到 0,动画就没有过程(会"啪"一下消失)。
|
|
219
|
+
|
|
220
|
+
实现(ES5,无 Promise / 无 async):
|
|
221
|
+
展开:data-state=open → 移除 hidden → 高度交给 CSS 过渡
|
|
222
|
+
收起:先量 scrollHeight 写进 CSS 变量(给退场动画一个起点)
|
|
223
|
+
→ 下一帧再切 data-state=closed(触发过渡)
|
|
224
|
+
→ transitionend 后加 hidden(真正移出无障碍树)
|
|
225
|
+
================================================================== */
|
|
226
|
+
Accordion.prototype.reduced = function () {
|
|
227
|
+
return typeof window.matchMedia === 'function' &&
|
|
228
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/* 🔴 锁高:收起动画期间**列表高度不变** ⇒ 下方 trigger 不位移。
|
|
232
|
+
量化依据:收起时下方 trigger 上移 **131px**,手指必然点空。
|
|
233
|
+
⚠️ 只在**收起**时锁 —— 展开时下方项本来就要让位,锁了反而不自然。 */
|
|
234
|
+
/* ⚠️ 保留但不用:实测锁列表高度解决不了"项在列表内部上移"(见 set() 注释) */
|
|
235
|
+
Accordion.prototype.lock = function () {};
|
|
236
|
+
|
|
237
|
+
Accordion.prototype.unlock = function () {};
|
|
238
|
+
|
|
239
|
+
Accordion.prototype.set = function (item, open) {
|
|
240
|
+
var btn = item.querySelector('.accordion__trigger');
|
|
241
|
+
var panel = item.querySelector('.accordion__panel');
|
|
242
|
+
if (!btn || !panel) return;
|
|
243
|
+
if (btn.getAttribute('aria-disabled') === 'true') return;
|
|
244
|
+
|
|
245
|
+
var self = this;
|
|
246
|
+
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
247
|
+
|
|
248
|
+
if (open) {
|
|
249
|
+
// ── 展开:直接给 open 状态。CSS 的 0fr → 1fr 过渡自己会动
|
|
250
|
+
panel.removeAttribute('hidden');
|
|
251
|
+
panel.style.visibility = '';
|
|
252
|
+
panel.setAttribute('data-state', 'open');
|
|
253
|
+
} else {
|
|
254
|
+
// ── 收起:先量高度(退场动画的起点),再切状态
|
|
255
|
+
if (self.reduced()) {
|
|
256
|
+
panel.setAttribute('data-state', 'closed');
|
|
257
|
+
panel.setAttribute('hidden', '');
|
|
258
|
+
panel.style.visibility = '';
|
|
259
|
+
} else {
|
|
260
|
+
var inner = panel.querySelector('.accordion__panel-inner');
|
|
261
|
+
// 🔴 量的是 **inner** 的高度,不是 panel 的
|
|
262
|
+
// (panel 是 grid 容器,0fr 时它自身高度为 0,量不到内容)
|
|
263
|
+
var h = inner ? inner.getBoundingClientRect().height : 0;
|
|
264
|
+
panel.style.setProperty('--acc-h', h + 'px');
|
|
265
|
+
// ⚠️ 这里**曾经**加过「锁住列表高度」,实测**无效**,已撤销:
|
|
266
|
+
// 锁的是**列表**高度,但第 1 项是在列表**内部**收缩的,
|
|
267
|
+
// 第 2 项照样在列表里上移(实测仍移 131px)。
|
|
268
|
+
// ⇒ 任何高度动画都会推动下方内容,这是**固有代价**
|
|
269
|
+
// (Radix 等通行实现同样如此)。
|
|
270
|
+
// 下一帧再切 closed ⇒ 浏览器先认下 1fr 这个起点,再过渡到 0fr
|
|
271
|
+
raf(function () {
|
|
272
|
+
panel.setAttribute('data-state', 'closed');
|
|
273
|
+
// 过渡结束后真正隐藏(移出无障碍树)+ 解锁
|
|
274
|
+
var settle = function () {
|
|
275
|
+
if (panel.getAttribute('data-state') === 'closed') {
|
|
276
|
+
panel.setAttribute('hidden', '');
|
|
277
|
+
panel.style.removeProperty('--acc-h');
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
once(panel, 'transitionend', settle);
|
|
281
|
+
// 兜底:万一 transitionend 没触发(标签页在后台等)
|
|
282
|
+
setTimeout(settle, 400);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* 手风琴没有 data-value ⇒ value 用按钮 id(组件自生成、实例内唯一)*/
|
|
288
|
+
flEmit(this.root, 'fl-change', { value: btn.id, open: open, item: btn });
|
|
289
|
+
if (typeof this.opts.onChange === 'function') {
|
|
290
|
+
this.opts.onChange(btn, open);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
Accordion.prototype.toggle = function (item) {
|
|
295
|
+
var opening = item.querySelector('.accordion__trigger')
|
|
296
|
+
.getAttribute('aria-expanded') !== 'true';
|
|
297
|
+
if (this.single && opening) {
|
|
298
|
+
// 只允许开一个 ⇒ 先全关
|
|
299
|
+
var self = this;
|
|
300
|
+
toArray(this.items).forEach(function (it) {
|
|
301
|
+
if (it !== item) self.set(it, false);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
this.set(item, opening);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/* 找 trigger 所属的 item —— 自己往上走(ES5 安全的 closest) */
|
|
308
|
+
Accordion.prototype.itemOf = function (btn) {
|
|
309
|
+
var el = btn;
|
|
310
|
+
while (el && el !== this.root) {
|
|
311
|
+
if (el.className &&
|
|
312
|
+
(' ' + el.className + ' ').indexOf(' accordion__item ') > -1) {
|
|
313
|
+
return el;
|
|
314
|
+
}
|
|
315
|
+
el = el.parentNode;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
Accordion.prototype.bind = function () {
|
|
321
|
+
var self = this;
|
|
322
|
+
|
|
323
|
+
/* 🔴 修「收起后点另一项,第一次没反应」(真机实测反馈)
|
|
324
|
+
|
|
325
|
+
真因:**收起动画期间整个列表在向上位移**。
|
|
326
|
+
第 1 项的 panel 从 117px 缩到 0,第 2 项的 trigger **跟着往上挪**;
|
|
327
|
+
手指瞄准的是落下那一瞬间的位置,浏览器派发 touch 时元素已经移开
|
|
328
|
+
⇒ **点在空处,什么也没发生** —— 用户以为要点两次。
|
|
329
|
+
|
|
330
|
+
⚠️ 我第一版修法是「动画期间锁住点击 + 结束后补偿」。
|
|
331
|
+
**实测反而更糟**(补偿没生效,第一次点击彻底丢失)。
|
|
332
|
+
⇒ 已撤销那个锁。
|
|
333
|
+
⇒ 现在的修法是 CSS 一处:收起中的 panel `pointer-events:none`,
|
|
334
|
+
事件能穿透到下层 trigger(见 accordion.css)。
|
|
335
|
+
⇒ 剩下的"极短窗口内布局位移"是**高度动画的固有代价** ——
|
|
336
|
+
Radix 等通行实现同样如此。240ms 的窗口,人类感知不到。
|
|
337
|
+
*/
|
|
338
|
+
this.root.style.setProperty('--acc-dur', this.dur + 'ms');
|
|
339
|
+
|
|
340
|
+
this.root.addEventListener('click', function (e) {
|
|
341
|
+
var btn = closest(e.target, '.accordion__trigger');
|
|
342
|
+
if (!btn || !self.root.contains(btn)) return;
|
|
343
|
+
if (btn.getAttribute('aria-disabled') === 'true') return;
|
|
344
|
+
var item = self.itemOf(btn);
|
|
345
|
+
if (item) self.toggle(item);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// 🔴 键盘:Enter / Space —— **刻意不监听 keydown**。
|
|
349
|
+
// 原生 <button> 本身就响应这两个键并触发 click,
|
|
350
|
+
// 写了反而**双触发**(keydown toggle 一次 + click 又 toggle 一次)。
|
|
351
|
+
// 这是「用原生控件」的又一处收益:**无障碍行为免费且必然正确**。
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
window.Accordion = Accordion;
|
|
355
|
+
|
|
356
|
+
function init() {
|
|
357
|
+
var nodes = document.querySelectorAll('[data-accordion]');
|
|
358
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
359
|
+
nodes[i].accordionInstance = new Accordion(nodes[i]);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// @autoinit: [data-accordion] —— 自动初始化;与 API.md「自动初始化」表由 autoinit-gate 双向校验
|
|
364
|
+
if (document.readyState === 'loading') {
|
|
365
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
366
|
+
} else {
|
|
367
|
+
init();
|
|
368
|
+
}
|
|
369
|
+
})();
|
|
@@ -0,0 +1,223 @@
|
|
|
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>accordion · 折叠面板</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/page.css">
|
|
11
|
+
<link rel="stylesheet" href="../../03-patterns/accordion/accordion.css">
|
|
12
|
+
<style>
|
|
13
|
+
p { margin: 0 0 var(--sp-3); }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body class="page page">
|
|
17
|
+
<main class="wrap">
|
|
18
|
+
<h1>accordion · 折叠面板</h1>
|
|
19
|
+
<p class="lede">
|
|
20
|
+
用于把长表单或长内容分成几段。<strong>默认只开一个</strong>(同一个问题的不同答案),
|
|
21
|
+
加 <code>accordion--multi</code> 可同时开多个(各自独立的信息块)。
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<h2 class="rule-top">单开(默认)</h2>
|
|
25
|
+
<div class="accordion" data-accordion>
|
|
26
|
+
<div class="accordion__item">
|
|
27
|
+
<h3 class="accordion__heading">
|
|
28
|
+
<button class="accordion__trigger" aria-expanded="true">
|
|
29
|
+
<span class="accordion__label">公司基本信息</span>
|
|
30
|
+
<span class="accordion__marker" aria-hidden="true"></span>
|
|
31
|
+
</button>
|
|
32
|
+
</h3>
|
|
33
|
+
<div class="accordion__panel">
|
|
34
|
+
<div class="accordion__panel-inner">
|
|
35
|
+
<p>公司名称、证券代码、行业分类、上市日期与所属交易所。</p>
|
|
36
|
+
<p>这些字段决定后面所有指标怎么算,缺任何一项都会让勾稽校验失败。</p>
|
|
37
|
+
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="accordion__item">
|
|
42
|
+
<h3 class="accordion__heading">
|
|
43
|
+
<button class="accordion__trigger" aria-expanded="false">
|
|
44
|
+
<span class="accordion__label">数据源与口径</span>
|
|
45
|
+
<span class="accordion__marker" aria-hidden="true"></span>
|
|
46
|
+
</button>
|
|
47
|
+
</h3>
|
|
48
|
+
<div class="accordion__panel" hidden>
|
|
49
|
+
<div class="accordion__panel-inner">
|
|
50
|
+
<p>抓取来源、更新频率、复核规则,以及异常时的处理方式。</p>
|
|
51
|
+
<p>失败会重试三次,仍失败则标记为「待复核」,不会静默跳过。</p>
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="accordion__item">
|
|
57
|
+
<h3 class="accordion__heading">
|
|
58
|
+
<button class="accordion__trigger" aria-expanded="false">
|
|
59
|
+
<span class="accordion__label">审计记录</span>
|
|
60
|
+
<span class="accordion__marker" aria-hidden="true"></span>
|
|
61
|
+
</button>
|
|
62
|
+
</h3>
|
|
63
|
+
<div class="accordion__panel" hidden>
|
|
64
|
+
<div class="accordion__panel-inner">
|
|
65
|
+
<p>历次复核结论、责任人与时间戳,全部留痕不可删除。</p>
|
|
66
|
+
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<p class="small">打开第二项时,第一项会<strong>自动收起</strong>。</p>
|
|
72
|
+
|
|
73
|
+
<h2 class="rule-top">多开</h2>
|
|
74
|
+
<div class="accordion" data-accordion data-accordion-multi>
|
|
75
|
+
<div class="accordion__item">
|
|
76
|
+
<h3 class="accordion__heading">
|
|
77
|
+
<button class="accordion__trigger" aria-expanded="true">
|
|
78
|
+
<span class="accordion__label">网络与抓取</span>
|
|
79
|
+
<span class="accordion__marker" aria-hidden="true"></span>
|
|
80
|
+
</button>
|
|
81
|
+
</h3>
|
|
82
|
+
<div class="accordion__panel">
|
|
83
|
+
<div class="accordion__panel-inner">
|
|
84
|
+
<p>数据源:交易所公开文件、公司定期报告、第三方行情镜像。</p>
|
|
85
|
+
<p>抓取频率:日频(收盘后)与季频(财报期),失败自动重试三次。</p>
|
|
86
|
+
<p>最后成功:今天 18:24 · 下一轮:明日 09:10</p>
|
|
87
|
+
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="accordion__item">
|
|
92
|
+
<h3 class="accordion__heading">
|
|
93
|
+
<button class="accordion__trigger" aria-expanded="true">
|
|
94
|
+
<span class="accordion__label">指标与口径</span>
|
|
95
|
+
<span class="accordion__marker" aria-hidden="true"></span>
|
|
96
|
+
</button>
|
|
97
|
+
</h3>
|
|
98
|
+
<div class="accordion__panel">
|
|
99
|
+
<div class="accordion__panel-inner">
|
|
100
|
+
<p>口径以最新年报为准;年报未披露的用最近一期季报并标注。</p>
|
|
101
|
+
<p>跨期变动超过 5% 需人工复核,勾稽关系 E1–E22 全部通过才入库。</p>
|
|
102
|
+
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="accordion__item">
|
|
107
|
+
<h3 class="accordion__heading">
|
|
108
|
+
<button class="accordion__trigger" aria-expanded="false">
|
|
109
|
+
<span class="accordion__label">复核记录</span>
|
|
110
|
+
<span class="accordion__marker" aria-hidden="true"></span>
|
|
111
|
+
</button>
|
|
112
|
+
</h3>
|
|
113
|
+
<div class="accordion__panel" hidden>
|
|
114
|
+
<div class="accordion__panel-inner">
|
|
115
|
+
<p>最近一次复核:昨天 21:03,结论为通过,责任人本人。</p>
|
|
116
|
+
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<p class="small">
|
|
122
|
+
加了 <code>data-accordion-multi</code>:各段独立,不互相影响。
|
|
123
|
+
</p>
|
|
124
|
+
<p class="small">
|
|
125
|
+
<strong>为什么这里写了好几行字</strong>:
|
|
126
|
+
原来每段只有一句「网络」「计算」类的话(11–16 字符),
|
|
127
|
+
<strong>视觉上就是个空壳</strong> —— 真机实测后判定为
|
|
128
|
+
「感觉非常廉价」「像是没多写、东西被吃掉了」。
|
|
129
|
+
⇒ <strong>面板只有一行字时,用户无法判断这个组件在真实场景里长什么样。</strong>
|
|
130
|
+
demo 要展示的就是真实信息密度。
|
|
131
|
+
</p>
|
|
132
|
+
|
|
133
|
+
<h2 class="rule-top">禁用的分段</h2>
|
|
134
|
+
<div class="accordion" data-accordion>
|
|
135
|
+
<div class="accordion__item">
|
|
136
|
+
<h3 class="accordion__heading">
|
|
137
|
+
<button class="accordion__trigger" aria-expanded="false">
|
|
138
|
+
<span class="accordion__label">可用</span>
|
|
139
|
+
<span class="accordion__marker" aria-hidden="true"></span>
|
|
140
|
+
</button>
|
|
141
|
+
</h3>
|
|
142
|
+
<div class="accordion__panel" hidden>
|
|
143
|
+
<div class="accordion__panel-inner"><p>能点开。</p>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="accordion__item">
|
|
148
|
+
<h3 class="accordion__heading">
|
|
149
|
+
<button class="accordion__trigger" aria-expanded="false" aria-disabled="true">
|
|
150
|
+
<span class="accordion__label">未开通(禁用)</span>
|
|
151
|
+
<span class="accordion__marker" aria-hidden="true"></span>
|
|
152
|
+
</button>
|
|
153
|
+
</h3>
|
|
154
|
+
<div class="accordion__panel" hidden>
|
|
155
|
+
<div class="accordion__panel-inner"><p>点不开。</p>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<h2 class="rule-top">结构要求(APG 原文)</h2>
|
|
162
|
+
<div class="box">
|
|
163
|
+
<p><strong>heading 包住 button</strong>,不是 button 套 heading:</p>
|
|
164
|
+
<p><code><h3><button aria-expanded="true">标题</button></h3></code></p>
|
|
165
|
+
<p class="small">
|
|
166
|
+
APG 原文:<em>"place the heading tag around the button,
|
|
167
|
+
<strong>not inside it</strong>, as this would prevent some screen readers
|
|
168
|
+
(such as VoiceOver and TalkBack) from reading it."</em>
|
|
169
|
+
写反了 VoiceOver / TalkBack <strong>读不出标题</strong>。
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<h2 class="rule-top">键盘(APG 原文)</h2>
|
|
174
|
+
<div class="box">
|
|
175
|
+
<p><strong>Enter / Space</strong> 切换 —— 但本组件<strong>没有写 keydown 监听</strong></p>
|
|
176
|
+
<p class="small">
|
|
177
|
+
因为原生 <code><button></code> <strong>本身就响应这两个键并触发 click</strong>
|
|
178
|
+
—— 写了反而<strong>双触发</strong>(keydown 切一次、click 又切一次)。
|
|
179
|
+
这是「用原生控件」的又一处收益:<strong>无障碍行为免费且必然正确</strong>。
|
|
180
|
+
</p>
|
|
181
|
+
<p class="small" style="margin-top:var(--sp-3)">
|
|
182
|
+
⚠️ <strong>本组件故意没有方向键 / Home / End</strong>。
|
|
183
|
+
APG 明确写:<em>"arrow / Home / End key navigation is
|
|
184
|
+
<strong>no longer part of the APG Accordion keyboard interaction</strong>."</em>
|
|
185
|
+
那是 <strong>tabs</strong> 的契约(roving tabindex)——
|
|
186
|
+
照抄给 accordion 是把两个模式混了。
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<h2 class="rule-top">role="region" 的数量上限</h2>
|
|
191
|
+
<div class="box">
|
|
192
|
+
<p class="small">
|
|
193
|
+
APG 警告:超过约 6 个可同时展开的面板时,给每个 panel 加
|
|
194
|
+
<code>role="region"</code> 会造成
|
|
195
|
+
<strong>"landmark region proliferation"</strong>(地标泛滥)——
|
|
196
|
+
读屏用户的地标列表会被几十个「分区」淹没。
|
|
197
|
+
⇒ <strong>面板数 > 6 时不要加</strong>,本组件用
|
|
198
|
+
<code>accordion--many</code> 关闭它。
|
|
199
|
+
</p>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<h2 class="rule-top">反例</h2>
|
|
203
|
+
<div class="box bad">
|
|
204
|
+
<h3>反例:用 div 当 trigger</h3>
|
|
205
|
+
<p class="small">
|
|
206
|
+
<code><div class="accordion__trigger"></code> 键盘上 Tab 不到、
|
|
207
|
+
也不响应 Enter/Space。<strong>APG 要求 header 里是
|
|
208
|
+
<code>role="button"</code> 的元素</strong>,用原生
|
|
209
|
+
<code><button></code> 最省事也最可靠。
|
|
210
|
+
</p>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="box bad">
|
|
213
|
+
<h3>反例:折叠时只用 max-height: 0</h3>
|
|
214
|
+
<p class="small">
|
|
215
|
+
内容在无障碍树里<strong>仍然存在</strong> —— 读屏会念出「折叠」区域里的全部文字,
|
|
216
|
+
键盘也能 Tab 进去。<strong>必须用 <code>hidden</code> 或
|
|
217
|
+
<code>display:none</code></strong>,让它真正离开无障碍树。
|
|
218
|
+
</p>
|
|
219
|
+
</div>
|
|
220
|
+
</main>
|
|
221
|
+
<script src="accordion.js"></script>
|
|
222
|
+
</body>
|
|
223
|
+
</html>
|