@radix-ng/primitives 1.0.10 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +26 -5
  2. package/fesm2022/radix-ng-primitives-autocomplete.mjs +88 -21
  3. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -1
  4. package/fesm2022/radix-ng-primitives-checkbox.mjs +199 -129
  5. package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
  6. package/fesm2022/radix-ng-primitives-combobox.mjs +203 -52
  7. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  8. package/fesm2022/radix-ng-primitives-composite.mjs +221 -36
  9. package/fesm2022/radix-ng-primitives-composite.mjs.map +1 -1
  10. package/fesm2022/radix-ng-primitives-core.mjs +352 -24
  11. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  12. package/fesm2022/radix-ng-primitives-date-field.mjs +11 -5
  13. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  14. package/fesm2022/radix-ng-primitives-field.mjs +127 -23
  15. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
  16. package/fesm2022/radix-ng-primitives-form.mjs +35 -4
  17. package/fesm2022/radix-ng-primitives-form.mjs.map +1 -1
  18. package/fesm2022/radix-ng-primitives-input.mjs +87 -15
  19. package/fesm2022/radix-ng-primitives-input.mjs.map +1 -1
  20. package/fesm2022/radix-ng-primitives-menu.mjs +34 -7
  21. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  22. package/fesm2022/radix-ng-primitives-number-field.mjs +38 -7
  23. package/fesm2022/radix-ng-primitives-number-field.mjs.map +1 -1
  24. package/fesm2022/radix-ng-primitives-popper.mjs +80 -11
  25. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  26. package/fesm2022/radix-ng-primitives-radio.mjs +49 -2
  27. package/fesm2022/radix-ng-primitives-radio.mjs.map +1 -1
  28. package/fesm2022/radix-ng-primitives-scroll-area.mjs +6 -3
  29. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  30. package/fesm2022/radix-ng-primitives-select.mjs +70 -14
  31. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  32. package/fesm2022/radix-ng-primitives-signal-forms.mjs +27 -11
  33. package/fesm2022/radix-ng-primitives-signal-forms.mjs.map +1 -1
  34. package/fesm2022/radix-ng-primitives-slider.mjs +17 -2
  35. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  36. package/fesm2022/radix-ng-primitives-switch.mjs +60 -7
  37. package/fesm2022/radix-ng-primitives-switch.mjs.map +1 -1
  38. package/fesm2022/radix-ng-primitives-time-field.mjs +12 -6
  39. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  40. package/fesm2022/radix-ng-primitives-toggle-group.mjs +25 -3
  41. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  42. package/package.json +8 -3
  43. package/types/radix-ng-primitives-autocomplete.d.ts +16 -2
  44. package/types/radix-ng-primitives-checkbox.d.ts +66 -44
  45. package/types/radix-ng-primitives-combobox.d.ts +74 -3
  46. package/types/radix-ng-primitives-composite.d.ts +48 -2
  47. package/types/radix-ng-primitives-core.d.ts +212 -96
  48. package/types/radix-ng-primitives-date-field.d.ts +7 -4
  49. package/types/radix-ng-primitives-field.d.ts +55 -8
  50. package/types/radix-ng-primitives-form.d.ts +9 -0
  51. package/types/radix-ng-primitives-input.d.ts +10 -2
  52. package/types/radix-ng-primitives-menu.d.ts +45 -4
  53. package/types/radix-ng-primitives-navigation-menu.d.ts +2 -2
  54. package/types/radix-ng-primitives-number-field.d.ts +12 -5
  55. package/types/radix-ng-primitives-popover.d.ts +2 -2
  56. package/types/radix-ng-primitives-popper.d.ts +60 -15
  57. package/types/radix-ng-primitives-preview-card.d.ts +2 -2
  58. package/types/radix-ng-primitives-radio.d.ts +6 -1
  59. package/types/radix-ng-primitives-select.d.ts +25 -4
  60. package/types/radix-ng-primitives-signal-forms.d.ts +22 -8
  61. package/types/radix-ng-primitives-slider.d.ts +2 -1
  62. package/types/radix-ng-primitives-switch.d.ts +19 -3
  63. package/types/radix-ng-primitives-time-field.d.ts +9 -6
  64. package/types/radix-ng-primitives-toggle-group.d.ts +5 -1
  65. package/types/radix-ng-primitives-tooltip.d.ts +2 -2
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
- import { forwardRef, input, booleanAttribute, output, linkedSignal, untracked, Directive, inject, isDevMode, HOST_TAG_NAME, ElementRef, InjectionToken, computed, APP_ID, Injectable, DOCUMENT, PLATFORM_ID, DestroyRef, signal, model, effect, afterNextRender, assertInInjectionContext, Injector } from '@angular/core';
3
- import { NG_VALUE_ACCESSOR } from '@angular/forms';
2
+ import { forwardRef, input, booleanAttribute, output, linkedSignal, untracked, Directive, inject, Injector, DestroyRef, signal, computed, isDevMode, HOST_TAG_NAME, ElementRef, InjectionToken, APP_ID, Injectable, DOCUMENT, PLATFORM_ID, model, effect, afterNextRender, assertInInjectionContext } from '@angular/core';
3
+ import { NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
4
4
  import { getLocalTimeZone, CalendarDateTime, ZonedDateTime, getDayOfWeek, DateFormatter, createCalendar, toCalendar, CalendarDate, Time, startOfMonth, endOfMonth, today } from '@internationalized/date';
5
5
  import { isPlatformBrowser, DOCUMENT as DOCUMENT$1 } from '@angular/common';
6
6
 
@@ -113,6 +113,103 @@ function injectControlValueAccessor() {
113
113
  return inject(RdxControlValueAccessor);
114
114
  }
115
115
 
116
+ function toValidationErrors(errors) {
117
+ if (!errors) {
118
+ return [];
119
+ }
120
+ return Object.entries(errors).map(([kind, details]) => {
121
+ const message = typeof details === 'string'
122
+ ? details
123
+ : details && typeof details === 'object' && typeof details['message'] === 'string'
124
+ ? details['message']
125
+ : undefined;
126
+ return message === undefined ? { kind } : { kind, message };
127
+ });
128
+ }
129
+ /**
130
+ * Lazily connects to the same-host `NgControl` without creating a circular dependency while Angular
131
+ * is resolving its `ControlValueAccessor`. The unified `AbstractControl.events` stream mirrors
132
+ * programmatic value, status, validation, and interaction-state changes as well as user-driven ones.
133
+ *
134
+ * Must be called in an injection context.
135
+ *
136
+ * @ignore
137
+ */
138
+ function injectNgControlState() {
139
+ const injector = inject(Injector);
140
+ const destroyRef = inject(DestroyRef);
141
+ const connected = signal(false, { debugName: 'RdxNgControlState.connected' });
142
+ const name = signal(undefined, { debugName: 'RdxNgControlState.name' });
143
+ const value = signal(undefined, { debugName: 'RdxNgControlState.value' });
144
+ const valid = signal(false, { debugName: 'RdxNgControlState.valid' });
145
+ const invalid = signal(false, { debugName: 'RdxNgControlState.invalid' });
146
+ const pending = signal(false, { debugName: 'RdxNgControlState.pending' });
147
+ const disabled = signal(false, { debugName: 'RdxNgControlState.disabled' });
148
+ const rawErrors = signal(null, { debugName: 'RdxNgControlState.rawErrors' });
149
+ const errors = computed(() => toValidationErrors(rawErrors()), {
150
+ debugName: 'RdxNgControlState.errors'
151
+ });
152
+ const dirty = signal(false, { debugName: 'RdxNgControlState.dirty' });
153
+ const touched = signal(false, { debugName: 'RdxNgControlState.touched' });
154
+ let destroyed = false;
155
+ let unsubscribe;
156
+ let resetControl;
157
+ const connect = () => {
158
+ if (destroyed) {
159
+ return;
160
+ }
161
+ const ngControl = injector.get(NgControl, null, { self: true, optional: true });
162
+ const control = ngControl?.control;
163
+ // Signal Forms' `FormField` is also discoverable through Angular's forms DI integration, but
164
+ // it is not an `AbstractControl` and owns state through signals instead of `control.events`.
165
+ if (!control || typeof control.events?.subscribe !== 'function') {
166
+ return;
167
+ }
168
+ resetControl = (nextValue) => control.reset(nextValue);
169
+ const sync = () => {
170
+ name.set(ngControl.name == null ? undefined : String(ngControl.name));
171
+ value.set(control.value);
172
+ valid.set(control.valid);
173
+ invalid.set(control.invalid);
174
+ pending.set(control.pending);
175
+ disabled.set(control.disabled);
176
+ rawErrors.set(control.errors);
177
+ dirty.set(control.dirty);
178
+ touched.set(control.touched);
179
+ };
180
+ sync();
181
+ const subscription = control.events.subscribe(sync);
182
+ unsubscribe = () => subscription.unsubscribe();
183
+ connected.set(true);
184
+ };
185
+ // `NgControl` resolves its value accessor from this same element. Defer the lookup until every
186
+ // directive on the host has finished constructing to avoid a DI cycle.
187
+ queueMicrotask(connect);
188
+ destroyRef.onDestroy(() => {
189
+ destroyed = true;
190
+ unsubscribe?.();
191
+ });
192
+ return {
193
+ connected: connected.asReadonly(),
194
+ name: name.asReadonly(),
195
+ value: value.asReadonly(),
196
+ valid: valid.asReadonly(),
197
+ invalid: invalid.asReadonly(),
198
+ pending: pending.asReadonly(),
199
+ disabled: disabled.asReadonly(),
200
+ errors,
201
+ dirty: dirty.asReadonly(),
202
+ touched: touched.asReadonly(),
203
+ reset: (nextValue) => {
204
+ if (!resetControl) {
205
+ return false;
206
+ }
207
+ resetControl(nextValue);
208
+ return true;
209
+ }
210
+ };
211
+ }
212
+
116
213
  /**
117
214
  * The `clamp` function restricts a number within a specified range by returning the value itself if it
118
215
  * falls within the range, or the closest boundary value if it exceeds the range.
@@ -1405,7 +1502,7 @@ function useDateField(props) {
1405
1502
  return prevValue;
1406
1503
  const str = prevValue.toString();
1407
1504
  if (str.length === 1) {
1408
- props.modelValue.set(undefined);
1505
+ props.modelValue.set(null);
1409
1506
  return null;
1410
1507
  }
1411
1508
  return Number.parseInt(str.slice(0, -1));
@@ -1899,11 +1996,30 @@ function itemToStringLabel(value) {
1899
1996
  return String(value);
1900
1997
  }
1901
1998
  /**
1902
- * Converts an item value to the string used for form serialization. Defaults to the same rules as
1903
- * {@link itemToStringLabel}; kept as a separate export so a primitive can diverge label vs. value.
1999
+ * Converts an item value to the string used for form serialization. A conventional `{ value, label }`
2000
+ * item serializes its `value` member; other non-string values use Base UI-compatible JSON serialization
2001
+ * with `String()` as a fallback. Kept separate so a primitive can diverge label vs. value.
1904
2002
  */
1905
2003
  function itemToStringValue(value) {
1906
- return itemToStringLabel(value);
2004
+ if (value !== null && typeof value === 'object' && 'value' in value && 'label' in value) {
2005
+ return serializeValue(value.value);
2006
+ }
2007
+ return serializeValue(value);
2008
+ }
2009
+ /** Base UI-compatible serialization fallback for native form values. */
2010
+ function serializeValue(value) {
2011
+ if (isNullish(value)) {
2012
+ return '';
2013
+ }
2014
+ if (typeof value === 'string') {
2015
+ return value;
2016
+ }
2017
+ try {
2018
+ return JSON.stringify(value) ?? String(value);
2019
+ }
2020
+ catch {
2021
+ return String(value);
2022
+ }
1907
2023
  }
1908
2024
  /**
1909
2025
  * Compares two item values for equality using an optional {@link ItemValueComparator}.
@@ -2035,8 +2151,8 @@ function provideExistingToken(token, type) {
2035
2151
 
2036
2152
  /**
2037
2153
  * Builds the shared form-UI state and its mutators from a control's input signals, removing the
2038
- * per-control copy-paste of the `invalidState`/`touchedState`/`dirtyState` computeds and the
2039
- * `markAsTouched`/`markDirty` logic.
2154
+ * per-control copy-paste of the validation/interaction computeds and the `markAsTouched`/`markDirty`
2155
+ * logic.
2040
2156
  *
2041
2157
  * **Why the inputs stay on the control (not in here):** Angular's compiler only discovers
2042
2158
  * `input()`/`model()` declared as field initializers, and Signal Forms binds form-written state
@@ -2049,22 +2165,58 @@ function provideExistingToken(token, type) {
2049
2165
  function createFormUiState(options) {
2050
2166
  const dirtyValue = signal(false, /* @ts-ignore */
2051
2167
  ...(ngDevMode ? [{ debugName: "dirtyValue" }] : /* istanbul ignore next */ []));
