fix(runtime): support native user interactions

req: examples/005

req: form/002

req: convention/004

req: dx/008
This commit is contained in:
slhx agent
2026-05-11 08:26:13 +02:00
parent 2db7ed7a3c
commit 5c1dc853d8
5 changed files with 39 additions and 45 deletions
@@ -16,7 +16,8 @@
<section class="workspace">
<aside class="command-card">
<h2>Create an issue</h2>
<form data-slhx-handle="launch_work" data-slhx-form="launch_work" data-slhx-disable-while-pending>
<form id="launch-work" data-slhx-handle="launch_work" data-slhx-form="launch_work" data-slhx-disable-while-pending>
<input type="hidden" name="__h" value="539242093">
<label>Title <input name="title" required="required" value="Ship typed effects"></label>
<label>Lane
<select name="lane" required="required">
+6 -37
View File
@@ -59,59 +59,28 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
.await?;
wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice.id().id), "Push simulated").await?;
driver
.execute(
r#"
document.querySelector("input[name='title']").value = "Browser verified issue";
document.querySelector("input[name='impact']").value = "8";
document.querySelector("select[name='lane']").value = "product";
"#,
Vec::new(),
)
.await?;
driver
.execute(
&format!(
r#"
return fetch("/", {{
method: "POST",
headers: {{ "Content-Type": "application/x-www-form-urlencoded" }},
body: "__h={}&title=Browser+verified+issue&lane=product&impact=8"
}})
.then((response) => response.arrayBuffer())
.then((buffer) => {{ window.slhx.applyBatch(buffer, document.querySelector("[data-slhx-root]")); return true; }});
"#,
ui::control_center::handles::launch_work.id().id
),
Vec::new(),
)
.await?;
wait_for_text(&driver, "body", "Browser verified issue").await?;
assert_text(&driver, "width:88%").await?;
drag_card_to_lane(&driver, 4, "runtime").await?;
wait_for_text(&driver, ".lane[data-lane='runtime'] .work-card[data-key='4']", "Active").await?;
drag_card_to_lane(&driver, 2, "product").await?;
wait_for_text(&driver, ".lane[data-lane='product'] .work-card[data-key='2']", "Shipped").await?;
wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice.id().id), "Drag-and-drop move persisted").await?;
driver
.find(By::Css(&card_button_selector(ui::control_center::handles::spotlight_work.id().id, 4)))
.find(By::Css(&card_button_selector(ui::control_center::handles::spotlight_work.id().id, 2)))
.await?
.click()
.await?;
wait_for_text(
&driver,
&slot_selector(ui::control_center::slots::inspector.id().id),
"Browser verified issue · lane=Runtime · stage=Active · impact=8",
"Stream typed presence · lane=Product · stage=Shipped · impact=7",
)
.await?;
driver
.find(By::Css(&card_button_selector(ui::control_center::handles::advance_work.id().id, 4)))
.find(By::Css(&card_button_selector(ui::control_center::handles::advance_work.id().id, 3)))
.await?
.click()
.await?;
wait_for_text(&driver, ".lane[data-lane='product'] .work-card[data-key='4']", "Shipped").await?;
wait_for_text(&driver, ".lane[data-lane='product'] .work-card[data-key='3']", "Active").await?;
driver
.find(By::Css(&handle_selector(ui::control_center::handles::simulate_push.id().id)))
+7 -2
View File
@@ -346,11 +346,16 @@ fn __slhx_lower_html(html: &str, table: &[(&str, &str, u32)]) -> ::std::string::
}
fn __slhx_replace_attr(mut html: ::std::string::String, attr: &str, name: &str, runtime_attr: &str, id: u32) -> ::std::string::String {
let replacement = ::std::format!("{runtime_attr}=\"{id}\"");
let replacement = if runtime_attr == "value" {
::std::format!("{attr}=\"{name}\" {runtime_attr}=\"{id}\"")
} else {
::std::format!("{runtime_attr}=\"{id}\"")
};
let double = ::std::format!("{attr}=\"{name}\"");
html = html.replace(&double, &replacement);
let single_replacement = replacement.replace('"', "'");
let single = ::std::format!("{attr}='{name}'");
html.replace(&single, &replacement)
html.replace(&single, &single_replacement)
}
fn __slhx_lower_static_attr(mut html: ::std::string::String, attr: &str, runtime_attr: &str) -> ::std::string::String {
+21 -5
View File
@@ -20,6 +20,21 @@
return node && node.closest ? node.closest(`[${ROOT}]`) : null;
}
function formOwner(el) {
if (!el || el.tagName === "FORM") return el;
if (el.getAttribute && el.getAttribute("form")) return document.getElementById(el.getAttribute("form"));
const closest = el.closest ? el.closest("form") : null;
if (closest) return closest;
const root = rootOf(el);
return root && root.querySelector("form[data-slhx-form]");
}
function formHandleId(form) {
if (!form) return null;
const raw = form.getAttribute(HID) || (form.querySelector(`[${HID}]`) && form.querySelector(`[${HID}]`).getAttribute(HID));
return raw && /^\d+$/.test(raw) ? raw : null;
}
function closestInRoot(start, root, selector) {
for (let node = start; node && node !== root.parentNode; node = node.parentNode) {
if (node.matches && node.matches(selector)) return node;
@@ -53,9 +68,9 @@
}
function formDataFor(el, eventName) {
const form = el.tagName === "FORM" ? el : el.closest("form");
const form = formOwner(el);
const data = form ? new FormData(form) : new FormData();
const id = handleId(el) || (form && handleId(form));
const id = handleId(el) || formHandleId(form);
if (id && !data.has("__h")) data.set("__h", id);
const dragKey = eventName === "drop" && dragKeys.get(rootOf(el));
if (dragKey && !data.has("work_id")) data.set("work_id", dragKey);
@@ -420,15 +435,16 @@
}
if (name === "click") {
const submitter = closestInRoot(event.target, root, "button[type=submit], input[type=submit], button:not([type])");
const form = submitter && submitter.closest("form");
if (form && root.contains(form) && handleId(form)) {
const form = formOwner(submitter);
if (form && root.contains(form) && formHandleId(form)) {
if (form.reportValidity && !form.reportValidity()) return;
event.preventDefault();
schedule(form, "submit");
return;
}
}
const el = closestInRoot(event.target, root, `[${HID}], [data-slhx-boost] form`);
let el = closestInRoot(event.target, root, `[${HID}], [data-slhx-boost] form`);
if (name === "submit" && !el && event.target && event.target.tagName === "FORM" && formHandleId(event.target)) el = event.target;
if (!el || defaultEvent(el) !== name) return;
event.preventDefault();
schedule(el, name);
+3
View File
@@ -22,6 +22,9 @@ fn runtime_clicking_submitter_schedules_form_submit() {
// req: convention/004
let source = slhx_js::RUNTIME_JS;
assert!(source.contains("function formOwner(el)"));
assert!(source.contains("function formHandleId(form)"));
assert!(source.contains("document.getElementById(el.getAttribute(\"form\"))"));
assert!(source.contains("const direct = closestInRoot(event.target, root, `[${HID}]`)"));
assert!(source.contains("button[type=submit], input[type=submit], button:not([type])"));
assert!(source.contains("form.reportValidity && !form.reportValidity()"));