| 159 |
159 |
|
//! the same magnitude with its sign off the depth. Both values are the measured
|
| 160 |
160 |
|
//! consensus rather than a new opinion.
|
| 161 |
161 |
|
//!
|
|
162 |
+ |
//! # 0.23.0: a table lays itself out, and a row shows its controls
|
|
163 |
+ |
//!
|
|
164 |
+ |
//! Three things a description could say and this renderer had no rule for,
|
|
165 |
+ |
//! found together by rendering the MNW server's SSH-keys settings tab through
|
|
166 |
+ |
//! `quasi` and preferring the hand-written Askama original.
|
|
167 |
+ |
//!
|
|
168 |
+ |
//! **A table had no layout at all.** [`list::narrowing_css`] emits the track
|
|
169 |
+ |
//! list, and it has to be called with the columns, so it works where the
|
|
170 |
+ |
//! columns are known at build time: goingson builds `tables.css` in its own
|
|
171 |
+ |
//! `build.rs` and is untouched. A table a description produced knows its
|
|
172 |
+ |
//! columns at render time, and the rules would have had to travel with the
|
|
173 |
+ |
//! markup: a `<style>` element per table, which needs `style-src
|
|
174 |
+ |
//! 'unsafe-inline'` that the MNW server is working to drop, or the head, which
|
|
175 |
+ |
//! an htmx fragment swap does not carry. [`table_rules`] lays a table out with
|
|
176 |
+ |
//! `display: table` instead, which aligns columns across rows knowing nothing
|
|
177 |
+ |
//! about how many there are. [`Priority`](makeover_layout::Priority) hiding
|
|
178 |
+ |
//! moves from a generated rule per dropped column to one rule per drop class,
|
|
179 |
+ |
//! and [`list::column_classes`] is what puts those classes on a cell. A header
|
|
180 |
+ |
//! row emitted by a renderer's own code should call it too, or the header and
|
|
181 |
+ |
//! the body disagree about which column just dropped.
|
|
182 |
+ |
//!
|
|
183 |
+ |
//! **A destructive button had nowhere for its tone to land.** `.button` carried
|
|
184 |
+ |
//! no tone, on the reading that a control's colour is its surface. Every
|
|
185 |
+ |
//! consumer had written the danger rule itself. It joins the badge in taking
|
|
186 |
+ |
//! the four tones as colour, off `data-tone`.
|
|
187 |
+ |
//!
|
|
188 |
+ |
//! **A list rendered as a bulleted list**, because nothing here reset the `ul`
|
|
189 |
+ |
//! a renderer emits for one.
|
|
190 |
+ |
//!
|
|
191 |
+ |
//! `RowPart::revealed_on_hover` also stops being honoured, which its own doc
|
|
192 |
+ |
//! sanctions: a renderer decides. The rule hid a row's actions until hover,
|
|
193 |
+ |
//! and every escape it grew was a report that hiding was wrong for somebody:
|
|
194 |
+ |
//! `focus-within` for the keyboard, the capability gate for a fingertip with no
|
|
195 |
+ |
//! way to unhide. What survived hid the controls from pointer users alone, who
|
|
196 |
+ |
//! are the ones scanning a list to learn what can be done to a row.
|
|
197 |
+ |
//!
|
| 162 |
198 |
|
//! # 0.17.0: the depth classes stop being controls
|
| 163 |
199 |
|
//!
|
| 164 |
200 |
|
//! [`depth_rules`] gave `.raised` the whole interactive set. A depth is a
|
| 627 |
663 |
|
);
|
| 628 |
664 |
|
}
|
| 629 |
665 |
|
|
|
666 |
+ |
// A button carries the four tones a badge does. It had none, on the reading
|
|
667 |
+ |
// that a control's colour is its surface rather than its text, and that
|
|
668 |
+ |
// reading has one hole big enough to matter: the button that destroys
|
|
669 |
+ |
// something. Every consumer had written that rule itself, and a description
|
|
670 |
+ |
// that says `Tone::Danger` on an act had nowhere for it to land.
|
|
671 |
+ |
//
|
|
672 |
+ |
// Colour and not a fill, matching the badge. A red surface is a decision
|
|
673 |
+ |
// about emphasis that belongs to an app's own layer, and two of them
|
|
674 |
+ |
// fighting is worse than neither.
|
|
675 |
+ |
let button = class("button", opts);
|
|
676 |
+ |
for tone in [Tone::Info, Tone::Success, Tone::Warning, Tone::Danger] {
|
|
677 |
+ |
let _ = writeln!(
|
|
678 |
+ |
css,
|
|
679 |
+ |
".{button}[data-tone=\"{0}\"] {{\n color: var(--{0});\n}}",
|
|
680 |
+ |
tone.token()
|
|
681 |
+ |
);
|
|
682 |
+ |
}
|
|
683 |
+ |
|
| 630 |
684 |
|
// A chip holds itself down, which is `Depth::pressed` arrived at
|
| 631 |
685 |
|
// independently by two apps. `removable` is a remove affordance, so it is
|
| 632 |
686 |
|
// markup and waits for phase B.
|
| 679 |
733 |
|
/// [`part_class`] carries the same list and the same obligation.
|
| 680 |
734 |
|
fn row_rules(opts: &Emit) -> String {
|
| 681 |
735 |
|
let mut css = String::new();
|
| 682 |
|
- |
let row = class("row", opts);
|
|
736 |
+ |
|
|
737 |
+ |
// The container the rows sit in. Three zeroes and a keyword, all of them
|
|
738 |
+ |
// undoing a browser default the description never asked for: a described
|
|
739 |
+ |
// list of SSH keys is not a bulleted list, and it rendered as one because
|
|
740 |
+ |
// nothing here said otherwise. Not a size: there is no magnitude in it,
|
|
741 |
+ |
// which is the line this crate holds.
|
|
742 |
+ |
let _ = writeln!(
|
|
743 |
+ |
css,
|
|
744 |
+ |
".{} {{\n list-style: none;\n margin: 0;\n padding: 0;\n}}",
|
|
745 |
+ |
class("list", opts)
|
|
746 |
+ |
);
|
|
747 |
+ |
|
| 683 |
748 |
|
for part in [
|
| 684 |
749 |
|
RowPart::Primary,
|
| 685 |
750 |
|
RowPart::Secondary,
|
| 703 |
768 |
|
let _ = writeln!(css, ".{c} {{\n color: var(--{});\n}}", part.intent());
|
| 704 |
769 |
|
}
|
| 705 |
770 |
|
|
| 706 |
|
- |
if part.revealed_on_hover() {
|
| 707 |
|
- |
// Hidden rather than absent: the row must not change height when
|
| 708 |
|
- |
// the pointer arrives. `focus-within` carries the keyboard, which
|
| 709 |
|
- |
// hover on its own would lock out.
|
| 710 |
|
- |
//
|
| 711 |
|
- |
// Transparent rather than `visibility: hidden`, which was the first
|
| 712 |
|
- |
// form and defeated the very escape above: a `visibility: hidden`
|
| 713 |
|
- |
// element is out of the focus order and out of the accessibility
|
| 714 |
|
- |
// tree, so tabbing could never reach an action and could never
|
| 715 |
|
- |
// trigger the row's `focus-within`. goingson had reached the same
|
| 716 |
|
- |
// opacity form independently, on its own comment "always in the DOM
|
| 717 |
|
- |
// for keyboard and screen readers".
|
| 718 |
|
- |
//
|
| 719 |
|
- |
// `pointer-events` rides along because opacity leaves the hit area
|
| 720 |
|
- |
// behind: without it a renderer with no hover carries an invisible
|
| 721 |
|
- |
// tappable control. Keyboard focus is unaffected by it.
|
| 722 |
|
- |
// The hide is gated too, which it was not in 0.10.0, and that was
|
| 723 |
|
- |
// an incomplete capability answer rather than a deliberate one.
|
| 724 |
|
- |
// Ungated, a fingertip got actions hidden with no hover to bring
|
| 725 |
|
- |
// them back, so both webview apps hand-wrote the same
|
| 726 |
|
- |
// `opacity: 1` to undo it: goingson in its touch block and
|
| 727 |
|
- |
// Balanced Breakfast in its own. A primitive that owns the hiding
|
| 728 |
|
- |
// owes the answer for the device that cannot unhide, and the
|
| 729 |
|
- |
// answer both consumers already reached is not to hide at all.
|
| 730 |
|
- |
css.push_str(&gated(
|
| 731 |
|
- |
hover_condition(),
|
| 732 |
|
- |
&format!(".{c} {{\n opacity: 0;\n pointer-events: none;\n}}\n"),
|
| 733 |
|
- |
));
|
| 734 |
|
- |
|
| 735 |
|
- |
// The two halves split here, where they used to be one selector
|
| 736 |
|
- |
// list. Hover-to-reveal is the literal case `Affordance::Hover`
|
| 737 |
|
- |
// was written from, and on a touchscreen it does not fail
|
| 738 |
|
- |
// gracefully: the actions are simply unreachable, because there
|
| 739 |
|
- |
// is no pointer to bring them back. So the hover half is gated
|
| 740 |
|
- |
// and the app owes those rows another way in.
|
| 741 |
|
- |
//
|
| 742 |
|
- |
// `focus-within` stays outside the query. A touchscreen device
|
| 743 |
|
- |
// with a keyboard attached is a real thing, and it is the one
|
| 744 |
|
- |
// path to these actions that survives the gate.
|
| 745 |
|
- |
let revealed = " opacity: 1;\n pointer-events: auto;\n";
|
| 746 |
|
- |
css.push_str(&gated(
|
| 747 |
|
- |
hover_condition(),
|
| 748 |
|
- |
&format!(".{row}:hover .{c} {{\n{revealed}}}\n"),
|
| 749 |
|
- |
));
|
| 750 |
|
- |
let _ = write!(css, ".{row}:focus-within .{c} {{\n{revealed}}}\n");
|
| 751 |
|
- |
}
|
|
771 |
+ |
// `RowPart::revealed_on_hover` is deliberately not honoured, and its
|
|
772 |
+ |
// own doc is what sanctions that: "a renderer with no hover shows it
|
|
773 |
+ |
// always. That is a renderer decision and this returning `true` does
|
|
774 |
+ |
// not forbid it." This renderer now makes the same decision for every
|
|
775 |
+ |
// device, and the reasoning is the one the gate had already been
|
|
776 |
+ |
// forced into twice.
|
|
777 |
+ |
//
|
|
778 |
+ |
// The rule was `opacity: 0` gated to pointer devices, revealed on
|
|
779 |
+ |
// `:hover` and on `:focus-within`. Each escape it needed was a report
|
|
780 |
+ |
// that hiding was wrong for somebody: `focus-within` because tabbing
|
|
781 |
+ |
// could never reach an action; the gate because a fingertip had no way
|
|
782 |
+ |
// to unhide, which both webview apps had already hand-written
|
|
783 |
+ |
// `opacity: 1` to undo. What was left was a control hidden from
|
|
784 |
+ |
// exactly one group: people using a pointer, who are also the group
|
|
785 |
+ |
// scanning a list to find out what can be done to a row.
|
|
786 |
+ |
//
|
|
787 |
+ |
// A settings screen is where that reads worst: the whole reason to be
|
|
788 |
+ |
// on it is to remove a key, and the button doing so was invisible
|
|
789 |
+ |
// until pointed at. A table cell's actions were never hidden, so the
|
|
790 |
+ |
// two arrangements now agree.
|
| 752 |
791 |
|
}
|
| 753 |
792 |
|
css
|
| 754 |
793 |
|
}
|
| 886 |
925 |
|
css
|
| 887 |
926 |
|
}
|
| 888 |
927 |
|
|
|
928 |
+ |
/// The frame a table sits in.
|
|
929 |
+ |
///
|
|
930 |
+ |
/// # Why this is a CSS table and not the grid the rest of the module assumes
|
|
931 |
+ |
///
|
|
932 |
+ |
/// A grid row needs `grid-template-columns`, which has to name every column in
|
|
933 |
+ |
/// order, so it cannot be written without knowing the columns. That is what
|
|
934 |
+ |
/// [`list::narrowing_css`] is for, and it works: goingson builds `tables.css` in
|
|
935 |
+ |
/// its own `build.rs` out of it, and nothing here changes that.
|
|
936 |
+ |
///
|
|
937 |
+ |
/// It does not work for a table a *description* produced. Those columns are
|
|
938 |
+ |
/// known at render time rather than at build time, and the rules would have to
|
|
939 |
+ |
/// travel with the markup: a `<style>` element per table, which needs
|
|
940 |
+ |
/// `style-src 'unsafe-inline'` and so blocks the MNW server's standing plan to
|
|
941 |
+ |
/// drop it. The head is not an escape: a table swapped in by htmx after a
|
|
942 |
+ |
/// delete arrives as a fragment with no head at all.
|
|
943 |
+ |
///
|
|
944 |
+ |
/// A CSS table aligns its columns across rows knowing nothing about how many
|
|
945 |
+ |
/// there are, so there is no track list to emit and nothing per-table to carry.
|
|
946 |
+ |
/// The cost is that a described table cannot take a per-column fixed length,
|
|
947 |
+ |
/// which costs nothing today: [`Sizing`](list::Sizing) is looked up by column
|
|
948 |
+ |
/// name and a description carries no lengths to put in it, so every track a
|
|
949 |
+ |
/// described table could ask for is already content, fill or auto.
|
|
950 |
+ |
///
|
|
951 |
+ |
/// [`Priority`] hiding is unchanged in kind. It moves from a generated
|
|
952 |
+ |
/// `display: none` per dropped column to one rule per drop class, which is the
|
|
953 |
+ |
/// same fact addressed by class rather than by cutoff, and still never by
|
|
954 |
+ |
/// position.
|
|
955 |
+ |
fn table_rules(opts: &Emit) -> String {
|
|
956 |
+ |
let table = class("table", opts);
|
|
957 |
+ |
let head = class("table-head", opts);
|
|
958 |
+ |
let row = class("table-row", opts);
|
|
959 |
+ |
let heading = class("table-heading", opts);
|
|
960 |
+ |
let cell = class("cell", opts);
|
|
961 |
+ |
let mut css = String::new();
|
|
962 |
+ |
|
|
963 |
+ |
let _ = writeln!(
|
|
964 |
+ |
css,
|
|
965 |
+ |
".{table} {{\n display: table;\n width: 100%;\n}}"
|
|
966 |
+ |
);
|
|
967 |
+ |
let _ = writeln!(
|
|
968 |
+ |
css,
|
|
969 |
+ |
".{head},\n.{row} {{\n display: table-row;\n}}"
|
|
970 |
+ |
);
|
|
971 |
+ |
let _ = writeln!(
|
|
972 |
+ |
css,
|
|
973 |
+ |
".{heading},\n.{cell} {{\n display: table-cell;\n}}"
|
|
974 |
+ |
);
|
|
975 |
+ |
|
|
976 |
+ |
// A content column shrinks to what is in it. `width: 1%` is how a CSS table
|
|
977 |
+ |
// is told that: auto layout hands the slack to the columns that asked for
|
|
978 |
+ |
// room, and a column asking for almost none gets what it needs and no more.
|
|
979 |
+ |
// The `nowrap` is what stops it being given less by wrapping.
|
|
980 |
+ |
let _ = writeln!(
|
|
981 |
+ |
css,
|
|
982 |
+ |
".{} {{\n white-space: nowrap;\n width: 1%;\n}}",
|
|
983 |
+ |
class("cell-content", opts)
|
|
984 |
+ |
);
|
|
985 |
+ |
|
|
986 |
+ |
// A fixed column has no length to be fixed to. The description carries none
|
|
987 |
+ |
// and `Sizing` is not reachable from here, so it behaves as content: the
|
|
988 |
+ |
// honest answer to a width nobody supplied, and the same one `Sizing::track`
|
|
989 |
+ |
// gives it.
|
|
990 |
+ |
let _ = writeln!(
|
|
991 |
+ |
css,
|
|
992 |
+ |
".{} {{\n white-space: nowrap;\n}}",
|
|
993 |
+ |
class("cell-fixed", opts)
|
|
994 |
+ |
);
|
|
995 |
+ |
|
|
996 |
+ |
// Optional columns go at the narrowest class, secondary ones go with them,
|
|
997 |
+ |
// which is the cutoff walk `kept_at` describes said as two media queries.
|
|
998 |
+ |
// Essential columns have no rule at all, because never dropping is what not
|
|
999 |
+ |
// being mentioned already means.
|
|
1000 |
+ |
for (size, drops) in [
|
|
1001 |
+ |
(SizeClass::Compact, &["cell-drops-first", "cell-drops-next"][..]),
|
|
1002 |
+ |
(SizeClass::Medium, &["cell-drops-first"][..]),
|
|
1003 |
+ |
] {
|
|
1004 |
+ |
let selectors: Vec<String> = drops
|
|
1005 |
+ |
.iter()
|
|
1006 |
+ |
.map(|drop| format!(".{}", class(drop, opts)))
|
|
1007 |
+ |
.collect();
|
|
1008 |
+ |
css.push_str(&gated(
|
|
1009 |
+ |
Some(&size.media_condition()),
|
|
1010 |
+ |
&format!("{} {{\n display: none;\n}}\n", selectors.join(",\n")),
|
|
1011 |
+ |
));
|
|
1012 |
+ |
}
|
|
1013 |
+ |
|
|
1014 |
+ |
css
|
|
1015 |
+ |
}
|
|
1016 |
+ |
|
| 889 |
1017 |
|
/// The component layer: every named thing phase A emits.
|
| 890 |
1018 |
|
///
|
| 891 |
1019 |
|
/// No scrollbar track. It was on the phase A list and came off: eight lines of
|
| 901 |
1029 |
|
css.push_str(&progress_rules(opts));
|
| 902 |
1030 |
|
css.push_str(&figure_rules(opts));
|
| 903 |
1031 |
|
css.push_str(&state_rules(opts));
|
|
1032 |
+ |
css.push_str(&table_rules(opts));
|
| 904 |
1033 |
|
css
|
| 905 |
1034 |
|
}
|
| 906 |
1035 |
|
|
| 1214 |
1343 |
|
let condition = format!("@media {}", Density::Pointer.media_condition());
|
| 1215 |
1344 |
|
assert!(css.contains(&condition));
|
| 1216 |
1345 |
|
|
| 1217 |
|
- |
// The row reveal splits: hover inside the query, focus-within outside,
|
| 1218 |
|
- |
// or a touchscreen with a keyboard loses its only way to the actions.
|
| 1219 |
|
- |
// Compared by indentation rather than by brace-hunting, because both
|
| 1220 |
|
- |
// sit inside the cascade layer now and every brace is nested.
|
| 1221 |
|
- |
let indent = |needle: &str| {
|
| 1222 |
|
- |
let line = css
|
| 1223 |
|
- |
.lines()
|
| 1224 |
|
- |
.find(|l| l.contains(needle))
|
| 1225 |
|
- |
.unwrap_or_else(|| panic!("no line for {needle}"));
|
| 1226 |
|
- |
line.len() - line.trim_start().len()
|
| 1227 |
|
- |
};
|
| 1228 |
|
- |
let hover = indent(".row:hover .row-actions");
|
| 1229 |
|
- |
let keyboard = indent(".row:focus-within .row-actions");
|
| 1230 |
|
- |
// The hide is gated with the reveal. Ungated it leaves a fingertip
|
| 1231 |
|
- |
// with actions it cannot bring back, which is what both webview apps
|
| 1232 |
|
- |
// were undoing by hand.
|
| 1233 |
|
- |
let hide = css
|
|
1346 |
+ |
// What is gated is every hover state the surfaces carry. The row's
|
|
1347 |
+ |
// actions used to be the other half of this test and are not gated any
|
|
1348 |
+ |
// more, because they are not hidden any more: a rule that reveals
|
|
1349 |
+ |
// nothing needs no capability answer.
|
|
1350 |
+ |
let gated: Vec<&str> = css
|
| 1234 |
1351 |
|
.lines()
|
| 1235 |
|
- |
.position(|l| l.trim() == ".row-actions {")
|
| 1236 |
|
- |
.expect("hide rule");
|
| 1237 |
|
- |
let query = css
|
| 1238 |
|
- |
.lines()
|
| 1239 |
|
- |
.take(hide)
|
| 1240 |
|
- |
.enumerate()
|
| 1241 |
|
- |
.filter(|(_, l)| l.trim_start().starts_with("@media"))
|
| 1242 |
|
- |
.map(|(i, _)| i)
|
| 1243 |
|
- |
.last()
|
| 1244 |
|
- |
.expect("a query precedes it");
|
| 1245 |
|
- |
assert!(hide - query < 3, "the hide is not inside the query");
|
| 1246 |
|
- |
assert!(
|
| 1247 |
|
- |
hover > keyboard,
|
| 1248 |
|
- |
"hover reveal must be nested inside the capability query and the \
|
| 1249 |
|
- |
keyboard reveal must not be: hover indent {hover}, keyboard {keyboard}"
|
| 1250 |
|
- |
);
|
|
1352 |
+ |
.filter(|line| line.contains(":hover"))
|
|
1353 |
+ |
.collect();
|
|
1354 |
+ |
assert!(!gated.is_empty(), "{css}");
|
|
1355 |
+ |
for line in gated {
|
|
1356 |
+ |
let indent = line.len() - line.trim_start().len();
|
|
1357 |
+ |
assert!(indent > 4, "an ungated hover rule: {line}");
|
|
1358 |
+ |
}
|
|
1359 |
+ |
assert!(!css.contains(".row:hover"), "{css}");
|
| 1251 |
1360 |
|
}
|
| 1252 |
1361 |
|
|
| 1253 |
1362 |
|
#[test]
|
| 1365 |
1474 |
|
}
|
| 1366 |
1475 |
|
|
| 1367 |
1476 |
|
#[test]
|
| 1368 |
|
- |
fn row_actions_are_revealed_without_moving_the_row() {
|
|
1477 |
+ |
fn a_rows_actions_are_shown_at_rest() {
|
| 1369 |
1478 |
|
let css = row_rules(&Emit::default());
|
| 1370 |
|
- |
assert!(css.contains(".row-actions {\n opacity: 0;"));
|
| 1371 |
|
- |
// Not display:none, which would reflow the row under the pointer.
|
| 1372 |
|
- |
assert!(!css.contains("display: none"));
|
| 1373 |
|
- |
// Hover alone would lock the keyboard out.
|
| 1374 |
|
- |
assert!(css.contains(".row:focus-within .row-actions"));
|
|
1479 |
+ |
|
|
1480 |
+ |
// The hover reveal is gone, and with it every escape it needed. What
|
|
1481 |
+ |
// it hid was hidden from pointer users alone, who are the ones
|
|
1482 |
+ |
// scanning a list to learn what can be done to a row.
|
|
1483 |
+ |
assert!(!css.contains("opacity"), "{css}");
|
|
1484 |
+ |
assert!(!css.contains("pointer-events"), "{css}");
|
|
1485 |
+ |
assert!(!css.contains(":hover"), "{css}");
|
|
1486 |
+ |
assert!(!css.contains(":focus-within"), "{css}");
|
|
1487 |
+ |
|
|
1488 |
+ |
// Nor is it hidden any other way. `display: none` would reflow the row
|
|
1489 |
+ |
// and `visibility: hidden` would take the actions out of the focus
|
|
1490 |
+ |
// order; the point is that neither is reached for.
|
|
1491 |
+ |
assert!(!css.contains("display: none"), "{css}");
|
|
1492 |
+ |
assert!(!css.contains("visibility:"), "{css}");
|
|
1493 |
+ |
|
|
1494 |
+ |
// The description still says the part is revealed on hover. That is a
|
|
1495 |
+ |
// renderer's decision to make, in the words of the method's own doc,
|
|
1496 |
+ |
// and this renderer now decides otherwise for every device.
|
| 1375 |
1497 |
|
assert!(RowPart::Actions.revealed_on_hover());
|
| 1376 |
1498 |
|
}
|
| 1377 |
1499 |
|
|
| 1378 |
|
- |
#[test]
|
| 1379 |
|
- |
fn a_hidden_row_action_is_still_focusable_and_not_tappable() {
|
| 1380 |
|
- |
let css = row_rules(&Emit::default());
|
| 1381 |
|
- |
// `visibility: hidden` takes the actions out of the focus order, so the
|
| 1382 |
|
- |
// `focus-within` reveal above could never fire from an action itself.
|
| 1383 |
|
- |
assert!(!css.contains("visibility:"));
|
| 1384 |
|
- |
// Opacity leaves the hit area behind, so the pair travels together or
|
| 1385 |
|
- |
// the row carries an invisible tappable control.
|
| 1386 |
|
- |
assert!(
|
| 1387 |
|
- |
css.contains(
|
| 1388 |
|
- |
".row-actions {\n opacity: 0;\n pointer-events: none;\n }"
|
| 1389 |
|
- |
)
|
| 1390 |
|
- |
);
|
| 1391 |
|
- |
assert!(css.contains("opacity: 1;\n pointer-events: auto;"));
|
| 1392 |
|
- |
|
| 1393 |
|
- |
// And on a device with no hover the row carries no such control at
|
| 1394 |
|
- |
// all, because the hide never applies there. That is what the comment
|
| 1395 |
|
- |
// above used to be asking for and could not get: the hide is inside
|
| 1396 |
|
- |
// the capability query with the reveal.
|
| 1397 |
|
- |
let hide = css.find(".row-actions {").expect("hide rule");
|
| 1398 |
|
- |
let query = css[..hide].rfind("@media").expect("a query precedes it");
|
| 1399 |
|
- |
assert!(
|
| 1400 |
|
- |
css[query..hide].find('}').is_none(),
|
| 1401 |
|
- |
"the hide escaped the query"
|
| 1402 |
|
- |
);
|
| 1403 |
|
- |
}
|
| 1404 |
|
- |
|
| 1405 |
1500 |
|
#[test]
|
| 1406 |
1501 |
|
fn the_three_text_parts_take_their_intents_and_actions_inherits() {
|
| 1407 |
1502 |
|
let css = row_rules(&Emit::default());
|
| 1549 |
1644 |
|
assert!(!css.contains(".badge {"));
|
| 1550 |
1645 |
|
}
|
| 1551 |
1646 |
|
|
|
1647 |
+ |
#[test]
|
|
1648 |
+ |
fn a_destructive_button_has_somewhere_for_its_tone_to_land() {
|
|
1649 |
+ |
let css = component_rules(&Emit::default());
|
|
1650 |
+ |
|
|
1651 |
+ |
for tone in [Tone::Info, Tone::Success, Tone::Warning, Tone::Danger] {
|
|
1652 |
+ |
assert!(
|
|
1653 |
+ |
css.contains(&format!(".button[data-tone=\"{}\"]", tone.token())),
|
|
1654 |
+ |
"{css}"
|
|
1655 |
+ |
);
|
|
1656 |
+ |
}
|
|
1657 |
+ |
// Colour, not a fill. A red surface is an app's decision about emphasis.
|
|
1658 |
+ |
assert!(!css.contains(".button[data-tone=\"danger\"] {\n background"));
|
|
1659 |
+ |
}
|
|
1660 |
+ |
|
|
1661 |
+ |
#[test]
|
|
1662 |
+ |
fn a_described_list_is_not_a_bulleted_list() {
|
|
1663 |
+ |
let css = component_rules(&Emit::default());
|
|
1664 |
+ |
assert!(css.contains(".list {\n list-style: none;"), "{css}");
|
|
1665 |
+ |
}
|
|
1666 |
+ |
|
|
1667 |
+ |
#[test]
|
|
1668 |
+ |
fn a_table_lays_itself_out_without_being_told_its_columns() {
|
|
1669 |
+ |
// The whole point of the CSS table. A described table's columns are
|
|
1670 |
+ |
// known at render time, so anything the stylesheet has to be told about
|
|
1671 |
+ |
// them would have to travel with the markup.
|
|
1672 |
+ |
let css = component_rules(&Emit::default());
|
|
1673 |
+ |
|
|
1674 |
+ |
assert!(css.contains(".table {\n display: table;"), "{css}");
|
|
1675 |
+ |
assert!(css.contains("display: table-row;"), "{css}");
|
|
1676 |
+ |
assert!(css.contains("display: table-cell;"), "{css}");
|
|
1677 |
+ |
assert!(!css.contains("grid-template-columns"), "{css}");
|
|
1678 |
+ |
}
|
|
1679 |
+ |
|
|
1680 |
+ |
#[test]
|
|
1681 |
+ |
fn a_column_drops_by_its_priority_and_never_by_its_position() {
|
|
1682 |
+ |
let css = component_rules(&Emit::default());
|
|
1683 |
+ |
|
|
1684 |
+ |
// Optional goes at the narrowest class and secondary goes with it,
|
|
1685 |
+ |
// which is `kept_at`'s cutoff walk said as two queries.
|
|
1686 |
+ |
let compact = css
|
|
1687 |
+ |
.find(&format!("@media {}", SizeClass::Compact.media_condition()))
|
|
1688 |
+ |
.expect("a compact query");
|
|
1689 |
+ |
let medium = css
|
|
1690 |
+ |
.find(&format!("@media {}", SizeClass::Medium.media_condition()))
|
|
1691 |
+ |
.expect("a medium query");
|
|
1692 |
+ |
assert!(css[compact..].contains(".cell-drops-next"), "{css}");
|
|
1693 |
+ |
assert!(!css[medium..].contains(".cell-drops-next"), "{css}");
|
|
1694 |
+ |
|
|
1695 |
+ |
// Essential columns are never mentioned, because not being mentioned is
|
|
1696 |
+ |
// already what never dropping means.
|
|
1697 |
+ |
assert!(!css.contains(".cell-keeps"), "{css}");
|
|
1698 |
+ |
|
|
1699 |
+ |
// And nothing counts. `nth-child` is the bug the priority vocabulary
|
|
1700 |
+ |
// exists to end.
|
|
1701 |
+ |
assert!(!css.contains("nth-child"), "{css}");
|
|
1702 |
+ |
}
|
|
1703 |
+ |
|
| 1552 |
1704 |
|
#[test]
|
| 1553 |
1705 |
|
fn the_whole_sheet_still_names_every_colour() {
|
| 1554 |
1706 |
|
// The crate's founding property, asserted over the component layer and
|
| 1592 |
1744 |
|
|| value.starts_with("\"\\2")
|
| 1593 |
1745 |
|
|| matches!(
|
| 1594 |
1746 |
|
value.trim_end_matches(';'),
|
| 1595 |
|
- |
"0" | "1" | "none" | "auto" | "not-allowed" | "pointer"
|
|
1747 |
+ |
"0" | "1"
|
|
1748 |
+ |
| "none"
|
|
1749 |
+ |
| "auto"
|
|
1750 |
+ |
| "not-allowed"
|
|
1751 |
+ |
| "pointer"
|
|
1752 |
+ |
// The table frame. `display` is structure and not a
|
|
1753 |
+ |
// size; `nowrap` is what makes a content column
|
|
1754 |
+ |
// content. The two widths are the awkward pair and
|
|
1755 |
+ |
// they are still not sizes: `100%` is "all of
|
|
1756 |
+ |
// whatever you were given" and `1%` is the CSS
|
|
1757 |
+ |
// table idiom for "shrink to fit", which is a
|
|
1758 |
+ |
// behaviour spelled as a number because CSS has no
|
|
1759 |
+ |
// keyword for it. Neither names a magnitude, which
|
|
1760 |
+ |
// is the thing this crate leaves to
|
|
1761 |
+ |
// makeover-geometry.
|
|
1762 |
+ |
| "table"
|
|
1763 |
+ |
| "table-row"
|
|
1764 |
+ |
| "table-cell"
|
|
1765 |
+ |
| "nowrap"
|
|
1766 |
+ |
| "100%"
|
|
1767 |
+ |
| "1%"
|
| 1596 |
1768 |
|
),
|
| 1597 |
1769 |
|
"unrecognised literal value: {line}"
|
| 1598 |
1770 |
|
);
|