//! The library page's tab strip, described. //! //! Shape 2, step 1 (`6b24f2df`), and the first described tab strip in the tree. //! An Askama entry point rather than a mounted screen, the same shape //! `widgets::carousel` has: `pages/library.html` is still an Askama document and //! this is one region inside it. //! //! # The panels are not described and are not meant to be //! //! Max's ruling: describe the strip, leave the panels as routes. So every tab is //! a [`RegionKind::Handover`], a place and nothing else, and what lands in one //! is whatever its route already answered with. That is the honest kind for a //! region whose contents are Askama's, and it is what lets the five panel routes //! in `routes::pages::public` stay untouched. //! //! # Who fetches, and why the shown tab is different //! //! `dfbc88ce`: the strip button carries its panel's address and the panel emits //! no load trigger, so a reader downloads the tab they pressed and not the four //! they did not. The shown tab carries no address at all and arrives with its //! contents already in it, which is `9b958e7b`: a screen renders once at final //! geometry, and a placeholder on the panel being looked at is the one place //! that rule bites hardest. It is also what `library.html` already did with its //! `{% include %}`, so this conversion changes the page's request count by zero. //! //! # Why each tab says what it replaces //! //! Three of the five panels are plain Askama routes that quasi never sees, and a //! route that cannot name a region leaves the answer wherever htmx's default //! puts it, which is inside the button that was pressed. [`Action::replacing`] //! is exactly this case and its own doc says so. The other two are described //! screens that name their region themselves, so they are left to, and their //! regions were renamed to match the ids here. See `library_contacts::REGION` //! and `forum_memberships::LIBRARY_REGION`. //! //! # What changed for a reader, and it is one thing //! //! The overflow control. `Fallback::Menu` is declared and makeover's own CSS //! renders a menu run as `flex-wrap: wrap`, so a narrow viewport wraps the strip //! instead of folding the last tabs behind a More button. `core/tabs.ts`'s //! `tabOverflow` still serves the four Askama strips and does not see this one: //! it looks for `.tabs`, and the renderer writes `.selector[data-selector=tab]`. //! //! # Two things the markup said and the description cannot //! //! `aria-label="Library sections"` on the strip, and `title="Updates from //! creators you follow"` on the Feed tab. A region's own accessible name and a //! control's tooltip are both absent from the vocabulary: [`Slot::label`] is a //! child's tab name, which is a different thing, and nothing carries a hint. //! //! Dropped rather than worked around, and filed, because a `Node::Text` smuggled //! in to stand for a label is how a vocabulary stops being one. The strip sits //! under the page's `
your purchases
", has_mt, can_create) } #[test] fn the_page_makes_no_more_requests_than_it_did_before() { // The whole safety argument for this conversion. `library.html` rendered // the shown panel inline and fetched the rest on a press; if the // described strip fetched on load instead, the page would go from zero // panel requests to four, each its own set of queries. let html = strip(true, true); assert!(!html.contains("hx-trigger=\"load\""), "{html}"); assert!(html.contains("your purchases
"), "{html}"); assert_eq!(html.matches("hx-get=").count(), 4, "{html}"); } #[test] fn every_tab_but_the_shown_one_says_where_its_answer_lands() { // Three of these routes are Askama and name no region, so without this // the answer swaps into the button that was pressed -- which is what // `DELETE /api/users/me/ssh-keys` did before the described screens // served their own writes. let html = strip(true, true); // The Askama tabs. `library-communities` and `library-contacts` are // deliberately absent: both are described screens that name their own // region, so the strip must NOT retarget them. Until `64b33b26` they // were here, because the switch was off in tests and every tab was // Askama. for panel in ["library-feed", "library-collections"] { assert!(html.contains(&format!("hx-target=\"#{panel}\"")), "{html}"); assert!(html.contains(&format!("id=\"{panel}\"")), "{html}"); } // Every tab still gets its frame, described or not. for panel in ["library-communities", "library-contacts"] { assert!(html.contains(&format!("id=\"{panel}\"")), "{html}"); assert!( !html.contains(&format!("hx-target=\"#{panel}\"")), "{panel} is described and names its own region:\n{html}" ); } // The shown panel is not fetched, so it has nowhere to aim and says so // by carrying no transport at all. assert!(!html.contains("hx-target=\"#library-purchases\""), "{html}"); } #[test] fn a_described_panel_is_left_to_name_its_own_region() { // Decision 7. A described route answers with a fragment naming what it // changed, so a target written here would override an answer that // already knew better. Both described panels, and it follows the panel // rather than a switch since `64b33b26` deleted the switch -- // `library-communities` used to be asserted the other way here, because // it was described but not switched on in this test. let html = html("your purchases
", true, true); for panel in ["library-contacts", "library-communities"] { assert!( !html.contains(&format!("hx-target=\"#{panel}\"")), "a described panel retargets its own answer:\n{html}" ); } // And the ones still served by Askama are untouched by that. assert!(html.contains("hx-target=\"#library-feed\""), "{html}"); } #[test] fn the_two_gated_tabs_leave_when_their_test_fails() { // Membership was conditional before it was described and stays so. The // recon that planned this shape recorded the library strip as five // unconditional buttons, which the template contradicts twice. let both = strip(true, true); assert!(both.contains(">Communities"), "{both}"); assert!(both.contains(">Contacts"), "{both}"); let neither = strip(false, false); assert!(!neither.contains(">Communities"), "{neither}"); assert!(!neither.contains(">Contacts"), "{neither}"); // And the strip is still a strip, with the shown panel where it was. assert!(neither.contains("role=\"tablist\""), "{neither}"); assert!(neither.contains("your purchases
"), "{neither}"); } #[test] fn the_strip_says_what_it_does_when_it_runs_out_of_room() { // `Run` has no `Default`, so this is not something a strip can forget; // what it can do is pick the wrong one. `Menu` is the page's own // behaviour today -- the overflow goes behind a More control. assert!(strip(true, true).contains("run-menu")); } }