2168
+ const errorsState = computed(() => {
2169
+ const ownErrors = options.errors() ?? [];
2170
+ const ngControlErrors = options.ngControlState?.connected() ? options.ngControlState.errors() : [];
2171
+ return ngControlErrors.length > 0 ? [...ownErrors, ...ngControlErrors] : ownErrors;
2172
+ }, /* @ts-ignore */
2173
+ ...(ngDevMode ? [{ debugName: "errorsState" }] : /* istanbul ignore next */ []));
2174
+ const invalidState = computed(() => options.invalid() ||
2175
+ errorsState().length > 0 ||
2176
+ Boolean(options.ngControlState?.connected() && options.ngControlState.invalid()), /* @ts-ignore */
2177
+ ...(ngDevMode ? [{ debugName: "invalidState" }] : /* istanbul ignore next */ []));
2178
+ const pendingState = computed(() => (options.pending?.() ?? false) ||
2179
+ Boolean(options.ngControlState?.connected() && options.ngControlState.pending()), /* @ts-ignore */
2180
+ ...(ngDevMode ? [{ debugName: "pendingState" }] : /* istanbul ignore next */ []));
2181
+ const validState = computed(() => {
2182
+ if (pendingState()) {
2183
+ return null;
2184
+ }
2185
+ if (invalidState()) {
2186
+ return false;
2187
+ }
2188
+ if (options.ngControlState?.connected() && options.ngControlState.disabled()) {
2189
+ return null;
2190
+ }
2191
+ return true;
2192
+ }, /* @ts-ignore */
2193
+ ...(ngDevMode ? [{ debugName: "validState" }] : /* istanbul ignore next */ []));
2052
2194
  return {
2053
- invalidState: computed(() => options.invalid() || (options.errors()?.length ?? 0) > 0),
2054
- touchedState: computed(() => options.touched()),
2055
- dirtyState: computed(() => options.dirty() || dirtyValue()),
2195
+ invalidState,
2196
+ pendingState,
2197
+ validState,
2198
+ errorsState,
2199
+ touchedState: computed(() => options.ngControlState?.connected() ? options.ngControlState.touched() : options.touched()),
2200
+ dirtyState: computed(() => options.ngControlState?.connected() ? options.ngControlState.dirty() : options.dirty() || dirtyValue()),
2056
2201
  markDirty: () => dirtyValue.set(true),
2057
2202
  markAsTouched: () => {
2058
2203
  options.cva?.markAsTouched();
2059
2204
  options.touched.set(true);
2060
2205
  options.touch.emit();
2061
- }
2206
+ },
2207
+ resetInteractionState: () => {
2208
+ options.touched.set(false);
2209
+ dirtyValue.set(false);
2210
+ },
2211
+ resetDirtyState: () => dirtyValue.set(false)
2062
2212
  };
