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