Skip to main content

dioxus_field/
lib.rs

1//! A form-library-agnostic field convention for Dioxus.
2//!
3//! Use this crate to connect form-library-owned values and metadata to field-shaped widgets without
4//! coupling the widget library to a form implementation. [`Binding`] is the upper-level reactive
5//! contract, including independent Commit and Focus Exit reports. Widget registries that do not
6//! depend on this crate can instead accept separate `value`, `on_change`, and `on_commit` props
7//! matching the lower-level [`BindingPropTrio`] contract.
8//!
9//! # Quick start
10//!
11//! ```rust
12//! use dioxus::prelude::*;
13//! use dioxus_field::Field;
14//!
15//! fn app() -> Element {
16//!     let mut name = use_signal(String::new);
17//!
18//!     rsx! {
19//!         Field { context: name,
20//!             input {
21//!                 value: name,
22//!                 oninput: move |event| name.set(event.value()),
23//!             }
24//!         }
25//!     }
26//! }
27//! ```
28
29use std::{
30    any::Any,
31    cell::{Cell, RefCell},
32    fmt,
33    rc::Rc,
34};
35
36use dioxus::prelude::{Props, dioxus_elements, rsx};
37use dioxus_core::{
38    Attribute, AttributeValue, Callback, Element, current_scope_id, has_context, provide_context,
39    try_consume_context, use_hook,
40};
41use dioxus_hooks::{use_effect, use_reactive, use_signal};
42use dioxus_signals::{ReadSignal, ReadableExt, Signal, WritableExt};
43
44pub mod testing;
45
46/// Initial presentation metadata for one field-shaped value.
47///
48/// `invalid: None` derives invalidity from whether `errors` is empty. Setting it to `Some` keeps
49/// invalidity independently controlled by the metadata producer.
50#[allow(
51    clippy::struct_excessive_bools,
52    reason = "these independent presentation flags have no invalid combinations"
53)]
54#[derive(Clone, Debug, Default, PartialEq, Eq)]
55pub struct FieldMetaValues {
56    /// The rendered control's element id.
57    pub id: Option<Rc<str>>,
58    /// The rendered control's name.
59    pub name: Option<Rc<str>>,
60    /// Whether the field is required according to its producer.
61    pub required: bool,
62    /// Whether the field is disabled according to its producer.
63    pub disabled: bool,
64    /// An explicit invalid state, or `None` to derive it from `errors`.
65    pub invalid: Option<bool>,
66    /// Pre-rendered error text.
67    pub errors: Vec<Rc<str>>,
68    /// Whether the field is touched according to its producer.
69    pub touched: bool,
70    /// Whether the field is dirty according to its producer.
71    pub dirty: bool,
72}
73
74/// Explicit state that wins over the resolved metadata's own state.
75///
76/// This is the whole override set for a field part, and the state subset of the control path
77/// carried by [`FieldControlOptions`]. A `None` field defers to the metadata.
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
79pub struct FieldStateOverrides {
80    /// Overrides the metadata's invalid state when present.
81    pub invalid: Option<bool>,
82    /// Overrides the metadata's disabled state when present.
83    pub disabled: Option<bool>,
84    /// Overrides the metadata's required state when present.
85    pub required: Option<bool>,
86}
87
88/// How one rendered element spells a field attribute that has both a native and an ARIA form.
89///
90/// The question this answers is *attribute applicability* — does this element accept this
91/// attribute — not which accessibility spelling reads better. Only the widget knows the element it
92/// renders and the role that element carries, so the widget supplies it.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum AttributeSurface {
96    /// Emit the native HTML attribute, such as `required` or `disabled`.
97    #[default]
98    Native,
99    /// Emit the ARIA attribute, such as `aria-required` or `aria-disabled`.
100    Aria,
101    /// Emit neither spelling.
102    ///
103    /// Use this where the attribute is invalid on the rendered element and no ARIA spelling
104    /// applies to its role either.
105    Omit,
106}
107
108/// How one rendered element exposes validity.
109///
110/// Validity has no native spelling, so this axis has no `Native` variant. It gates `aria-invalid`
111/// and `aria-errormessage` together, since a validity reference without a validity state is not
112/// meaningful.
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
114#[non_exhaustive]
115pub enum ValiditySurface {
116    /// Emit `aria-invalid`, and `aria-errormessage` while invalid.
117    #[default]
118    Aria,
119    /// Emit neither, for roles where `aria-invalid` is unsupported or deprecated.
120    Omit,
121}
122
123/// Whether one rendered element accepts the `name` attribute.
124///
125/// `name` has no ARIA spelling, so this axis has no `Aria` variant.
126#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
127#[non_exhaustive]
128pub enum NameSurface {
129    /// Emit `name`.
130    #[default]
131    Native,
132    /// Emit nothing.
133    ///
134    /// Controls rooted on a `div` need this: `name` is not a valid attribute there, and such
135    /// controls do not participate in native form submission.
136    Omit,
137}
138
139/// How one rendered element spells its field state, one axis per attribute.
140///
141/// The axes are independent because their validity lattices disagree pairwise: native `disabled`
142/// is legal on a `button` where native `required` is not, and `aria-invalid` is unsupported on
143/// some roles where `aria-disabled` is fine. A single index over all of them cannot describe any
144/// element correctly.
145///
146/// The `data-*` state attributes are outside this type. They are valid on every element, so
147/// [`FieldMeta::attributes_for`] always emits them regardless of the surface — an `Omit` axis
148/// suppresses only the attribute the element cannot carry, never the styling hook.
149#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
150pub struct FieldSurface {
151    /// How the element spells its required state.
152    pub required: AttributeSurface,
153    /// How the element spells its disabled state.
154    pub disabled: AttributeSurface,
155    /// How the element spells its validity.
156    pub validity: ValiditySurface,
157    /// Whether the element carries a `name`.
158    pub name: NameSurface,
159}
160
161impl FieldSurface {
162    /// `input`, `textarea`, and `select` — every axis that *has* a native spelling uses it, and
163    /// this is the default. Validity stays ARIA, since no element spells validity natively.
164    pub const NATIVE: Self = Self {
165        required: AttributeSurface::Native,
166        disabled: AttributeSurface::Native,
167        validity: ValiditySurface::Aria,
168        name: NameSurface::Native,
169    };
170
171    /// `button[role=checkbox|switch]` — native `disabled` and `name` are legal on a `button`,
172    /// native `required` is not.
173    pub const BUTTON_WIDGET: Self = Self {
174        required: AttributeSurface::Aria,
175        disabled: AttributeSurface::Native,
176        validity: ValiditySurface::Aria,
177        name: NameSurface::Native,
178    };
179
180    /// `div[role=radiogroup]` — no native attribute applies, and a `div` carries no `name`.
181    ///
182    /// A control rooted on `role=group` should start here and set `validity` to
183    /// [`ValiditySurface::Omit`], since `aria-invalid` is deprecated on that role.
184    pub const ARIA_WIDGET: Self = Self {
185        required: AttributeSurface::Aria,
186        disabled: AttributeSurface::Aria,
187        validity: ValiditySurface::Aria,
188        name: NameSurface::Omit,
189    };
190}
191
192/// Everything a field-aware control tells [`FieldMeta::attributes_for`] about itself.
193///
194/// Overrides are resolved before any attribute is built, so an overridden state is never emitted
195/// twice and never has to be filtered back out of the result.
196///
197/// ```rust
198/// # use std::rc::Rc;
199/// # use dioxus_field::{FieldControlOptions, FieldSurface};
200/// let options = FieldControlOptions::new()
201///     .required(Some(true))
202///     .name(Some(Rc::from("terms")))
203///     .surface(FieldSurface::BUTTON_WIDGET);
204/// ```
205#[derive(Clone, Debug, Default, PartialEq)]
206pub struct FieldControlOptions {
207    state: FieldStateOverrides,
208    id: Option<Rc<str>>,
209    name: Option<Rc<str>>,
210    surface: FieldSurface,
211}
212
213impl FieldControlOptions {
214    /// Creates options that override nothing and render onto [`FieldSurface::NATIVE`].
215    #[must_use]
216    pub fn new() -> Self {
217        Self::default()
218    }
219
220    /// Replaces the whole state override set.
221    #[must_use]
222    pub fn state(mut self, state: FieldStateOverrides) -> Self {
223        self.state = state;
224        self
225    }
226
227    /// Overrides the metadata's invalid state.
228    #[must_use]
229    pub fn invalid(mut self, invalid: Option<bool>) -> Self {
230        self.state.invalid = invalid;
231        self
232    }
233
234    /// Overrides the metadata's disabled state.
235    #[must_use]
236    pub fn disabled(mut self, disabled: Option<bool>) -> Self {
237        self.state.disabled = disabled;
238        self
239    }
240
241    /// Overrides the metadata's required state.
242    #[must_use]
243    pub fn required(mut self, required: Option<bool>) -> Self {
244        self.state.required = required;
245        self
246    }
247
248    /// Overrides the metadata's control id.
249    ///
250    /// This replaces the emitted value; it does not suppress the attribute. `id` is a global
251    /// attribute, so there is no element that must not carry one.
252    #[must_use]
253    pub fn id(mut self, id: Option<Rc<str>>) -> Self {
254        self.id = id;
255        self
256    }
257
258    /// Overrides the metadata's control name.
259    ///
260    /// This replaces the emitted value. To suppress the attribute on an element that cannot carry
261    /// it, set [`FieldSurface::name`] to [`NameSurface::Omit`] instead.
262    #[must_use]
263    pub fn name(mut self, name: Option<Rc<str>>) -> Self {
264        self.name = name;
265        self
266    }
267
268    /// Declares how the rendered element spells each field attribute.
269    #[must_use]
270    pub fn surface(mut self, surface: FieldSurface) -> Self {
271        self.surface = surface;
272        self
273    }
274}
275
276/// Signal-backed presentation metadata for one field-shaped value.
277///
278/// The flag meanings are producer-defined. This type does not track an initial value or classify
279/// validity. Error strings are already formatted for display before they cross this boundary.
280#[derive(Clone, Copy, PartialEq)]
281pub struct FieldMeta {
282    id: Signal<Option<Rc<str>>>,
283    fallback_id: Signal<Rc<str>>,
284    name: Signal<Option<Rc<str>>>,
285    required: Signal<bool>,
286    disabled: Signal<bool>,
287    invalid: Signal<Option<bool>>,
288    errors: Signal<Vec<Rc<str>>>,
289    touched: Signal<bool>,
290    dirty: Signal<bool>,
291    registered_ids: Signal<RegisteredIds>,
292}
293
294impl fmt::Debug for FieldMeta {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        let id = self
297            .id
298            .peek()
299            .clone()
300            .unwrap_or_else(|| self.fallback_id.peek().clone());
301
302        f.debug_struct("FieldMeta")
303            .field("id", &id)
304            .field("name", &*self.name.peek())
305            .field("required", &*self.required.peek())
306            .field("disabled", &*self.disabled.peek())
307            .field("invalid", &*self.invalid.peek())
308            .field("errors", &*self.errors.peek())
309            .field("touched", &*self.touched.peek())
310            .field("dirty", &*self.dirty.peek())
311            .finish_non_exhaustive()
312    }
313}
314
315impl FieldMeta {
316    /// Returns the rendered control id.
317    ///
318    /// Metadata always carries an id. When its producer supplies none,
319    /// [`use_field_meta_state`] generates one that is stable for the owning scope's lifetime, so
320    /// [`Label`]'s `for`, `aria-labelledby`, `aria-describedby`, and `aria-errormessage` all
321    /// resolve instead of silently leaving the control unnamed.
322    pub fn id(&self) -> Rc<str> {
323        (self.id)().unwrap_or_else(|| (self.fallback_id)())
324    }
325
326    /// Replaces the rendered control id, or restores the generated fallback with `None`.
327    pub fn set_id(&mut self, id: Option<Rc<str>>) {
328        self.id.set(id);
329    }
330
331    /// Returns the rendered control name.
332    pub fn name(&self) -> Option<Rc<str>> {
333        (self.name)()
334    }
335
336    /// Replaces the rendered control name.
337    pub fn set_name(&mut self, name: Option<Rc<str>>) {
338        self.name.set(name);
339    }
340
341    /// Returns whether the field is required.
342    pub fn required(&self) -> bool {
343        (self.required)()
344    }
345
346    /// Replaces the producer-defined required state.
347    pub fn set_required(&mut self, required: bool) {
348        self.required.set(required);
349    }
350
351    /// Returns whether the field is disabled.
352    pub fn disabled(&self) -> bool {
353        (self.disabled)()
354    }
355
356    /// Replaces the producer-defined disabled state.
357    pub fn set_disabled(&mut self, disabled: bool) {
358        self.disabled.set(disabled);
359    }
360
361    /// Returns whether the field is invalid.
362    ///
363    /// An explicit invalid state wins; otherwise invalidity is derived from whether errors exist.
364    pub fn invalid(&self) -> bool {
365        (self.invalid)().unwrap_or_else(|| !(self.errors)().is_empty())
366    }
367
368    /// Sets an explicit invalid state, or restores error-derived invalidity with `None`.
369    pub fn set_invalid(&mut self, invalid: Option<bool>) {
370        self.invalid.set(invalid);
371    }
372
373    /// Returns the pre-rendered error text.
374    pub fn errors(&self) -> Vec<Rc<str>> {
375        (self.errors)()
376    }
377
378    /// Replaces the pre-rendered error text.
379    pub fn set_errors(&mut self, errors: Vec<Rc<str>>) {
380        self.errors.set(errors);
381    }
382
383    /// Returns whether the field is touched.
384    pub fn touched(&self) -> bool {
385        (self.touched)()
386    }
387
388    /// Replaces the producer-defined touched state.
389    pub fn set_touched(&mut self, touched: bool) {
390        self.touched.set(touched);
391    }
392
393    /// Returns whether the field is dirty.
394    pub fn dirty(&self) -> bool {
395        (self.dirty)()
396    }
397
398    /// Replaces the producer-defined dirty state.
399    pub fn set_dirty(&mut self, dirty: bool) {
400        self.dirty.set(dirty);
401    }
402
403    /// Registers a label element id until the returned registration is dropped.
404    ///
405    /// Registered label ids reach the control through `aria-labelledby`, which is the only naming
406    /// path available to a control rooted on an element `<label for>` cannot address.
407    #[must_use]
408    pub fn register_label_id(&mut self, id: Rc<str>) -> FieldMetaIdRegistration {
409        self.register_id(RegisteredIdKind::Label, id)
410    }
411
412    /// Registers a description element id until the returned registration is dropped.
413    #[must_use]
414    pub fn register_description_id(&mut self, id: Rc<str>) -> FieldMetaIdRegistration {
415        self.register_id(RegisteredIdKind::Description, id)
416    }
417
418    /// Registers an error element id until the returned registration is dropped.
419    #[must_use]
420    pub fn register_error_id(&mut self, id: Rc<str>) -> FieldMetaIdRegistration {
421        self.register_id(RegisteredIdKind::Error, id)
422    }
423
424    /// Returns attributes for a rendered control using the metadata's own state, on a
425    /// [`FieldSurface::NATIVE`] element.
426    pub fn attributes(&self) -> Vec<Attribute> {
427        self.attributes_for(&FieldControlOptions::default())
428    }
429
430    /// Returns attributes for a rendered control, resolving explicit overrides and the element's
431    /// attribute surface.
432    ///
433    /// Overrides are resolved first and only the resolved state is emitted, so the caller never
434    /// filters the result and no attribute appears twice.
435    ///
436    /// # Guarantees
437    ///
438    /// The returned vector is **sorted by attribute name** and carries at most one entry per
439    /// attribute name and namespace.
440    ///
441    /// The sort is what `dioxus-core` requires of any spread list: its attribute diff is a sorted
442    /// merge-join, so an unsorted list makes a later render drop attributes that did not change.
443    /// The single entry per name guards the neighbouring failure, where a spread carrying one name
444    /// twice and dropping to once emits a removal, deleting an attribute the new render still has.
445    ///
446    /// To combine this with a widget's own attributes, pass both to [`merge_attributes`], which
447    /// preserves the guarantee and resolves each name last-wins. To *replace* a value the metadata
448    /// supplied, set the matching override on [`FieldControlOptions`] rather than adding a second
449    /// entry — that is what the overrides are for.
450    ///
451    /// # Emitted attributes
452    ///
453    /// - `id`, always, from the override or the metadata.
454    /// - `name`, when [`FieldSurface::name`] is [`NameSurface::Native`] and a name resolves.
455    /// - `required` or `aria-required="true"`, when required, per [`FieldSurface::required`].
456    /// - `disabled` or `aria-disabled="true"`, when disabled, per [`FieldSurface::disabled`].
457    /// - `aria-invalid`, and `aria-errormessage` while invalid, per [`FieldSurface::validity`].
458    ///   `aria-errormessage` takes a single IDREF in ARIA 1.2, so it references only the first
459    ///   mounted error part; every error id also reaches `aria-describedby` while invalid.
460    /// - `aria-labelledby` and `aria-describedby`, from the currently mounted parts. Both are
461    ///   legal on every role in play, so neither has a surface axis.
462    /// - `data-required`, `data-disabled`, `data-invalid`, `data-touched`, and `data-dirty`, from
463    ///   the resolved state, absent when false and independent of the surface.
464    pub fn attributes_for(&self, options: &FieldControlOptions) -> Vec<Attribute> {
465        let required = options.state.required.unwrap_or_else(|| self.required());
466        let disabled = options.state.disabled.unwrap_or_else(|| self.disabled());
467        let invalid = options.state.invalid.unwrap_or_else(|| self.invalid());
468        let id = options.id.clone().unwrap_or_else(|| self.id());
469        let name = options.name.clone().or_else(|| self.name());
470        let registered_ids = (self.registered_ids)();
471        let error_ids = registered_ids.ids(RegisteredIdKind::Error);
472        let mut described_by = registered_ids.ids(RegisteredIdKind::Description);
473        if invalid {
474            described_by.extend(error_ids.iter().cloned());
475        }
476        let mut attributes = Vec::new();
477
478        attributes.push(Attribute::new("id", id.to_string(), None, false));
479
480        if options.surface.name == NameSurface::Native {
481            push_optional_text(&mut attributes, "name", name);
482        }
483
484        push_surface_state(
485            &mut attributes,
486            options.surface.required,
487            ("required", "aria-required"),
488            required,
489        );
490        push_surface_state(
491            &mut attributes,
492            options.surface.disabled,
493            ("disabled", "aria-disabled"),
494            disabled,
495        );
496
497        if options.surface.validity == ValiditySurface::Aria {
498            attributes.push(Attribute::new(
499                "aria-invalid",
500                invalid.to_string(),
501                None,
502                false,
503            ));
504            if invalid {
505                push_optional_text(
506                    &mut attributes,
507                    "aria-errormessage",
508                    error_ids.first().cloned(),
509                );
510            }
511        }
512
513        push_optional_text(
514            &mut attributes,
515            "aria-labelledby",
516            join_ids(&registered_ids.ids(RegisteredIdKind::Label)),
517        );
518        push_optional_text(&mut attributes, "aria-describedby", join_ids(&described_by));
519
520        push_state(&mut attributes, "data-required", required);
521        push_state(&mut attributes, "data-disabled", disabled);
522        push_state(&mut attributes, "data-invalid", invalid);
523        push_state(&mut attributes, "data-touched", self.touched());
524        push_state(&mut attributes, "data-dirty", self.dirty());
525
526        normalize_attributes(&mut attributes);
527
528        attributes
529    }
530
531    fn register_id(&mut self, kind: RegisteredIdKind, id: Rc<str>) -> FieldMetaIdRegistration {
532        let token = self.registered_ids.with_mut(|ids| ids.insert(kind, id));
533
534        FieldMetaIdRegistration {
535            registered_ids: self.registered_ids,
536            token,
537        }
538    }
539
540    /// Replaces producer-owned metadata values without disturbing registered part ids.
541    pub fn set_values(&mut self, values: FieldMetaValues) {
542        if *self.id.peek() != values.id {
543            self.id.set(values.id);
544        }
545        if *self.name.peek() != values.name {
546            self.name.set(values.name);
547        }
548        if *self.required.peek() != values.required {
549            self.required.set(values.required);
550        }
551        if *self.disabled.peek() != values.disabled {
552            self.disabled.set(values.disabled);
553        }
554        if *self.invalid.peek() != values.invalid {
555            self.invalid.set(values.invalid);
556        }
557        if *self.errors.peek() != values.errors {
558            self.errors.set(values.errors);
559        }
560        if *self.touched.peek() != values.touched {
561            self.touched.set(values.touched);
562        }
563        if *self.dirty.peek() != values.dirty {
564            self.dirty.set(values.dirty);
565        }
566    }
567}
568
569/// Creates signal-backed field metadata owned by the current component scope.
570///
571/// When `initial.id` is `None`, the metadata falls back to an id generated for this hook, stable
572/// for the owning scope's lifetime. Setting the id back to `None` later restores that fallback, so
573/// a control resolved through this metadata always has an id to be labelled and described by.
574pub fn use_field_meta_state(initial: FieldMetaValues) -> FieldMeta {
575    let FieldMetaValues {
576        id,
577        name,
578        required,
579        disabled,
580        invalid,
581        errors,
582        touched,
583        dirty,
584    } = initial;
585
586    FieldMeta {
587        id: use_signal(|| id),
588        fallback_id: use_signal(|| generated_id("field")),
589        name: use_signal(|| name),
590        required: use_signal(|| required),
591        disabled: use_signal(|| disabled),
592        invalid: use_signal(|| invalid),
593        errors: use_signal(|| errors),
594        touched: use_signal(|| touched),
595        dirty: use_signal(|| dirty),
596        registered_ids: use_signal(RegisteredIds::default),
597    }
598}
599
600fn use_synced_field_meta_state(values: &FieldMetaValues) -> FieldMeta {
601    let meta = use_field_meta_state(values.clone());
602    use_effect(use_reactive(values, move |values| {
603        let mut meta = meta;
604        meta.set_values(values);
605    }));
606
607    meta
608}
609
610/// A lifecycle-bound description or error id registration.
611pub struct FieldMetaIdRegistration {
612    registered_ids: Signal<RegisteredIds>,
613    token: u64,
614}
615
616impl fmt::Debug for FieldMetaIdRegistration {
617    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
618        f.debug_struct("FieldMetaIdRegistration")
619            .field("token", &self.token)
620            .finish_non_exhaustive()
621    }
622}
623
624impl Drop for FieldMetaIdRegistration {
625    fn drop(&mut self) {
626        self.registered_ids
627            .with_mut(|ids| ids.entries.retain(|entry| entry.token != self.token));
628    }
629}
630
631#[derive(Clone, Copy, PartialEq, Eq)]
632enum RegisteredIdKind {
633    Label,
634    Description,
635    Error,
636}
637
638#[derive(Clone, Default, PartialEq, Eq)]
639struct RegisteredIds {
640    next_token: u64,
641    entries: Vec<RegisteredId>,
642}
643
644impl RegisteredIds {
645    fn insert(&mut self, kind: RegisteredIdKind, id: Rc<str>) -> u64 {
646        let token = self.next_token;
647        self.next_token += 1;
648        self.entries.push(RegisteredId { token, kind, id });
649
650        token
651    }
652
653    /// Returns the ids of one kind in registration order, which is the order ARIA id references
654    /// are rendered in.
655    fn ids(&self, kind: RegisteredIdKind) -> Vec<Rc<str>> {
656        self.entries
657            .iter()
658            .filter(|entry| entry.kind == kind)
659            .map(|entry| Rc::clone(&entry.id))
660            .collect()
661    }
662}
663
664fn join_ids(ids: &[Rc<str>]) -> Option<Rc<str>> {
665    (!ids.is_empty()).then(|| {
666        Rc::from(
667            ids.iter()
668                .map(AsRef::as_ref)
669                .collect::<Vec<_>>()
670                .join(" ")
671                .as_str(),
672        )
673    })
674}
675
676/// A per-scope counter that keeps generated ids unique within one component.
677#[derive(Clone)]
678struct GeneratedIdCounter(Rc<Cell<u64>>);
679
680/// Generates an id unique to the calling scope and to this call's position within it.
681///
682/// Call this only from a hook initializer so the value is computed once and stays stable for the
683/// scope's lifetime. The counter lives in the scope's own context, which keeps generated ids
684/// deterministic for a given [`dioxus_core::VirtualDom`] rather than dependent on global state.
685fn generated_id(prefix: &str) -> Rc<str> {
686    let counter = has_context::<GeneratedIdCounter>()
687        .unwrap_or_else(|| provide_context(GeneratedIdCounter(Rc::new(Cell::new(0)))));
688    let index = counter.0.get();
689    counter.0.set(index + 1);
690
691    Rc::from(format!("dxf-{prefix}-{}-{index}", current_scope_id().0).as_str())
692}
693
694/// Resolves a field part's own element id, generating a stable one when the caller supplies none.
695fn use_part_id(explicit: Option<Rc<str>>, prefix: &'static str) -> Rc<str> {
696    let generated = use_hook(|| generated_id(prefix));
697
698    explicit.unwrap_or(generated)
699}
700
701// Dioxus cannot compose an `Into<Rc<str>>` conversion with an optional prop setter.
702#[doc(hidden)]
703pub struct OptionalRcStrPropMarker;
704
705impl<'a> dioxus_core::SuperFrom<&'a str, OptionalRcStrPropMarker> for Option<Rc<str>> {
706    fn super_from(value: &'a str) -> Self {
707        Some(Rc::from(value))
708    }
709}
710
711impl dioxus_core::SuperFrom<String, OptionalRcStrPropMarker> for Option<Rc<str>> {
712    fn super_from(value: String) -> Self {
713        Some(Rc::from(value))
714    }
715}
716
717impl dioxus_core::SuperFrom<Box<str>, OptionalRcStrPropMarker> for Option<Rc<str>> {
718    fn super_from(value: Box<str>) -> Self {
719        Some(Rc::from(value))
720    }
721}
722
723impl<'a> dioxus_core::SuperFrom<std::borrow::Cow<'a, str>, OptionalRcStrPropMarker>
724    for Option<Rc<str>>
725{
726    fn super_from(value: std::borrow::Cow<'a, str>) -> Self {
727        Some(Rc::from(value))
728    }
729}
730
731#[derive(Clone, PartialEq, Eq)]
732struct RegisteredId {
733    token: u64,
734    kind: RegisteredIdKind,
735    id: Rc<str>,
736}
737
738fn push_optional_text(attributes: &mut Vec<Attribute>, name: &'static str, value: Option<Rc<str>>) {
739    if let Some(value) = value {
740        attributes.push(Attribute::new(name, value.to_string(), None, false));
741    }
742}
743
744fn push_bool(attributes: &mut Vec<Attribute>, name: &'static str, value: bool) {
745    if value {
746        attributes.push(Attribute::new(name, true, None, false));
747    }
748}
749
750/// Pushes one state in the spelling its surface calls for, and nothing when `value` is false.
751///
752/// The native spelling is a boolean attribute; the ARIA one is `="true"`. Both are absent when
753/// false, so a selector never has to distinguish `false` from unset.
754fn push_surface_state(
755    attributes: &mut Vec<Attribute>,
756    surface: AttributeSurface,
757    (native, aria): (&'static str, &'static str),
758    value: bool,
759) {
760    match surface {
761        AttributeSurface::Native => push_bool(attributes, native, value),
762        AttributeSurface::Aria => push_state(attributes, aria, value),
763        AttributeSurface::Omit => {}
764    }
765}
766
767/// Pushes `name="true"` when `value`, and nothing otherwise.
768///
769/// Both the `data-*` state attributes and the ARIA states this crate emits use the same
770/// absent-when-false convention, so a selector never has to distinguish `false` from unset.
771fn push_state(attributes: &mut Vec<Attribute>, name: &'static str, value: bool) {
772    if value {
773        attributes.push(Attribute::new(name, "true", None, false));
774    }
775}
776
777/// Merges ordered attribute groups into one list a widget can spread.
778///
779/// Groups are resolved **last-wins**: where two groups set the same attribute name and namespace,
780/// the later group's value survives. Order the groups from weakest to strongest — for a
781/// field-aware control that is typically the metadata attributes, then the widget's own base
782/// attributes, then its explicit props, then the caller's forwarded attributes.
783///
784/// Passing ordered groups rather than one pre-concatenated list is the point. Concatenating the
785/// metadata and explicit groups before the call moves the widget's base attributes past both, so
786/// base silently outranks an explicit `name` or `required` it was meant to lose to.
787///
788/// `class` is the exception to last-wins: values are **concatenated**, weakest first, so a widget's
789/// own classes survive a caller's. Replacing there would silently unstyle the widget. Every other
790/// name, `style` included, resolves last-wins.
791///
792/// The result carries the same guarantee as [`FieldMeta::attributes_for`]: sorted by attribute
793/// name, at most one entry per name and namespace. `dioxus-core` requires the sort of any spread,
794/// and the deduplication keeps a name that appears twice and later drops to once from deleting the
795/// attribute outright.
796///
797/// Widgets already merging through `merge_attributes` in `dioxus-primitives` do not need this one.
798/// That helper also sorts, deduplicates, and concatenates `class`, so either satisfies the
799/// guarantee — but they are separate implementations, so do not assume they agree on every detail.
800///
801/// ```rust
802/// # use dioxus_core::Attribute;
803/// # use dioxus_field::merge_attributes;
804/// let merged = merge_attributes(vec![
805///     vec![Attribute::new("name", "from-meta", None, false)],
806///     vec![Attribute::new("name", "from-explicit", None, false)],
807/// ]);
808///
809/// assert_eq!(merged.len(), 1);
810/// ```
811pub fn merge_attributes(groups: Vec<Vec<Attribute>>) -> Vec<Attribute> {
812    let mut attributes = groups.into_iter().flatten().collect::<Vec<_>>();
813    normalize_attributes(&mut attributes);
814
815    attributes
816}
817
818/// Sorts by attribute name and keeps the last entry for each name and namespace.
819///
820/// `dioxus-core` diffs a spread attribute list with a sorted merge-join keyed on the attribute
821/// name, so an unsorted or duplicated list makes the next render emit removals for attributes that
822/// are still present. Every list this crate hands to `rsx!` passes through here, including after
823/// caller attributes are appended — appending last is what makes a caller's attribute win its
824/// name.
825fn normalize_attributes(attributes: &mut Vec<Attribute>) {
826    attributes.sort_by(|left, right| {
827        left.name
828            .cmp(right.name)
829            .then_with(|| left.namespace.cmp(&right.namespace))
830    });
831    attributes.dedup_by(|later, earlier| {
832        let duplicate = later.name == earlier.name && later.namespace == earlier.namespace;
833
834        if duplicate {
835            if let ("class", AttributeValue::Text(kept), AttributeValue::Text(dropped)) =
836                (later.name, &later.value, &earlier.value)
837            {
838                // Classes compose rather than replace: a widget's own classes and a caller's are
839                // both meant to apply, and last-wins here would silently unstyle the widget.
840                let combined = format!("{dropped} {kept}");
841                later.value = AttributeValue::Text(combined);
842            }
843
844            std::mem::swap(later, earlier);
845        }
846
847        duplicate
848    });
849}
850
851/// Describes whether a value write came from user interaction or application code.
852#[derive(Clone, Copy, Debug, PartialEq, Eq)]
853pub enum ChangeOrigin {
854    /// The user changed the value through a widget.
855    User,
856    /// Application code changed the value.
857    Programmatic,
858}
859
860/// A reactive, two-way binding to one field-shaped value.
861///
862/// # Interaction boundaries
863///
864/// [`Binding::commit`] and [`Binding::focus_exit`] report independent facts:
865///
866/// - **Commit** is the widget-defined end of one interaction unit. A switch click or slider release
867///   can commit while the control remains focused.
868/// - **Focus Exit** means focus left the widget's complete logical focus scope. An unchanged native
869///   text input can report Focus Exit without committing a value change.
870///
871/// ## Native control example
872///
873/// A switch click can call `write`, then `commit`, while focus remains on the switch. A later
874/// departure from the switch calls `focus_exit`. An unchanged text input that loses focus can call
875/// only `focus_exit` because there was no value interaction to commit.
876///
877/// ## Compound-widget example
878///
879/// For a compound widget, the logical focus scope includes its owned controls and popup or portal
880/// content. Moving focus from a combobox trigger into its popup does not report Focus Exit. When a
881/// selection writes and commits before focus leaves that complete scope, report the write, then
882/// Commit, then Focus Exit.
883///
884/// Equality compares the binding's producer-defined identity. Equal bindings are guaranteed to
885/// represent interchangeable read, write, Commit, and Focus Exit behavior; producers may
886/// conservatively return unequal bindings when they cannot prove that interchangeability.
887pub struct Binding<T: 'static> {
888    /// The binding's reactive value.
889    pub read: ReadSignal<T>,
890    write: Callback<(T, ChangeOrigin)>,
891    commit: Callback<()>,
892    focus_exit: Callback<()>,
893    identity: BindingIdentity,
894}
895
896impl<T: 'static> Binding<T> {
897    /// Creates a binding identified by its exact read, write, and commit handles.
898    ///
899    /// Focus Exit is a no-op unless replaced with [`Binding::with_focus_exit`] or
900    /// [`Binding::with_focus_exit_using_identity`].
901    pub fn new(
902        read: ReadSignal<T>,
903        write: Callback<(T, ChangeOrigin)>,
904        commit: Callback<()>,
905    ) -> Self {
906        Self::new_with_identity(read, write, commit, (read, write, commit))
907    }
908
909    /// Creates a binding with a producer-defined comparable identity.
910    ///
911    /// Equal identities must always represent interchangeable read, write, and commit behavior.
912    /// This constructor installs no-op Focus Exit behavior, so that behavior is interchangeable as
913    /// well. Producers that cannot prove interchangeability should use [`Binding::new`] instead.
914    pub fn new_with_identity<I>(
915        read: ReadSignal<T>,
916        write: Callback<(T, ChangeOrigin)>,
917        commit: Callback<()>,
918        identity: I,
919    ) -> Self
920    where
921        I: PartialEq + 'static,
922    {
923        Self {
924            read,
925            write,
926            commit,
927            focus_exit: Callback::new(|()| {}),
928            identity: BindingIdentity::new(identity),
929        }
930    }
931
932    /// Adds the callback invoked when focus leaves the widget's complete logical focus scope.
933    ///
934    /// This builder also incorporates the callback into binding identity, preserving the guarantee
935    /// that equal bindings have interchangeable Focus Exit behavior. It does not alter Commit or
936    /// imply any form-library blur, touched, or validation semantics.
937    #[must_use]
938    pub fn with_focus_exit(mut self, focus_exit: Callback<()>) -> Self {
939        self.identity = BindingIdentity::new((self.identity, focus_exit));
940        self.focus_exit = focus_exit;
941        self
942    }
943
944    /// Adds Focus Exit behavior covered by the binding's existing comparable identity.
945    ///
946    /// Unlike [`Binding::with_focus_exit`], this builder does not incorporate the callback's
947    /// allocation identity. Calling it asserts that bindings with equal existing identities also
948    /// have interchangeable Focus Exit behavior, in addition to interchangeable read, write, and
949    /// Commit behavior. Producers that cannot prove this must use [`Binding::with_focus_exit`]
950    /// instead.
951    ///
952    /// This builder does not alter Commit or imply any form-library blur, touched, or validation
953    /// semantics.
954    #[must_use]
955    pub fn with_focus_exit_using_identity(mut self, focus_exit: Callback<()>) -> Self {
956        self.focus_exit = focus_exit;
957        self
958    }
959
960    /// Writes a value and preserves where the change originated.
961    pub fn write(&self, value: T, origin: ChangeOrigin) {
962        self.write.call((value, origin));
963    }
964
965    /// Reports the widget-defined end of one interaction unit.
966    pub fn commit(&self) {
967        self.commit.call(());
968    }
969
970    /// Reports that focus left the widget's complete logical focus scope.
971    ///
972    /// This is independent from [`Binding::commit`]. Widgets are responsible for defining their
973    /// complete scope, including owned child controls and popup or portal content, and for
974    /// suppressing reports while focus moves within it.
975    pub fn focus_exit(&self) {
976        self.focus_exit.call(());
977    }
978
979    /// Decomposes this binding into the dependency-free widget prop contract.
980    ///
981    /// The lower-level `on_change` callback has no origin parameter, so its writes are user writes.
982    pub fn into_trio(self) -> BindingPropTrio<T> {
983        let value = self.read;
984        let on_commit = self.commit;
985        let on_change = Callback::new(move |value| self.write(value, ChangeOrigin::User));
986
987        BindingPropTrio {
988            value,
989            on_change,
990            on_commit,
991        }
992    }
993}
994
995impl<T: fmt::Debug + 'static> fmt::Debug for Binding<T> {
996    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
997        f.debug_struct("Binding")
998            .field("read", &*self.read.peek())
999            .finish_non_exhaustive()
1000    }
1001}
1002
1003impl<T: 'static> Clone for Binding<T> {
1004    fn clone(&self) -> Self {
1005        Self {
1006            read: self.read,
1007            write: self.write,
1008            commit: self.commit,
1009            focus_exit: self.focus_exit,
1010            identity: self.identity.clone(),
1011        }
1012    }
1013}
1014
1015impl<T: 'static> PartialEq for Binding<T> {
1016    fn eq(&self, other: &Self) -> bool {
1017        self.identity == other.identity
1018    }
1019}
1020
1021impl<T: 'static> From<Signal<T>> for Binding<T> {
1022    fn from(signal: Signal<T>) -> Self {
1023        let read = ReadSignal::from(signal);
1024        let mut writer = signal;
1025        let write = Callback::new(move |(value, _origin)| writer.set(value));
1026        let commit = Callback::new(|()| {});
1027
1028        Self::new_with_identity(read, write, commit, signal)
1029    }
1030}
1031
1032impl<T: 'static> From<(ReadSignal<T>, Callback<T>)> for Binding<T> {
1033    fn from((read, on_change): (ReadSignal<T>, Callback<T>)) -> Self {
1034        let write = Callback::new(move |(value, _origin)| on_change.call(value));
1035        let commit = Callback::new(|()| {});
1036
1037        Self::new_with_identity(read, write, commit, (read, on_change))
1038    }
1039}
1040
1041impl<T: 'static> From<T> for Binding<T> {
1042    fn from(value: T) -> Self {
1043        Signal::new(value).into()
1044    }
1045}
1046
1047/// A carrier for the lower-level prop contract implemented by field-shaped widgets.
1048///
1049/// Decompose this carrier into three separate props to keep a widget independent from this crate.
1050/// Since `on_change` does not carry a [`ChangeOrigin`], calling it represents a user change.
1051pub struct BindingPropTrio<T: 'static> {
1052    /// The reactive value read by the widget.
1053    pub value: ReadSignal<T>,
1054    /// The callback invoked when user interaction changes the value.
1055    pub on_change: Callback<T>,
1056    /// The callback invoked at the widget-defined end of an interaction unit.
1057    pub on_commit: Callback<()>,
1058}
1059
1060impl<T: fmt::Debug + 'static> fmt::Debug for BindingPropTrio<T> {
1061    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1062        f.debug_struct("BindingPropTrio")
1063            .field("value", &*self.value.peek())
1064            .field("on_change", &self.on_change)
1065            .field("on_commit", &self.on_commit)
1066            .finish()
1067    }
1068}
1069
1070impl<T: 'static> From<Binding<T>> for BindingPropTrio<T> {
1071    fn from(binding: Binding<T>) -> Self {
1072        binding.into_trio()
1073    }
1074}
1075
1076/// A field-scoped slot through which a widget exposes its focus behavior.
1077#[derive(Clone, Default)]
1078pub struct FocusRequest(Rc<RefCell<FocusRequestState>>);
1079
1080impl FocusRequest {
1081    /// Registers the callback used by [`FocusRequest::request`].
1082    ///
1083    /// Dropping the returned registration removes this callback without disturbing a newer
1084    /// registration in the same slot.
1085    #[must_use]
1086    pub fn register(&self, callback: Callback<()>) -> FocusRegistration {
1087        let mut state = self.0.borrow_mut();
1088        let token = state.next_token;
1089        state.next_token += 1;
1090        state.current = Some((token, callback));
1091
1092        FocusRegistration {
1093            request: self.clone(),
1094            token,
1095        }
1096    }
1097
1098    /// Requests focus from the currently registered widget.
1099    ///
1100    /// Returns whether a widget was registered to receive the request.
1101    pub fn request(&self) -> bool {
1102        let callback = self.0.borrow().current.map(|(_, callback)| callback);
1103
1104        if let Some(callback) = callback {
1105            callback.call(());
1106            true
1107        } else {
1108            false
1109        }
1110    }
1111}
1112
1113impl fmt::Debug for FocusRequest {
1114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1115        f.debug_struct("FocusRequest")
1116            .field("registered", &self.0.borrow().current.is_some())
1117            .finish()
1118    }
1119}
1120
1121impl PartialEq for FocusRequest {
1122    fn eq(&self, other: &Self) -> bool {
1123        Rc::ptr_eq(&self.0, &other.0)
1124    }
1125}
1126
1127#[derive(Default)]
1128struct FocusRequestState {
1129    next_token: u64,
1130    current: Option<(u64, Callback<()>)>,
1131}
1132
1133/// A lifecycle-bound focus callback registration.
1134pub struct FocusRegistration {
1135    request: FocusRequest,
1136    token: u64,
1137}
1138
1139impl fmt::Debug for FocusRegistration {
1140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1141        f.debug_struct("FocusRegistration")
1142            .field("request", &self.request)
1143            .field("token", &self.token)
1144            .finish()
1145    }
1146}
1147
1148impl Drop for FocusRegistration {
1149    fn drop(&mut self) {
1150        let mut state = self.request.0.borrow_mut();
1151
1152        if state.current.is_some_and(|(token, _)| token == self.token) {
1153            state.current = None;
1154        }
1155    }
1156}
1157
1158/// A requested binding type did not match the binding stored in a [`FieldContext`].
1159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1160pub struct BindingTypeMismatch {
1161    actual_type_name: &'static str,
1162    requested_type_name: &'static str,
1163}
1164
1165impl BindingTypeMismatch {
1166    /// Returns the value type of the binding stored in the context.
1167    pub const fn actual_type_name(&self) -> &'static str {
1168        self.actual_type_name
1169    }
1170
1171    /// Returns the value type requested by the control.
1172    pub const fn requested_type_name(&self) -> &'static str {
1173        self.requested_type_name
1174    }
1175}
1176
1177impl fmt::Display for BindingTypeMismatch {
1178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1179        write!(
1180            f,
1181            "Field Context contains a binding for {}, but a binding for {} was requested",
1182            self.actual_type_name, self.requested_type_name
1183        )
1184    }
1185}
1186
1187impl std::error::Error for BindingTypeMismatch {}
1188
1189/// Type-erased context for one field's binding, metadata, and focus request slot.
1190///
1191/// The context itself is intentionally not generic. This lets [`use_binding`] distinguish an
1192/// absent context from a present context containing the wrong value type, and lets [`Field`]
1193/// accept any value type without becoming generic itself.
1194///
1195/// # Equality
1196///
1197/// Two contexts are equal when their bindings are equal under [`Binding`]'s identity equality and
1198/// their metadata is equal, regardless of when either context was constructed. The focus request
1199/// slot is intentionally excluded: [`Field`] pins the slot of the first context it receives for
1200/// its lifetime, so the slot carried by a context built on a later render is never observed by
1201/// descendants, and comparing it would only defeat memoization.
1202#[derive(Clone)]
1203pub struct FieldContext {
1204    binding: Option<ErasedBinding>,
1205    meta: Option<FieldMeta>,
1206    meta_values: Option<FieldMetaValues>,
1207    focus_request: FocusRequest,
1208}
1209
1210impl FieldContext {
1211    /// Creates context for a binding.
1212    pub fn new<T: 'static>(binding: Binding<T>) -> Self {
1213        Self {
1214            binding: Some(ErasedBinding::new(binding)),
1215            meta: None,
1216            meta_values: None,
1217            focus_request: FocusRequest::default(),
1218        }
1219    }
1220
1221    /// Creates context with no value binding or metadata.
1222    pub fn empty() -> Self {
1223        Self {
1224            binding: None,
1225            meta: None,
1226            meta_values: None,
1227            focus_request: FocusRequest::default(),
1228        }
1229    }
1230
1231    /// Replaces the context's value binding.
1232    #[must_use]
1233    pub fn with_binding<T: 'static>(mut self, binding: Binding<T>) -> Self {
1234        self.binding = Some(ErasedBinding::new(binding));
1235        self
1236    }
1237
1238    /// Adds signal-backed metadata to the context.
1239    #[must_use]
1240    pub fn with_meta(mut self, meta: FieldMeta) -> Self {
1241        self.meta = Some(meta);
1242        self.meta_values = None;
1243        self
1244    }
1245
1246    /// Adds producer values that [`Field`] realizes as signal-backed metadata.
1247    #[must_use]
1248    pub fn with_meta_values(mut self, values: FieldMetaValues) -> Self {
1249        self.meta = None;
1250        self.meta_values = Some(values);
1251        self
1252    }
1253
1254    /// Returns the context's metadata, when present.
1255    pub fn meta(&self) -> Option<FieldMeta> {
1256        self.meta
1257    }
1258
1259    /// Returns the context's focus request slot.
1260    ///
1261    /// [`Field`] pins the slot of the first context it receives, so producers that request focus
1262    /// through a context must keep that context stable across renders.
1263    pub fn focus_request(&self) -> FocusRequest {
1264        self.focus_request.clone()
1265    }
1266
1267    /// Resolves the context binding for `T`.
1268    ///
1269    /// # Panics
1270    ///
1271    /// Panics when the field context contains no binding or a binding for a different value type.
1272    pub fn resolve<T: 'static>(&self) -> Binding<T> {
1273        match self.try_resolve() {
1274            Ok(Some(binding)) => binding,
1275            Ok(None) => panic!("Field Context contains no value binding"),
1276            Err(mismatch) => panic!("{mismatch}"),
1277        }
1278    }
1279
1280    /// Tries to resolve the context binding for `T` without panicking.
1281    ///
1282    /// `Ok(Some(_))` contains a matching binding, `Ok(None)` means the context has no value
1283    /// binding, and `Err(_)` reports the actual and requested value types when they differ. This
1284    /// lets a control support more than one binding type while preserving absence as the signal to
1285    /// use standalone state.
1286    ///
1287    /// # Errors
1288    ///
1289    /// Returns [`BindingTypeMismatch`] when the context contains a binding whose value type is not
1290    /// `T`.
1291    ///
1292    /// ```rust
1293    /// use dioxus_field::{Binding, BindingTypeMismatch, FieldContext};
1294    ///
1295    /// #[derive(Clone, Copy)]
1296    /// enum CheckboxState {
1297    ///     Checked,
1298    ///     Indeterminate,
1299    ///     Unchecked,
1300    /// }
1301    ///
1302    /// enum CheckboxBinding {
1303    ///     State(Binding<CheckboxState>),
1304    ///     Boolean(Binding<bool>),
1305    /// }
1306    ///
1307    /// fn resolve_checkbox_binding(
1308    ///     context: &FieldContext,
1309    /// ) -> Result<Option<CheckboxBinding>, BindingTypeMismatch> {
1310    ///     match context.try_resolve::<CheckboxState>() {
1311    ///         Ok(Some(binding)) => Ok(Some(CheckboxBinding::State(binding))),
1312    ///         Ok(None) => Ok(None),
1313    ///         Err(_) => context
1314    ///             .try_resolve::<bool>()
1315    ///             .map(|binding| binding.map(CheckboxBinding::Boolean)),
1316    ///     }
1317    /// }
1318    /// ```
1319    pub fn try_resolve<T: 'static>(&self) -> Result<Option<Binding<T>>, BindingTypeMismatch> {
1320        let Some(erased) = &self.binding else {
1321            return Ok(None);
1322        };
1323
1324        let binding = erased
1325            .binding
1326            .downcast_ref::<Binding<T>>()
1327            .ok_or(BindingTypeMismatch {
1328                actual_type_name: erased.value_type_name,
1329                requested_type_name: std::any::type_name::<T>(),
1330            })?;
1331
1332        Ok(Some(binding.clone()))
1333    }
1334
1335    fn with_focus_request(mut self, focus_request: FocusRequest) -> Self {
1336        self.focus_request = focus_request;
1337        self
1338    }
1339}
1340
1341impl fmt::Debug for FieldContext {
1342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1343        f.debug_struct("FieldContext")
1344            .field(
1345                "value_type_name",
1346                &self.binding.as_ref().map(|binding| binding.value_type_name),
1347            )
1348            .field("meta", &self.meta)
1349            .field("meta_values", &self.meta_values)
1350            .field("focus_request", &self.focus_request)
1351            .finish_non_exhaustive()
1352    }
1353}
1354
1355impl PartialEq for FieldContext {
1356    fn eq(&self, other: &Self) -> bool {
1357        self.binding == other.binding
1358            && self.meta == other.meta
1359            && self.meta_values == other.meta_values
1360    }
1361}
1362
1363impl<T: 'static> From<Binding<T>> for FieldContext {
1364    fn from(binding: Binding<T>) -> Self {
1365        Self::new(binding)
1366    }
1367}
1368
1369impl<T: 'static> From<Signal<T>> for FieldContext {
1370    fn from(signal: Signal<T>) -> Self {
1371        Self::new(Binding::<T>::from(signal))
1372    }
1373}
1374
1375/// A value binding erased to `dyn Any` together with the comparator for its concrete type.
1376///
1377/// The comparator is captured at erasure time so [`FieldContext`] equality can delegate to
1378/// [`Binding`]'s identity equality instead of comparing wrapper allocations.
1379#[derive(Clone)]
1380struct ErasedBinding {
1381    binding: Rc<dyn Any>,
1382    value_type_name: &'static str,
1383    eq: fn(&dyn Any, &dyn Any) -> bool,
1384}
1385
1386impl ErasedBinding {
1387    fn new<T: 'static>(binding: Binding<T>) -> Self {
1388        Self {
1389            binding: Rc::new(binding),
1390            value_type_name: std::any::type_name::<T>(),
1391            eq: |left, right| match (
1392                left.downcast_ref::<Binding<T>>(),
1393                right.downcast_ref::<Binding<T>>(),
1394            ) {
1395                (Some(left), Some(right)) => left == right,
1396                _ => false,
1397            },
1398        }
1399    }
1400}
1401
1402impl PartialEq for ErasedBinding {
1403    fn eq(&self, other: &Self) -> bool {
1404        (self.eq)(&*self.binding, &*other.binding)
1405    }
1406}
1407
1408/// Provides a binding as the current scope's [`FieldContext`].
1409///
1410/// The provided context keeps the focus request slot of the context this scope provided on an
1411/// earlier render, so widgets that memoized on that render stay registered with the slot producers
1412/// observe.
1413pub fn provide_field_context<T: 'static>(binding: Binding<T>) -> FieldContext {
1414    let mut context = FieldContext::new(binding);
1415
1416    if let Some(existing) = has_context::<FieldContext>() {
1417        context = context.with_focus_request(existing.focus_request());
1418    }
1419
1420    provide_context(context)
1421}
1422
1423/// Resolves a binding using explicit prop, [`FieldContext`], then uncontrolled-state precedence.
1424///
1425/// The internal signal hook is called regardless of which source wins so the resolution order can
1426/// change between renders without violating Dioxus's hook ordering rules.
1427///
1428/// # Panics
1429///
1430/// Panics when there is no explicit binding and the Field Context contains a binding for a value
1431/// type other than `T`.
1432pub fn use_binding<T: 'static>(explicit: Option<Binding<T>>, default: T) -> Binding<T> {
1433    let internal = use_signal(|| default);
1434
1435    if let Some(binding) = explicit {
1436        return binding;
1437    }
1438
1439    if let Some(context) = try_consume_context::<FieldContext>() {
1440        match context.try_resolve() {
1441            Ok(Some(binding)) => return binding,
1442            Ok(None) => {}
1443            Err(mismatch) => panic!("{mismatch}"),
1444        }
1445    }
1446
1447    internal.into()
1448}
1449
1450/// Resolves metadata using explicit prop, [`FieldContext`], then standalone-state precedence.
1451///
1452/// The standalone state hook is always called so the source can change between renders without
1453/// violating Dioxus's hook ordering rules.
1454pub fn use_field_meta(explicit: Option<FieldMeta>) -> FieldMeta {
1455    use_resolved_field_meta(explicit.as_ref()).0
1456}
1457
1458/// Where a part's resolved metadata came from.
1459///
1460/// A part that renders a reference *to a control* needs this: an id resolved from metadata nobody
1461/// else holds addresses no rendered element, so the reference would dangle.
1462#[derive(Clone, Copy, PartialEq, Eq)]
1463enum FieldMetaSource {
1464    /// An explicit prop or the Field Context. A control may hold the same metadata, and if the
1465    /// caller passed it deliberately, one is meant to.
1466    Shared,
1467    /// This part's own standalone state, which by construction no control is reading.
1468    Standalone,
1469}
1470
1471/// Resolves metadata as [`use_field_meta`] does, and reports which source won.
1472fn use_resolved_field_meta(explicit: Option<&FieldMeta>) -> (FieldMeta, FieldMetaSource) {
1473    let internal = use_field_meta_state(FieldMetaValues::default());
1474
1475    explicit
1476        .copied()
1477        .or_else(|| try_consume_context::<FieldContext>().and_then(|context| context.meta()))
1478        .map_or((internal, FieldMetaSource::Standalone), |meta| {
1479            (meta, FieldMetaSource::Shared)
1480        })
1481}
1482
1483/// Resolves the current [`FocusRequest`], or creates a standalone slot when no context exists.
1484pub fn use_focus_request() -> FocusRequest {
1485    let internal = use_hook(FocusRequest::default);
1486
1487    try_consume_context::<FieldContext>().map_or(internal, |context| context.focus_request())
1488}
1489
1490/// Registers a widget focus callback with the resolved [`FocusRequest`] for this component's
1491/// lifetime.
1492///
1493/// # The callback must be render-stable
1494///
1495/// Pass a callback whose identity survives a re-render — [`dioxus_hooks::use_callback`] produces
1496/// one. [`Callback::new`] does not: it allocates a fresh generational box per call and compares by
1497/// pointer identity, so calling it in a component body re-registers on **every** render. One slot
1498/// is shared by the whole field, so a widget that re-registers steals the slot from a sibling
1499/// widget that legitimately owns it, and focus ownership ends up decided by render recency rather
1500/// than by structure. Each re-registration also leaks a generational box until the component
1501/// unmounts.
1502///
1503/// # Which element to register
1504///
1505/// Register the element that actually receives focus, and let the callback do nothing when that
1506/// element cannot take focus — a disabled control should not move focus at all. Never focus a
1507/// proxy element and never blur: both hand the user's focus to something they did not ask for,
1508/// and `HTMLElement.focus()` reports success either way, so nothing downstream can detect it.
1509pub fn use_focus_registration(callback: Callback<()>) -> FocusRequest {
1510    let request = use_focus_request();
1511    let active = use_hook(|| Rc::new(RefCell::new(None::<ActiveFocusRegistration>)));
1512    let should_replace = active
1513        .borrow()
1514        .as_ref()
1515        .is_none_or(|active| active.request != request || active.callback != callback);
1516
1517    if should_replace {
1518        let registration = request.register(callback);
1519        active.borrow_mut().replace(ActiveFocusRegistration {
1520            request: request.clone(),
1521            callback,
1522            _registration: registration,
1523        });
1524    }
1525
1526    request
1527}
1528
1529struct ActiveFocusRegistration {
1530    request: FocusRequest,
1531    callback: Callback<()>,
1532    _registration: FocusRegistration,
1533}
1534
1535/// Props for the headless [`Field`] context provider.
1536#[derive(Clone, Debug, Props, PartialEq)]
1537pub struct FieldProps {
1538    /// The [`FieldContext`] provided to descendants.
1539    ///
1540    /// Accepts a [`FieldContext`], a [`Binding`], or a [`Signal`]. The prop is named after its
1541    /// payload rather than the binding it may carry, since a context can also hold only metadata.
1542    #[props(into)]
1543    pub context: FieldContext,
1544    /// Attributes forwarded to the rendered `div`.
1545    ///
1546    /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1547    /// of any spread list. A forwarded attribute wins its name.
1548    #[props(extends = GlobalAttributes)]
1549    pub attributes: Vec<Attribute>,
1550    /// Field content.
1551    pub children: Element,
1552}
1553
1554/// Provides one [`FieldContext`] and renders an unstyled `div` around its children.
1555///
1556/// On Dioxus 0.7.10, pass listeners through an explicit `attributes: vec![...]` prop so listener
1557/// ordering remains visible at the call site.
1558///
1559/// # Memoization
1560///
1561/// Children authored inline in `rsx!` and inline listener attributes compare unequal on every
1562/// parent render, so a `Field` receiving either re-renders with its parent regardless of
1563/// [`FieldContext`] equality. Forwarding a received element through the `children` prop keeps it
1564/// comparable; context equality then decides whether `Field` re-renders.
1565#[allow(non_snake_case)]
1566#[allow(
1567    clippy::missing_errors_doc,
1568    reason = "Dioxus Element uses Result as its renderer protocol"
1569)]
1570pub fn Field(props: FieldProps) -> Element {
1571    let has_meta_values = props.context.meta_values.is_some();
1572    let meta_values = props.context.meta_values.clone().unwrap_or_default();
1573    let synced_meta = use_synced_field_meta_state(&meta_values);
1574    let mut context = props.context;
1575
1576    if has_meta_values {
1577        context = context.with_meta(synced_meta);
1578    }
1579
1580    let focus_request = use_hook(|| context.focus_request());
1581    provide_context(context.with_focus_request(focus_request));
1582    let mut attributes = props.attributes;
1583    normalize_attributes(&mut attributes);
1584
1585    rsx! {
1586        div { ..attributes, {props.children} }
1587    }
1588}
1589
1590/// Props for the headless [`Label`] part.
1591#[derive(Clone, Debug, Props, PartialEq)]
1592pub struct LabelProps {
1593    /// The rendered `label`'s own id, registered with the resolved metadata for this part's
1594    /// lifetime. Defaults to a generated id.
1595    ///
1596    /// The control reaches this id through `aria-labelledby`, which is the only naming path
1597    /// available to a control rooted on an element `<label for>` cannot address — `for` requires a
1598    /// labelable element, and widgets rooted on a `div` are not one.
1599    #[props(default)]
1600    pub id: Option<Rc<str>>,
1601    /// Explicit metadata, which wins over Field Context metadata.
1602    #[props(default)]
1603    pub meta: Option<FieldMeta>,
1604    /// Explicit invalid state, which wins over the metadata state.
1605    #[props(default)]
1606    pub invalid: Option<bool>,
1607    /// Explicit disabled state, which wins over the metadata state.
1608    #[props(default)]
1609    pub disabled: Option<bool>,
1610    /// Explicit required state, which wins over the metadata state.
1611    #[props(default)]
1612    pub required: Option<bool>,
1613    /// Attributes forwarded to the rendered `label`.
1614    ///
1615    /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1616    /// of any spread list. A forwarded attribute wins its name.
1617    #[props(extends = GlobalAttributes)]
1618    pub attributes: Vec<Attribute>,
1619    /// Label content.
1620    pub children: Element,
1621}
1622
1623/// Renders an unstyled `label` associated with the resolved metadata's control id.
1624///
1625/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1626/// Running standalone means no control shares the metadata, so no `for` is emitted — the label
1627/// still carries its own id, which is the reference a control would reach through
1628/// `aria-labelledby`.
1629#[allow(non_snake_case)]
1630#[allow(
1631    clippy::missing_errors_doc,
1632    reason = "Dioxus Element uses Result as its renderer protocol"
1633)]
1634pub fn Label(props: LabelProps) -> Element {
1635    let (meta, source) = use_resolved_field_meta(props.meta.as_ref());
1636    let id = use_part_id(props.id, "label");
1637    use_field_meta_id_registration(&meta, RegisteredIdKind::Label, Rc::clone(&id));
1638    // Metadata the label resolved for itself alone addresses no rendered control, so pointing
1639    // `for` at its generated id would dangle. Emitting nothing is what 0.1.0 did, and is honest.
1640    let control_id = (source == FieldMetaSource::Shared).then(|| meta.id().to_string());
1641    let attributes = part_attributes(
1642        &meta,
1643        FieldStateOverrides {
1644            invalid: props.invalid,
1645            disabled: props.disabled,
1646            required: props.required,
1647        },
1648        props.attributes,
1649    );
1650
1651    rsx! {
1652        label { id: id.to_string(), r#for: control_id, ..attributes, {props.children} }
1653    }
1654}
1655
1656/// Props for the headless [`FieldDescription`] part.
1657#[derive(Clone, Debug, Props, PartialEq)]
1658pub struct FieldDescriptionProps {
1659    /// The rendered description's own id, registered with the resolved field metadata for this
1660    /// part's lifetime. Defaults to a generated id.
1661    #[props(default)]
1662    pub id: Option<Rc<str>>,
1663    /// Explicit metadata, which wins over Field Context metadata.
1664    #[props(default)]
1665    pub meta: Option<FieldMeta>,
1666    /// Explicit invalid state, which wins over the metadata state.
1667    #[props(default)]
1668    pub invalid: Option<bool>,
1669    /// Explicit disabled state, which wins over the metadata state.
1670    #[props(default)]
1671    pub disabled: Option<bool>,
1672    /// Explicit required state, which wins over the metadata state.
1673    #[props(default)]
1674    pub required: Option<bool>,
1675    /// Attributes forwarded to the rendered description `div`.
1676    ///
1677    /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1678    /// of any spread list. A forwarded attribute wins its name.
1679    #[props(extends = GlobalAttributes)]
1680    pub attributes: Vec<Attribute>,
1681    /// Description content.
1682    pub children: Element,
1683}
1684
1685/// Renders an unstyled description and registers its id for `aria-describedby` chaining.
1686///
1687/// When no id is supplied, this part generates one that remains stable for its mounted lifetime.
1688///
1689/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1690#[allow(non_snake_case)]
1691#[allow(
1692    clippy::missing_errors_doc,
1693    reason = "Dioxus Element uses Result as its renderer protocol"
1694)]
1695pub fn FieldDescription(props: FieldDescriptionProps) -> Element {
1696    let meta = use_field_meta(props.meta);
1697    let id = use_part_id(props.id, "description");
1698    use_field_meta_id_registration(&meta, RegisteredIdKind::Description, Rc::clone(&id));
1699    let attributes = part_attributes(
1700        &meta,
1701        FieldStateOverrides {
1702            invalid: props.invalid,
1703            disabled: props.disabled,
1704            required: props.required,
1705        },
1706        props.attributes,
1707    );
1708
1709    rsx! {
1710        div { id: id.to_string(), ..attributes, {props.children} }
1711    }
1712}
1713
1714/// Props for the headless [`FieldError`] part.
1715#[derive(Clone, Debug, Props, PartialEq)]
1716pub struct FieldErrorProps {
1717    /// The rendered error region's own id, registered with the resolved field metadata for this
1718    /// part's lifetime. Defaults to a generated id.
1719    #[props(default)]
1720    pub id: Option<Rc<str>>,
1721    /// Explicit metadata, which wins over Field Context metadata.
1722    #[props(default)]
1723    pub meta: Option<FieldMeta>,
1724    /// Explicit invalid state, which wins over the metadata state.
1725    #[props(default)]
1726    pub invalid: Option<bool>,
1727    /// Explicit disabled state used by data-state attributes.
1728    #[props(default)]
1729    pub disabled: Option<bool>,
1730    /// Explicit required state used by data-state attributes.
1731    #[props(default)]
1732    pub required: Option<bool>,
1733    /// Attributes forwarded to the rendered error `div`.
1734    ///
1735    /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1736    /// of any spread list. A forwarded attribute wins its name.
1737    #[props(extends = GlobalAttributes)]
1738    pub attributes: Vec<Attribute>,
1739}
1740
1741/// Renders pre-formatted field errors in an unstyled polite live region, one element per error.
1742///
1743/// The live region stays mounted while the field is valid, holding no children. A live region that
1744/// enters the accessibility tree in the same update as its content is not announced reliably, so
1745/// the region has to exist before the first error arrives.
1746///
1747/// When no id is supplied, this part generates one that remains stable for its mounted lifetime.
1748///
1749/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1750#[allow(non_snake_case)]
1751#[allow(
1752    clippy::missing_errors_doc,
1753    reason = "Dioxus Element uses Result as its renderer protocol"
1754)]
1755pub fn FieldError(props: FieldErrorProps) -> Element {
1756    let meta = use_field_meta(props.meta);
1757    let id = use_part_id(props.id, "error");
1758    use_field_meta_id_registration(&meta, RegisteredIdKind::Error, Rc::clone(&id));
1759    let invalid = props.invalid.unwrap_or_else(|| meta.invalid());
1760    let errors = if invalid { meta.errors() } else { Vec::new() };
1761    let attributes = part_attributes(
1762        &meta,
1763        FieldStateOverrides {
1764            invalid: props.invalid,
1765            disabled: props.disabled,
1766            required: props.required,
1767        },
1768        props.attributes,
1769    );
1770
1771    rsx! {
1772        div {
1773            id: id.to_string(),
1774            aria_live: "polite",
1775            ..attributes,
1776            for error in errors {
1777                div { "{error}" }
1778            }
1779        }
1780    }
1781}
1782
1783/// Builds a field part's `data-*` state attributes and appends the caller's forwarded ones.
1784///
1785/// Forwarded attributes go last, so a caller's attribute wins its name once
1786/// [`normalize_attributes`] resolves the result.
1787fn part_attributes(
1788    meta: &FieldMeta,
1789    overrides: FieldStateOverrides,
1790    forwarded: Vec<Attribute>,
1791) -> Vec<Attribute> {
1792    let required = overrides.required.unwrap_or_else(|| meta.required());
1793    let disabled = overrides.disabled.unwrap_or_else(|| meta.disabled());
1794    let invalid = overrides.invalid.unwrap_or_else(|| meta.invalid());
1795    let mut attributes = Vec::new();
1796
1797    push_state(&mut attributes, "data-required", required);
1798    push_state(&mut attributes, "data-disabled", disabled);
1799    push_state(&mut attributes, "data-invalid", invalid);
1800    push_state(&mut attributes, "data-touched", meta.touched());
1801    push_state(&mut attributes, "data-dirty", meta.dirty());
1802
1803    attributes.extend(forwarded);
1804    normalize_attributes(&mut attributes);
1805
1806    attributes
1807}
1808
1809fn use_field_meta_id_registration(meta: &FieldMeta, kind: RegisteredIdKind, id: Rc<str>) {
1810    let active = use_hook(|| Rc::new(RefCell::new(None::<ActiveFieldMetaIdRegistration>)));
1811    let should_replace = active
1812        .borrow()
1813        .as_ref()
1814        .is_none_or(|active| active.meta != *meta || active.kind != kind || active.id != id);
1815
1816    if should_replace {
1817        let mut writable_meta = *meta;
1818        let registration = writable_meta.register_id(kind, id.clone());
1819        active.borrow_mut().replace(ActiveFieldMetaIdRegistration {
1820            meta: *meta,
1821            kind,
1822            id,
1823            _registration: registration,
1824        });
1825    }
1826}
1827
1828struct ActiveFieldMetaIdRegistration {
1829    meta: FieldMeta,
1830    kind: RegisteredIdKind,
1831    id: Rc<str>,
1832    _registration: FieldMetaIdRegistration,
1833}
1834
1835#[derive(Clone)]
1836struct BindingIdentity(Rc<dyn ComparableIdentity>);
1837
1838impl BindingIdentity {
1839    fn new<I: PartialEq + 'static>(identity: I) -> Self {
1840        Self(Rc::new(identity))
1841    }
1842}
1843
1844impl PartialEq for BindingIdentity {
1845    fn eq(&self, other: &Self) -> bool {
1846        self.0.equals(other.0.as_ref())
1847    }
1848}
1849
1850trait ComparableIdentity: Any {
1851    fn equals(&self, other: &dyn ComparableIdentity) -> bool;
1852}
1853
1854impl<I: PartialEq + 'static> ComparableIdentity for I {
1855    fn equals(&self, other: &dyn ComparableIdentity) -> bool {
1856        let other = other as &dyn Any;
1857        other.downcast_ref::<I>().is_some_and(|other| self == other)
1858    }
1859}