| 68 |
68 |
|
/// makes this additive.
|
| 69 |
69 |
|
pub(crate) fn chrome_html(
|
| 70 |
70 |
|
chrome: &Chrome,
|
| 71 |
|
- |
at: Option<&str>,
|
| 72 |
71 |
|
opts: &Emit,
|
| 73 |
72 |
|
fills: &HashMap<String, String>,
|
| 74 |
73 |
|
out: &mut String,
|
| 75 |
74 |
|
) {
|
| 76 |
|
- |
// First among the chrome, which is the one placement decision this renderer
|
| 77 |
|
- |
// does make and it makes it in document order rather than in pixels: a
|
| 78 |
|
- |
// navigation landmark that comes after everything else is reachable and
|
| 79 |
|
- |
// reads as an afterthought. Where it lands visually is still the
|
| 80 |
|
- |
// stylesheet's, same as a panel's.
|
| 81 |
|
- |
nav_html(&chrome.nav, at, opts, out);
|
| 82 |
75 |
|
for binding in &chrome.bindings {
|
| 83 |
76 |
|
binding_html(binding, out);
|
| 84 |
77 |
|
}
|
| 109 |
102 |
|
|
| 110 |
103 |
|
/// The nav: the app's places, as links, with the current one marked.
|
| 111 |
104 |
|
///
|
|
105 |
+ |
/// Emitted before the screen rather than with the rest of the chrome, and that
|
|
106 |
+ |
/// is the one placement decision this renderer makes. It makes it in document
|
|
107 |
+ |
/// order and not in pixels: a navigation that comes after the content is a
|
|
108 |
+ |
/// landmark a screen reader reaches last and a block a stylesheet has to lift
|
|
109 |
+ |
/// with `position: fixed` and then pay for in padding. Where it lands visually
|
|
110 |
+ |
/// is still the stylesheet's, the same as a panel's.
|
|
111 |
+ |
///
|
| 112 |
112 |
|
/// A `nav` element and not a list of buttons. These are addresses, so they are
|
| 113 |
113 |
|
/// links: middle-click opens one in a tab, the status bar shows where it goes,
|
| 114 |
114 |
|
/// and a screen reader lands on a navigation landmark. None of that is
|
| 374 |
374 |
|
#[test]
|
| 375 |
375 |
|
fn an_app_with_no_chrome_emits_nothing_at_all() {
|
| 376 |
376 |
|
let mut out = String::new();
|
| 377 |
|
- |
chrome_html(
|
| 378 |
|
- |
&Chrome::new(),
|
| 379 |
|
- |
None,
|
| 380 |
|
- |
&Emit::default(),
|
| 381 |
|
- |
&HashMap::new(),
|
| 382 |
|
- |
&mut out,
|
| 383 |
|
- |
);
|
|
377 |
+ |
chrome_html(&Chrome::new(), &Emit::default(), &HashMap::new(), &mut out);
|
| 384 |
378 |
|
assert!(out.is_empty());
|
| 385 |
379 |
|
}
|
| 386 |
380 |
|
|
| 394 |
388 |
|
Node::text("00:12:04"),
|
| 395 |
389 |
|
);
|
| 396 |
390 |
|
let mut out = String::new();
|
| 397 |
|
- |
chrome_html(&chrome, None, &Emit::default(), &HashMap::new(), &mut out);
|
|
391 |
+ |
chrome_html(&chrome, &Emit::default(), &HashMap::new(), &mut out);
|
| 398 |
392 |
|
assert!(out.contains("id=\"timer\""), "{out}");
|
| 399 |
393 |
|
assert!(out.contains("chrome-panel"), "{out}");
|
| 400 |
394 |
|
assert!(out.contains("00:12:04"), "{out}");
|
| 415 |
409 |
|
Node::text("00:12:04"),
|
| 416 |
410 |
|
);
|
| 417 |
411 |
|
let mut out = String::new();
|
| 418 |
|
- |
chrome_html(&chrome, None, &Emit::default(), &HashMap::new(), &mut out);
|
|
412 |
+ |
chrome_html(&chrome, &Emit::default(), &HashMap::new(), &mut out);
|
| 419 |
413 |
|
// The annotation is for whoever finds an empty div in devtools, so it
|
| 420 |
414 |
|
// has to survive the swap that fills the div. Before the element, not
|
| 421 |
415 |
|
// within it.
|
| 433 |
427 |
|
.presenting("timer", Role::Activity, Node::text("00:12:04"))
|
| 434 |
428 |
|
.presenting("sync", Role::Status, Node::text("Synced"));
|
| 435 |
429 |
|
let mut out = String::new();
|
| 436 |
|
- |
chrome_html(&chrome, None, &Emit::default(), &HashMap::new(), &mut out);
|
|
430 |
+ |
chrome_html(&chrome, &Emit::default(), &HashMap::new(), &mut out);
|
| 437 |
431 |
|
|
| 438 |
432 |
|
assert!(out.contains("id=\"timer\""), "{out}");
|
| 439 |
433 |
|
assert!(out.contains("id=\"sync\""), "{out}");
|
| 448 |
442 |
|
fn the_nav_is_links_because_the_places_are_addresses() {
|
| 449 |
443 |
|
use quasi_router::{Action, Place};
|
| 450 |
444 |
|
|
| 451 |
|
- |
let chrome = Chrome::new().offering(Place::new("time", "Time", Action::get("/day")));
|
|
445 |
+ |
let places = [Place::new("time", "Time", Action::get("/day"))];
|
| 452 |
446 |
|
let mut out = String::new();
|
| 453 |
|
- |
chrome_html(&chrome, None, &Emit::default(), &HashMap::new(), &mut out);
|
|
447 |
+ |
nav_html(&places, None, &Emit::default(), &mut out);
|
| 454 |
448 |
|
|
| 455 |
449 |
|
// A link and not a button: middle-click opens a tab, the status bar
|
| 456 |
450 |
|
// shows where it goes, and a screen reader lands on a landmark.
|
| 464 |
458 |
|
fn the_place_the_screen_names_is_the_one_marked_current() {
|
| 465 |
459 |
|
use quasi_router::{Action, Place};
|
| 466 |
460 |
|
|
| 467 |
|
- |
let chrome = Chrome::new()
|
| 468 |
|
- |
.offering(Place::new("work", "Work", Action::get("/tasks")).within([
|
|
461 |
+ |
let places = [
|
|
462 |
+ |
Place::new("work", "Work", Action::get("/tasks")).within([
|
| 469 |
463 |
|
Place::new("tasks", "Tasks", Action::get("/tasks")),
|
| 470 |
464 |
|
Place::new("board", "Board", Action::get("/board")),
|
| 471 |
|
- |
]))
|
| 472 |
|
- |
.offering(Place::new("time", "Time", Action::get("/day")));
|
|
465 |
+ |
]),
|
|
466 |
+ |
Place::new("time", "Time", Action::get("/day")),
|
|
467 |
+ |
];
|
| 473 |
468 |
|
|
| 474 |
469 |
|
let mut out = String::new();
|
| 475 |
|
- |
chrome_html(
|
| 476 |
|
- |
&chrome,
|
| 477 |
|
- |
Some("board"),
|
| 478 |
|
- |
&Emit::default(),
|
| 479 |
|
- |
&HashMap::new(),
|
| 480 |
|
- |
&mut out,
|
| 481 |
|
- |
);
|
|
470 |
+ |
nav_html(&places, Some("board"), &Emit::default(), &mut out);
|
| 482 |
471 |
|
|
| 483 |
472 |
|
// The place itself, and the place holding it. Both are where the user
|
| 484 |
473 |
|
// is, and a tab bar that lit the pill and not the tab would be lying
|
| 497 |
486 |
|
// A confirmation drawn over one, a detail reached from a row. The last
|
| 498 |
487 |
|
// place staying lit would be the nav claiming the user is somewhere
|
| 499 |
488 |
|
// they left.
|
| 500 |
|
- |
let chrome = Chrome::new().offering(Place::new("time", "Time", Action::get("/day")));
|
|
489 |
+ |
let places = [Place::new("time", "Time", Action::get("/day"))];
|
| 501 |
490 |
|
let mut out = String::new();
|
| 502 |
|
- |
chrome_html(&chrome, None, &Emit::default(), &HashMap::new(), &mut out);
|
|
491 |
+ |
nav_html(&places, None, &Emit::default(), &mut out);
|
| 503 |
492 |
|
assert!(!out.contains("aria-current"), "{out}");
|
| 504 |
493 |
|
|
| 505 |
494 |
|
// And a key no place carries is the same: nothing marked, no error. The
|
| 506 |
495 |
|
// nav is the app's and so is the key, and a renderer is the wrong place
|
| 507 |
496 |
|
// to discover an app disagreeing with itself.
|
| 508 |
497 |
|
let mut other = String::new();
|
| 509 |
|
- |
chrome_html(
|
| 510 |
|
- |
&chrome,
|
| 511 |
|
- |
Some("nowhere"),
|
| 512 |
|
- |
&Emit::default(),
|
| 513 |
|
- |
&HashMap::new(),
|
| 514 |
|
- |
&mut other,
|
| 515 |
|
- |
);
|
|
498 |
+ |
nav_html(&places, Some("nowhere"), &Emit::default(), &mut other);
|
| 516 |
499 |
|
assert!(!other.contains("aria-current"), "{other}");
|
| 517 |
500 |
|
}
|
| 518 |
501 |
|
|
|
502 |
+ |
#[test]
|
|
503 |
+ |
fn the_nav_comes_before_the_content_it_navigates() {
|
|
504 |
+ |
use quasi_http::Serves as _;
|
|
505 |
+ |
use quasi_router::{Action, Node, Place, Screen, Slot};
|
|
506 |
+ |
|
|
507 |
+ |
// Document order, which is the one placement decision this renderer
|
|
508 |
+ |
// makes. A navigation after the content is a landmark reached last and
|
|
509 |
+ |
// a block the stylesheet has to lift with `position: fixed` and then
|
|
510 |
+ |
// pay for in padding.
|
|
511 |
+ |
let webview =
|
|
512 |
+ |
crate::Webview::new().with_shell(crate::Shell::default().with_chrome(
|
|
513 |
+ |
Chrome::new().offering(Place::new("time", "Time", Action::get("/day"))),
|
|
514 |
+ |
));
|
|
515 |
+ |
let screen = Screen::list_detail("Day", false)
|
|
516 |
+ |
.at_place("time")
|
|
517 |
+ |
.with(Slot::new("body", quasi_router::RegionKind::Pane).with(Node::text("the day")));
|
|
518 |
+ |
|
|
519 |
+ |
let out = webview.screen(&screen);
|
|
520 |
+ |
let nav = out.find("data-chrome=\"nav\"").expect("drawn");
|
|
521 |
+ |
let main = out.find("<main").expect("drawn");
|
|
522 |
+ |
assert!(nav < main, "{out}");
|
|
523 |
+ |
assert!(out.contains("aria-current=\"page\""), "{out}");
|
|
524 |
+ |
}
|
|
525 |
+ |
|
| 519 |
526 |
|
#[test]
|
| 520 |
527 |
|
fn a_nav_alone_still_gets_the_overlay_container() {
|
| 521 |
528 |
|
use quasi_router::{Action, Place};
|
| 524 |
531 |
|
// binding involved, so any app declaring chrome gets a container.
|
| 525 |
532 |
|
let chrome = Chrome::new().offering(Place::new("time", "Time", Action::get("/day")));
|
| 526 |
533 |
|
let mut out = String::new();
|
| 527 |
|
- |
chrome_html(&chrome, None, &Emit::default(), &HashMap::new(), &mut out);
|
|
534 |
+ |
chrome_html(&chrome, &Emit::default(), &HashMap::new(), &mut out);
|
| 528 |
535 |
|
assert!(out.contains(OVERLAY_ID), "{out}");
|
| 529 |
536 |
|
}
|
| 530 |
537 |
|
|
| 534 |
541 |
|
|
| 535 |
542 |
|
let chrome = Chrome::new().bind("ctrl+k", "Search", Action::get("/palette"));
|
| 536 |
543 |
|
let mut out = String::new();
|
| 537 |
|
- |
chrome_html(&chrome, None, &Emit::default(), &HashMap::new(), &mut out);
|
|
544 |
+ |
chrome_html(&chrome, &Emit::default(), &HashMap::new(), &mut out);
|
| 538 |
545 |
|
assert!(out.contains("hx-get=\"/palette\""), "{out}");
|
| 539 |
546 |
|
assert!(out.contains("hx-target=\"#quasi-overlay\""), "{out}");
|
| 540 |
547 |
|
assert!(out.contains("from:body"), "{out}");
|
| 553 |
560 |
|
.bind("dpad-left", "Nope", Action::get("/nope"))
|
| 554 |
561 |
|
.bind("ctrl+k", "Search", Action::get("/palette"));
|
| 555 |
562 |
|
let mut out = String::new();
|
| 556 |
|
- |
chrome_html(&chrome, None, &Emit::default(), &HashMap::new(), &mut out);
|
|
563 |
+ |
chrome_html(&chrome, &Emit::default(), &HashMap::new(), &mut out);
|
| 557 |
564 |
|
assert!(!out.contains("/nope"), "{out}");
|
| 558 |
565 |
|
assert!(out.contains("/palette"), "{out}");
|
| 559 |
566 |
|
}
|