agentcss 0.13.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/01-tokens/behavior/dismissable.js +31 -15
- package/02-primitives/combobox/combobox.js +6 -14
- package/02-primitives/popover/popover.js +6 -14
- package/02-primitives/select/README.md +5 -0
- package/02-primitives/select/select.js +6 -14
- package/03-patterns/drawer/drawer.js +86 -8
- package/03-patterns/dropdown/README.md +20 -4
- package/03-patterns/dropdown/dropdown.js +106 -15
- package/03-patterns/nav/nav.js +6 -14
- package/03-patterns/overlay/overlay.js +85 -16
- package/03-patterns/tooltip/tooltip.js +6 -14
- package/04-recipes/table/table.css +8 -4
- package/CHANGELOG.md +166 -2
- package/CONTRIBUTING.md +1 -1
- package/README.zh-CN.md +1 -1
- package/START-HERE.md +10 -10
- package/SUPPORT.md +7 -1
- package/ai/START-HERE.md +1 -1
- package/ai/behaviors.json +10 -10
- package/ai/components.json +12 -21
- package/dist/01-tokens/behavior/dismissable.js +3 -1
- package/dist/02-primitives/combobox/combobox.js +3 -1
- package/dist/02-primitives/popover/popover.js +3 -1
- package/dist/02-primitives/select/select.js +3 -1
- package/dist/03-patterns/drawer/drawer.js +73 -7
- package/dist/03-patterns/dropdown/dropdown.js +89 -2
- package/dist/03-patterns/nav/nav.js +3 -1
- package/dist/03-patterns/overlay/overlay.js +72 -7
- package/dist/03-patterns/tooltip/tooltip.js +3 -1
- package/dist/COSTS.md +10 -10
- package/docs/A11Y-MANUAL-PROTOCOL.md +84 -23
- package/docs/ACCESSIBILITY.md +11 -6
- package/docs/CONVENTIONS.md +9 -0
- package/index.html +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ var flDismissable = function (opts) {
|
|
|
5
5
|
var opt = opts || {};
|
|
6
6
|
var escOn = opt.escOn || [];
|
|
7
7
|
var inside = opt.inside || [];
|
|
8
|
+
var outside = opt.outside !== false;
|
|
8
9
|
var when = opt.when || function () { return true; };
|
|
9
10
|
var onDismiss = opt.onDismiss || function () {};
|
|
10
11
|
|
|
@@ -45,7 +46,8 @@ var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
|
45
46
|
for (var j = 0; j < escOn.length; j++) {
|
|
46
47
|
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
|
|
50
|
+
if (outside) shared.stack.push(rec);
|
|
49
51
|
if (!shared.bound) {
|
|
50
52
|
shared.bound = true;
|
|
51
53
|
document.addEventListener('click', onDocClick, true);
|
|
@@ -5,6 +5,7 @@ var flDismissable = function (opts) {
|
|
|
5
5
|
var opt = opts || {};
|
|
6
6
|
var escOn = opt.escOn || [];
|
|
7
7
|
var inside = opt.inside || [];
|
|
8
|
+
var outside = opt.outside !== false;
|
|
8
9
|
var when = opt.when || function () { return true; };
|
|
9
10
|
var onDismiss = opt.onDismiss || function () {};
|
|
10
11
|
|
|
@@ -45,7 +46,8 @@ var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
|
45
46
|
for (var j = 0; j < escOn.length; j++) {
|
|
46
47
|
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
|
|
50
|
+
if (outside) shared.stack.push(rec);
|
|
49
51
|
if (!shared.bound) {
|
|
50
52
|
shared.bound = true;
|
|
51
53
|
document.addEventListener('click', onDocClick, true);
|
|
@@ -5,6 +5,7 @@ var flDismissable = function (opts) {
|
|
|
5
5
|
var opt = opts || {};
|
|
6
6
|
var escOn = opt.escOn || [];
|
|
7
7
|
var inside = opt.inside || [];
|
|
8
|
+
var outside = opt.outside !== false;
|
|
8
9
|
var when = opt.when || function () { return true; };
|
|
9
10
|
var onDismiss = opt.onDismiss || function () {};
|
|
10
11
|
|
|
@@ -45,7 +46,8 @@ var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
|
45
46
|
for (var j = 0; j < escOn.length; j++) {
|
|
46
47
|
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
|
|
50
|
+
if (outside) shared.stack.push(rec);
|
|
49
51
|
if (!shared.bound) {
|
|
50
52
|
shared.bound = true;
|
|
51
53
|
document.addEventListener('click', onDocClick, true);
|
|
@@ -55,6 +55,69 @@ clear: function () { saved = null; },
|
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
var flDismissable = function (opts) {
|
|
59
|
+
var opt = opts || {};
|
|
60
|
+
var escOn = opt.escOn || [];
|
|
61
|
+
var inside = opt.inside || [];
|
|
62
|
+
var outside = opt.outside !== false;
|
|
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
|
+
|
|
83
|
+
var closed = onDismiss(e, 'esc');
|
|
84
|
+
if (closed === false) return;
|
|
85
|
+
if (e.preventDefault) e.preventDefault();
|
|
86
|
+
if (e.stopPropagation) e.stopPropagation();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function onDocClick(e) {
|
|
90
|
+
var top = null;
|
|
91
|
+
for (var i = shared.stack.length - 1; i >= 0; i--) {
|
|
92
|
+
if (shared.stack[i].when()) { top = shared.stack[i]; break; }
|
|
93
|
+
}
|
|
94
|
+
if (!top || top.isInside(e.target)) return;
|
|
95
|
+
top.onDismiss(e, 'outside');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
99
|
+
|
|
100
|
+
for (var j = 0; j < escOn.length; j++) {
|
|
101
|
+
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (outside) shared.stack.push(rec);
|
|
105
|
+
if (!shared.bound) {
|
|
106
|
+
shared.bound = true;
|
|
107
|
+
document.addEventListener('click', onDocClick, true);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
destroy: function () {
|
|
112
|
+
for (var j = 0; j < escOn.length; j++) {
|
|
113
|
+
if (escOn[j]) escOn[j].removeEventListener('keydown', onKey);
|
|
114
|
+
}
|
|
115
|
+
var k = shared.stack.indexOf(rec);
|
|
116
|
+
if (k >= 0) shared.stack.splice(k, 1);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
58
121
|
var FOCUSABLE = [
|
|
59
122
|
'a[href]',
|
|
60
123
|
'button:not([disabled])',
|
|
@@ -176,11 +239,15 @@ else (f[0] || d).focus();
|
|
|
176
239
|
(f[0] || d).focus();
|
|
177
240
|
}
|
|
178
241
|
|
|
179
|
-
|
|
180
|
-
|
|
242
|
+
var dm = flDismissable({
|
|
243
|
+
escOn: [d],
|
|
244
|
+
inside: [d],
|
|
245
|
+
when: function () { return !closed; },
|
|
246
|
+
onDismiss: function () { close(); },
|
|
247
|
+
});
|
|
181
248
|
|
|
182
|
-
|
|
183
|
-
if (
|
|
249
|
+
function onKey(e) {
|
|
250
|
+
if (e.key !== 'Tab') return;
|
|
184
251
|
|
|
185
252
|
var list = [].slice.call(d.querySelectorAll(FOCUSABLE))
|
|
186
253
|
.filter(function (x) { return x.offsetParent !== null; });
|
|
@@ -192,10 +259,8 @@ e.preventDefault(); last.focus();
|
|
|
192
259
|
e.preventDefault(); first.focus();
|
|
193
260
|
}
|
|
194
261
|
}
|
|
195
|
-
function onBackdropClick(e) { if (e.target === host) close(); }
|
|
196
262
|
|
|
197
263
|
d.addEventListener('keydown', onKey);
|
|
198
|
-
host.addEventListener('click', onBackdropClick);
|
|
199
264
|
closeBtn.addEventListener('click', function () { close(); });
|
|
200
265
|
|
|
201
266
|
var closed = false;
|
|
@@ -204,7 +269,8 @@ if (closed) return;
|
|
|
204
269
|
closed = true;
|
|
205
270
|
d.setAttribute('data-state', 'leaving');
|
|
206
271
|
d.removeEventListener('keydown', onKey);
|
|
207
|
-
|
|
272
|
+
|
|
273
|
+
dm.destroy();
|
|
208
274
|
setBackgroundInert(false);
|
|
209
275
|
unlockScroll();
|
|
210
276
|
|
|
@@ -137,6 +137,7 @@ var flDismissable = function (opts) {
|
|
|
137
137
|
var opt = opts || {};
|
|
138
138
|
var escOn = opt.escOn || [];
|
|
139
139
|
var inside = opt.inside || [];
|
|
140
|
+
var outside = opt.outside !== false;
|
|
140
141
|
var when = opt.when || function () { return true; };
|
|
141
142
|
var onDismiss = opt.onDismiss || function () {};
|
|
142
143
|
|
|
@@ -177,7 +178,8 @@ var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
|
177
178
|
for (var j = 0; j < escOn.length; j++) {
|
|
178
179
|
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
179
180
|
}
|
|
180
|
-
|
|
181
|
+
|
|
182
|
+
if (outside) shared.stack.push(rec);
|
|
181
183
|
if (!shared.bound) {
|
|
182
184
|
shared.bound = true;
|
|
183
185
|
document.addEventListener('click', onDocClick, true);
|
|
@@ -194,6 +196,68 @@ if (k >= 0) shared.stack.splice(k, 1);
|
|
|
194
196
|
};
|
|
195
197
|
};
|
|
196
198
|
|
|
199
|
+
var flTypeahead = function (opts) {
|
|
200
|
+
var opt = opts || {};
|
|
201
|
+
var items = opt.items || function () { return []; };
|
|
202
|
+
var textOf = opt.textOf || function (x) { return String((x && x.textContent) || ''); };
|
|
203
|
+
var usable = opt.usable || function () { return true; };
|
|
204
|
+
var timeout = opt.timeout || 500;
|
|
205
|
+
var onHit = opt.onHit || function () {};
|
|
206
|
+
var buf = '';
|
|
207
|
+
var timer = null;
|
|
208
|
+
var last = -1;
|
|
209
|
+
|
|
210
|
+
function stop() {
|
|
211
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function repeated(s) {
|
|
215
|
+
if (s.length < 2) return false;
|
|
216
|
+
for (var i = 1; i < s.length; i++) {
|
|
217
|
+
if (s.charAt(i) !== s.charAt(0)) return false;
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function find(needle) {
|
|
223
|
+
var l = items() || [];
|
|
224
|
+
var n = l.length;
|
|
225
|
+
if (!n) return -1;
|
|
226
|
+
for (var k = 0; k < n; k++) {
|
|
227
|
+
var i = (last + 1 + k) % n;
|
|
228
|
+
if (!usable(l[i])) continue;
|
|
229
|
+
if (textOf(l[i]).toLowerCase().indexOf(needle) === 0) return i;
|
|
230
|
+
}
|
|
231
|
+
return -1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function type(ch) {
|
|
235
|
+
if (!ch) return -1;
|
|
236
|
+
buf += String(ch).toLowerCase();
|
|
237
|
+
stop();
|
|
238
|
+
timer = setTimeout(clear, timeout);
|
|
239
|
+
var needle = repeated(buf) ? buf.charAt(0) : buf;
|
|
240
|
+
var i = find(needle);
|
|
241
|
+
if (i >= 0) {
|
|
242
|
+
last = i;
|
|
243
|
+
onHit(i, (items() || [])[i], buf);
|
|
244
|
+
}
|
|
245
|
+
return i;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function clear() {
|
|
249
|
+
buf = '';
|
|
250
|
+
stop();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
type: type,
|
|
255
|
+
clear: clear,
|
|
256
|
+
buffer: function () { return buf; },
|
|
257
|
+
destroy: function () { stop(); },
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
|
|
197
261
|
function closest(el, sel) {
|
|
198
262
|
if (!el) return null;
|
|
199
263
|
if (el.closest) return el.closest(sel);
|
|
@@ -267,6 +331,8 @@ btn.setAttribute('aria-expanded', 'true');
|
|
|
267
331
|
active = -1;
|
|
268
332
|
|
|
269
333
|
focusAt(0);
|
|
334
|
+
|
|
335
|
+
typeahead.clear();
|
|
270
336
|
flEmit(root, 'fl-open', null);
|
|
271
337
|
if (opt.onOpen) opt.onOpen();
|
|
272
338
|
}
|
|
@@ -276,6 +342,7 @@ function close(restoreFocus) {
|
|
|
276
342
|
menu.hidden = true;
|
|
277
343
|
btn.setAttribute('aria-expanded', 'false');
|
|
278
344
|
active = -1;
|
|
345
|
+
typeahead.clear();
|
|
279
346
|
if (restoreFocus !== false) fr.restore();
|
|
280
347
|
flEmit(root, 'fl-close', null);
|
|
281
348
|
if (opt.onClose) opt.onClose();
|
|
@@ -305,6 +372,16 @@ tabindex: false,
|
|
|
305
372
|
onMove: function (i) { focusAt(i); },
|
|
306
373
|
});
|
|
307
374
|
|
|
375
|
+
var typeahead = flTypeahead({
|
|
376
|
+
items: function () { return itemsOf(menu); },
|
|
377
|
+
textOf: function (el) {
|
|
378
|
+
var s = String((el && el.textContent) || '').replace(/\s+/g, ' ').trim();
|
|
379
|
+
return s.replace(/^[^0-9A-Za-z\u4e00-\u9fa5]+/, '');
|
|
380
|
+
},
|
|
381
|
+
usable: function (el) { return el.getAttribute('aria-disabled') !== 'true'; },
|
|
382
|
+
onHit: function (i) { focusAt(i); },
|
|
383
|
+
});
|
|
384
|
+
|
|
308
385
|
if (!menu.id) menu.id = 'dd-menu-' + (new Date()).getTime();
|
|
309
386
|
btn.setAttribute('aria-haspopup', 'true');
|
|
310
387
|
btn.setAttribute('aria-expanded', 'false');
|
|
@@ -339,6 +416,13 @@ return;
|
|
|
339
416
|
|
|
340
417
|
if (closest(e.target, '[data-dd-menu]') !== menu) return;
|
|
341
418
|
|
|
419
|
+
if (k.length === 1 && k !== ' ' &&
|
|
420
|
+
!e.ctrlKey && !e.metaKey && !e.altKey && !e.isComposing) {
|
|
421
|
+
e.preventDefault();
|
|
422
|
+
typeahead.type(k);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
342
426
|
if (k === 'Tab') { close(false); return; }
|
|
343
427
|
|
|
344
428
|
if (k === 'Enter' || k === ' ' || k === 'Spacebar') {
|
|
@@ -358,7 +442,10 @@ onDismiss: function (e, reason) { close(reason === 'esc'); },
|
|
|
358
442
|
return {
|
|
359
443
|
open: open,
|
|
360
444
|
close: close,
|
|
361
|
-
destroy: function () {
|
|
445
|
+
destroy: function () {
|
|
446
|
+
roving.destroy(); dismiss.destroy(); typeahead.destroy();
|
|
447
|
+
menu.hidden = true;
|
|
448
|
+
},
|
|
362
449
|
};
|
|
363
450
|
}
|
|
364
451
|
|
|
@@ -59,6 +59,7 @@ var flDismissable = function (opts) {
|
|
|
59
59
|
var opt = opts || {};
|
|
60
60
|
var escOn = opt.escOn || [];
|
|
61
61
|
var inside = opt.inside || [];
|
|
62
|
+
var outside = opt.outside !== false;
|
|
62
63
|
var when = opt.when || function () { return true; };
|
|
63
64
|
var onDismiss = opt.onDismiss || function () {};
|
|
64
65
|
|
|
@@ -99,7 +100,8 @@ var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
|
99
100
|
for (var j = 0; j < escOn.length; j++) {
|
|
100
101
|
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
101
102
|
}
|
|
102
|
-
|
|
103
|
+
|
|
104
|
+
if (outside) shared.stack.push(rec);
|
|
103
105
|
if (!shared.bound) {
|
|
104
106
|
shared.bound = true;
|
|
105
107
|
document.addEventListener('click', onDocClick, true);
|
|
@@ -251,6 +251,69 @@ clear: function () { saved = null; },
|
|
|
251
251
|
};
|
|
252
252
|
};
|
|
253
253
|
|
|
254
|
+
var flDismissable = function (opts) {
|
|
255
|
+
var opt = opts || {};
|
|
256
|
+
var escOn = opt.escOn || [];
|
|
257
|
+
var inside = opt.inside || [];
|
|
258
|
+
var outside = opt.outside !== false;
|
|
259
|
+
var when = opt.when || function () { return true; };
|
|
260
|
+
var onDismiss = opt.onDismiss || function () {};
|
|
261
|
+
|
|
262
|
+
var shared = window.__flDismiss || (window.__flDismiss = { stack: [], bound: false });
|
|
263
|
+
|
|
264
|
+
function isInside(node) {
|
|
265
|
+
if (!node) return false;
|
|
266
|
+
for (var i = 0; i < inside.length; i++) {
|
|
267
|
+
var el = inside[i];
|
|
268
|
+
if (!el || !el.contains) continue;
|
|
269
|
+
if (el === node || el.contains(node)) return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function onKey(e) {
|
|
275
|
+
if (!when()) return;
|
|
276
|
+
var k = e.key;
|
|
277
|
+
if (k !== 'Escape' && k !== 'Esc' && e.keyCode !== 27) return;
|
|
278
|
+
|
|
279
|
+
var closed = onDismiss(e, 'esc');
|
|
280
|
+
if (closed === false) return;
|
|
281
|
+
if (e.preventDefault) e.preventDefault();
|
|
282
|
+
if (e.stopPropagation) e.stopPropagation();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function onDocClick(e) {
|
|
286
|
+
var top = null;
|
|
287
|
+
for (var i = shared.stack.length - 1; i >= 0; i--) {
|
|
288
|
+
if (shared.stack[i].when()) { top = shared.stack[i]; break; }
|
|
289
|
+
}
|
|
290
|
+
if (!top || top.isInside(e.target)) return;
|
|
291
|
+
top.onDismiss(e, 'outside');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
295
|
+
|
|
296
|
+
for (var j = 0; j < escOn.length; j++) {
|
|
297
|
+
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (outside) shared.stack.push(rec);
|
|
301
|
+
if (!shared.bound) {
|
|
302
|
+
shared.bound = true;
|
|
303
|
+
document.addEventListener('click', onDocClick, true);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
destroy: function () {
|
|
308
|
+
for (var j = 0; j < escOn.length; j++) {
|
|
309
|
+
if (escOn[j]) escOn[j].removeEventListener('keydown', onKey);
|
|
310
|
+
}
|
|
311
|
+
var k = shared.stack.indexOf(rec);
|
|
312
|
+
if (k >= 0) shared.stack.splice(k, 1);
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
|
|
254
317
|
function dialog(opts) {
|
|
255
318
|
opts = opts || {};
|
|
256
319
|
|
|
@@ -310,16 +373,16 @@ var uninstallIsolation = null;
|
|
|
310
373
|
openStack.push(stackEntry);
|
|
311
374
|
if (!useInert) uninstallIsolation = installIsolation(stackEntry);
|
|
312
375
|
|
|
376
|
+
var dm = flDismissable({
|
|
377
|
+
escOn: [box],
|
|
378
|
+
outside: false,
|
|
379
|
+
when: function () { return !closed; },
|
|
380
|
+
onDismiss: function () { close(); },
|
|
381
|
+
});
|
|
382
|
+
|
|
313
383
|
function focusables() { return focusableIn(box); }
|
|
314
384
|
|
|
315
385
|
box.addEventListener('keydown', function (e) {
|
|
316
|
-
if (e.key === 'Escape') {
|
|
317
|
-
e.preventDefault();
|
|
318
|
-
|
|
319
|
-
e.stopPropagation();
|
|
320
|
-
close();
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
386
|
if (e.key !== 'Tab') return;
|
|
324
387
|
|
|
325
388
|
var f = focusables();
|
|
@@ -370,6 +433,8 @@ function close() {
|
|
|
370
433
|
if (closed) return;
|
|
371
434
|
closed = true;
|
|
372
435
|
|
|
436
|
+
dm.destroy();
|
|
437
|
+
|
|
373
438
|
var ae = document.activeElement;
|
|
374
439
|
var focusIsMine = !ae || ae === document.body ||
|
|
375
440
|
box.contains(ae) || backdrop.contains(ae);
|
|
@@ -36,6 +36,7 @@ var flDismissable = function (opts) {
|
|
|
36
36
|
var opt = opts || {};
|
|
37
37
|
var escOn = opt.escOn || [];
|
|
38
38
|
var inside = opt.inside || [];
|
|
39
|
+
var outside = opt.outside !== false;
|
|
39
40
|
var when = opt.when || function () { return true; };
|
|
40
41
|
var onDismiss = opt.onDismiss || function () {};
|
|
41
42
|
|
|
@@ -76,7 +77,8 @@ var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
|
76
77
|
for (var j = 0; j < escOn.length; j++) {
|
|
77
78
|
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
|
|
81
|
+
if (outside) shared.stack.push(rec);
|
|
80
82
|
if (!shared.bound) {
|
|
81
83
|
shared.bound = true;
|
|
82
84
|
document.addEventListener('click', onDocClick, true);
|
package/dist/COSTS.md
CHANGED
|
@@ -22,36 +22,36 @@
|
|
|
22
22
|
| primitive | `button` | 2.7 KB | 1.0 KB | — | **3.7 KB** | 26.0 KB | −22.4 KB |
|
|
23
23
|
| primitive | `card` | 2.7 KB | 0.9 KB | — | **3.6 KB** | 23.1 KB | −19.5 KB |
|
|
24
24
|
| primitive | `choice` | 2.7 KB | 1.1 KB | — | **3.8 KB** | 25.0 KB | −21.2 KB |
|
|
25
|
-
| primitive | `combobox` | 2.7 KB | 1.0 KB | 5.4 KB | **9.0 KB** | 29.
|
|
25
|
+
| primitive | `combobox` | 2.7 KB | 1.0 KB | 5.4 KB | **9.0 KB** | 29.2 KB | −20.1 KB |
|
|
26
26
|
| primitive | `date-range` | 2.7 KB | 0.8 KB | 2.5 KB | **6.1 KB** | 26.1 KB | −20.1 KB |
|
|
27
27
|
| primitive | `delta` | 2.7 KB | 0.3 KB | — | **3.0 KB** | 22.4 KB | −19.4 KB |
|
|
28
28
|
| primitive | `input` | 2.7 KB | 1.0 KB | — | **3.7 KB** | 23.2 KB | −19.5 KB |
|
|
29
29
|
| primitive | `metric` | 2.7 KB | 0.5 KB | — | **3.2 KB** | 22.8 KB | −19.7 KB |
|
|
30
|
-
| primitive | `popover` | 2.7 KB | 0.8 KB | 2.2 KB | **5.7 KB** |
|
|
30
|
+
| primitive | `popover` | 2.7 KB | 0.8 KB | 2.2 KB | **5.7 KB** | 26.9 KB | −21.2 KB |
|
|
31
31
|
| primitive | `progress` | 2.7 KB | 0.7 KB | — | **3.4 KB** | 22.5 KB | −19.1 KB |
|
|
32
|
-
| primitive | `select` | 2.7 KB | 0.8 KB | 3.5 KB | **7.0 KB** | 29.
|
|
32
|
+
| primitive | `select` | 2.7 KB | 0.8 KB | 3.5 KB | **7.0 KB** | 29.2 KB | −22.2 KB |
|
|
33
33
|
| primitive | `separator` | 2.7 KB | 0.2 KB | — | **2.9 KB** | 20.9 KB | −18.0 KB |
|
|
34
34
|
| primitive | `skeleton` | 2.7 KB | 0.5 KB | — | **3.2 KB** | 21.1 KB | −17.9 KB |
|
|
35
35
|
| primitive | `switch` | 2.7 KB | 0.8 KB | — | **3.4 KB** | 26.8 KB | −23.3 KB |
|
|
36
36
|
| pattern | `accordion` | 2.7 KB | 1.3 KB | 1.8 KB | **5.8 KB** | 34.1 KB | −28.3 KB |
|
|
37
37
|
| pattern | `content` | 2.7 KB | 1.0 KB | — | **3.7 KB** | 24.2 KB | −20.5 KB |
|
|
38
|
-
| pattern | `drawer` | 2.7 KB | 0.8 KB | 2.
|
|
39
|
-
| pattern | `dropdown` | 2.7 KB | 0.8 KB |
|
|
38
|
+
| pattern | `drawer` | 2.7 KB | 0.8 KB | 2.4 KB | **5.9 KB** | 26.1 KB | −20.2 KB |
|
|
39
|
+
| pattern | `dropdown` | 2.7 KB | 0.8 KB | 3.4 KB | **6.9 KB** | 29.1 KB | −22.2 KB |
|
|
40
40
|
| pattern | `form-validation` | 2.7 KB | 0.8 KB | — | **3.5 KB** | 24.1 KB | −20.7 KB |
|
|
41
41
|
| pattern | `list` | 2.7 KB | 0.6 KB | — | **3.3 KB** | 22.0 KB | −18.7 KB |
|
|
42
|
-
| pattern | `nav` | 2.7 KB | 1.2 KB | 2.6 KB | **6.
|
|
43
|
-
| pattern | `overlay` | 2.7 KB | 1.1 KB |
|
|
42
|
+
| pattern | `nav` | 2.7 KB | 1.2 KB | 2.6 KB | **6.6 KB** | 30.7 KB | −24.1 KB |
|
|
43
|
+
| pattern | `overlay` | 2.7 KB | 1.1 KB | 3.3 KB | **7.1 KB** | 34.5 KB | −27.4 KB |
|
|
44
44
|
| pattern | `pagination` | 2.7 KB | 0.8 KB | 2.6 KB | **6.0 KB** | 30.1 KB | −24.1 KB |
|
|
45
45
|
| pattern | `reveal` | 2.7 KB | 0.3 KB | 1.2 KB | **4.2 KB** | 24.2 KB | −20.0 KB |
|
|
46
46
|
| pattern | `states` | 2.7 KB | 0.8 KB | — | **3.5 KB** | 23.6 KB | −20.1 KB |
|
|
47
47
|
| pattern | `tabs` | 2.7 KB | 0.7 KB | 2.0 KB | **5.4 KB** | 26.8 KB | −21.4 KB |
|
|
48
|
-
| pattern | `tooltip` | 2.7 KB | 0.5 KB | 1.9 KB | **5.1 KB** | 26.
|
|
48
|
+
| pattern | `tooltip` | 2.7 KB | 0.5 KB | 1.9 KB | **5.1 KB** | 26.0 KB | −20.9 KB |
|
|
49
49
|
| pattern | `tree` | 2.7 KB | 0.6 KB | 2.9 KB | **6.2 KB** | 27.8 KB | −21.6 KB |
|
|
50
50
|
|
|
51
51
|
## 三、全库
|
|
52
52
|
|
|
53
|
-
- 全部组件都用上(min,含 tokens 一份):**
|
|
54
|
-
- 源码版(带注释)全库 gzip:
|
|
53
|
+
- 全部组件都用上(min,含 tokens 一份):**62.6 KB**
|
|
54
|
+
- 源码版(带注释)全库 gzip:305.5 KB
|
|
55
55
|
|
|
56
56
|
> ⚠️ 源码版那个数字**不是**要付的钱:它包含我们的注释(全库 CSS 有 67% 是注释,字节口径)。
|
|
57
57
|
> 它的用途只有一个 —— 让人看清"dist 到底省了多少"。
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
但证明不了"念出来是否**合理**"。这一页把这件事变成**可执行、可复现、可填表**的动作,
|
|
5
5
|
时长约 20 分钟。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
`overlay` / `drawer`
|
|
9
|
-
|
|
7
|
+
**当前状态:已执行两轮(2026-10-30)。** 九个页面都跑过 Tab 遍历,浮层焦点在
|
|
8
|
+
`overlay` / `drawer` 上验到;**标题层级(`H` 键逐级跳)在第二轮补齐**——九个页面
|
|
9
|
+
共 107 个标题,逐条与页面结构对得上。第一轮修掉的缺陷、第二轮的破坏实验,
|
|
10
|
+
以及三趟才拿到干净数据的那三个操作坑,都写在第三节。
|
|
10
11
|
|
|
11
12
|
证据形态要说清楚:**不是人耳旁听**,是让 NVDA 把朗读日志写到文件,逐条取它交给语音
|
|
12
13
|
合成器的原始文本(`IO - speech.speech.speak`)。这比人耳更可复现、可存档,但它证明的
|
|
@@ -43,9 +44,17 @@ python3 -m http.server 8000 --bind 127.0.0.1
|
|
|
43
44
|
|
|
44
45
|
## 二 · 每页只做这 5 件事(NVDA 键位在括号里)
|
|
45
46
|
|
|
46
|
-
1.
|
|
47
|
-
|
|
47
|
+
1. **按 Tab 走一遍**(走的是焦点顺序,与浏览光标停在哪无关),听到的每个控件都要有
|
|
48
|
+
**说得通的名字**。听到 "button"、"未标记"、"undefined"、只有图标名 ⇒ ❌ 记下来。
|
|
48
49
|
2. **用读屏的"下一个标题"跳(NVDA `H`)**,标题层级应当是 1 → 2 → 3,不跳级。
|
|
50
|
+
|
|
51
|
+
> **三个实操坑**(2026-10-30 实测,三个都踩过,踩完才拿到干净结果):
|
|
52
|
+
> - **换一个查询串再打开**。NVDA 按文档 URL 记忆浏览光标:同一个 URL 再开一次会恢复
|
|
53
|
+
> 上次的位置,若上次停在文末,按 `H` 只会念「往下没有标题」——看起来像"本页没有标题"。
|
|
54
|
+
> - **`h1` 用 `H` 再 `SHIFT+H` 取**。加载后的光标落点不固定:有时正好在 `h1` 上
|
|
55
|
+
> (按 `↑` 直接念出「标题 1 级 + 页面名」),有时落在紧随其后的导语段落上。
|
|
56
|
+
> 稳妥的做法是先按一次 `H` 走到第一个 `h2`,再按 `SHIFT+H` 退回来 —— 那一条必然是 `h1`。
|
|
57
|
+
> - **末端那句「往下没有标题」是证据,不是失败**。它证明 `H` 已经走过本页最后一个标题。
|
|
49
58
|
3. **把焦点放到一个禁用控件上**,它应当被念成"不可用"(而不是干脆跳过或念成正常按钮)。
|
|
50
59
|
4. **触发表单错误**(页面里带校验的那块),错误文字应当被**紧跟控件**念出,
|
|
51
60
|
不是只在屏幕右上角飘一条。
|
|
@@ -61,28 +70,48 @@ python3 -m http.server 8000 --bind 127.0.0.1
|
|
|
61
70
|
|
|
62
71
|
| 页面 | ① 名字 | ② 标题层级 | ③ 禁用态 | ④ 错误说明 | ⑤ 浮层焦点 | 备注(听到了什么) |
|
|
63
72
|
|---|---|---|---|---|---|---|
|
|
64
|
-
| button | 通过 |
|
|
65
|
-
| input | 通过 |
|
|
66
|
-
| choice | 通过 |
|
|
67
|
-
| tabs | 通过 |
|
|
68
|
-
| overlay | 通过 |
|
|
69
|
-
| table | 通过 |
|
|
70
|
-
| **accordion** | 通过 |
|
|
71
|
-
| **drawer** | 通过 |
|
|
72
|
-
| **dropdown** | 通过 |
|
|
73
|
-
|
|
74
|
-
> **为什么后三行是补的**:`accordion` / `drawer` / `dropdown`
|
|
75
|
-
>
|
|
76
|
-
>
|
|
73
|
+
| button | 通过 | 通过(19/19) | 未执行 | — | — | 12 个按钮全部念出名字;加载态念「忙碌中」。标题从「1 级 button · 七态矩阵」→「2 级 七态」→「3 级 1. default …」,末条念「往下没有标题」 |
|
|
74
|
+
| input | 通过 | 通过(13/13) | 未执行 | 通过 | — | 只读念「只读」;错误念「错误的输入 · 请输入有效金额」,紧跟控件。标题 1 → 2 → 3 → 2 → 3 → 2 → 2,与结构一致 |
|
|
75
|
+
| choice | 通过 | 通过(10/10) | — | — | — | 「全选(下面 3 项)」念「部分选中」;单选念「时间范围 · 分组 · 已选中 · 3 之 1 项」。标题 1 → 2×6 → 3 → 3 → 2 |
|
|
76
|
+
| tabs | 通过 | 通过(10/10) | — | — | — | 念「选项卡组 · 选项卡 · 已选中 · 2 之 1 项」;错例组三个 `tabindex=0` 各停一次,正例组只停一次 —— 对照成立。标题 1 → 2×5 → 3×4 |
|
|
77
|
+
| overlay | 通过 | 通过(11/11,对话框标题念成 2 级) | — | — | 通过 | 打开后焦点进对话框(取消 / 确认标记),Esc 后回到「普通确认」,再 Tab 落到「删除这份报告」。标题 1 → 2×3 → 3×2 → 2×2 → 3×2 → 2 |
|
|
78
|
+
| table | 通过 | 通过(10/10) | — | — | — | 表格念「共 7 行 7 列 · 1 行 1 列」;🔴 **列头按钮把 `↕` 念了出来**(见下,已修)。标题 1 → 2×7 → 3 → 3 |
|
|
79
|
+
| **accordion** | 通过 | 通过(21/21) | 通过 | — | — | 「公司基本信息 · 按钮 · 已展开 · 标题 3 级」→ 回车念「已折叠」→ 再回车念「已展开」;`aria-disabled` 那栏念「不可用」。标题 1 → 2 → 3×3 → 2 → 3×3 → 2 → 3×3 → 2×4 → 3×5 |
|
|
80
|
+
| **drawer** | 通过 | 通过(6/6) | — | — | 通过 | 打开念「从left进来的抽屉 · 对话框」;Tab 在「取消 → 确定 → 关闭」之间循环;Esc 后回到「左侧 · 按钮」。标题 1 → 2×3 → 3×2 |
|
|
81
|
+
| **dropdown** | 通过 | 通过(7/7) | — | — | 通过(修复后) | 按钮念「菜单按钮 · 已折叠 · 子菜单」,展开念「已展开 · 菜单」。**修复前**:按 Enter 打开后焦点留在按钮上(读屏在浏览模式下把 Enter 变成点击,走 click 分支),修复后真机复测焦点落在「✓ 按日期」。方向键仍无法在项间移动 —— 极简 APG 对照页表现相同,属读屏浏览模式限制。标题 1 → 2×4 → 3×2 |
|
|
82
|
+
|
|
83
|
+
> **为什么后三行是补的**:`accordion` / `drawer` / `dropdown` 当时都是 beta 组件,
|
|
84
|
+
> 而它们升 stable 的判据之一就是「行为零欠账」——人工读屏是这件事**唯一的用户侧证据**。
|
|
85
|
+
> (0.14.0 把 drawer / overlay 的 dismissable 收编进核;accordion 的 `roving:gap`
|
|
86
|
+
> 经核对 APG 原文后判定为**判据写错**(APG Accordion 没有方向键),改为 `n/a`;
|
|
87
|
+
> 0.15.0 补上 dropdown 的 typeahead。`ai/components.json` 当前 **37 个全 stable**,
|
|
88
|
+
> 阶梯靠正向控制证明仍能区分有债的组件。)
|
|
77
89
|
> 只覆盖 `overlay` 一个浮层,等于对其他浮层的真实表现没有任何记录。
|
|
78
90
|
|
|
79
91
|
**本轮跑出来并已修掉的一条真缺陷**:`table` 的列头按钮被念成「公司 · `↕` · 按钮」——
|
|
80
92
|
那个 `↕` 来自 `table.css` 里 `.table__sort::after` 的 `content`,Chromium 把伪元素文本算进了
|
|
81
|
-
可访问名。**自动检查抓不到它**(可访问名"非空"
|
|
93
|
+
可访问名。**自动检查抓不到它**(可访问名"非空"就算过),当时那 212 道门禁里也没有一道看这个。
|
|
82
94
|
|
|
83
95
|
改法:把字形换成 `mask` 图像(`content` 留空),三态形状仍是 `↕ / ↑ / ↓`,
|
|
84
96
|
颜色靠 `currentColor` 跟着主题走,标记结构一个字符都不用动。
|
|
85
|
-
|
|
97
|
+
|
|
98
|
+
**为什么不用 `content: "↕" / ""` 那个一行改法**(2026-10-30 实测更正):
|
|
99
|
+
早先这里写着"Firefox 不支持替代文本语法"——**那是错的**。三个引擎实测下来都被接受,
|
|
100
|
+
而且都能把 `↕` 从可访问名里去掉:
|
|
101
|
+
|
|
102
|
+
| 写法 | Chromium | Firefox | WebKit |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| `content: "↕" / ""` | 显示 · 名=`公司` | 显示 · 名=`公司` | 显示 · 名=`公司` |
|
|
105
|
+
| `content: "↕" / "排序"` | 显示 · 名=`公司 排序` | 显示 · 名=`公司排序` | 显示 · 名=`公司排序` |
|
|
106
|
+
| `mask`(本库) | 显示 · 名=`公司` | 显示 · 名=`公司` | 显示 · 名=`公司` |
|
|
107
|
+
| `content: "↕"`(原写法) | 显示 · 名=`公司↕` | 显示 · 名=`公司↕` | 显示 · 名=`公司↕` |
|
|
108
|
+
|
|
109
|
+
真正拦住它的是**老引擎**:这套语法要 Chromium 108 / Firefox 128 / Safari 17.4 起才认,
|
|
110
|
+
而不认的引擎会**把整条声明丢掉** ⇒ 箭头彻底消失(比念出符号更糟)。
|
|
111
|
+
支持矩阵里有 Android 百度浏览器(Chromium 97,见 [LAYER-BASELINE](LAYER-BASELINE.md)),
|
|
112
|
+
落在门槛之外。🔴 **这一条是按版本门槛推断的,没有在 Chromium 97 上实测过** ——
|
|
113
|
+
说的就是"没测的不算已验证"。
|
|
114
|
+
|
|
86
115
|
改完用 NVDA 复听:表头只剩「公司 · 按钮」;`table-check` 的排序判据也跟着改成
|
|
87
116
|
读 `mask` 的自定义属性(破坏实验验证过:把降序箭头改成升序形状,判据当场红)。
|
|
88
117
|
|
|
@@ -93,11 +122,42 @@ python3 -m http.server 8000 --bind 127.0.0.1
|
|
|
93
122
|
自动门禁当时是绿的,因为它只测了 `↓` 这条键盘路径。
|
|
94
123
|
**修法与判据**:把聚焦首项收进 `open()`(两条路径共用),并补一条"点击打开也聚焦首项"。
|
|
95
124
|
|
|
125
|
+
### ② 那一格是怎么填的:两条通道,各测一半
|
|
126
|
+
|
|
127
|
+
标题层级容易被当成"看一眼 DOM 就知道"的事,但**"结构对不对"和"读屏跳得对不对"
|
|
128
|
+
是两件事**。所以这一格用两条互不依赖的通道各测一遍:
|
|
129
|
+
|
|
130
|
+
| 通道 | 覆盖 | 这一轮的结果 |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| 机器 · 运行时 DOM + 可访问性树 | **96 个页面、887 个标题**(`node_modules` / `dist` 之外的全部 HTML,空夹具除外) | 每页恰好一个 `h1`;跳级 0 处、空名 0 个;层级分布 `h1×96 · h2×581 · h3×210`(没有 h4–h6);DOM 里的标题数与无障碍树里的 heading 节点数**逐页相等** |
|
|
133
|
+
| 读屏 · NVDA `H` 键实录 | 协议表里的 **9 页、107 个标题** | **107/107 条**念出的级别与页面结构逐条一致;九页走到底时都念「往下没有标题」 |
|
|
134
|
+
|
|
135
|
+
机器侧不是"跑绿了就算"。这三条 axe 规则(`heading-order` /
|
|
136
|
+
`page-has-heading-one` / `empty-heading`)都做过破坏实验:
|
|
137
|
+
|
|
138
|
+
| 注入的破坏 | `heading-order` | `page-has-heading-one` | `empty-heading` |
|
|
139
|
+
|---|---|---|---|
|
|
140
|
+
| 基线(不动) | 绿 | 绿 | 绿 |
|
|
141
|
+
| `h2` 后插 `h4`(跳级) | **红** | 绿 | 绿 |
|
|
142
|
+
| 删掉唯一的 `h1` | 绿 | **红** | 绿 |
|
|
143
|
+
| 清空一个 `h3` 的文本 | 绿 | 绿 | **红** |
|
|
144
|
+
| 反向对照:`h3` 后插 `h4`(合法) | 绿 | 绿 | 绿 |
|
|
145
|
+
|
|
146
|
+
最后一行是关键:合法层级仍然全绿,说明它不是"见到 `h4` 就报"。
|
|
147
|
+
|
|
148
|
+
> **机器侧可复跑**:`node 05-audit/heading-outline.js`(先起本地静态服务),
|
|
149
|
+
> 末行会打印 `96 页 · 887 个标题(h1×96 · h2×581 · h3×210)· 0 页异常`。
|
|
150
|
+
> 它不是门禁,是取证工具:结构本身已由 axe 那三条规则守着,它负责的是
|
|
151
|
+
> "把整站每个标题都数一遍"这件事,以及第四条判据(DOM 标题数 == 无障碍树 heading 数)。
|
|
152
|
+
>
|
|
153
|
+
> **读屏侧不进仓库**:实录脚本依赖一台装了 NVDA 的 Windows 机。复核方式是按第二节
|
|
154
|
+
> 第 2 条自己走一遍——九页的层级序列在上表每行的备注里都抄了,对不上就是有问题。
|
|
155
|
+
|
|
96
156
|
状态只填三种:**通过** / **失败(附听到的内容)** / **未执行**。
|
|
97
157
|
"大概没问题"不属于这三种,不要写。
|
|
98
158
|
|
|
99
159
|
填写人 / 日期 / 系统与读屏版本:
|
|
100
|
-
`脚本驱动 NVDA(朗读取自 speech 日志原文,非人耳旁听)/ 2026-10-30 / Windows 10 22H2 19045 · NVDA 2026.3 · Edge`
|
|
160
|
+
`脚本驱动 NVDA(朗读取自 speech 日志原文,非人耳旁听)/ 2026-10-30,两轮 / Windows 10 22H2 19045 · NVDA 2026.3 · Edge`
|
|
101
161
|
|
|
102
162
|
---
|
|
103
163
|
|
|
@@ -113,5 +173,6 @@ python3 -m http.server 8000 --bind 127.0.0.1
|
|
|
113
173
|
## 五 · 为什么这份协议要写进仓库
|
|
114
174
|
|
|
115
175
|
因为"我们打算做读屏测试"和"我们做过读屏测试"在仓库里长得一模一样 ——
|
|
116
|
-
|
|
117
|
-
|
|
176
|
+
除非有一张**带状态列的表**。第一轮填这张表时,九页里有六页的"标题层级"写着"未执行";
|
|
177
|
+
那一列记录的不是进度,而是**边界**。第二轮把那一列填满,边界就移动了一格 ——
|
|
178
|
+
现在还剩 macOS VoiceOver 与 Android TalkBack 两格空白(见第四节第 2 条)。
|