agentcss 0.0.0-stage → 0.11.3
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 +3138 -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 +497 -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 +311 -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,129 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
drawer.css — 抽屉(D2 /
|
|
3
|
+
============================================================================
|
|
4
|
+
依据:APG Dialog 模式(Drawer 是从侧边滑入的 Dialog)
|
|
5
|
+
|
|
6
|
+
⭐ 与 dialog 的差别只有"从哪进来",**三条硬要求完全一样**:
|
|
7
|
+
① **焦点陷阱**:Tab 不能跑到抽屉外
|
|
8
|
+
② **Esc 关闭**
|
|
9
|
+
③ ⭐ **关闭后焦点归位**(回到触发它的按钮)
|
|
10
|
+
—— 这条最容易漏,且**影响最大**:
|
|
11
|
+
键盘用户被"扔"回页面顶部,得重新 Tab 一遍。
|
|
12
|
+
|
|
13
|
+
⚠️ 抽屉是**模态**(modal):背景必须 `inert` 或 `aria-hidden`,
|
|
14
|
+
否则读屏会同时读到抽屉与背景("双重朗读")。
|
|
15
|
+
============================================================================ */
|
|
16
|
+
|
|
17
|
+
.drawer-backdrop {
|
|
18
|
+
position: fixed;
|
|
19
|
+
inset: 0;
|
|
20
|
+
z-index: var(--z-modal);
|
|
21
|
+
/*🔴 改:原来是写死的 rgba(0,0,0,.42)——
|
|
22
|
+
暗色模式下遮罩明显偏黑(暗色下 --scrim 是 0.62 的黑,
|
|
23
|
+
而写死的 .42 在深底上几乎看不出来)。
|
|
24
|
+
overlay.css 与 nav.css 都用 var(--scrim),只有这里漏了 ⇒
|
|
25
|
+
同一套库里三个遮罩表现不一致。
|
|
26
|
+
统一走令牌后,暗色跟随自动生效。 */
|
|
27
|
+
background: var(--scrim);
|
|
28
|
+
display: flex;
|
|
29
|
+
/* 抽屉从右侧滑入(可改成 left / top / bottom)*/
|
|
30
|
+
justify-content: flex-end;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.drawer-backdrop[hidden] { display: none; }
|
|
34
|
+
|
|
35
|
+
.drawer {
|
|
36
|
+
width: min(420px, 92vw);
|
|
37
|
+
height: 100%;
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
background: var(--surface);
|
|
41
|
+
border-inline-start: 1px solid var(--border-decor);
|
|
42
|
+
box-shadow: var(--shadow-2);
|
|
43
|
+
/* 焦点在抽屉里时,浏览器不会自动滚进视野 ⇒ 手动处理 */
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.drawer--left { border-inline-start: 0; border-inline-end: 1px solid var(--border-decor); }
|
|
48
|
+
.drawer--top { width: 100%; height: min(60vh, 420px); border-inline-start: 0;
|
|
49
|
+
border-bottom: 1px solid var(--border-decor); }
|
|
50
|
+
.drawer--bottom { width: 100%; height: min(60vh, 420px); border-inline-start: 0;
|
|
51
|
+
border-top: 1px solid var(--border-decor); }
|
|
52
|
+
|
|
53
|
+
/* ---------- 头 ---------- */
|
|
54
|
+
.drawer__head {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: var(--sp-3);
|
|
58
|
+
padding: var(--sp-4);
|
|
59
|
+
border-bottom: 1px solid var(--border-decor);
|
|
60
|
+
flex: 0 0 auto;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.drawer__title {
|
|
64
|
+
flex: 1 1 auto;
|
|
65
|
+
margin: 0;
|
|
66
|
+
font-size: var(--fs-lg);
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.drawer__close {
|
|
71
|
+
flex: 0 0 auto;
|
|
72
|
+
width: 44px; /* 🔴 命中区(关闭按钮是独立目标,用 44)*/
|
|
73
|
+
height: 44px;
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
background: none;
|
|
78
|
+
border: 0;
|
|
79
|
+
border-radius: var(--r-sm);
|
|
80
|
+
color: var(--text-secondary);
|
|
81
|
+
font-size: 20px;
|
|
82
|
+
line-height: 1;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.drawer__close:focus-visible {
|
|
87
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
88
|
+
outline-offset: var(--focus-ring-offset);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media (hover: hover) and (pointer: fine) {
|
|
92
|
+
.drawer__close:hover { background: var(--surface-sunken); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ---------- 体 / 脚 ---------- */
|
|
96
|
+
.drawer__body {
|
|
97
|
+
flex: 1 1 auto;
|
|
98
|
+
padding: var(--sp-4);
|
|
99
|
+
overflow-y: auto;
|
|
100
|
+
-webkit-overflow-scrolling: touch;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.drawer__foot {
|
|
104
|
+
flex: 0 0 auto;
|
|
105
|
+
display: flex;
|
|
106
|
+
gap: var(--sp-2);
|
|
107
|
+
justify-content: flex-end;
|
|
108
|
+
padding: var(--sp-4);
|
|
109
|
+
border-top: 1px solid var(--border-decor);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ---------- 进出场动画 ----------
|
|
113
|
+
⚠️ 用 transform 而不是 left/right(合成层动画,不触发重排)。
|
|
114
|
+
⚠️ 动画只做"位移 + 少量透明度",**不做大幅缩放** —— 那看起来廉价。 */
|
|
115
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
116
|
+
/* --ease-drawer:滑入面板专用曲线(不是通用的 --ease-out) */
|
|
117
|
+
.drawer--right { animation: drawer-in-right 220ms var(--ease-drawer, ease); }
|
|
118
|
+
.drawer--left { animation: drawer-in-left 220ms var(--ease-drawer, ease); }
|
|
119
|
+
.drawer--top { animation: drawer-in-top 220ms var(--ease-drawer, ease); }
|
|
120
|
+
.drawer--bottom { animation: drawer-in-bottom 220ms var(--ease-drawer, ease); }
|
|
121
|
+
|
|
122
|
+
@keyframes drawer-in-right { from { transform: translateX(16px); opacity: .6; } }
|
|
123
|
+
@keyframes drawer-in-left { from { transform: translateX(-16px); opacity: .6; } }
|
|
124
|
+
@keyframes drawer-in-top { from { transform: translateY(-16px); opacity: .6; } }
|
|
125
|
+
@keyframes drawer-in-bottom { from { transform: translateY(16px); opacity: .6; } }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* 退场(JS 设这个属性,等动画结束再移除节点)*/
|
|
129
|
+
.drawer[data-state="leaving"] { opacity: 0; transition: opacity 160ms var(--ease-out, ease); }
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drawer.js — 抽屉(D2 /
|
|
3
|
+
*
|
|
4
|
+
* 依据:APG Dialog 模式
|
|
5
|
+
* ① **焦点陷阱**:Tab 在抽屉内循环
|
|
6
|
+
* ② **Esc 关闭**
|
|
7
|
+
* ③ ⭐ **关闭后焦点归位**(回到触发它的元素)
|
|
8
|
+
* ④ 背景 inert(避免读屏"双重朗读")
|
|
9
|
+
* ⑤ 锁滚动(背景不该跟着滚)
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ 与 dialog 的差别只是"从哪进来",契约完全一样。
|
|
12
|
+
*
|
|
13
|
+
* 无依赖 · ES5
|
|
14
|
+
*/
|
|
15
|
+
(function (global) {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
/* ==== BEHAVIOR INJECT BEGIN: emit ==== */
|
|
19
|
+
var flEmit = function (el, name, detail) {
|
|
20
|
+
if (!el || !el.dispatchEvent) return null;
|
|
21
|
+
var type = name.indexOf('fl-') === 0 ? name : 'fl-' + name;
|
|
22
|
+
var ev = null;
|
|
23
|
+
/* 老 WebView 没有 CustomEvent 构造函数 ⇒ 兜底走 createEvent(理由见文件头)*/
|
|
24
|
+
if (typeof window.CustomEvent === 'function') {
|
|
25
|
+
try {
|
|
26
|
+
ev = new window.CustomEvent(type, {
|
|
27
|
+
detail: detail || null, bubbles: true, cancelable: false
|
|
28
|
+
});
|
|
29
|
+
} catch (e) { ev = null; }
|
|
30
|
+
}
|
|
31
|
+
if (!ev) {
|
|
32
|
+
try {
|
|
33
|
+
ev = document.createEvent('CustomEvent');
|
|
34
|
+
ev.initCustomEvent(type, true, false, detail || null);
|
|
35
|
+
} catch (e2) { return null; }
|
|
36
|
+
}
|
|
37
|
+
el.dispatchEvent(ev);
|
|
38
|
+
return ev;
|
|
39
|
+
};
|
|
40
|
+
/* ==== BEHAVIOR INJECT END: emit ==== */
|
|
41
|
+
|
|
42
|
+
/* ==== BEHAVIOR INJECT BEGIN: focus-return ==== */
|
|
43
|
+
var flFocusReturn = function (opts) {
|
|
44
|
+
var opt = opts || {};
|
|
45
|
+
var saved = null;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
save: function () {
|
|
49
|
+
var a = document.activeElement;
|
|
50
|
+
saved = (a && a !== document.body && a.focus) ? a : null;
|
|
51
|
+
return saved;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
restore: function () {
|
|
55
|
+
var target = saved;
|
|
56
|
+
/* ⚠️ 见文档 ②:不判 contains 的话,元素被移除后 focus() 不报错
|
|
57
|
+
但焦点不动 ⇒ 用户以为还回去了,其实掉在 body 上。 */
|
|
58
|
+
if (!target || !document.contains(target)) target = opt.fallback || null;
|
|
59
|
+
if (!target) { saved = null; return false; }
|
|
60
|
+
try {
|
|
61
|
+
target.focus();
|
|
62
|
+
} catch (e) {
|
|
63
|
+
saved = null;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
var ok = document.activeElement === target;
|
|
67
|
+
saved = null; /* 见文档 ③ */
|
|
68
|
+
return ok;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
target: function () { return saved; },
|
|
72
|
+
clear: function () { saved = null; },
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/* ==== BEHAVIOR INJECT END: focus-return ==== */
|
|
76
|
+
|
|
77
|
+
var FOCUSABLE = [
|
|
78
|
+
'a[href]',
|
|
79
|
+
'button:not([disabled])',
|
|
80
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
81
|
+
'select:not([disabled])',
|
|
82
|
+
'textarea:not([disabled])',
|
|
83
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
84
|
+
].join(',');
|
|
85
|
+
|
|
86
|
+
var lockCount = 0, savedScroll = 0, inerts = [];
|
|
87
|
+
|
|
88
|
+
function lockScroll() {
|
|
89
|
+
if (lockCount === 0) {
|
|
90
|
+
savedScroll = window.pageYOffset || 0;
|
|
91
|
+
document.body.style.overflow = 'hidden';
|
|
92
|
+
}
|
|
93
|
+
lockCount++;
|
|
94
|
+
}
|
|
95
|
+
function unlockScroll() {
|
|
96
|
+
lockCount = Math.max(0, lockCount - 1);
|
|
97
|
+
if (lockCount === 0) {
|
|
98
|
+
document.body.style.overflow = '';
|
|
99
|
+
window.scrollTo(0, savedScroll);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setBackgroundInert(on) {
|
|
104
|
+
if (on) {
|
|
105
|
+
if (inerts.length) return;
|
|
106
|
+
/* ⚠️ 只把 body 的**直接子元素**(除抽屉容器)设 inert。
|
|
107
|
+
抽屉本身也在 body 下 ⇒ 必须排除,否则刚打开就 inert 了。 */
|
|
108
|
+
Array.prototype.forEach.call(document.body.children, function (n) {
|
|
109
|
+
if (n.hasAttribute && n.hasAttribute('data-drawer-host')) return;
|
|
110
|
+
if (n.tagName === 'SCRIPT' || n.tagName === 'STYLE') return;
|
|
111
|
+
n.inert = true;
|
|
112
|
+
n.setAttribute('aria-hidden', 'true');
|
|
113
|
+
inerts.push(n);
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
inerts.forEach(function (n) {
|
|
117
|
+
n.inert = false;
|
|
118
|
+
n.removeAttribute('aria-hidden');
|
|
119
|
+
});
|
|
120
|
+
inerts = [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function open(opt) {
|
|
125
|
+
opt = opt || {};
|
|
126
|
+
var place = opt.place || 'right';
|
|
127
|
+
|
|
128
|
+
/* ⭐ 记住"打开那一刻的焦点"—— 关闭时要还回去。
|
|
129
|
+
(必须在插入 DOM **之前**记录,否则会记到新节点上。)
|
|
130
|
+
走 focus-return 核(01-tokens/behavior/focus-return.js):
|
|
131
|
+
抽屉没有 fallback —— 打开它的元素若被删了,就维持现状不动,
|
|
132
|
+
不假装"还回去了"。 */
|
|
133
|
+
var fr = flFocusReturn({});
|
|
134
|
+
fr.save();
|
|
135
|
+
|
|
136
|
+
var host = document.createElement('div');
|
|
137
|
+
host.className = 'drawer-backdrop';
|
|
138
|
+
host.setAttribute('data-drawer-host', '');
|
|
139
|
+
if (opt.label) host.setAttribute('aria-label', opt.label);
|
|
140
|
+
|
|
141
|
+
var d = document.createElement('aside');
|
|
142
|
+
d.className = 'drawer drawer--' + place;
|
|
143
|
+
d.setAttribute('role', 'dialog');
|
|
144
|
+
d.setAttribute('aria-modal', 'true');
|
|
145
|
+
if (!d.id) d.id = 'drawer-' + (new Date()).getTime();
|
|
146
|
+
if (opt.title) d.setAttribute('aria-labelledby', d.id + '-title');
|
|
147
|
+
else d.setAttribute('aria-label', opt.label || '抽屉');
|
|
148
|
+
d.tabIndex = -1; /* 焦点陷阱的兜底落点 */
|
|
149
|
+
|
|
150
|
+
var head = document.createElement('div');
|
|
151
|
+
head.className = 'drawer__head';
|
|
152
|
+
var h = document.createElement('h2');
|
|
153
|
+
h.className = 'drawer__title';
|
|
154
|
+
h.id = d.id + '-title';
|
|
155
|
+
h.textContent = opt.title || '';
|
|
156
|
+
var closeBtn = document.createElement('button'); /* 🔴 不能叫 close:
|
|
157
|
+
下方有 `function close()`,函数声明会**提升并覆盖**同名变量,
|
|
158
|
+
导致上面所有 close.addEventListener 绑在函数上(静默失败)。 */
|
|
159
|
+
closeBtn.className = 'drawer__close';
|
|
160
|
+
closeBtn.type = 'button';
|
|
161
|
+
closeBtn.setAttribute('aria-label', '关闭');
|
|
162
|
+
closeBtn.textContent = '\u00D7';
|
|
163
|
+
head.appendChild(h);
|
|
164
|
+
head.appendChild(closeBtn);
|
|
165
|
+
|
|
166
|
+
var body = document.createElement('div');
|
|
167
|
+
body.className = 'drawer__body';
|
|
168
|
+
if (typeof opt.content === 'string') body.innerHTML = opt.content;
|
|
169
|
+
else if (opt.content) body.appendChild(opt.content);
|
|
170
|
+
|
|
171
|
+
d.appendChild(head);
|
|
172
|
+
d.appendChild(body);
|
|
173
|
+
|
|
174
|
+
if (opt.actions && opt.actions.length) {
|
|
175
|
+
var foot = document.createElement('div');
|
|
176
|
+
foot.className = 'drawer__foot';
|
|
177
|
+
opt.actions.forEach(function (a) {
|
|
178
|
+
var b = document.createElement('button');
|
|
179
|
+
b.type = 'button';
|
|
180
|
+
b.className = 'btn' + (a.variant ? ' btn--' + a.variant : '');
|
|
181
|
+
b.textContent = a.label;
|
|
182
|
+
b.addEventListener('click', function () {
|
|
183
|
+
if (a.onClick) a.onClick(close);
|
|
184
|
+
if (a.keepOpen !== true) close();
|
|
185
|
+
});
|
|
186
|
+
foot.appendChild(b);
|
|
187
|
+
});
|
|
188
|
+
d.appendChild(foot);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
host.appendChild(d);
|
|
192
|
+
document.body.appendChild(host);
|
|
193
|
+
|
|
194
|
+
setBackgroundInert(true);
|
|
195
|
+
lockScroll();
|
|
196
|
+
|
|
197
|
+
/* 初始焦点:优先指定项,其次第一个可聚焦元素,最后容器本身 */
|
|
198
|
+
var f = d.querySelectorAll(FOCUSABLE);
|
|
199
|
+
if (opt.initialFocus) {
|
|
200
|
+
var t = d.querySelector(opt.initialFocus);
|
|
201
|
+
if (t) t.focus();
|
|
202
|
+
else (f[0] || d).focus();
|
|
203
|
+
} else {
|
|
204
|
+
(f[0] || d).focus();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function onKey(e) {
|
|
208
|
+
var k = e.key;
|
|
209
|
+
/* ② Esc 关闭 */
|
|
210
|
+
if (k === 'Escape' || k === 'Esc') { e.preventDefault(); close(); return; }
|
|
211
|
+
if (k !== 'Tab') return;
|
|
212
|
+
/* ① 焦点陷阱:Tab / Shift+Tab 在抽屉内循环 */
|
|
213
|
+
var list = [].slice.call(d.querySelectorAll(FOCUSABLE))
|
|
214
|
+
.filter(function (x) { return x.offsetParent !== null; });
|
|
215
|
+
if (!list.length) { e.preventDefault(); d.focus(); return; }
|
|
216
|
+
var first = list[0], last = list[list.length - 1];
|
|
217
|
+
if (e.shiftKey && (document.activeElement === first || document.activeElement === d)) {
|
|
218
|
+
e.preventDefault(); last.focus();
|
|
219
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
220
|
+
e.preventDefault(); first.focus();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function onBackdropClick(e) { if (e.target === host) close(); }
|
|
224
|
+
|
|
225
|
+
d.addEventListener('keydown', onKey);
|
|
226
|
+
host.addEventListener('click', onBackdropClick);
|
|
227
|
+
closeBtn.addEventListener('click', function () { close(); });
|
|
228
|
+
|
|
229
|
+
var closed = false;
|
|
230
|
+
function close() {
|
|
231
|
+
if (closed) return;
|
|
232
|
+
closed = true;
|
|
233
|
+
d.setAttribute('data-state', 'leaving');
|
|
234
|
+
d.removeEventListener('keydown', onKey);
|
|
235
|
+
host.removeEventListener('click', onBackdropClick);
|
|
236
|
+
setBackgroundInert(false);
|
|
237
|
+
unlockScroll();
|
|
238
|
+
/* 动画结束再移除;reduced-motion 下没有 transition ⇒ 兜底定时器 */
|
|
239
|
+
var done = false;
|
|
240
|
+
function remove() {
|
|
241
|
+
if (done) return; done = true;
|
|
242
|
+
if (host.parentNode) host.parentNode.removeChild(host);
|
|
243
|
+
}
|
|
244
|
+
d.addEventListener('transitionend', remove, { once: true });
|
|
245
|
+
setTimeout(remove, 320);
|
|
246
|
+
/* ⭐ ③ 焦点归位:回到打开抽屉的那个元素(核里已处理
|
|
247
|
+
"元素被移除 / 不可聚焦"两种降级,都不抛错)。 */
|
|
248
|
+
fr.restore();
|
|
249
|
+
/* 抽屉用完即毁 ⇒ 必须在 host 被移出 DOM **之前**派发,否则没人收得到 */
|
|
250
|
+
flEmit(d, 'fl-close', null);
|
|
251
|
+
if (opt.onClose) opt.onClose();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { el: d, close: close };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
global.Drawer = { open: open };
|
|
258
|
+
})(window);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# dropdown · 下拉菜单
|
|
2
|
+
|
|
3
|
+
依据 **APG(ARIA Authoring Practices)Menu Button** 模式。
|
|
4
|
+
|
|
5
|
+
## 🔴 APG 的四条键盘契约(缺一条就不算合规)
|
|
6
|
+
|
|
7
|
+
| 键 | 行为 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `↑` `↓` | 在**项**之间移动 |
|
|
10
|
+
| `Esc` | 关闭,并把焦点**还给按钮** |
|
|
11
|
+
| `Tab` | 关闭菜单(焦点移到下一个元素)|
|
|
12
|
+
| `Enter` / `Space` | 激活当前项 |
|
|
13
|
+
|
|
14
|
+
## ⚠️ 另有两条容易漏的
|
|
15
|
+
|
|
16
|
+
- **关闭后焦点必须归位** —— 不归位,键盘用户在页面里迷路
|
|
17
|
+
- **菜单项命中区 ≥ 44px** —— 密集列表也必须够大(WCAG 2.5.5AAA)
|
|
18
|
+
|
|
19
|
+
## 结构
|
|
20
|
+
|
|
21
|
+
⚠️ **类名前缀是 `dd__`,不是 `dropdown__`**;标记是 `data-dd-*`。
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<div class="dd">
|
|
25
|
+
<button class="dd__btn" data-dd-btn aria-haspopup="menu" aria-expanded="false">
|
|
26
|
+
<span>操作</span><span class="dd__caret" aria-hidden="true"></span>
|
|
27
|
+
</button>
|
|
28
|
+
<div class="dd__menu" data-dd-menu role="menu">
|
|
29
|
+
<button class="dd__item" role="menuitem" data-value="edit">编辑</button>
|
|
30
|
+
<hr class="dd__sep" data-dd-sep role="separator">
|
|
31
|
+
<button class="dd__item" role="menuitem" data-value="del">删除</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| 部件 | 说明 |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `.dd` | 根 |
|
|
39
|
+
| `.dd__btn` / `[data-dd-btn]` | 触发按钮 |
|
|
40
|
+
| `.dd__menu` / `[data-dd-menu]` | 菜单本体,**必需** |
|
|
41
|
+
| `.dd__item` | 菜单项,带 `data-value` |
|
|
42
|
+
| `.dd__sep` / `[data-dd-sep]` | 分隔线 |
|
|
43
|
+
| `.dd__check` | 多选勾选位 |
|
|
44
|
+
| `.dd--end` | 菜单右对齐 |
|
|
45
|
+
|
|
46
|
+
## 启用
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const dd = Dropdown.create(元素);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `[data-dd-btn]` 找不到时**退回第一个 `<button>`**(省事的写法也能跑)
|
|
53
|
+
- `[data-dd-menu]` 找不到 → **直接抛错**
|
|
54
|
+
|
|
55
|
+
## 🔴 `role="menu"` 要么全设要么全不设
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// APG:菜单要么全 aria-hidden,要么全不设 —— 不能半吊子
|
|
59
|
+
menu.setAttribute('role', 'menu');
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`aria-hidden` 打了一半在个别项上,读屏会读到一半就断掉。
|
|
63
|
+
|
|
64
|
+
## 状态写在 DOM 属性上
|
|
65
|
+
|
|
66
|
+
`data-state` / `data-value`,**不用 class 切换** —— 这是全库 INVARIANT I-8
|
|
67
|
+
(状态必须活在 DOM 属性上),也是门禁会查的东西。
|
|
68
|
+
|
|
69
|
+
## 成熟度:beta(两条行为债,如实列出)
|
|
70
|
+
|
|
71
|
+
| 债 | 现在的后果 |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `focus-return:manual` | 关闭后归位是手写的,不是统一机制 |
|
|
74
|
+
| `typeahead:gap` | **没有类型搜索** —— APG 的 Menu Button 不硬性要求,但列表长时体验缺失 |
|
|
75
|
+
|
|
76
|
+
## 门禁
|
|
77
|
+
|
|
78
|
+
`05-audit/dropdown-check.js`
|
|
@@ -0,0 +1,101 @@
|
|
|
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>dropdown · 下拉菜单 · 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="../../02-primitives/button/button.css">
|
|
13
|
+
<link rel="stylesheet" href="dropdown.css">
|
|
14
|
+
<style>
|
|
15
|
+
.pad { padding: var(--sp-4) 0 var(--sp-9); }
|
|
16
|
+
.log {
|
|
17
|
+
margin-top: var(--sp-4); padding: var(--sp-3);
|
|
18
|
+
background: var(--surface-sunken); border-radius: var(--r-sm);
|
|
19
|
+
font-family: var(--font-mono, monospace); font-size: var(--fs-xs);
|
|
20
|
+
color: var(--text-secondary); min-height: 2.6em;
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body class="page page">
|
|
25
|
+
<main class="wrap">
|
|
26
|
+
|
|
27
|
+
<h1>dropdown · 下拉菜单</h1>
|
|
28
|
+
<p class="note">
|
|
29
|
+
依据 <strong>APG Menu Button</strong>:<code>↑↓</code> 移动 ·
|
|
30
|
+
<code>Esc</code> 关闭并<strong>把焦点还给按钮</strong> ·
|
|
31
|
+
<code>Tab</code> 关闭 · <code>Enter</code>/<code>Space</code> 激活 ·
|
|
32
|
+
<code>Home</code>/<code>End</code> 跳首末。
|
|
33
|
+
<br>⚠️ 菜单项 <code>tabindex="-1"</code>:<strong>不参与 Tab 序列</strong>,
|
|
34
|
+
因为 Tab 的语义是「关闭菜单」。
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<h2>1 · 基本用法(试试键盘)</h2>
|
|
38
|
+
<div class="pad">
|
|
39
|
+
<div class="dd" id="d1">
|
|
40
|
+
<button class="dd__btn" data-dd-btn>操作 <span class="dd__caret"></span></button>
|
|
41
|
+
<ul class="dd__menu" data-dd-menu hidden>
|
|
42
|
+
<li data-value="edit">编辑</li>
|
|
43
|
+
<li data-value="copy">复制</li>
|
|
44
|
+
<li data-value="paste">粘贴</li>
|
|
45
|
+
<li data-dd-sep></li>
|
|
46
|
+
<li data-value="del" aria-disabled="true">删除(不可用)</li>
|
|
47
|
+
</ul>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="log" id="log">(选中项会显示在这里)</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<h2>2 · 带勾选(选中性不只靠颜色)</h2>
|
|
53
|
+
<div class="pad">
|
|
54
|
+
<div class="dd" id="d2">
|
|
55
|
+
<button class="dd__btn" data-dd-btn>排序方式 <span class="dd__caret"></span></button>
|
|
56
|
+
<ul class="dd__menu" data-dd-menu hidden>
|
|
57
|
+
<li data-value="date"><span class="dd__check" data-check>✓</span>按日期</li>
|
|
58
|
+
<li data-value="name"><span class="dd__check" data-check></span>按名称</li>
|
|
59
|
+
<li data-value="size"><span class="dd__check" data-check></span>按大小</li>
|
|
60
|
+
</ul>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<h2>3 · 靠右对齐(窄屏不出屏)</h2>
|
|
65
|
+
<div class="pad">
|
|
66
|
+
<div class="dd dd--end" id="d3">
|
|
67
|
+
<button class="dd__btn" data-dd-btn>账号 <span class="dd__caret"></span></button>
|
|
68
|
+
<ul class="dd__menu" data-dd-menu hidden>
|
|
69
|
+
<li data-value="profile">个人资料</li>
|
|
70
|
+
<li data-value="billing">账单</li>
|
|
71
|
+
<li data-value="signout">退出登录</li>
|
|
72
|
+
</ul>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
</main>
|
|
77
|
+
|
|
78
|
+
<script src="dropdown.js"></script>
|
|
79
|
+
<script>
|
|
80
|
+
(function () {
|
|
81
|
+
var log = document.getElementById('log');
|
|
82
|
+
function onSelect(v) { log.textContent = '选中:' + v; }
|
|
83
|
+
|
|
84
|
+
Dropdown.create(document.getElementById('d1'), { onSelect: onSelect, label: '操作' });
|
|
85
|
+
Dropdown.create(document.getElementById('d2'), { onSelect: onSelect, label: '排序方式' });
|
|
86
|
+
Dropdown.create(document.getElementById('d3'), { onSelect: onSelect, label: '账号' });
|
|
87
|
+
|
|
88
|
+
/* 选中性:点中某项后把 ✓ 挪过去(不是只改颜色)*/
|
|
89
|
+
document.getElementById('d2').__dd = true;
|
|
90
|
+
var d2 = document.getElementById('d2');
|
|
91
|
+
d2.addEventListener('click', function (e) {
|
|
92
|
+
var li = e.target.closest('li[data-value]');
|
|
93
|
+
if (!li) return;
|
|
94
|
+
d2.querySelectorAll('[data-check]').forEach(function (s) { s.textContent = ''; });
|
|
95
|
+
var c = li.querySelector('[data-check]');
|
|
96
|
+
if (c) c.textContent = '✓';
|
|
97
|
+
});
|
|
98
|
+
})();
|
|
99
|
+
</script>
|
|
100
|
+
</body>
|
|
101
|
+
</html>
|