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">
|
<section class="workspace">
|
||||||
<aside class="command-card">
|
<aside class="command-card">
|
||||||
<h2>Create an issue</h2>
|
<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>Title <input name="title" required="required" value="Ship typed effects"></label>
|
||||||
<label>Lane
|
<label>Lane
|
||||||
<select name="lane" required="required">
|
<select name="lane" required="required">
|
||||||
|
|||||||
@@ -59,59 +59,28 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice.id().id), "Push simulated").await?;
|
wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice.id().id), "Push simulated").await?;
|
||||||
|
|
||||||
driver
|
drag_card_to_lane(&driver, 2, "product").await?;
|
||||||
.execute(
|
wait_for_text(&driver, ".lane[data-lane='product'] .work-card[data-key='2']", "Shipped").await?;
|
||||||
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?;
|
|
||||||
wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice.id().id), "Drag-and-drop move persisted").await?;
|
wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice.id().id), "Drag-and-drop move persisted").await?;
|
||||||
|
|
||||||
driver
|
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?
|
.await?
|
||||||
.click()
|
.click()
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_text(
|
wait_for_text(
|
||||||
&driver,
|
&driver,
|
||||||
&slot_selector(ui::control_center::slots::inspector.id().id),
|
&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?;
|
.await?;
|
||||||
|
|
||||||
driver
|
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?
|
.await?
|
||||||
.click()
|
.click()
|
||||||
.await?;
|
.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
|
driver
|
||||||
.find(By::Css(&handle_selector(ui::control_center::handles::simulate_push.id().id)))
|
.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 {
|
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}\"");
|
let double = ::std::format!("{attr}=\"{name}\"");
|
||||||
html = html.replace(&double, &replacement);
|
html = html.replace(&double, &replacement);
|
||||||
|
let single_replacement = replacement.replace('"', "'");
|
||||||
let single = ::std::format!("{attr}='{name}'");
|
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 {
|
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;
|
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) {
|
function closestInRoot(start, root, selector) {
|
||||||
for (let node = start; node && node !== root.parentNode; node = node.parentNode) {
|
for (let node = start; node && node !== root.parentNode; node = node.parentNode) {
|
||||||
if (node.matches && node.matches(selector)) return node;
|
if (node.matches && node.matches(selector)) return node;
|
||||||
@@ -53,9 +68,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formDataFor(el, eventName) {
|
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 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);
|
if (id && !data.has("__h")) data.set("__h", id);
|
||||||
const dragKey = eventName === "drop" && dragKeys.get(rootOf(el));
|
const dragKey = eventName === "drop" && dragKeys.get(rootOf(el));
|
||||||
if (dragKey && !data.has("work_id")) data.set("work_id", dragKey);
|
if (dragKey && !data.has("work_id")) data.set("work_id", dragKey);
|
||||||
@@ -420,15 +435,16 @@
|
|||||||
}
|
}
|
||||||
if (name === "click") {
|
if (name === "click") {
|
||||||
const submitter = closestInRoot(event.target, root, "button[type=submit], input[type=submit], button:not([type])");
|
const submitter = closestInRoot(event.target, root, "button[type=submit], input[type=submit], button:not([type])");
|
||||||
const form = submitter && submitter.closest("form");
|
const form = formOwner(submitter);
|
||||||
if (form && root.contains(form) && handleId(form)) {
|
if (form && root.contains(form) && formHandleId(form)) {
|
||||||
if (form.reportValidity && !form.reportValidity()) return;
|
if (form.reportValidity && !form.reportValidity()) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
schedule(form, "submit");
|
schedule(form, "submit");
|
||||||
return;
|
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;
|
if (!el || defaultEvent(el) !== name) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
schedule(el, name);
|
schedule(el, name);
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ fn runtime_clicking_submitter_schedules_form_submit() {
|
|||||||
// req: convention/004
|
// req: convention/004
|
||||||
let source = slhx_js::RUNTIME_JS;
|
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("const direct = closestInRoot(event.target, root, `[${HID}]`)"));
|
||||||
assert!(source.contains("button[type=submit], input[type=submit], button:not([type])"));
|
assert!(source.contains("button[type=submit], input[type=submit], button:not([type])"));
|
||||||
assert!(source.contains("form.reportValidity && !form.reportValidity()"));
|
assert!(source.contains("form.reportValidity && !form.reportValidity()"));
|
||||||
|
|||||||
Reference in New Issue
Block a user