use crate::form::{Markup, escape};
use crate::{Emit, class};
use makeover_layout::{Intent, Readiness, Tone};
use std::fmt::Write as _;
#[must_use]
pub fn placeholder_html(
state: Readiness,
message: &str,
action: Option<Markup<'_>>,
opts: &Emit,
) -> String {
if state.shows_content() {
return String::new();
}
let name = state_name(state);
let mut html = format!(
"<div class=\"{}\" data-state=\"{name}\"",
class("placeholder", opts)
);
if state.tone() != Tone::Neutral {
let _ = write!(html, " data-tone=\"{}\"", state.tone().token());
}
if state.tone() == Tone::Danger {
html.push_str(" role=\"alert\"");
} else {
html.push_str(" role=\"status\" aria-live=\"polite\"");
}
let _ = write!(
html,
"><p class=\"{}\">{}</p>",
class("placeholder-text", opts),
escape(message)
);
if let Some(Markup(markup)) = action {
let _ = write!(
html,
"<div class=\"{}\">{markup}</div>",
class("placeholder-action", opts)
);
}
html.push_str("</div>");
html
}
fn state_name(state: Readiness) -> &'static str {
match state {
Readiness::Ready => "ready",
Readiness::Pending => "pending",
Readiness::Empty => "empty",
Readiness::Failed => "failed",
_ => "unknown",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_state_that_shows_content_draws_no_stand_in() {
assert!(placeholder_html(Readiness::Ready, "x", None, &Emit::default()).is_empty());
}
#[test]
fn an_empty_region_is_not_announced_as_a_fault() {
let empty = placeholder_html(Readiness::Empty, "No projects yet", None, &Emit::default());
assert!(empty.contains(r#"role="status""#));
assert!(!empty.contains("data-tone"));
let failed = placeholder_html(
Readiness::Failed,
"Failed to load events",
None,
&Emit::default(),
);
assert!(failed.contains(r#"role="alert""#));
assert!(failed.contains(r#"data-tone="danger""#));
}
#[test]
fn the_message_is_escaped_and_the_action_is_not() {
let html = placeholder_html(
Readiness::Empty,
"No <b>projects</b> yet",
Some(Markup("<button>Add one</button>")),
&Emit::default(),
);
assert!(html.contains("<b>"));
assert!(!html.contains("<b>"));
assert!(html.contains("<button>Add one</button>"));
}
#[test]
fn a_state_with_no_action_emits_no_action_container() {
let html = placeholder_html(Readiness::Empty, "Nothing here", None, &Emit::default());
assert!(!html.contains("placeholder-action"));
}
#[test]
fn pending_draws_the_same_anatomy_as_the_other_two() {
let html = placeholder_html(Readiness::Pending, "Loading", None, &Emit::default());
assert!(html.contains(r#"data-state="pending""#));
assert!(html.contains("Loading"));
}
#[test]
fn the_prefix_reaches_every_class() {
let opts = Emit {
class_prefix: "mo-",
..Emit::default()
};
let html = placeholder_html(
Readiness::Empty,
"None",
Some(Markup("<button>Go</button>")),
&opts,
);
assert!(html.contains(r#"class="mo-placeholder""#));
assert!(html.contains(r#"class="mo-placeholder-text""#));
assert!(html.contains(r#"class="mo-placeholder-action""#));
}
}