2063
2213
  }
2064
2214
  /** Spread helper to wire {@link RdxFormUiStateContext} fields from an {@link RdxFormUiState} into a context factory. */
2065
2215
  function formUiStateContext(state) {
2066
2216
  return {
2067
2217
  invalidState: state.invalidState,
2218
+ pendingState: state.pendingState,
2219
+ validState: state.validState,
2068
2220
  touchedState: state.touchedState,
2069
2221
  dirtyState: state.dirtyState,
2070
2222
  markAsTouched: () => state.markAsTouched()
@@ -2084,15 +2236,21 @@ const RDX_FIELD_VALIDITY = new InjectionToken('RdxFieldValidity');
2084
2236
  /**
2085
2237
  * Tri-state display validity: when inside a `Field` the field's gated `validState` is the **single
2086
2238
  * source** (the control reflects it, including its `validationMode` neutral state); standalone, the
2087
- * control's own binary invalidity.
2239
+ * control's own tri-state validity (pending is neutral).
2088
2240
  *
2089
2241
  * **Contract:** inside a `Field` a control's own `invalid` / `errors` inputs are **not** displayed — the
2090
2242
  * Field owns displayed validity. Drive validity through the Field instead: bind `rdxSignalField`
2091
2243
  * (Signal Forms), or set `[invalid]` on `rdxFieldRoot`. The control's own `invalid`/`errors` are for
2092
2244
  * standalone use.
2093
2245
  */
2094
- function resolveDisplayValid(fieldValidity, ownInvalid) {
2095
- return fieldValidity ? fieldValidity() : ownInvalid() ? false : true;
2246
+ function resolveDisplayValid(fieldValidity, ownInvalid, ownPending) {
2247
+ if (fieldValidity) {
2248
+ return fieldValidity();
2249
+ }
2250
+ if (ownPending?.()) {
2251
+ return null;
2252
+ }
2253
+ return ownInvalid() ? false : true;
2096
2254
  }
2097
2255
  /**
2098
2256
  * Provide a control's {@link RdxFormUiState} on its host element so {@link RdxFormUiStateHost} can
@@ -2129,7 +2287,7 @@ class RdxFormUiStateHost {
2129
2287
  this.formUi = inject(RDX_FORM_UI_STATE);
2130
2288
  this.fieldValidity = inject(RDX_FIELD_VALIDITY, { optional: true });
2131
2289
  /** @ignore Tri-state display validity (enclosing Field's gated state, else own invalidity). */
2132
- this.displayValid = computed(() => resolveDisplayValid(this.fieldValidity, this.formUi.invalidState), /* @ts-ignore */
2290
+ this.displayValid = computed(() => this.fieldValidity ? this.fieldValidity() : this.formUi.validState(), /* @ts-ignore */
2133
2291
  ...(ngDevMode ? [{ debugName: "displayValid" }] : /* istanbul ignore next */ []));
2134
2292
  }
2135
2293
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxFormUiStateHost, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
@@ -2152,7 +2310,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
2152
2310
  }] });
