mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(forms): resolve placeholder text through its pseudo style
This commit is contained in:
+45
-15
@@ -21,11 +21,12 @@ where
|
||||
let controls = (0..world.boxes.len())
|
||||
.map(LayoutBoxId::from_index)
|
||||
.filter_map(|id| {
|
||||
form_control_texts(&world.boxes[id.index()].element_semantics).map(|texts| (id, texts))
|
||||
form_control_texts(&world.boxes[id.index()].element_semantics)
|
||||
.map(|(texts, placeholder)| (id, texts, placeholder))
|
||||
})
|
||||
.filter(|(_, texts)| !texts.is_empty())
|
||||
.filter(|(_, texts, _)| !texts.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
for (control, texts) in controls {
|
||||
for (control, texts, placeholder) in controls {
|
||||
let Some(owner) = world.boxes[control.index()].source else {
|
||||
continue;
|
||||
};
|
||||
@@ -39,11 +40,22 @@ where
|
||||
.clip_form_control_contents();
|
||||
}
|
||||
let owner_label = world.boxes[control.index()].source_label.clone();
|
||||
let wrapper_style = styles.anonymous_style(
|
||||
owner,
|
||||
&world.boxes[control.index()].style,
|
||||
LayoutDisplay::Block,
|
||||
)?;
|
||||
let placeholder_style = if placeholder {
|
||||
styles.placeholder_style(owner)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let wrapper_style = if let Some(style) = placeholder_style {
|
||||
let mut style = style.into_style();
|
||||
style.force_layout_display(LayoutDisplay::Block);
|
||||
style
|
||||
} else {
|
||||
styles.anonymous_style(
|
||||
owner,
|
||||
&world.boxes[control.index()].style,
|
||||
LayoutDisplay::Block,
|
||||
)?
|
||||
};
|
||||
let text_style = ResolvedLayoutStyle::text_leaf_from(&wrapper_style);
|
||||
for (row, text) in texts.into_iter().enumerate() {
|
||||
let mut wrapper = LayoutWorld::new_box(
|
||||
@@ -79,7 +91,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn form_control_texts(semantics: &Option<LayoutElementSemantics>) -> Option<Vec<Arc<str>>> {
|
||||
fn form_control_texts(semantics: &Option<LayoutElementSemantics>) -> Option<(Vec<Arc<str>>, bool)> {
|
||||
let semantics = semantics.as_ref()?;
|
||||
let LayoutElementCategory::FormControl(kind) = semantics.category else {
|
||||
return None;
|
||||
@@ -94,8 +106,23 @@ fn form_control_texts(semantics: &Option<LayoutElementSemantics>) -> Option<Vec<
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if semantics.is_select_listbox() {
|
||||
return Some(data.option_labels);
|
||||
return Some((data.option_labels, false));
|
||||
}
|
||||
let placeholder = data.value.is_empty()
|
||||
&& !data.placeholder.is_empty()
|
||||
&& matches!(
|
||||
kind,
|
||||
LayoutFormControlKind::TextArea
|
||||
| LayoutFormControlKind::Input(
|
||||
LayoutInputControlKind::Text
|
||||
| LayoutInputControlKind::Search
|
||||
| LayoutInputControlKind::Email
|
||||
| LayoutInputControlKind::Telephone
|
||||
| LayoutInputControlKind::Password
|
||||
| LayoutInputControlKind::Url
|
||||
| LayoutInputControlKind::Number
|
||||
)
|
||||
);
|
||||
let text = match kind {
|
||||
LayoutFormControlKind::Input(input) => match input {
|
||||
LayoutInputControlKind::Button => data.value.to_string(),
|
||||
@@ -161,11 +188,14 @@ fn form_control_texts(semantics: &Option<LayoutElementSemantics>) -> Option<Vec<
|
||||
| LayoutFormControlKind::Progress
|
||||
| LayoutFormControlKind::Meter => String::new(),
|
||||
};
|
||||
Some(if text.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![text.into()]
|
||||
})
|
||||
Some((
|
||||
if text.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![text.into()]
|
||||
},
|
||||
placeholder,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn select_row_height(font_size: f32, line_height: f32) -> f32 {
|
||||
|
||||
@@ -653,6 +653,13 @@ where
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn placeholder_style(
|
||||
&mut self,
|
||||
_node: N,
|
||||
) -> Result<Option<ResolvedLayoutPseudoStyle>, LayoutError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn anonymous_style(
|
||||
&mut self,
|
||||
_owner: N,
|
||||
|
||||
@@ -204,6 +204,19 @@ impl LayoutStyleResolver<DomHandle> for NativeLayoutStyleResolver<'_> {
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn placeholder_style(
|
||||
&mut self,
|
||||
node: DomHandle,
|
||||
) -> Result<Option<ResolvedLayoutPseudoStyle>, LayoutError> {
|
||||
Ok(self
|
||||
.reads
|
||||
.read(node)
|
||||
.pseudo_computed_values("placeholder")
|
||||
.map(|computed| {
|
||||
ResolvedLayoutPseudoStyle::new(ResolvedLayoutStyle::from_stylo(computed))
|
||||
}))
|
||||
}
|
||||
|
||||
fn anonymous_style(
|
||||
&mut self,
|
||||
owner: DomHandle,
|
||||
|
||||
@@ -11,6 +11,85 @@ fn dark_pixels(image: &moli_paint::RasterImage, x: u32, y: u32, width: u32, heig
|
||||
.count()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn placeholder_pseudo_style_tracks_empty_and_filled_form_controls() {
|
||||
run_page_vm_async_test(async move {
|
||||
let loader =
|
||||
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
||||
let mut page_vm = test_page_vm_with_loader_and_document_url(
|
||||
&loader,
|
||||
Vec::new(),
|
||||
Url::parse("https://example.com/placeholder-paint.html")?,
|
||||
);
|
||||
// Like Blink's TextFieldInputType::UpdatePlaceholderText, placeholder
|
||||
// content has its own pseudo style, independently of the input value.
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
document.head.innerHTML = `<style>
|
||||
html,body{margin:0}
|
||||
input,textarea{display:block;width:200px;height:40px;padding:0;border:0;
|
||||
color:red;font:20px/28px sans-serif}
|
||||
::placeholder{color:rgb(31,63,127);font-size:24px;opacity:0.5}
|
||||
</style>`;
|
||||
document.body.innerHTML=`<input placeholder=Example><input type=email placeholder=Example>
|
||||
<textarea placeholder=Example></textarea>`;
|
||||
'installed'
|
||||
"#,
|
||||
)?;
|
||||
page_vm.vm_mut().sync_live_document_style_sources();
|
||||
for (value, expected_color, expected_size) in [
|
||||
(
|
||||
"",
|
||||
moli_layout::PaintColor::new(31.0 / 255.0, 63.0 / 255.0, 127.0 / 255.0, 1.0),
|
||||
24.0,
|
||||
),
|
||||
(
|
||||
"Example",
|
||||
moli_layout::PaintColor::new(1.0, 0.0, 0.0, 1.0),
|
||||
20.0,
|
||||
),
|
||||
(
|
||||
"",
|
||||
moli_layout::PaintColor::new(31.0 / 255.0, 63.0 / 255.0, 127.0 / 255.0, 1.0),
|
||||
24.0,
|
||||
),
|
||||
] {
|
||||
page_vm.vm_mut().eval(&format!(
|
||||
"document.querySelectorAll('input,textarea').forEach(el=>el.value={});'updated'",
|
||||
serde_json::to_string(value)?,
|
||||
))?;
|
||||
let snapshot = page_vm
|
||||
.vm_mut()
|
||||
.screenshot_layout_snapshot(moli_layout::PaintViewport::new(240, 180, 1.0))?
|
||||
.expect("placeholder layout");
|
||||
let runs = snapshot
|
||||
.fragments
|
||||
.iter()
|
||||
.filter_map(|fragment| match fragment {
|
||||
moli_layout::PaintFragment::GlyphRun(run) => Some(run),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(runs.len(), 3, "each control paints one label");
|
||||
for run in runs {
|
||||
assert_eq!(run.color, expected_color, "value={value:?}");
|
||||
assert_eq!(run.font_size, expected_size);
|
||||
}
|
||||
let placeholder_layers = snapshot.fragments.iter().filter(|fragment| matches!(
|
||||
fragment, moli_layout::PaintFragment::PushLayer { opacity, .. } if *opacity == 0.5
|
||||
)).count();
|
||||
assert_eq!(
|
||||
placeholder_layers,
|
||||
if value.is_empty() { 3 } else { 0 },
|
||||
"placeholder opacity must not affect typed text"
|
||||
);
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.await
|
||||
.expect("placeholder paint fixture should run");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn screenshot_select_listboxes_paint_every_option_and_clip_their_contents() {
|
||||
run_page_vm_async_test(async move {
|
||||
|
||||
Reference in New Issue
Block a user