Something went wrong. Try again.
Paper Mario atproto mod hosting site starhaven.dev
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122//! Shared rendering for form fields and their validation errors.//!//! Validation itself stays in each handler - only the repeated markup//! (label/legend, the `.error` div, `aria-invalid`/`aria-describedby`//! wiring) and the error summary live here.//!//! Every field renders its `.error` slot whether or not it has one, hidden//! until it does, so a form validated in the browser has somewhere to put a//! message without building the element itself.
use maud::{html, Markup, PreEscaped};
/// A single text input, labeled, with an optional hint and error.pub fn text_field( id: &str, label: &str, value: &str, hint: Option<Markup>, error: Option<&str>,) -> Markup { let error_id = format!("{id}-error"); html! { div.form-item { label for=(id) { (label) } @if let Some(hint) = hint { div.hint { (hint) } } div.error id=(error_id) hidden[error.is_none()] { (error.unwrap_or_default()) } input type="text" id=(id) name=(id) value=(value) aria-invalid=[error.is_some().then_some("true")] aria-describedby=[error.is_some().then_some(error_id.as_str())]; } }}
/// A multi-line text input, labeled, with an optional hint and error.pub fn textarea_field( id: &str, label: &str, value: &str, hint: Option<Markup>, error: Option<&str>,) -> Markup { let error_id = format!("{id}-error"); html! { div.form-item { label for=(id) { (label) } @if let Some(hint) = hint { div.hint { (hint) } } div.error id=(error_id) hidden[error.is_none()] { (error.unwrap_or_default()) } textarea id=(id) name=(id) aria-invalid=[error.is_some().then_some("true")] aria-describedby=[error.is_some().then_some(error_id.as_str())] { (value) } } }}
/// A group of radio buttons under one legend, with a shared error.////// `options` pairs each radio's `value` with its label.pub fn radio_group<L: std::fmt::Display>( id: &str, legend: &str, options: &[(&str, L)], selected: Option<&str>, error: Option<&str>,) -> Markup { let error_id = format!("{id}-error"); html! { fieldset.form-item.radio-group aria-invalid=[error.is_some().then_some("true")] aria-describedby=[error.is_some().then_some(error_id.as_str())] { legend { (legend) } @if let Some(error) = error { div.error id=(error_id) { (error) } } @for (value, label) in options { div.radio-item { input type="radio" id={ (id) "-" (value) } name=(id) value=(value) checked[selected == Some(*value)]; label for={ (id) "-" (value) } { (label) } } } } }}
/// The messages to summarize at the top of a form: pairs of the id of the/// input each is about (`None` for a failure not tied to one field) and the/// message itself.pub type ErrorSummary<'a> = Vec<(Option<&'a str>, &'a str)>;
/// A focusable box listing every error, each linking to its field, shown/// above the form's heading. Moves focus to itself on load, so screen/// readers announce it immediately and sighted users land on it first.////// Renders nothing if `errors` is empty.pub fn error_summary(errors: &ErrorSummary) -> Markup { html! { @if !errors.is_empty() { script defer { (PreEscaped("document.getElementById('error-summary').focus();")) } div.accent-red role="alert" tabindex="-1" id="error-summary" { h2 { "There's a problem" } ul.error-list { @for (field_id, message) in errors { li { @if let Some(field_id) = field_id { a href={ "#" (field_id) } { (message) } } @else { (message) } } } } } } }}