2153
2311
  /**
2154
2312
  * Abstract base that declares the optional `FormUiControl` state inputs
2155
- * (`invalid`/`errors`/`touched`/`dirty` + the `touch` output) once and builds the control's
2313
+ * (`invalid`/`pending`/`errors`/`touched`/`dirty` + the `touch` output) once and builds the control's
2156
2314
  * {@link RdxFormUiState} from them, so a control directive can inherit the whole surface with a
2157
2315
  * single `extends` instead of re-declaring it.
2158
2316
  *
@@ -2170,6 +2328,8 @@ class RdxFormUiControlBase {
2170
2328
  constructor() {
2171
2329
  /** Whether the control is invalid. A non-empty {@link errors} list also marks it invalid. */
2172
2330
  this.invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2331
+ /** Whether async validation is pending. Pending controls publish neither `data-valid` nor `data-invalid`. */
2332
+ this.pending = input(false, { ...(ngDevMode ? { debugName: "pending" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2173
2333
  /** Whether the control has been touched. A `model()` so Signal Forms can write it; set on blur/focus-out. */
2174
2334
  this.touched = model(false, /* @ts-ignore */
2175
2335
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
@@ -2180,15 +2340,20 @@ class RdxFormUiControlBase {
2180
2340
  ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
2181
2341
  /** Emits when the control is touched, notifying Signal Forms (stable Angular 22 contract). */
2182
2342
  this.touch = output();
2343
+ this.ngControlState = injectNgControlState();
2183
2344
  /** The shared form-UI state derived from the inputs above. Call `formUi.markDirty()` on value change. */
2184
2345
  this.formUi = createFormUiState({
2185
2346
  invalid: this.invalid,
2347
+ pending: this.pending,
2186
2348
  errors: this.errors,
2187
2349
  touched: this.touched,
2188
2350
  touch: this.touch,
2189
2351
  dirty: this.dirty,
2352
+ ngControlState: this.ngControlState,
2190
2353
  cva: this.formUiTouchTarget()
2191
2354
  });
2355
+ /** Validation errors from the control inputs and a same-host Reactive/template-driven form control. */
2356
+ this.validationErrors = this.formUi.errorsState;
2192
2357
  /** The enclosing Field's tri-state display validity, if any. `protected` so a control whose own
2193
2358
  * invalidity is richer than `formUi.invalidState` (e.g. date/time-field add a parse check) can build
2194
2359
  * its own `displayValid` from it. */
@@ -2196,27 +2361,58 @@ class RdxFormUiControlBase {
2196
2361
  /**
2197
2362
  * Tri-state *displayed* validity for controls that bind their own host attributes (radio, switch,
2198
2363
  * number-field): the enclosing Field's gated state when inside a `rdxFieldRoot`, else this control's
2199
- * own binary invalidity (`formUi.invalidState`). Bind `data-valid`/`data-invalid`/`aria-invalid` to
2364
+ * own validity (`formUi.pendingState` is neutral; otherwise `formUi.invalidState` is binary). Bind
2365
+ * `data-valid`/`data-invalid`/`aria-invalid` to
2200
2366
  * this so a neutral field shows neither. Controls whose `invalidState` is richer than
2201
- * `formUi.invalidState` override this with `resolveDisplayValid(this.fieldValidity, this.invalidState)`.
2367
+ * `formUi.invalidState` override this with
2368
+ * `resolveDisplayValid(this.fieldValidity, this.invalidState, this.formUi.pendingState)`.
2202
2369
  */
2203
- this.displayValid = computed(() => resolveDisplayValid(this.fieldValidity, this.formUi.invalidState), /* @ts-ignore */
2370
+ this.displayValid = computed(() => this.fieldValidity ? this.fieldValidity() : this.formUi.validState(), /* @ts-ignore */
2204
2371
  ...(ngDevMode ? [{ debugName: "displayValid" }] : /* istanbul ignore next */ []));
2372
+ // Reactive/template-driven forms own their NgControl interaction state. Signal Forms instead
2373
+ // writes the public touched/dirty members, so standalone controls retain the existing fallback.
2374
+ effect(() => {
2375
+ if (this.ngControlState.connected()) {
2376
+ this.touched.set(this.ngControlState.touched());
2377
+ if (!this.ngControlState.dirty()) {
2378
+ this.formUi.resetDirtyState?.();
2379
+ }
2380
+ return;
2381
+ }
2382
+ if (!this.dirty()) {
2383
+ this.formUi.resetDirtyState?.();
2384
+ }
2385
+ }, { debugName: 'RdxFormUiControlBase.syncInteractionState' });
2205
2386
  }
2206
2387
  /**
2207
2388
  * Override to bridge the control's `ControlValueAccessor` into `markAsTouched` (dual controls —
2208
2389
  * return `injectControlValueAccessor()` or a `{ markAsTouched }` adapter). Default: no CVA
2209
- * (Signal-Forms-only controls such as select).
2390
+ * (Signal-Forms-only controls such as date-field).
2210
2391
  */
2211
2392
  formUiTouchTarget() {
2212
2393
  return null;
2213
2394
  }
2395
+ /**
2396
+ * Resets a same-host Reactive/template-driven control without reporting the reset as a user edit.
2397
+ * Returns `false` when no Angular `NgControl` is connected, so a control can fall back to its CVA
2398
+ * change callback for an unusually early native reset.
2399
+ */
2400
+ resetNgControl(value) {
2401
+ return this.ngControlState.reset(value);
2402
+ }
2403
+ /**
2404
+ * Reset control-owned interaction state. Angular Signal Forms calls this optional custom-control
2405
+ * hook from `FieldState.reset()` after restoring the field value.
2406
+ */
2407
+ reset() {
2408
+ this.formUi.resetInteractionState?.();
2409
+ }
2214
2410
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxFormUiControlBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2215
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxFormUiControlBase, isStandalone: true, inputs: { invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touched: "touchedChange", touch: "touch" }, ngImport: i0 }); }
2411
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxFormUiControlBase, isStandalone: true, inputs: { invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, pending: { classPropertyName: "pending", publicName: "pending", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touched: "touchedChange", touch: "touch" }, ngImport: i0 }); }
2216
2412
  }
2217
2413
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxFormUiControlBase, decorators: [{
2218
2414
  type: Directive
2219
- }], propDecorators: { invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }] } });
2415
+ }], ctorParameters: () => [], propDecorators: { invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], pending: [{ type: i0.Input, args: [{ isSignal: true, alias: "pending", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }] } });
2220
2416
 
