| 1260 |
1260 |
|
css
|
| 1261 |
1261 |
|
}
|
| 1262 |
1262 |
|
|
|
1263 |
+ |
/// What a wait looks like, for the attribute that has been describing one to
|
|
1264 |
+ |
/// nobody.
|
|
1265 |
+ |
///
|
|
1266 |
+ |
/// Wiki `loading-and-progress-standard`, phase 2. `data-awaiting` has been
|
|
1267 |
+ |
/// emitted since the vocabulary gained `Awaiting`, with
|
|
1268 |
+ |
/// `data-awaiting="determinate"` and a `data-awaiting-amount` beside it or
|
|
1269 |
+ |
/// `data-awaiting="indeterminate"` alone, and until 0.60.0 **nothing in the tree
|
|
1270 |
+ |
/// styled either**. Determinate and indeterminate rendered identically, which is
|
|
1271 |
+ |
/// to say not at all. This is the half that was missing.
|
|
1272 |
+ |
///
|
|
1273 |
+ |
/// # Why it is keyed on `aria-busy` and not on the attribute alone
|
|
1274 |
+ |
///
|
|
1275 |
+ |
/// `data-awaiting` is a fact about the control: pressing this waits. It is true
|
|
1276 |
+ |
/// when the page is painted and it stays true. Whether a wait is *running* is
|
|
1277 |
+ |
/// true only between two events, so it is the binder's to set, and `aria-busy`
|
|
1278 |
+ |
/// is the standard spelling of it — announced as well as drawn, which a class
|
|
1279 |
+ |
/// of our own would not be.
|
|
1280 |
+ |
///
|
|
1281 |
+ |
/// That also keeps this crate out of any one client library's vocabulary.
|
|
1282 |
+ |
/// `quasi-webview` sets `aria-busy` from htmx's request events; a host driving
|
|
1283 |
+ |
/// the same markup another way sets it the same way and gets the same drawing.
|
|
1284 |
+ |
///
|
|
1285 |
+ |
/// # The two drawings
|
|
1286 |
+ |
///
|
|
1287 |
+ |
/// One pseudo-element either way, so no renderer has to emit an extra node.
|
|
1288 |
+ |
///
|
|
1289 |
+ |
/// Indeterminate is the activity mark of rule 2: a small square that blinks.
|
|
1290 |
+ |
/// Determinate is a trough with a fill, drawn as a single gradient whose stop is
|
|
1291 |
+ |
/// `--awaiting-share`, a plain number from 0 to 1 that the binder sets from
|
|
1292 |
+ |
/// bytes it has actually watched land. A determinate control with nothing
|
|
1293 |
+ |
/// setting the share draws an empty trough rather than a full one, which is the
|
|
1294 |
+ |
/// honest reading: the size is known and the delivery is not.
|
|
1295 |
+ |
///
|
|
1296 |
+ |
/// **What the bar may not do**, from rule 1 and from `Awaiting`'s own docs:
|
|
1297 |
+ |
/// what is done over what there is, and never a remaining time, an arrival time
|
|
1298 |
+ |
/// or a rate extrapolated forward. Nothing here can express one, which is
|
|
1299 |
+ |
/// deliberate — the only input is a share of a measured payload.
|
|
1300 |
+ |
///
|
|
1301 |
+ |
/// # Sizes, and the deferral rule
|
|
1302 |
+ |
///
|
|
1303 |
+ |
/// `progress_rules` emits the tones and never the width, because the width is
|
|
1304 |
+ |
/// not this crate's to know. A pseudo-element has no intrinsic size at all, so
|
|
1305 |
+ |
/// the same treatment would render nothing anywhere. Both sizes are therefore
|
|
1306 |
+ |
/// custom properties with defaults: an app that wants a different mark sets
|
|
1307 |
+ |
/// `--awaiting-mark` and `--awaiting-bar` once, and one that says nothing gets a
|
|
1308 |
+ |
/// mark that is visible.
|
|
1309 |
+ |
///
|
|
1310 |
+ |
/// # The cadence, and what happens without it
|
|
1311 |
+ |
///
|
|
1312 |
+ |
/// `--cadence-activity` comes from `makeover-timing` through `makeover-build`,
|
|
1313 |
+ |
/// and is a half-period, so a full cycle is twice it. It is used bare rather
|
|
1314 |
+ |
/// than with a fallback: a number written here would be a second heartbeat for
|
|
1315 |
+ |
/// a mark three renderers draw.
|
|
1316 |
+ |
///
|
|
1317 |
+ |
/// A sheet assembled without the time axis leaves `animation-duration` invalid,
|
|
1318 |
+ |
/// which resolves to `0s`, which runs no animation and leaves the base style —
|
|
1319 |
+ |
/// a lit, still mark. That is also exactly what the reduced-motion block does,
|
|
1320 |
+ |
/// since it sets the cadence to `0ms`. Both fall out of one rule because the
|
|
1321 |
+ |
/// base style is lit and the keyframes do the dimming, which is the ordering
|
|
1322 |
+ |
/// `makeover_timing::reduced_motion_css` asks its consumers for by name.
|
|
1323 |
+ |
fn awaiting_rules(opts: &Emit) -> String {
|
|
1324 |
+ |
let mut css = String::new();
|
|
1325 |
+ |
|
|
1326 |
+ |
// Dimming rather than lighting, so a zero-length animation leaves a lit
|
|
1327 |
+ |
// mark rather than a blank one. See `makeover_timing::reduced_motion_css`.
|
|
1328 |
+ |
css.push_str(
|
|
1329 |
+ |
"@keyframes makeover-activity {\n \
|
|
1330 |
+ |
0%, 49.99% {\n background: var(--action);\n }\n \
|
|
1331 |
+ |
50%, 100% {\n background: var(--surface-sunken);\n }\n\
|
|
1332 |
+ |
}\n",
|
|
1333 |
+ |
);
|
|
1334 |
+ |
|
|
1335 |
+ |
// Nothing is drawn until something is waiting. `content` on the base rule
|
|
1336 |
+ |
// rather than on the busy one keeps the box the same box across the
|
|
1337 |
+ |
// transition, so a mark appearing does not reflow the line it is in.
|
|
1338 |
+ |
let _ = writeln!(
|
|
1339 |
+ |
css,
|
|
1340 |
+ |
"[data-awaiting]::after {{\n \
|
|
1341 |
+ |
content: \"\";\n \
|
|
1342 |
+ |
display: none;\n \
|
|
1343 |
+ |
margin-inline-start: var(--awaiting-gap, 0.5ch);\n \
|
|
1344 |
+ |
vertical-align: baseline;\n\
|
|
1345 |
+ |
}}"
|
|
1346 |
+ |
);
|
|
1347 |
+ |
|
|
1348 |
+ |
let _ = writeln!(
|
|
1349 |
+ |
css,
|
|
1350 |
+ |
"[data-awaiting][aria-busy=\"true\"]::after {{\n \
|
|
1351 |
+ |
display: inline-block;\n \
|
|
1352 |
+ |
inline-size: var(--awaiting-mark, 0.5em);\n \
|
|
1353 |
+ |
block-size: var(--awaiting-mark, 0.5em);\n \
|
|
1354 |
+ |
background: var(--action);\n \
|
|
1355 |
+ |
opacity: 1;\n \
|
|
1356 |
+ |
animation: makeover-activity calc(var(--cadence-activity) * 2) \
|
|
1357 |
+ |
step-end infinite;\n\
|
|
1358 |
+ |
}}"
|
|
1359 |
+ |
);
|
|
1360 |
+ |
|
|
1361 |
+ |
// The measured half. A wider box, no blink, and a gradient whose stop is
|
|
1362 |
+ |
// the share: the fill and the trough in one paint, so the markup stays one
|
|
1363 |
+ |
// pseudo-element on both branches.
|
|
1364 |
+ |
//
|
|
1365 |
+ |
// `--awaiting-share` unset is an empty trough, not a full one. A bar that
|
|
1366 |
+ |
// read full because nobody was counting would be the confidently-wrong
|
|
1367 |
+ |
// drawing rule 1 exists to forbid.
|
|
1368 |
+ |
//
|
|
1369 |
+ |
// The trough takes an edge for the reason a well does: a bar at zero share
|
|
1370 |
+ |
// is otherwise a rectangle of the surface it sits on, which is nothing at
|
|
1371 |
+ |
// all. `border_width` rather than a literal, the way `depth_rule` and
|
|
1372 |
+ |
// `track_rules` write theirs.
|
|
1373 |
+ |
let _ = writeln!(
|
|
1374 |
+ |
css,
|
|
1375 |
+ |
"[data-awaiting=\"determinate\"][aria-busy=\"true\"]::after {{\n \
|
|
1376 |
+ |
inline-size: var(--awaiting-bar, 6em);\n \
|
|
1377 |
+ |
animation: none;\n \
|
|
1378 |
+ |
outline: {} solid var(--border);\n \
|
|
1379 |
+ |
outline-offset: -{};\n \
|
|
1380 |
+ |
background: linear-gradient(\n \
|
|
1381 |
+ |
to inline-end,\n \
|
|
1382 |
+ |
var(--action) 0 calc(var(--awaiting-share, 0) * 100%),\n \
|
|
1383 |
+ |
var(--surface-sunken) 0\n \
|
|
1384 |
+ |
);\n\
|
|
1385 |
+ |
}}",
|
|
1386 |
+ |
opts.border_width, opts.border_width
|
|
1387 |
+ |
);
|
|
1388 |
+ |
|
|
1389 |
+ |
css
|
|
1390 |
+ |
}
|
|
1391 |
+ |
|
| 1263 |
1392 |
|
/// A strip of figures, and the two spans inside each one.
|
| 1264 |
1393 |
|
///
|
| 1265 |
1394 |
|
/// Colour only, which is the deferral rule applied to a component that badly
|
| 1749 |
1878 |
|
css.push_str(&row_rules(opts));
|
| 1750 |
1879 |
|
css.push_str(&run_rules(opts));
|
| 1751 |
1880 |
|
css.push_str(&progress_rules(opts));
|
|
1881 |
+ |
css.push_str(&awaiting_rules(opts));
|
| 1752 |
1882 |
|
css.push_str(&figure_rules(opts));
|
| 1753 |
1883 |
|
css.push_str(&picture_rules(opts));
|
| 1754 |
1884 |
|
css.push_str(&showing_rules(opts));
|
| 2688 |
2818 |
|
assert!(crate::vocabulary::names(&Emit::default()).contains("row-relaxed"));
|
| 2689 |
2819 |
|
}
|
| 2690 |
2820 |
|
|
|
2821 |
+ |
#[test]
|
|
2822 |
+ |
fn the_two_kinds_of_wait_stop_rendering_identically() {
|
|
2823 |
+ |
// The state `d43ea1c5` fixes: the emitter had been writing
|
|
2824 |
+ |
// `data-awaiting` for months and nothing styled either value, so a
|
|
2825 |
+ |
// measured wait and an unmeasured one drew the same nothing.
|
|
2826 |
+ |
let css = stylesheet(&Emit::default());
|
|
2827 |
+ |
assert!(css.contains("[data-awaiting]::after"), "{css}");
|
|
2828 |
+ |
assert!(
|
|
2829 |
+ |
css.contains("[data-awaiting][aria-busy=\"true\"]::after"),
|
|
2830 |
+ |
"the mark is drawn only while something is actually waiting"
|
|
2831 |
+ |
);
|
|
2832 |
+ |
assert!(
|
|
2833 |
+ |
css.contains("[data-awaiting=\"determinate\"][aria-busy=\"true\"]::after"),
|
|
2834 |
+ |
"the measured half is its own drawing"
|
|
2835 |
+ |
);
|
|
2836 |
+ |
}
|
|
2837 |
+ |
|
|
2838 |
+ |
#[test]
|
|
2839 |
+ |
fn the_blink_takes_its_cadence_and_never_names_one() {
|
|
2840 |
+ |
// Three renderers draw this mark. A number written here would be a
|
|
2841 |
+ |
// second heartbeat for one wait.
|
|
2842 |
+ |
let css = stylesheet(&Emit::default());
|
|
2843 |
+ |
assert!(
|
|
2844 |
+ |
css.contains("calc(var(--cadence-activity) * 2)"),
|
|
2845 |
+ |
"a half-period doubled, not a literal"
|
|
2846 |
+ |
);
|
|
2847 |
+ |
assert!(!css.contains("500ms"), "{css}");
|
|
2848 |
+ |
}
|
|
2849 |
+ |
|
|
2850 |
+ |
#[test]
|
|
2851 |
+ |
fn a_bar_nobody_is_counting_is_empty_rather_than_full() {
|
|
2852 |
+ |
// Rule 1. The share defaults to zero, so a determinate control with no
|
|
2853 |
+ |
// binder watching bytes draws a trough. A default of 1 would be the
|
|
2854 |
+ |
// confidently-wrong drawing the rule exists to forbid.
|
|
2855 |
+ |
let css = stylesheet(&Emit::default());
|
|
2856 |
+ |
assert!(css.contains("var(--awaiting-share, 0)"), "{css}");
|
|
2857 |
+ |
}
|
|
2858 |
+ |
|
|
2859 |
+ |
#[test]
|
|
2860 |
+ |
fn motion_off_leaves_the_mark_lit_because_the_keyframes_do_the_dimming() {
|
|
2861 |
+ |
// `makeover_timing::reduced_motion_css` sets `--cadence-activity: 0ms`,
|
|
2862 |
+ |
// and a zero-length animation leaves the element in its base style
|
|
2863 |
+ |
// rather than at its last keyframe. So the base has to be the lit one.
|
|
2864 |
+ |
// The inverted spelling would blank the mark for a reader who asked for
|
|
2865 |
+ |
// less motion, which answers a request nobody made.
|
|
2866 |
+ |
let css = stylesheet(&Emit::default());
|
|
2867 |
+ |
let busy = css
|
|
2868 |
+ |
.split("[data-awaiting][aria-busy=\"true\"]::after {")
|
|
2869 |
+ |
.nth(1)
|
|
2870 |
+ |
.expect("the busy rule");
|
|
2871 |
+ |
let busy = busy.split('}').next().expect("its body");
|
|
2872 |
+ |
assert!(
|
|
2873 |
+ |
busy.contains("background: var(--action);"),
|
|
2874 |
+ |
"the base state is lit: {busy}"
|
|
2875 |
+ |
);
|
|
2876 |
+ |
}
|
|
2877 |
+ |
|
| 2691 |
2878 |
|
#[test]
|
| 2692 |
2879 |
|
fn the_whole_sheet_still_names_every_colour() {
|
| 2693 |
2880 |
|
// The crate's founding property, asserted over the component layer and
|
| 2738 |
2925 |
|
|| matches!(
|
| 2739 |
2926 |
|
value.trim_end_matches(';'),
|
| 2740 |
2927 |
|
"0" | "1"
|
|
2928 |
+ |
// The wait's mark and bar, 0.60.0. An empty
|
|
2929 |
+ |
// `content` is what brings a pseudo-element into
|
|
2930 |
+ |
// existence with nothing in it, and `step-end`
|
|
2931 |
+ |
// is an easing: the blink is two states, not a
|
|
2932 |
+ |
// slide between them. Neither is a colour, and
|
|
2933 |
+ |
// neither is a magnitude.
|
|
2934 |
+ |
| "\"\""
|
|
2935 |
+ |
// Where the mark sits on the line it joins.
|
|
2936 |
+ |
// Alignment is structure, the same way `display`
|
|
2937 |
+ |
// is, and `baseline` is the initial value said out
|
|
2938 |
+ |
// loud so a host stylesheet cannot leave it
|
|
2939 |
+ |
// wherever an earlier rule put it.
|
|
2940 |
+ |
| "baseline"
|
|
2941 |
+ |
| "inline-block"
|
| 2741 |
2942 |
|
| "none"
|
| 2742 |
2943 |
|
| "auto"
|
| 2743 |
2944 |
|
| "not-allowed"
|