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`].
900    pub fn new(
901        read: ReadSignal<T>,
902        write: Callback<(T, ChangeOrigin)>,
903        commit: Callback<()>,
904    ) -> Self {
905        Self::new_with_identity(read, write, commit, (read, write, commit))
906    }
907
908    /// Creates a binding with a producer-defined comparable identity.
909    ///
910    /// Equal identities must always represent interchangeable read, write, and commit behavior.
911    /// This constructor installs no-op Focus Exit behavior, so that behavior is interchangeable as
912    /// well. Producers that cannot prove interchangeability should use [`Binding::new`] instead.
913    pub fn new_with_identity<I>(
914        read: ReadSignal<T>,
915        write: Callback<(T, ChangeOrigin)>,
916        commit: Callback<()>,
917        identity: I,
918    ) -> Self
919    where
920        I: PartialEq + 'static,
921    {
922        Self {
923            read,
924            write,
925            commit,
926            focus_exit: Callback::new(|()| {}),
927            identity: BindingIdentity::new(identity),
928        }
929    }
930
931    /// Adds the callback invoked when focus leaves the widget's complete logical focus scope.
932    ///
933    /// This builder also incorporates the callback into binding identity, preserving the guarantee
934    /// that equal bindings have interchangeable Focus Exit behavior. It does not alter Commit or
935    /// imply any form-library blur, touched, or validation semantics.
936    #[must_use]
937    pub fn with_focus_exit(mut self, focus_exit: Callback<()>) -> Self {
938        self.identity = BindingIdentity::new((self.identity, focus_exit));
939        self.focus_exit = focus_exit;
940        self
941    }
942
943    /// Writes a value and preserves where the change originated.
944    pub fn write(&self, value: T, origin: ChangeOrigin) {
945        self.write.call((value, origin));
946    }
947
948    /// Reports the widget-defined end of one interaction unit.
949    pub fn commit(&self) {
950        self.commit.call(());
951    }
952
953    /// Reports that focus left the widget's complete logical focus scope.
954    ///
955    /// This is independent from [`Binding::commit`]. Widgets are responsible for defining their
956    /// complete scope, including owned child controls and popup or portal content, and for
957    /// suppressing reports while focus moves within it.
958    pub fn focus_exit(&self) {
959        self.focus_exit.call(());
960    }
961
962    /// Decomposes this binding into the dependency-free widget prop contract.
963    ///
964    /// The lower-level `on_change` callback has no origin parameter, so its writes are user writes.
965    pub fn into_trio(self) -> BindingPropTrio<T> {
966        let value = self.read;
967        let on_commit = self.commit;
968        let on_change = Callback::new(move |value| self.write(value, ChangeOrigin::User));
969
970        BindingPropTrio {
971            value,
972            on_change,
973            on_commit,
974        }
975    }
976}
977
978impl<T: fmt::Debug + 'static> fmt::Debug for Binding<T> {
979    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
980        f.debug_struct("Binding")
981            .field("read", &*self.read.peek())
982            .finish_non_exhaustive()
983    }
984}
985
986impl<T: 'static> Clone for Binding<T> {
987    fn clone(&self) -> Self {
988        Self {
989            read: self.read,
990            write: self.write,
991            commit: self.commit,
992            focus_exit: self.focus_exit,
993            identity: self.identity.clone(),
994        }
995    }
996}
997
998impl<T: 'static> PartialEq for Binding<T> {
999    fn eq(&self, other: &Self) -> bool {
1000        self.identity == other.identity
1001    }
1002}
1003
1004impl<T: 'static> From<Signal<T>> for Binding<T> {
1005    fn from(signal: Signal<T>) -> Self {
1006        let read = ReadSignal::from(signal);
1007        let mut writer = signal;
1008        let write = Callback::new(move |(value, _origin)| writer.set(value));
1009        let commit = Callback::new(|()| {});
1010
1011        Self::new_with_identity(read, write, commit, signal)
1012    }
1013}
1014
1015impl<T: 'static> From<(ReadSignal<T>, Callback<T>)> for Binding<T> {
1016    fn from((read, on_change): (ReadSignal<T>, Callback<T>)) -> Self {
1017        let write = Callback::new(move |(value, _origin)| on_change.call(value));
1018        let commit = Callback::new(|()| {});
1019
1020        Self::new_with_identity(read, write, commit, (read, on_change))
1021    }
1022}
1023
1024impl<T: 'static> From<T> for Binding<T> {
1025    fn from(value: T) -> Self {
1026        Signal::new(value).into()
1027    }
1028}
1029
1030/// A carrier for the lower-level prop contract implemented by field-shaped widgets.
1031///
1032/// Decompose this carrier into three separate props to keep a widget independent from this crate.
1033/// Since `on_change` does not carry a [`ChangeOrigin`], calling it represents a user change.
1034pub struct BindingPropTrio<T: 'static> {
1035    /// The reactive value read by the widget.
1036    pub value: ReadSignal<T>,
1037    /// The callback invoked when user interaction changes the value.
1038    pub on_change: Callback<T>,
1039    /// The callback invoked at the widget-defined end of an interaction unit.
1040    pub on_commit: Callback<()>,
1041}
1042
1043impl<T: fmt::Debug + 'static> fmt::Debug for BindingPropTrio<T> {
1044    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1045        f.debug_struct("BindingPropTrio")
1046            .field("value", &*self.value.peek())
1047            .field("on_change", &self.on_change)
1048            .field("on_commit", &self.on_commit)
1049            .finish()
1050    }
1051}
1052
1053impl<T: 'static> From<Binding<T>> for BindingPropTrio<T> {
1054    fn from(binding: Binding<T>) -> Self {
1055        binding.into_trio()
1056    }
1057}
1058
1059/// A field-scoped slot through which a widget exposes its focus behavior.
1060#[derive(Clone, Default)]
1061pub struct FocusRequest(Rc<RefCell<FocusRequestState>>);
1062
1063impl FocusRequest {
1064    /// Registers the callback used by [`FocusRequest::request`].
1065    ///
1066    /// Dropping the returned registration removes this callback without disturbing a newer
1067    /// registration in the same slot.
1068    #[must_use]
1069    pub fn register(&self, callback: Callback<()>) -> FocusRegistration {
1070        let mut state = self.0.borrow_mut();
1071        let token = state.next_token;
1072        state.next_token += 1;
1073        state.current = Some((token, callback));
1074
1075        FocusRegistration {
1076            request: self.clone(),
1077            token,
1078        }
1079    }
1080
1081    /// Requests focus from the currently registered widget.
1082    ///
1083    /// Returns whether a widget was registered to receive the request.
1084    pub fn request(&self) -> bool {
1085        let callback = self.0.borrow().current.map(|(_, callback)| callback);
1086
1087        if let Some(callback) = callback {
1088            callback.call(());
1089            true
1090        } else {
1091            false
1092        }
1093    }
1094}
1095
1096impl fmt::Debug for FocusRequest {
1097    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1098        f.debug_struct("FocusRequest")
1099            .field("registered", &self.0.borrow().current.is_some())
1100            .finish()
1101    }
1102}
1103
1104impl PartialEq for FocusRequest {
1105    fn eq(&self, other: &Self) -> bool {
1106        Rc::ptr_eq(&self.0, &other.0)
1107    }
1108}
1109
1110#[derive(Default)]
1111struct FocusRequestState {
1112    next_token: u64,
1113    current: Option<(u64, Callback<()>)>,
1114}
1115
1116/// A lifecycle-bound focus callback registration.
1117pub struct FocusRegistration {
1118    request: FocusRequest,
1119    token: u64,
1120}
1121
1122impl fmt::Debug for FocusRegistration {
1123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1124        f.debug_struct("FocusRegistration")
1125            .field("request", &self.request)
1126            .field("token", &self.token)
1127            .finish()
1128    }
1129}
1130
1131impl Drop for FocusRegistration {
1132    fn drop(&mut self) {
1133        let mut state = self.request.0.borrow_mut();
1134
1135        if state.current.is_some_and(|(token, _)| token == self.token) {
1136            state.current = None;
1137        }
1138    }
1139}
1140
1141/// Type-erased context for one field's binding, metadata, and focus request slot.
1142///
1143/// The context itself is intentionally not generic. This lets [`use_binding`] distinguish an
1144/// absent context from a present context containing the wrong value type, and lets [`Field`]
1145/// accept any value type without becoming generic itself.
1146///
1147/// # Equality
1148///
1149/// Two contexts are equal when their bindings are equal under [`Binding`]'s identity equality and
1150/// their metadata is equal, regardless of when either context was constructed. The focus request
1151/// slot is intentionally excluded: [`Field`] pins the slot of the first context it receives for
1152/// its lifetime, so the slot carried by a context built on a later render is never observed by
1153/// descendants, and comparing it would only defeat memoization.
1154#[derive(Clone)]
1155pub struct FieldContext {
1156    binding: Option<ErasedBinding>,
1157    meta: Option<FieldMeta>,
1158    meta_values: Option<FieldMetaValues>,
1159    focus_request: FocusRequest,
1160}
1161
1162impl FieldContext {
1163    /// Creates context for a binding.
1164    pub fn new<T: 'static>(binding: Binding<T>) -> Self {
1165        Self {
1166            binding: Some(ErasedBinding::new(binding)),
1167            meta: None,
1168            meta_values: None,
1169            focus_request: FocusRequest::default(),
1170        }
1171    }
1172
1173    /// Creates context with no value binding or metadata.
1174    pub fn empty() -> Self {
1175        Self {
1176            binding: None,
1177            meta: None,
1178            meta_values: None,
1179            focus_request: FocusRequest::default(),
1180        }
1181    }
1182
1183    /// Replaces the context's value binding.
1184    #[must_use]
1185    pub fn with_binding<T: 'static>(mut self, binding: Binding<T>) -> Self {
1186        self.binding = Some(ErasedBinding::new(binding));
1187        self
1188    }
1189
1190    /// Adds signal-backed metadata to the context.
1191    #[must_use]
1192    pub fn with_meta(mut self, meta: FieldMeta) -> Self {
1193        self.meta = Some(meta);
1194        self.meta_values = None;
1195        self
1196    }
1197
1198    /// Adds producer values that [`Field`] realizes as signal-backed metadata.
1199    #[must_use]
1200    pub fn with_meta_values(mut self, values: FieldMetaValues) -> Self {
1201        self.meta = None;
1202        self.meta_values = Some(values);
1203        self
1204    }
1205
1206    /// Returns the context's metadata, when present.
1207    pub fn meta(&self) -> Option<FieldMeta> {
1208        self.meta
1209    }
1210
1211    /// Returns the context's focus request slot.
1212    ///
1213    /// [`Field`] pins the slot of the first context it receives, so producers that request focus
1214    /// through a context must keep that context stable across renders.
1215    pub fn focus_request(&self) -> FocusRequest {
1216        self.focus_request.clone()
1217    }
1218
1219    /// Resolves the context binding for `T`.
1220    ///
1221    /// # Panics
1222    ///
1223    /// Panics when the field context contains no binding or a binding for a different value type.
1224    pub fn resolve<T: 'static>(&self) -> Binding<T> {
1225        let erased = self
1226            .binding
1227            .as_ref()
1228            .unwrap_or_else(|| panic!("Field Context contains no value binding"));
1229
1230        erased
1231            .binding
1232            .downcast_ref::<Binding<T>>()
1233            .unwrap_or_else(|| {
1234                panic!(
1235                    "Field Context contains a binding for {}, but a binding for {} was requested",
1236                    erased.value_type_name,
1237                    std::any::type_name::<T>()
1238                )
1239            })
1240            .clone()
1241    }
1242
1243    fn try_resolve<T: 'static>(&self) -> Option<Binding<T>> {
1244        self.binding.as_ref().map(|_| self.resolve())
1245    }
1246
1247    fn with_focus_request(mut self, focus_request: FocusRequest) -> Self {
1248        self.focus_request = focus_request;
1249        self
1250    }
1251}
1252
1253impl fmt::Debug for FieldContext {
1254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1255        f.debug_struct("FieldContext")
1256            .field(
1257                "value_type_name",
1258                &self.binding.as_ref().map(|binding| binding.value_type_name),
1259            )
1260            .field("meta", &self.meta)
1261            .field("meta_values", &self.meta_values)
1262            .field("focus_request", &self.focus_request)
1263            .finish_non_exhaustive()
1264    }
1265}
1266
1267impl PartialEq for FieldContext {
1268    fn eq(&self, other: &Self) -> bool {
1269        self.binding == other.binding
1270            && self.meta == other.meta
1271            && self.meta_values == other.meta_values
1272    }
1273}
1274
1275impl<T: 'static> From<Binding<T>> for FieldContext {
1276    fn from(binding: Binding<T>) -> Self {
1277        Self::new(binding)
1278    }
1279}
1280
1281impl<T: 'static> From<Signal<T>> for FieldContext {
1282    fn from(signal: Signal<T>) -> Self {
1283        Self::new(Binding::<T>::from(signal))
1284    }
1285}
1286
1287/// A value binding erased to `dyn Any` together with the comparator for its concrete type.
1288///
1289/// The comparator is captured at erasure time so [`FieldContext`] equality can delegate to
1290/// [`Binding`]'s identity equality instead of comparing wrapper allocations.
1291#[derive(Clone)]
1292struct ErasedBinding {
1293    binding: Rc<dyn Any>,
1294    value_type_name: &'static str,
1295    eq: fn(&dyn Any, &dyn Any) -> bool,
1296}
1297
1298impl ErasedBinding {
1299    fn new<T: 'static>(binding: Binding<T>) -> Self {
1300        Self {
1301            binding: Rc::new(binding),
1302            value_type_name: std::any::type_name::<T>(),
1303            eq: |left, right| match (
1304                left.downcast_ref::<Binding<T>>(),
1305                right.downcast_ref::<Binding<T>>(),
1306            ) {
1307                (Some(left), Some(right)) => left == right,
1308                _ => false,
1309            },
1310        }
1311    }
1312}
1313
1314impl PartialEq for ErasedBinding {
1315    fn eq(&self, other: &Self) -> bool {
1316        (self.eq)(&*self.binding, &*other.binding)
1317    }
1318}
1319
1320/// Provides a binding as the current scope's [`FieldContext`].
1321///
1322/// The provided context keeps the focus request slot of the context this scope provided on an
1323/// earlier render, so widgets that memoized on that render stay registered with the slot producers
1324/// observe.
1325pub fn provide_field_context<T: 'static>(binding: Binding<T>) -> FieldContext {
1326    let mut context = FieldContext::new(binding);
1327
1328    if let Some(existing) = has_context::<FieldContext>() {
1329        context = context.with_focus_request(existing.focus_request());
1330    }
1331
1332    provide_context(context)
1333}
1334
1335/// Resolves a binding using explicit prop, [`FieldContext`], then uncontrolled-state precedence.
1336///
1337/// The internal signal hook is called regardless of which source wins so the resolution order can
1338/// change between renders without violating Dioxus's hook ordering rules.
1339pub fn use_binding<T: 'static>(explicit: Option<Binding<T>>, default: T) -> Binding<T> {
1340    let internal = use_signal(|| default);
1341
1342    if let Some(binding) = explicit {
1343        return binding;
1344    }
1345
1346    if let Some(binding) =
1347        try_consume_context::<FieldContext>().and_then(|context| context.try_resolve())
1348    {
1349        return binding;
1350    }
1351
1352    internal.into()
1353}
1354
1355/// Resolves metadata using explicit prop, [`FieldContext`], then standalone-state precedence.
1356///
1357/// The standalone state hook is always called so the source can change between renders without
1358/// violating Dioxus's hook ordering rules.
1359pub fn use_field_meta(explicit: Option<FieldMeta>) -> FieldMeta {
1360    use_resolved_field_meta(explicit.as_ref()).0
1361}
1362
1363/// Where a part's resolved metadata came from.
1364///
1365/// A part that renders a reference *to a control* needs this: an id resolved from metadata nobody
1366/// else holds addresses no rendered element, so the reference would dangle.
1367#[derive(Clone, Copy, PartialEq, Eq)]
1368enum FieldMetaSource {
1369    /// An explicit prop or the Field Context. A control may hold the same metadata, and if the
1370    /// caller passed it deliberately, one is meant to.
1371    Shared,
1372    /// This part's own standalone state, which by construction no control is reading.
1373    Standalone,
1374}
1375
1376/// Resolves metadata as [`use_field_meta`] does, and reports which source won.
1377fn use_resolved_field_meta(explicit: Option<&FieldMeta>) -> (FieldMeta, FieldMetaSource) {
1378    let internal = use_field_meta_state(FieldMetaValues::default());
1379
1380    explicit
1381        .copied()
1382        .or_else(|| try_consume_context::<FieldContext>().and_then(|context| context.meta()))
1383        .map_or((internal, FieldMetaSource::Standalone), |meta| {
1384            (meta, FieldMetaSource::Shared)
1385        })
1386}
1387
1388/// Resolves the current [`FocusRequest`], or creates a standalone slot when no context exists.
1389pub fn use_focus_request() -> FocusRequest {
1390    let internal = use_hook(FocusRequest::default);
1391
1392    try_consume_context::<FieldContext>().map_or(internal, |context| context.focus_request())
1393}
1394
1395/// Registers a widget focus callback with the resolved [`FocusRequest`] for this component's
1396/// lifetime.
1397///
1398/// # The callback must be render-stable
1399///
1400/// Pass a callback whose identity survives a re-render — [`dioxus_hooks::use_callback`] produces
1401/// one. [`Callback::new`] does not: it allocates a fresh generational box per call and compares by
1402/// pointer identity, so calling it in a component body re-registers on **every** render. One slot
1403/// is shared by the whole field, so a widget that re-registers steals the slot from a sibling
1404/// widget that legitimately owns it, and focus ownership ends up decided by render recency rather
1405/// than by structure. Each re-registration also leaks a generational box until the component
1406/// unmounts.
1407///
1408/// # Which element to register
1409///
1410/// Register the element that actually receives focus, and let the callback do nothing when that
1411/// element cannot take focus — a disabled control should not move focus at all. Never focus a
1412/// proxy element and never blur: both hand the user's focus to something they did not ask for,
1413/// and `HTMLElement.focus()` reports success either way, so nothing downstream can detect it.
1414pub fn use_focus_registration(callback: Callback<()>) -> FocusRequest {
1415    let request = use_focus_request();
1416    let active = use_hook(|| Rc::new(RefCell::new(None::<ActiveFocusRegistration>)));
1417    let should_replace = active
1418        .borrow()
1419        .as_ref()
1420        .is_none_or(|active| active.request != request || active.callback != callback);
1421
1422    if should_replace {
1423        let registration = request.register(callback);
1424        active.borrow_mut().replace(ActiveFocusRegistration {
1425            request: request.clone(),
1426            callback,
1427            _registration: registration,
1428        });
1429    }
1430
1431    request
1432}
1433
1434struct ActiveFocusRegistration {
1435    request: FocusRequest,
1436    callback: Callback<()>,
1437    _registration: FocusRegistration,
1438}
1439
1440/// Props for the headless [`Field`] context provider.
1441#[derive(Clone, Debug, Props, PartialEq)]
1442pub struct FieldProps {
1443    /// The [`FieldContext`] provided to descendants.
1444    ///
1445    /// Accepts a [`FieldContext`], a [`Binding`], or a [`Signal`]. The prop is named after its
1446    /// payload rather than the binding it may carry, since a context can also hold only metadata.
1447    #[props(into)]
1448    pub context: FieldContext,
1449    /// Attributes forwarded to the rendered `div`.
1450    ///
1451    /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1452    /// of any spread list. A forwarded attribute wins its name.
1453    #[props(extends = GlobalAttributes)]
1454    pub attributes: Vec<Attribute>,
1455    /// Field content.
1456    pub children: Element,
1457}
1458
1459/// Provides one [`FieldContext`] and renders an unstyled `div` around its children.
1460///
1461/// On Dioxus 0.7.10, pass listeners through an explicit `attributes: vec![...]` prop so listener
1462/// ordering remains visible at the call site.
1463///
1464/// # Memoization
1465///
1466/// Children authored inline in `rsx!` and inline listener attributes compare unequal on every
1467/// parent render, so a `Field` receiving either re-renders with its parent regardless of
1468/// [`FieldContext`] equality. Forwarding a received element through the `children` prop keeps it
1469/// comparable; context equality then decides whether `Field` re-renders.
1470#[allow(non_snake_case)]
1471#[allow(
1472    clippy::missing_errors_doc,
1473    reason = "Dioxus Element uses Result as its renderer protocol"
1474)]
1475pub fn Field(props: FieldProps) -> Element {
1476    let has_meta_values = props.context.meta_values.is_some();
1477    let meta_values = props.context.meta_values.clone().unwrap_or_default();
1478    let synced_meta = use_synced_field_meta_state(&meta_values);
1479    let mut context = props.context;
1480
1481    if has_meta_values {
1482        context = context.with_meta(synced_meta);
1483    }
1484
1485    let focus_request = use_hook(|| context.focus_request());
1486    provide_context(context.with_focus_request(focus_request));
1487    let mut attributes = props.attributes;
1488    normalize_attributes(&mut attributes);
1489
1490    rsx! {
1491        div { ..attributes, {props.children} }
1492    }
1493}
1494
1495/// Props for the headless [`Label`] part.
1496#[derive(Clone, Debug, Props, PartialEq)]
1497pub struct LabelProps {
1498    /// The rendered `label`'s own id, registered with the resolved metadata for this part's
1499    /// lifetime. Defaults to a generated id.
1500    ///
1501    /// The control reaches this id through `aria-labelledby`, which is the only naming path
1502    /// available to a control rooted on an element `<label for>` cannot address — `for` requires a
1503    /// labelable element, and widgets rooted on a `div` are not one.
1504    #[props(default)]
1505    pub id: Option<Rc<str>>,
1506    /// Explicit metadata, which wins over Field Context metadata.
1507    #[props(default)]
1508    pub meta: Option<FieldMeta>,
1509    /// Explicit invalid state, which wins over the metadata state.
1510    #[props(default)]
1511    pub invalid: Option<bool>,
1512    /// Explicit disabled state, which wins over the metadata state.
1513    #[props(default)]
1514    pub disabled: Option<bool>,
1515    /// Explicit required state, which wins over the metadata state.
1516    #[props(default)]
1517    pub required: Option<bool>,
1518    /// Attributes forwarded to the rendered `label`.
1519    ///
1520    /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1521    /// of any spread list. A forwarded attribute wins its name.
1522    #[props(extends = GlobalAttributes)]
1523    pub attributes: Vec<Attribute>,
1524    /// Label content.
1525    pub children: Element,
1526}
1527
1528/// Renders an unstyled `label` associated with the resolved metadata's control id.
1529///
1530/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1531/// Running standalone means no control shares the metadata, so no `for` is emitted — the label
1532/// still carries its own id, which is the reference a control would reach through
1533/// `aria-labelledby`.
1534#[allow(non_snake_case)]
1535#[allow(
1536    clippy::missing_errors_doc,
1537    reason = "Dioxus Element uses Result as its renderer protocol"
1538)]
1539pub fn Label(props: LabelProps) -> Element {
1540    let (meta, source) = use_resolved_field_meta(props.meta.as_ref());
1541    let id = use_part_id(props.id, "label");
1542    use_field_meta_id_registration(&meta, RegisteredIdKind::Label, Rc::clone(&id));
1543    // Metadata the label resolved for itself alone addresses no rendered control, so pointing
1544    // `for` at its generated id would dangle. Emitting nothing is what 0.1.0 did, and is honest.
1545    let control_id = (source == FieldMetaSource::Shared).then(|| meta.id().to_string());
1546    let attributes = part_attributes(
1547        &meta,
1548        FieldStateOverrides {
1549            invalid: props.invalid,
1550            disabled: props.disabled,
1551            required: props.required,
1552        },
1553        props.attributes,
1554    );
1555
1556    rsx! {
1557        label { id: id.to_string(), r#for: control_id, ..attributes, {props.children} }
1558    }
1559}
1560
1561/// Props for the headless [`FieldDescription`] part.
1562#[derive(Clone, Debug, Props, PartialEq)]
1563pub struct FieldDescriptionProps {
1564    /// The rendered description's own id, registered with the resolved field metadata for this
1565    /// part's lifetime. Defaults to a generated id.
1566    #[props(default)]
1567    pub id: Option<Rc<str>>,
1568    /// Explicit metadata, which wins over Field Context metadata.
1569    #[props(default)]
1570    pub meta: Option<FieldMeta>,
1571    /// Explicit invalid state, which wins over the metadata state.
1572    #[props(default)]
1573    pub invalid: Option<bool>,
1574    /// Explicit disabled state, which wins over the metadata state.
1575    #[props(default)]
1576    pub disabled: Option<bool>,
1577    /// Explicit required state, which wins over the metadata state.
1578    #[props(default)]
1579    pub required: Option<bool>,
1580    /// Attributes forwarded to the rendered description `div`.
1581    ///
1582    /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1583    /// of any spread list. A forwarded attribute wins its name.
1584    #[props(extends = GlobalAttributes)]
1585    pub attributes: Vec<Attribute>,
1586    /// Description content.
1587    pub children: Element,
1588}
1589
1590/// Renders an unstyled description and registers its id for `aria-describedby` chaining.
1591///
1592/// When no id is supplied, this part generates one that remains stable for its mounted lifetime.
1593///
1594/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1595#[allow(non_snake_case)]
1596#[allow(
1597    clippy::missing_errors_doc,
1598    reason = "Dioxus Element uses Result as its renderer protocol"
1599)]
1600pub fn FieldDescription(props: FieldDescriptionProps) -> Element {
1601    let meta = use_field_meta(props.meta);
1602    let id = use_part_id(props.id, "description");
1603    use_field_meta_id_registration(&meta, RegisteredIdKind::Description, Rc::clone(&id));
1604    let attributes = part_attributes(
1605        &meta,
1606        FieldStateOverrides {
1607            invalid: props.invalid,
1608            disabled: props.disabled,
1609            required: props.required,
1610        },
1611        props.attributes,
1612    );
1613
1614    rsx! {
1615        div { id: id.to_string(), ..attributes, {props.children} }
1616    }
1617}
1618
1619/// Props for the headless [`FieldError`] part.
1620#[derive(Clone, Debug, Props, PartialEq)]
1621pub struct FieldErrorProps {
1622    /// The rendered error region's own id, registered with the resolved field metadata for this
1623    /// part's lifetime. Defaults to a generated id.
1624    #[props(default)]
1625    pub id: Option<Rc<str>>,
1626    /// Explicit metadata, which wins over Field Context metadata.
1627    #[props(default)]
1628    pub meta: Option<FieldMeta>,
1629    /// Explicit invalid state, which wins over the metadata state.
1630    #[props(default)]
1631    pub invalid: Option<bool>,
1632    /// Explicit disabled state used by data-state attributes.
1633    #[props(default)]
1634    pub disabled: Option<bool>,
1635    /// Explicit required state used by data-state attributes.
1636    #[props(default)]
1637    pub required: Option<bool>,
1638    /// Attributes forwarded to the rendered error `div`.
1639    ///
1640    /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1641    /// of any spread list. A forwarded attribute wins its name.
1642    #[props(extends = GlobalAttributes)]
1643    pub attributes: Vec<Attribute>,
1644}
1645
1646/// Renders pre-formatted field errors in an unstyled polite live region, one element per error.
1647///
1648/// The live region stays mounted while the field is valid, holding no children. A live region that
1649/// enters the accessibility tree in the same update as its content is not announced reliably, so
1650/// the region has to exist before the first error arrives.
1651///
1652/// When no id is supplied, this part generates one that remains stable for its mounted lifetime.
1653///
1654/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1655#[allow(non_snake_case)]
1656#[allow(
1657    clippy::missing_errors_doc,
1658    reason = "Dioxus Element uses Result as its renderer protocol"
1659)]
1660pub fn FieldError(props: FieldErrorProps) -> Element {
1661    let meta = use_field_meta(props.meta);
1662    let id = use_part_id(props.id, "error");
1663    use_field_meta_id_registration(&meta, RegisteredIdKind::Error, Rc::clone(&id));
1664    let invalid = props.invalid.unwrap_or_else(|| meta.invalid());
1665    let errors = if invalid { meta.errors() } else { Vec::new() };
1666    let attributes = part_attributes(
1667        &meta,
1668        FieldStateOverrides {
1669            invalid: props.invalid,
1670            disabled: props.disabled,
1671            required: props.required,
1672        },
1673        props.attributes,
1674    );
1675
1676    rsx! {
1677        div {
1678            id: id.to_string(),
1679            aria_live: "polite",
1680            ..attributes,
1681            for error in errors {
1682                div { "{error}" }
1683            }
1684        }
1685    }
1686}
1687
1688/// Builds a field part's `data-*` state attributes and appends the caller's forwarded ones.
1689///
1690/// Forwarded attributes go last, so a caller's attribute wins its name once
1691/// [`normalize_attributes`] resolves the result.
1692fn part_attributes(
1693    meta: &FieldMeta,
1694    overrides: FieldStateOverrides,
1695    forwarded: Vec<Attribute>,
1696) -> Vec<Attribute> {
1697    let required = overrides.required.unwrap_or_else(|| meta.required());
1698    let disabled = overrides.disabled.unwrap_or_else(|| meta.disabled());
1699    let invalid = overrides.invalid.unwrap_or_else(|| meta.invalid());
1700    let mut attributes = Vec::new();
1701
1702    push_state(&mut attributes, "data-required", required);
1703    push_state(&mut attributes, "data-disabled", disabled);
1704    push_state(&mut attributes, "data-invalid", invalid);
1705    push_state(&mut attributes, "data-touched", meta.touched());
1706    push_state(&mut attributes, "data-dirty", meta.dirty());
1707
1708    attributes.extend(forwarded);
1709    normalize_attributes(&mut attributes);
1710
1711    attributes
1712}
1713
1714fn use_field_meta_id_registration(meta: &FieldMeta, kind: RegisteredIdKind, id: Rc<str>) {
1715    let active = use_hook(|| Rc::new(RefCell::new(None::<ActiveFieldMetaIdRegistration>)));
1716    let should_replace = active
1717        .borrow()
1718        .as_ref()
1719        .is_none_or(|active| active.meta != *meta || active.kind != kind || active.id != id);
1720
1721    if should_replace {
1722        let mut writable_meta = *meta;
1723        let registration = writable_meta.register_id(kind, id.clone());
1724        active.borrow_mut().replace(ActiveFieldMetaIdRegistration {
1725            meta: *meta,
1726            kind,
1727            id,
1728            _registration: registration,
1729        });
1730    }
1731}
1732
1733struct ActiveFieldMetaIdRegistration {
1734    meta: FieldMeta,
1735    kind: RegisteredIdKind,
1736    id: Rc<str>,
1737    _registration: FieldMetaIdRegistration,
1738}
1739
1740#[derive(Clone)]
1741struct BindingIdentity(Rc<dyn ComparableIdentity>);
1742
1743impl BindingIdentity {
1744    fn new<I: PartialEq + 'static>(identity: I) -> Self {
1745        Self(Rc::new(identity))
1746    }
1747}
1748
1749impl PartialEq for BindingIdentity {
1750    fn eq(&self, other: &Self) -> bool {
1751        self.0.equals(other.0.as_ref())
1752    }
1753}
1754
1755trait ComparableIdentity: Any {
1756    fn equals(&self, other: &dyn ComparableIdentity) -> bool;
1757}
1758
1759impl<I: PartialEq + 'static> ComparableIdentity for I {
1760    fn equals(&self, other: &dyn ComparableIdentity) -> bool {
1761        let other = other as &dyn Any;
1762        other.downcast_ref::<I>().is_some_and(|other| self == other)
1763    }
1764}