2221
2417
  /** The default validation-display mode when neither `rdxFieldRoot` nor `rdxFormRoot` sets one. */
2222
2418
  const RDX_DEFAULT_VALIDATION_MODE = 'onBlur';
@@ -3207,6 +3403,138 @@ function getActiveElement(root = document) {
3207
3403
  return activeElement;
3208
3404
  }
3209
3405
 
3406
+ /**
3407
+ * Converts a scalar or array model to HTML form values. `null` and `undefined` represent no
3408
+ * successful control; arrays deliberately become repeated entries under the same field name.
3409
+ */
3410
+ function serializeNativeFormValue(value, itemToStringValue$1 = itemToStringValue) {
3411
+ const values = Array.isArray(value) ? value : [value];
3412
+ return values.flatMap((entry) => (entry == null ? [] : [itemToStringValue$1(entry)]));
3413
+ }
3414
+ const NOOP_NATIVE_FORM_CONTROL = {
3415
+ ownerForm: () => null,
3416
+ sync: () => undefined,
3417
+ requestSubmit: () => undefined
3418
+ };
3419
+ /**
3420
+ * Adds native form semantics to a composite control, rendering hidden inputs only when the control
3421
+ * has no native successful control of its own.
3422
+ *
3423
+ * A hidden input is necessary for a headless root to enter the platform's successful-controls tree
3424
+ * (including older engines that do not dispatch `formdata` for `new FormData(form)`). One input is
3425
+ * created per serialized value, preserving repeated-field semantics; controls with their own native
3426
+ * input use this solely for reset and imperative-submit coordination.
3427
+ *
3428
+ * Generated inputs are browser-only. Rendering imperative sibling nodes on the server would put DOM
3429
+ * outside Angular's hydration graph; the client creates them after its view has been claimed instead.
3430
+ */
3431
+ function useNativeFormControl(options) {
3432
+ const host = inject(ElementRef).nativeElement;
3433
+ const ownerDocument = host.ownerDocument;
3434
+ const destroyRef = inject(DestroyRef);
3435
+ if (!ownerDocument || !isPlatformBrowser(inject(PLATFORM_ID))) {
3436
+ return NOOP_NATIVE_FORM_CONTROL;
3437
+ }
3438
+ const inputs = [];
3439
+ let defaultValue;
3440
+ let defaultValues = [];
3441
+ let hasDefaultValue = false;
3442
+ let alive = true;
3443
+ let viewClaimed = false;
3444
+ const ownerForm = () => {
3445
+ const formId = options.form();
3446
+ const candidate = formId
3447
+ ? ownerDocument.getElementById(formId)
3448
+ : typeof host.closest === 'function'
3449
+ ? host.closest('form')
3450
+ : host.parentElement?.closest('form');
3451
+ return candidate?.localName === 'form' ? candidate : null;
3452
+ };
3453
+ const sync = () => {
3454
+ options.syncNativeControl?.();
3455
+ const name = options.name();
3456
+ const form = options.form();
3457
+ const disabled = options.disabled();
3458
+ const nativeControlOwnsValue = options.hasNativeControl?.() ?? false;
3459
+ const values = options.value && options.serialize ? options.serialize(options.value()) : [];
3460
+ // During hydration Angular must claim every server-rendered sibling before an imperative input
3461
+ // can be inserted. The after-render callback below performs the first DOM synchronization.
3462
+ if (!viewClaimed) {
3463
+ return;
3464
+ }
3465
+ const shouldRender = Boolean(name && options.serialize && !nativeControlOwnsValue);
3466
+ const count = shouldRender ? values.length : 0;
3467
+ while (inputs.length > count) {
3468
+ inputs.pop()?.remove();
3469
+ }
3470
+ while (inputs.length < count) {
3471
+ const input = ownerDocument.createElement('input');
3472
+ input.type = 'hidden';
3473
+ const previous = inputs.at(-1);
3474
+ host.parentNode?.insertBefore(input, previous ? previous.nextSibling : host.nextSibling);
3475
+ inputs.push(input);
3476
+ }
3477
+ for (let index = 0; index < inputs.length; index++) {
3478
+ const input = inputs[index];
3479
+ input.name = name;
3480
+ input.value = values[index];
3481
+ input.disabled = disabled;
3482
+ if (form) {
3483
+ input.setAttribute('form', form);
3484
+ }
3485
+ else {
3486
+ input.removeAttribute('form');
3487
+ }
3488
+ // Native reset reads `defaultValue`; retain the original default rather than accidentally
3489
+ // promoting later user edits to the reset baseline.
3490
+ if (!input.hasAttribute('data-rdx-native-form-control')) {
3491
+ input.defaultValue = defaultValues[index] ?? values[index];
3492
+ input.setAttribute('data-rdx-native-form-control', '');
3493
+ }
3494
+ }
3495
+ };
3496
+ effect(() => {
3497
+ if (!hasDefaultValue && options.defaultValue) {
3498
+ defaultValue = options.defaultValue();
3499
+ defaultValues = options.serialize ? options.serialize(defaultValue) : [];
3500
+ hasDefaultValue = true;
3501
+ }
3502
+ });
3503
+ effect(sync);
3504
+ afterNextRender(() => {
3505
+ viewClaimed = true;
3506
+ untracked(sync);
3507
+ });
3508
+ const onReset = (event) => {
3509
+ if (event.target !== ownerForm() || !options.onReset || !hasDefaultValue) {
3510
+ return;
3511
+ }
3512
+ // Native controls reset after the event. Check `defaultPrevented` in the microtask so a later
3513
+ // bubble listener can still cancel the reset before the model follows it.
3514
+ queueMicrotask(() => {
3515
+ if (alive && !event.defaultPrevented) {
3516
+ untracked(() => options.onReset(defaultValue));
3517
+ }
3518
+ });
3519
+ };
3520
+ ownerDocument.addEventListener('reset', onReset, true);
3521
+ destroyRef.onDestroy(() => {
3522
+ alive = false;
3523
+ ownerDocument.removeEventListener('reset', onReset, true);
3524
+ for (const input of inputs) {
3525
+ input.remove();
3526
+ }
3527
+ });
3528
+ return {
3529
+ ownerForm,
3530
+ sync,
3531
+ requestSubmit: () => {
3532
+ untracked(sync);
3533
+ ownerForm()?.requestSubmit();
3534
+ }
3535
+ };
3536
+ }
3537
+
3210
3538
  /**
3211
3539
  * Creates a resize observer effect for element
3212
3540
  *
@@ -4284,5 +4612,5 @@ var RdxPositionAlign;
4284
4612
  * Generated bundle index. Do not edit.
4285
4613
  */
