max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01DwpiantpUgohzML4xr6KeQ
7 files changed,
+17 insertions,
-213 deletions
| @@ -887,7 +887,7 @@ | |||
| 887 | 887 | // hears it the same way it hears a typed one. | |
| 888 | 888 | pass.stirred.insert(name.to_owned()); | |
| 889 | 889 | if let Some(action) = &described.writes { | |
| 890 | - | let mut payload = gathered(pass, described.over.as_deref()); | |
| 890 | + | let mut payload = Params::new(); | |
| 891 | 891 | payload.insert(name.to_owned(), if on { "on" } else { "" }.to_owned()); | |
| 892 | 892 | pass.fire(action, payload, None); | |
| 893 | 893 | } | |
| @@ -916,7 +916,7 @@ | |||
| 916 | 916 | let Some(action) = &described.writes else { | |
| 917 | 917 | return; | |
| 918 | 918 | }; | |
| 919 | - | let mut payload = gathered(pass, described.over.as_deref()); | |
| 919 | + | let mut payload = Params::new(); | |
| 920 | 920 | payload.insert(lower_name.to_owned(), lower_value.to_owned()); | |
| 921 | 921 | payload.insert(upper_name.to_owned(), upper_value.to_owned()); | |
| 922 | 922 | pass.fire(action, payload, None); | |
| @@ -1102,11 +1102,7 @@ | |||
| 1102 | 1102 | { | |
| 1103 | 1103 | pass.view.wrote(&name, &buffer); | |
| 1104 | 1104 | if let Some(action) = &described.writes { | |
| 1105 | - | // The ticks travel with it when the picker writes over the screen's | |
| 1106 | - | // selection: a bulk bar's "Set priority" is a field over the set | |
| 1107 | - | // where its "Delete" is an act over the same set, and a handler | |
| 1108 | - | // reads the two the same way. | |
| 1109 | - | let mut payload = gathered(pass, described.over.as_deref()); | |
| 1105 | + | let mut payload = Params::new(); | |
| 1110 | 1106 | payload.insert(name.clone(), buffer.clone()); | |
| 1111 | 1107 | // Both ends, because an interval is one answer. A handler reading | |
| 1112 | 1108 | // only the end that moved would narrow the filter to a single | |
| @@ -1136,9 +1132,8 @@ | |||
| 1136 | 1132 | if now >= due { | |
| 1137 | 1133 | pass.view.consulted(Asking::Field(name.clone()), at); | |
| 1138 | 1134 | // This box's value, plus whatever else the question said it | |
| 1139 | - | // carries. No ticks: a consult asks about this box, and | |
| 1140 | - | // `Field::over` belongs to `changes` and says which set a picker | |
| 1141 | - | // writes over. | |
| 1135 | + | // carries. No ticks: a consult asks about this box, never about a | |
| 1136 | + | // set of rows. | |
| 1142 | 1137 | let mut payload = pass.view.contributed(&consult.sends); | |
| 1143 | 1138 | payload.insert(name.clone(), buffer.clone()); | |
| 1144 | 1139 | pass.fire(&consult.action, payload, None); |
| @@ -2141,8 +2141,6 @@ | |||
| 2141 | 2141 | /// wiki `explicit-commit-affordance`: the reader ticks rows, chooses a | |
| 2142 | 2142 | /// value, and the write lands on every row ticked. That is a large enough | |
| 2143 | 2143 | /// act to deserve a control that says so before it happens. | |
| 2144 | - | /// [`over`](Self::over) states that a write covers the selection; it does | |
| 2145 | - | /// not make a silent one acceptable. | |
| 2146 | 2144 | /// | |
| 2147 | 2145 | /// **Anything that does not write.** A filter, a sort, a re-ask: a control | |
| 2148 | 2146 | /// whose action fetches a different view of data it leaves alone. Saying it | |
| @@ -2189,46 +2187,6 @@ | |||
| 2189 | 2187 | /// [`after`](Consult::after): a wait the description states rather than a | |
| 2190 | 2188 | /// number each renderer picks. Writes complete; questions debounce. | |
| 2191 | 2189 | pub writes: Option<Action>, | |
| 2192 | - | /// The [`Screen::selection`] this writes over, if it writes over one. | |
| 2193 | - | /// | |
| 2194 | - | /// # Ruled against, `b1e49553`, 2026-08-28. Nothing in the tree carries it. | |
| 2195 | - | /// | |
| 2196 | - | /// It was added because a bulk bar is not only buttons. Measured on | |
| 2197 | - | /// goingson's task list, 2026-08-15: of the five controls over its | |
| 2198 | - | /// selection, "Complete" and "Delete" are acts and three apply a value to | |
| 2199 | - | /// the set, and said with acts alone a project picker over 40 projects is | |
| 2200 | - | /// 40 buttons in a bar. That measurement still holds. What was wrong was | |
| 2201 | - | /// the conclusion drawn from it. | |
| 2202 | - | /// | |
| 2203 | - | /// A picker over the selection is an [`Act`] that | |
| 2204 | - | /// [`asks`](Act::asks), not a [`Field`] that writes. The value rides with | |
| 2205 | - | /// the press, so 40 projects are still one control, and the press is the | |
| 2206 | - | /// commit the reader needs. wiki `explicit-commit-affordance` requires | |
| 2207 | - | /// that commit: a tick stages and never writes (`5f2b8753`), so a bar whose | |
| 2208 | - | /// ticks stage and whose pickers write on change is half staged and half | |
| 2209 | - | /// live. A write landing on N rows that are mostly off screen is the case | |
| 2210 | - | /// where a missing indication costs most. | |
| 2211 | - | /// | |
| 2212 | - | /// The two bulk bars written after that measurement had already reached | |
| 2213 | - | /// this on their own: goingson's `emails.rs` says Snooze with | |
| 2214 | - | /// `Act::asking`, and MNW's content table says Set Price and Add Tag the | |
| 2215 | - | /// same way. goingson's task list was the last consumer and was converted | |
| 2216 | - | /// with this ruling. | |
| 2217 | - | /// | |
| 2218 | - | /// # What it still means, for as long as it is here | |
| 2219 | - | /// | |
| 2220 | - | /// Sends what a [`writes`](Self::writes) write sends, the value under this | |
| 2221 | - | /// field's [`name`](Self::name), plus every ticked row's value under | |
| 2222 | - | /// [`Node::TICKED`], exactly as [`Act::over`] does. | |
| 2223 | - | /// | |
| 2224 | - | /// Only meaningful beside [`writes`](Self::writes): a field with no route | |
| 2225 | - | /// of its own submits with its form, and a form gathering the screen's | |
| 2226 | - | /// ticks is a different thing that nothing has asked for. | |
| 2227 | - | /// | |
| 2228 | - | /// Kept rather than removed here because removing a vocabulary member is | |
| 2229 | - | /// its own decision with its own renderer and test cost, and this task's | |
| 2230 | - | /// business was the bar. It has no consumer as of the conversion above. | |
| 2231 | - | pub over: Option<String>, | |
| 2232 | 2190 | /// What this asks while the user is still typing, and how long it waits. | |
| 2233 | 2191 | /// | |
| 2234 | 2192 | /// See [`Consult`]. Distinct from [`writes`](Self::writes) in the two | |
| @@ -2591,7 +2549,6 @@ | |||
| 2591 | 2549 | value: None, | |
| 2592 | 2550 | upper_value: None, | |
| 2593 | 2551 | writes: None, | |
| 2594 | - | over: None, | |
| 2595 | 2552 | consults: Vec::new(), | |
| 2596 | 2553 | suggests: None, | |
| 2597 | 2554 | repeats: None, | |
| @@ -2820,28 +2777,16 @@ | |||
| 2820 | 2777 | /// What every renderer draws for a member of [`Act::asks`]. A field there | |
| 2821 | 2778 | /// is answered by the press that asked for it, so a | |
| 2822 | 2779 | /// [`writes`](Self::writes) route on it would fire a second write for the | |
| 2823 | - | /// same value, and [`over`](Self::over) would send the ticks twice. | |
| 2824 | - | /// [`consults`](Self::consults) survives: asking whether a tag slug is | |
| 2825 | - | /// taken is a question about the value, not a write of it. | |
| 2780 | + | /// same value. [`consults`](Self::consults) survives: asking whether a tag | |
| 2781 | + | /// slug is taken is a question about the value, not a write of it. | |
| 2826 | 2782 | #[must_use] | |
| 2827 | 2783 | pub fn as_asked(&self) -> Self { | |
| 2828 | 2784 | Self { | |
| 2829 | 2785 | writes: None, | |
| 2830 | - | over: None, | |
| 2831 | 2786 | ..self.clone() | |
| 2832 | 2787 | } | |
| 2833 | 2788 | } | |
| 2834 | 2789 | ||
| 2835 | - | /// Changing this writes over the screen's selection, chaining. | |
| 2836 | - | /// | |
| 2837 | - | /// Says nothing on its own: pair it with [`writes`](Self::writes), which | |
| 2838 | - | /// is the route the value and the ticks are sent to. | |
| 2839 | - | #[must_use] | |
| 2840 | - | pub fn over(mut self, selection: impl Into<String>) -> Self { | |
| 2841 | - | self.over = Some(selection.into()); | |
| 2842 | - | self | |
| 2843 | - | } | |
| 2844 | - | ||
| 2845 | 2790 | /// A select offering the given options. | |
| 2846 | 2791 | pub fn select(name: impl Into<String>, label: impl Into<String>, options: Vec<Choice>) -> Self { | |
| 2847 | 2792 | Self { | |
| @@ -4758,9 +4703,9 @@ | |||
| 4758 | 4703 | /// screen already knows, and a fragment is markup a terminal or an egui | |
| 4759 | 4704 | /// host has nowhere to put. | |
| 4760 | 4705 | /// | |
| 4761 | - | /// A field here carries no [`Field::writes`] and no [`Field::over`]. It is | |
| 4762 | - | /// answered by the control that asked for it, so a renderer that honoured a | |
| 4763 | - | /// write on it as well would fire twice for one value. | |
| 4706 | + | /// A field here carries no [`Field::writes`]. It is answered by the control | |
| 4707 | + | /// that asked for it, so a renderer that honoured a write on it as well | |
| 4708 | + | /// would fire twice for one value. | |
| 4764 | 4709 | /// | |
| 4765 | 4710 | /// # The disclosure is the renderer's | |
| 4766 | 4711 | /// | |
| @@ -8107,15 +8052,13 @@ | |||
| 8107 | 8052 | #[test] | |
| 8108 | 8053 | fn a_box_a_verb_asked_for_writes_nothing_of_its_own() { | |
| 8109 | 8054 | // The value is answered by the press. A write of the box's own would | |
| 8110 | - | // send it twice, and `over` on it would send the ticks twice. | |
| 8055 | + | // send it twice. | |
| 8111 | 8056 | let asked = Field::new(layout::FieldKind::Text, "tag", "Tag slug") | |
| 8112 | 8057 | .writes(Action::post("/items/tag")) | |
| 8113 | - | .over("chosen") | |
| 8114 | 8058 | .consulting(Consult::new(Action::get("/tags/known"))) | |
| 8115 | 8059 | .as_asked(); | |
| 8116 | 8060 | ||
| 8117 | 8061 | assert!(asked.writes.is_none()); | |
| 8118 | - | assert!(asked.over.is_none()); | |
| 8119 | 8062 | // A question about the value survives: asking whether a slug is taken | |
| 8120 | 8063 | // is not a write of it. | |
| 8121 | 8064 | assert_eq!(asked.consults.len(), 1); |
| @@ -237,12 +237,6 @@ | |||
| 237 | 237 | /// to a region: the runtime holds the candidates on the view, the drawing | |
| 238 | 238 | /// puts them under the box, and picking one writes the value. | |
| 239 | 239 | pub suggests: Option<Consult>, | |
| 240 | - | /// The screen's selection it writes over, if it writes over one. | |
| 241 | - | /// | |
| 242 | - | /// [`Spot::Act`]'s `over`, on the other kind of control. A bulk bar holds | |
| 243 | - | /// both: "Delete" is an act over the set and "Set priority" is a picker | |
| 244 | - | /// over it. | |
| 245 | - | pub over: Option<String>, | |
| 246 | 240 | } | |
| 247 | 241 | ||
| 248 | 242 | impl Spot { | |
| @@ -850,7 +844,6 @@ | |||
| 850 | 844 | writes: field.writes.clone(), | |
| 851 | 845 | consults: field.consults.clone(), | |
| 852 | 846 | suggests: field.suggests.clone(), | |
| 853 | - | over: field.over.clone(), | |
| 854 | 847 | })), | |
| 855 | 848 | }); | |
| 856 | 849 | } |
| @@ -1041,7 +1041,7 @@ | |||
| 1041 | 1041 | // asks *while it is being typed into*, which is why | |
| 1042 | 1042 | // `after_typing` is not called here. | |
| 1043 | 1043 | if let Some(action) = field.writes.clone() { | |
| 1044 | - | let mut payload = self.gathering(field.over.as_deref()); | |
| 1044 | + | let mut payload = Params::new(); | |
| 1045 | 1045 | payload.insert(name, value); | |
| 1046 | 1046 | return self.send(&action, payload); | |
| 1047 | 1047 | } | |
| @@ -1848,7 +1848,6 @@ | |||
| 1848 | 1848 | } | |
| 1849 | 1849 | let action = field.writes.clone()?; | |
| 1850 | 1850 | let name = field.name.clone(); | |
| 1851 | - | let over = field.over.clone(); | |
| 1852 | 1851 | // Seeded from the description when the box is untouched, which is what | |
| 1853 | 1852 | // is showing in it. `unwrap_or_default` here read an untouched box as | |
| 1854 | 1853 | // empty, so walking through a form wrote every prefilled field it | |
| @@ -1859,7 +1858,7 @@ | |||
| 1859 | 1858 | return None; | |
| 1860 | 1859 | } | |
| 1861 | 1860 | self.view.wrote(&name, &value); | |
| 1862 | - | let mut payload = self.gathering(over.as_deref()); | |
| 1861 | + | let mut payload = Params::new(); | |
| 1863 | 1862 | payload.insert(name, value); | |
| 1864 | 1863 | Some(self.send(&action, payload)) | |
| 1865 | 1864 | } | |
| @@ -1869,7 +1868,6 @@ | |||
| 1869 | 1868 | return Step::Idle; | |
| 1870 | 1869 | }; | |
| 1871 | 1870 | let name = field.name.clone(); | |
| 1872 | - | let over = field.over.clone(); | |
| 1873 | 1871 | ||
| 1874 | 1872 | // `8032fe61`. A keystroke is not a write. `Field::writes` means the | |
| 1875 | 1873 | // change is complete, which is what `quasi-webview` has always emitted | |
| @@ -1888,18 +1886,13 @@ | |||
| 1888 | 1886 | || field.kind.offers_themes()); | |
| 1889 | 1887 | if let Some(action) = field.writes.clone().filter(|_| !built_up) { | |
| 1890 | 1888 | let value = self.view.edit(&name).unwrap_or_default().to_string(); | |
| 1891 | - | // The ticks travel with it when the picker writes over the | |
| 1892 | - | // screen's selection, the same set an act over one sends. A | |
| 1893 | - | // bulk bar's "Set priority" is this and its "Delete" is the | |
| 1894 | - | // act, and a handler reads both the same way. | |
| 1895 | - | let mut payload = self.gathering(over.as_deref()); | |
| 1889 | + | let mut payload = Params::new(); | |
| 1896 | 1890 | payload.insert(name, value); | |
| 1897 | 1891 | return self.send(&action, payload); | |
| 1898 | 1892 | } | |
| 1899 | 1893 | ||
| 1900 | 1894 | // Asking about the value carries the value and nothing else. No ticks: | |
| 1901 | - | // a consult is a question about this box, not a control over a set, | |
| 1902 | - | // and `Field::over` is documented as meaningless without `changes`. | |
| 1895 | + | // a consult is a question about this box, not a control over a set. | |
| 1903 | 1896 | // The question the field owns is asked alongside the ones it merely | |
| 1904 | 1897 | // asks, and it is asked the same way: the answer differs, not the | |
| 1905 | 1898 | // asking. Dropping under its floor closes the list rather than leaving |
| @@ -1238,27 +1238,6 @@ | |||
| 1238 | 1238 | assert!(matches!(runtime.key(Key::Backspace), Step::Idle)); | |
| 1239 | 1239 | } | |
| 1240 | 1240 | ||
| 1241 | - | #[test] | |
| 1242 | - | fn a_consult_asks_about_the_box_and_not_about_the_selection() { | |
| 1243 | - | // `Field::over` belongs to `changes`: it says which set a picker writes | |
| 1244 | - | // over. A question about what is in this box gathers no ticks, or a | |
| 1245 | - | // validate route would receive a bag of unrelated row values. | |
| 1246 | - | let mut runtime = Runtime::new(screen_of([ | |
| 1247 | - | Node::list([Row::new("One").selectable(true)]), | |
| 1248 | - | Node::field( | |
| 1249 | - | Field::new(layout::FieldKind::Text, "slug", "Slug") | |
| 1250 | - | .consults(Action::get("/api/validate/slug")) | |
| 1251 | - | .over("rows"), | |
| 1252 | - | ), | |
| 1253 | - | ])); | |
| 1254 | - | // The caret starts on the row; one Tab is past it and into the box. | |
| 1255 | - | runtime.key(Key::Tab); | |
| 1256 | - | let Step::CallAfter { asks } = runtime.key(Key::Char('a')) else { | |
| 1257 | - | panic!("the caret is in the field"); | |
| 1258 | - | }; | |
| 1259 | - | assert_eq!(asks[0].request.payload.get(Node::TICKED), None); | |
| 1260 | - | } | |
| 1261 | - | ||
| 1262 | 1241 | #[test] | |
| 1263 | 1242 | fn one_keystroke_can_pose_two_questions_at_two_waits() { | |
| 1264 | 1243 | // `N8`. MNW's discover search asks a suggestion route and a results route | |
| @@ -2045,48 +2024,6 @@ | |||
| 2045 | 2024 | assert_eq!(request.path, "/tasks/archive"); | |
| 2046 | 2025 | } | |
| 2047 | 2026 | ||
| 2048 | - | #[test] | |
| 2049 | - | fn a_picker_over_the_selection_sends_the_ticks_with_its_value() { | |
| 2050 | - | // The half `Act::over` could not cover: three of the five controls on | |
| 2051 | - | // goingson's bulk bar apply a value to the set rather than acting on it, | |
| 2052 | - | // and said with acts alone a project picker over forty projects is forty | |
| 2053 | - | // buttons. | |
| 2054 | - | let mut runtime = Runtime::new( | |
| 2055 | - | Screen::sidebar_content("Tasks").selecting("chosen").with( | |
| 2056 | - | Slot::new("main", RegionKind::Pane) | |
| 2057 | - | .with(Node::Table { | |
| 2058 | - | columns: vec![quasi_router::Column::new("title")], | |
| 2059 | - | rows: vec![Cells::new(["First"]).ticking("t-1", false)], | |
| 2060 | - | more: None, | |
| 2061 | - | }) | |
| 2062 | - | .with(Node::field( | |
| 2063 | - | Field::new(layout::FieldKind::Text, "priority", "Priority") | |
| 2064 | - | .writes(Action::post("/tasks/list/priority")) | |
| 2065 | - | .over("chosen"), | |
| 2066 | - | )), | |
| 2067 | - | ), | |
| 2068 | - | ); | |
| 2069 | - | ||
| 2070 | - | assert!(matches!(runtime.key(Key::Char(' ')), Step::Idle)); | |
| 2071 | - | runtime.key(Key::Tab); | |
| 2072 | - | runtime.key(Key::Char('H')); | |
| 2073 | - | // On leaving rather than on the keystroke since `8032fe61`. What this test | |
| 2074 | - | // is about is unchanged: the ticks travel with the picker's value. | |
| 2075 | - | let Step::Call(request) = runtime.key(Key::Tab) else { | |
| 2076 | - | panic!("a field that writes as it changes calls when the value is complete"); | |
| 2077 | - | }; | |
| 2078 | - | ||
| 2079 | - | assert_eq!(request.path, "/tasks/list/priority"); | |
| 2080 | - | assert_eq!(request.payload.get("priority"), Some("H")); | |
| 2081 | - | assert_eq!( | |
| 2082 | - | request | |
| 2083 | - | .payload | |
| 2084 | - | .get_all(quasi_router::Node::TICKED) | |
| 2085 | - | .collect::<Vec<_>>(), | |
| 2086 | - | ["t-1"] | |
| 2087 | - | ); | |
| 2088 | - | } | |
| 2089 | - | ||
| 2090 | 2027 | #[test] | |
| 2091 | 2028 | fn a_control_that_asks_for_a_value_draws_the_box_and_sends_what_is_typed() { | |
| 2092 | 2029 | // `033ff3ca`. MNW's bulk bar reveals a box behind "Set Price"; a terminal |
| @@ -315,34 +315,10 @@ | |||
| 315 | 315 | } | |
| 316 | 316 | ||
| 317 | 317 | if let Some(action) = writes { | |
| 318 | - | // A field writing over the screen's selection gathers every tick, the | |
| 319 | - | // same way a commit act does and through the same selector. `Act::over` | |
| 320 | - | // was the whole story until a bulk bar turned out to hold pickers as | |
| 321 | - | // well as buttons: "set the project on these forty" is a control with | |
| 322 | - | // options, not forty buttons. | |
| 323 | - | let gathers = field | |
| 324 | - | .over | |
| 325 | - | .as_ref() | |
| 326 | - | .map(|_| format!(".{}", class("row-select", opts))); | |
| 327 | 318 | out.push_str("<div"); | |
| 328 | 319 | class_attr(&["field-writes"], opts, out); | |
| 329 | 320 | width_attr(field.width, out); | |
| 330 | - | // The same hook an act over a selection carries. The script disables | |
| 331 | - | // the control inside this wrapper rather than the wrapper, which is | |
| 332 | - | // what a browser can actually disable. | |
| 333 | - | if let Some(over) = &field.over { | |
| 334 | - | out.push_str(" data-over=\""); | |
| 335 | - | escape_into(over, out); | |
| 336 | - | out.push('"'); | |
| 337 | - | } | |
| 338 | - | action_attrs( | |
| 339 | - | action, | |
| 340 | - | Fires::ChangeInside, | |
| 341 | - | None, | |
| 342 | - | gathers.as_deref(), | |
| 343 | - | false, | |
| 344 | - | out, | |
| 345 | - | ); | |
| 321 | + | action_attrs(action, Fires::ChangeInside, None, None, false, out); | |
| 346 | 322 | out.push('>'); | |
| 347 | 323 | } | |
| 348 | 324 |
| @@ -3408,15 +3408,6 @@ | |||
| 3408 | 3408 | )); | |
| 3409 | 3409 | assert!(act.contains("data-over=\"chosen\""), "{act}"); | |
| 3410 | 3410 | ||
| 3411 | - | // A picker over the set carries it too. The script disables the control | |
| 3412 | - | // inside the wrapper, which is what a browser can disable. | |
| 3413 | - | let picker = fragment(&Node::field( | |
| 3414 | - | Field::select("priority", "Priority", vec![Choice::plain("High")]) | |
| 3415 | - | .writes(Action::post("/tasks/list/priority")) | |
| 3416 | - | .over("chosen"), | |
| 3417 | - | )); | |
| 3418 | - | assert!(picker.contains("data-over=\"chosen\""), "{picker}"); | |
| 3419 | - | ||
| 3420 | 3411 | // And an ordinary control does not, so the script never touches it. | |
| 3421 | 3412 | let plain = fragment(&Node::Act(Act::new("Save", Action::post("/save")))); | |
| 3422 | 3413 | assert!(!plain.contains("data-over"), "{plain}"); | |
| @@ -3456,8 +3447,7 @@ | |||
| 3456 | 3447 | let html = fragment(&Node::Act( | |
| 3457 | 3448 | Act::new("Add Tag", Action::post("/items/tag")).asking( | |
| 3458 | 3449 | Field::new(layout::FieldKind::Text, "tag", "Tag slug") | |
| 3459 | - | .writes(Action::post("/items/tag/live")) | |
| 3460 | - | .over("chosen"), | |
| 3450 | + | .writes(Action::post("/items/tag/live")), | |
| 3461 | 3451 | ), | |
| 3462 | 3452 | )); | |
| 3463 | 3453 | ||
| @@ -3601,29 +3591,6 @@ | |||
| 3601 | 3591 | assert!(!plain.contains("row-select"), "{plain}"); | |
| 3602 | 3592 | } | |
| 3603 | 3593 | ||
| 3604 | - | #[test] | |
| 3605 | - | fn a_picker_can_write_over_the_selection_and_still_find_its_own_value() { | |
| 3606 | - | // A bulk bar is not only buttons: "set the project on these forty" is a | |
| 3607 | - | // control with options. Both selectors land in ONE `hx-include`, because | |
| 3608 | - | // two of them on an element is markup a parser drops half of -- which is | |
| 3609 | - | // what a field over a selection would have emitted before `Fires::include`. | |
| 3610 | - | let html = fragment(&Node::field( | |
| 3611 | - | Field::select( | |
| 3612 | - | "priority", | |
| 3613 | - | "Priority", | |
| 3614 | - | vec![Choice::plain("High"), Choice::plain("Low")], | |
| 3615 | - | ) | |
| 3616 | - | .writes(Action::post("/tasks/list/priority")) | |
| 3617 | - | .over("chosen"), | |
| 3618 | - | )); | |
| 3619 | - | ||
| 3620 | - | assert_eq!(html.matches("hx-include").count(), 1, "{html}"); | |
| 3621 | - | assert!( | |
| 3622 | - | html.contains("hx-include=\"find input, find select, find textarea, .row-select\""), | |
| 3623 | - | "{html}" | |
| 3624 | - | ); | |
| 3625 | - | } | |
| 3626 | - | ||
| 3627 | 3594 | #[test] | |
| 3628 | 3595 | fn a_field_that_writes_without_a_selection_gathers_only_itself() { | |
| 3629 | 3596 | let html = fragment(&Node::field( |