fix(runtime): support native user interactions
req: examples/005 req: form/002 req: convention/004 req: dx/008
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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()"));
|
||||
|
||||
Reference in New Issue
Block a user