4286
4614
 
4287
- export { A, ALT, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP, ASTERISK, BACKSPACE, CAPS_LOCK, CONTROL, CTRL, DELETE, DOCS_BASE_URL, END, ENTER, ESCAPE, F1, F10, F11, F12, F2, F3, F4, F5, F6, F7, F8, F9, HOME, META, P, PAGE_DOWN, PAGE_UP, RDX_DEFAULT_VALIDATION_MODE, RDX_FIELD_VALIDITY, RDX_FLOATING_REGISTRATION, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_TREE, RDX_FORM_UI_STATE, RDX_INTERNAL_BACKDROP_ATTR, RDX_SCROLL_LOCKED_ATTR, RdxControlValueAccessor, RdxFloatingNode, RdxFloatingNodeRegistration, RdxFloatingRegistrationContext, RdxFloatingRootContext, RdxFloatingTree, RdxFormUiControlBase, RdxFormUiStateHost, RdxIdGenerator, RdxLiveAnnouncer, RdxPositionAlign, RdxPositionSide, RdxTriggerRegistry, SHIFT, SPACE, SPACE_CODE, TAB, TIME_GRANULARITIES, a, areAllDaysBetweenValid, clamp, createCancelableChangeEventDetails, createContent, createContext, createFloatingEvents, createFloatingRootContext, createFormUiState, createFormatter, createMonth, createMonths, docsUrl, elementSize, formUiStateContext, getActiveElement, getDaysBetween, getDaysInMonth, getDefaultDate, getDefaultTime, getLastFirstDayOfWeek, getMaxTransitionDuration, getNextLastDayOfWeek, getOptsByGranularity, getPlaceholder, getSegmentElements, getWeekNumber, handleAndDispatchCustomEvent, handleCalendarInitialFocus, hasTime, initializeSegmentValues, injectControlValueAccessor, injectDocument, injectFloatingRootContext, injectId, isAcceptableSegmentKey, isAfter, isAfterOrSame, isBefore, isBeforeOrSame, isBetween, isBetweenInclusive, isCalendarDateTime, isEqual, isItemEqualToValue, isNullish, isNumberString, isSegmentNavigationKey, isValidationRevealed, isZonedDateTime, itemToStringLabel, itemToStringValue, j, k, n, normalizeDateStep, normalizeHour12, normalizeHourCycle, p, provideExistingToken, provideFloatingRegistration, provideFloatingRootContext, provideFloatingTree, provideFormUiState, provideValueAccessor, rdxCheckLabelElement, rdxCheckTriggerElement, rdxDevError, rdxDevWarning, resetRdxDevWarnings, resizeEffect, resolveDisplayValid, resolveFloatingTree, roundToStepPrecision, segmentBuilders, setupInternalBackdrop, snapValueToStep, syncSegmentValues, syncTimeSegmentValues, toDate, useAnchoredScrollLock, useArrowNavigation, useDateField, useFilter, useGraceArea, useListHighlight, usePointerDrag, useScrollLock, useTransitionStatus, watch };
4615
+ export { A, ALT, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP, ASTERISK, BACKSPACE, CAPS_LOCK, CONTROL, CTRL, DELETE, DOCS_BASE_URL, END, ENTER, ESCAPE, F1, F10, F11, F12, F2, F3, F4, F5, F6, F7, F8, F9, HOME, META, P, PAGE_DOWN, PAGE_UP, RDX_DEFAULT_VALIDATION_MODE, RDX_FIELD_VALIDITY, RDX_FLOATING_REGISTRATION, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_TREE, RDX_FORM_UI_STATE, RDX_INTERNAL_BACKDROP_ATTR, RDX_SCROLL_LOCKED_ATTR, RdxControlValueAccessor, RdxFloatingNode, RdxFloatingNodeRegistration, RdxFloatingRegistrationContext, RdxFloatingRootContext, RdxFloatingTree, RdxFormUiControlBase, RdxFormUiStateHost, RdxIdGenerator, RdxLiveAnnouncer, RdxPositionAlign, RdxPositionSide, RdxTriggerRegistry, SHIFT, SPACE, SPACE_CODE, TAB, TIME_GRANULARITIES, a, areAllDaysBetweenValid, clamp, createCancelableChangeEventDetails, createContent, createContext, createFloatingEvents, createFloatingRootContext, createFormUiState, createFormatter, createMonth, createMonths, docsUrl, elementSize, formUiStateContext, getActiveElement, getDaysBetween, getDaysInMonth, getDefaultDate, getDefaultTime, getLastFirstDayOfWeek, getMaxTransitionDuration, getNextLastDayOfWeek, getOptsByGranularity, getPlaceholder, getSegmentElements, getWeekNumber, handleAndDispatchCustomEvent, handleCalendarInitialFocus, hasTime, initializeSegmentValues, injectControlValueAccessor, injectDocument, injectFloatingRootContext, injectId, injectNgControlState, isAcceptableSegmentKey, isAfter, isAfterOrSame, isBefore, isBeforeOrSame, isBetween, isBetweenInclusive, isCalendarDateTime, isEqual, isItemEqualToValue, isNullish, isNumberString, isSegmentNavigationKey, isValidationRevealed, isZonedDateTime, itemToStringLabel, itemToStringValue, j, k, n, normalizeDateStep, normalizeHour12, normalizeHourCycle, p, provideExistingToken, provideFloatingRegistration, provideFloatingRootContext, provideFloatingTree, provideFormUiState, provideValueAccessor, rdxCheckLabelElement, rdxCheckTriggerElement, rdxDevError, rdxDevWarning, resetRdxDevWarnings, resizeEffect, resolveDisplayValid, resolveFloatingTree, roundToStepPrecision, segmentBuilders, serializeNativeFormValue, setupInternalBackdrop, snapValueToStep, syncSegmentValues, syncTimeSegmentValues, toDate, useAnchoredScrollLock, useArrowNavigation, useDateField, useFilter, useGraceArea, useListHighlight, useNativeFormControl, usePointerDrag, useScrollLock, useTransitionStatus, watch };
4288
4616
  //# sourceMappingURL=radix-ng-primitives-core.mjs.map