@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
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @radix-ng/primitives
2
2
 
3
- > **Headless, signals-first UI primitives for Angular.**
3
+ > **Headless, signals-first UI primitives with first-class Angular Forms support.**
4
4
 
5
5
  Radix NG is a low-level UI primitive library for Angular with a focus on accessibility,
6
6
  customization, and developer experience. The primitives are **headless** — they ship no styles and
@@ -30,18 +30,18 @@ Each primitive is its own secondary entry point — import only what you use:
30
30
  ```ts
31
31
  import { Component } from '@angular/core';
32
32
  import {
33
+ RdxCollapsiblePanelDirective,
33
34
  RdxCollapsibleRootDirective,
34
- RdxCollapsibleTriggerDirective,
35
- RdxCollapsibleContentDirective
35
+ RdxCollapsibleTriggerDirective
36
36
  } from '@radix-ng/primitives/collapsible';
37
37
 
38
38
  @Component({
39
39
  selector: 'app-demo',
40
- imports: [RdxCollapsibleRootDirective, RdxCollapsibleTriggerDirective, RdxCollapsibleContentDirective],
40
+ imports: [RdxCollapsibleRootDirective, RdxCollapsibleTriggerDirective, RdxCollapsiblePanelDirective],
41
41
  template: `
42
42
  <div rdxCollapsibleRoot>
43
43
  <button rdxCollapsibleTrigger>Toggle</button>
44
- <div rdxCollapsibleContent>Content</div>
44
+ <div rdxCollapsiblePanel>Content</div>
45
45
  </div>
46
46
  `
47
47
  })
@@ -51,6 +51,27 @@ export class DemoComponent {}
51
51
  Style the parts via the `data-*` attributes they expose (e.g. `[data-state="open"]`,
52
52
  `[data-disabled]`) with any CSS approach — Tailwind, CSS modules, vanilla CSS.
53
53
 
54
+ ## Angular forms, one Field contract
55
+
56
+ The same controls and accessible Field parts work across every Angular form API while Angular owns
57
+ the model and validation:
58
+
59
+ ```html
60
+ <!-- Reactive Forms / ngModel -->
61
+ <input rdxInput formControlName="email" rdxNgControlField />
62
+
63
+ <!-- Signal Forms -->
64
+ <input rdxInput [formField]="accountForm.email" rdxSignalField />
65
+ ```
66
+
67
+ ControlValueAccessors remain available for Reactive and template-driven forms. The optional
68
+ `@radix-ng/primitives/signal-forms` entry adds Angular Signal Forms without a separate Radix NG form
69
+ model. Signal Forms is stable in Angular 22; Reactive Forms and `ngModel` remain supported on Angular
70
+ 21 and 22. Compare the [paired recipes and runtime-verified
71
+ matrix](https://radix-ng.com/docs/?path=/docs/primitives-signal-forms--docs#one-recipe-two-angular-apis),
72
+ and see the [migration guide](https://radix-ng.com/docs/?path=/docs/guides-forms-migration--docs) when
73
+ upgrading from 1.0.10.
74
+
54
75
  ## Documentation
55
76
 
56
77
  - 📖 [radix-ng.com](https://radix-ng.com) — documentation, examples & API reference
@@ -8,7 +8,7 @@ import * as i1$2 from '@radix-ng/primitives/dismissable-layer';
8
8
  import { RdxFloatingInsideElement, RdxDismiss } from '@radix-ng/primitives/dismissable-layer';
9
9
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
10
10
  import * as i2 from '@radix-ng/primitives/core';
11
- import { RdxFormUiControlBase, createFloatingRootContext, rdxDevWarning, isItemEqualToValue, itemToStringLabel, createCancelableChangeEventDetails, provideFloatingTree, provideFloatingRootContext, setupInternalBackdrop, injectId, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, useAnchoredScrollLock, RdxFloatingNodeRegistration, rdxDevError } from '@radix-ng/primitives/core';
11
+ import { RdxFormUiControlBase, createFloatingRootContext, useNativeFormControl, serializeNativeFormValue, rdxDevWarning, isItemEqualToValue, itemToStringLabel, createCancelableChangeEventDetails, provideFloatingTree, provideFloatingRootContext, setupInternalBackdrop, injectId, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, useAnchoredScrollLock, RdxFloatingNodeRegistration, rdxDevError } from '@radix-ng/primitives/core';
12
12
  import { injectDirection } from '@radix-ng/primitives/direction-provider';
13
13
  import { injectFieldRootContext } from '@radix-ng/primitives/field';
14
14
 
@@ -89,6 +89,9 @@ const context = () => {
89
89
  dir: root.dir,
90
90
  value: root.value,
91
91
  inputValue: root.value,
92
+ name: root.name,
93
+ form: root.form,
94
+ inputOwnsFormValue: root.inputOwnsFormValue,
92
95
  open: root.open,
93
96
  present: root.present,
94
97
  multiple: root.alwaysFalse,
@@ -100,6 +103,11 @@ const context = () => {
100
103
  modal: root.modal,
101
104
  virtualized: root.virtualized,
102
105
  grid: root.grid,
106
+ invalidState: root.invalidState,
107
+ pendingState: root.formUi.pendingState,
108
+ validState: root.formUi.validState,
109
+ touchedState: root.touchedState,
110
+ dirtyState: root.dirtyState,
103
111
  filteredItems: engine.filteredItems,
104
112
  highlightedItem: engine.highlightedItem,
105
113
  highlightedIndex: engine.highlightedIndex,
@@ -142,9 +150,6 @@ const context = () => {
142
150
  setInputValue: (value) => root.setQuery(value),
143
151
  openAndHighlight: (edge, reason, event) => root.openAndHighlight(edge, reason, event),
144
152
  navigateByKeyboard: (direction, event) => root.navigateByKeyboard(direction, event),
145
- invalidState: root.invalidState,
146
- touchedState: root.touchedState,
147
- dirtyState: root.dirtyState,
148
153
  select: (item, event) => root.handleSelect(item, event),
149
154
  selectIndex: (index, event) => root.selectIndex(index, event),
150
155
  selectHighlighted: (event) => root.selectHighlighted(event),
@@ -264,6 +269,12 @@ class RdxAutocompleteRoot extends RdxFormUiControlBase {
264
269
  this.readOnly = input(false, { ...(ngDevMode ? { debugName: "readOnly" } : /* istanbul ignore next */ {}), alias: 'readOnly', transform: booleanAttribute });
265
270
  /** Whether a value is required (for forms). */
266
271
  this.required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
272
+ /** Name used when serializing this composite control into native `FormData`. */
273
+ this.name = input(/* @ts-ignore */
274
+ ...(ngDevMode ? [undefined, { debugName: "name" }] : /* istanbul ignore next */ []));
275
+ /** Id of an external form that owns this control. */
276
+ this.form = input(/* @ts-ignore */
277
+ ...(ngDevMode ? [undefined, { debugName: "form" }] : /* istanbul ignore next */ []));
267
278
  /** Whether keyboard navigation wraps at the list boundaries. */
268
279
  this.loopFocus = input(true, { ...(ngDevMode ? { debugName: "loopFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
269
280
  /**
@@ -304,6 +315,9 @@ class RdxAutocompleteRoot extends RdxFormUiControlBase {
304
315
  */
305
316
  this.filter = input(undefined, /* @ts-ignore */
306
317
  ...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
318
+ /** Locale for the default `contains` filter's string comparison. Defaults to the runtime locale. */
319
+ this.locale = input(/* @ts-ignore */
320
+ ...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
307
321
  /** Maximum number of matching items to show. `-1` (default) means no limit. */
308
322
  this.limit = input(-1, { ...(ngDevMode ? { debugName: "limit" } : /* istanbul ignore next */ {}), transform: numberAttribute });
309
323
  /** The full set of item values for {@link virtualized} mode. */
@@ -328,16 +342,42 @@ class RdxAutocompleteRoot extends RdxFormUiControlBase {
328
342
  /** Constant signals exposed to the combobox context (autocomplete is always single-value). */
329
343
  this.alwaysFalse = signal(false, /* @ts-ignore */
330
344
  ...(ngDevMode ? [{ debugName: "alwaysFalse" }] : /* istanbul ignore next */ []));
345
+ this.inputOwnsFormValue = computed(() => this.engine.inputLayout() !== 'inside', /* @ts-ignore */
346
+ ...(ngDevMode ? [{ debugName: "inputOwnsFormValue" }] : /* istanbul ignore next */ []));
331
347
  this.noneMode = signal('none', /* @ts-ignore */
332
348
  ...(ngDevMode ? [{ debugName: "noneMode" }] : /* istanbul ignore next */ []));
333
349
  this.cvaDisabled = signal(false, /* @ts-ignore */
334
350
  ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
335
351
  this.disabledState = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
336
352
  ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
353
+ this.nativeFormControl = useNativeFormControl({
354
+ name: this.name,
355
+ form: this.form,
356
+ disabled: this.disabledState,
357
+ value: this.value,
358
+ serialize: serializeNativeFormValue,
359
+ hasNativeControl: this.inputOwnsFormValue,
360
+ syncNativeControl: () => {
361
+ const input = this.engine.inputElement();
362
+ if (input && this.inputOwnsFormValue()) {
363
+ input.value = this.value();
364
+ }
365
+ },
366
+ defaultValue: () => this.defaultValue() ?? this.value(),
367
+ onReset: (value) => {
368
+ this.writeValue(value);
369
+ if (!this.resetNgControl(value)) {
370
+ this.onChange?.(value);
371
+ }
372
+ this.formUi.resetInteractionState?.();
373
+ }
374
+ });
337
375
  this.requiredState = computed(() => this.required(), /* @ts-ignore */
338
376
  ...(ngDevMode ? [{ debugName: "requiredState" }] : /* istanbul ignore next */ []));
339
377
  /** @ignore */
340
378
  this.invalidState = this.formUi.invalidState;
379
+ this.pendingState = this.formUi.pendingState;
380
+ this.validState = this.formUi.validState;
341
381
  /** @ignore */
342
382
  this.touchedState = this.formUi.touchedState;
343
383
  /** @ignore */
@@ -375,6 +415,7 @@ class RdxAutocompleteRoot extends RdxFormUiControlBase {
375
415
  virtualized: this.virtualized,
376
416
  items: this.items,
377
417
  filter: this.filter,
418
+ locale: this.locale,
378
419
  limit: this.limit,
379
420
  grid: this.grid,
380
421
  rowOf: (element) => element.closest('[rdxAutocompleteRow]'),
@@ -538,7 +579,7 @@ class RdxAutocompleteRoot extends RdxFormUiControlBase {
538
579
  }
539
580
  maybeSubmit() {
540
581
  if (this.submitOnItemClick()) {
541
- this.engine.inputElement()?.form?.requestSubmit?.();
582
+ this.nativeFormControl.requestSubmit();
542
583
  }
543
584
  }
544
585
  selectHighlighted(event = new Event('autocomplete.item-press')) {
@@ -670,7 +711,7 @@ class RdxAutocompleteRoot extends RdxFormUiControlBase {
670
711
  this.cvaDisabled.set(isDisabled);
671
712
  }
672
713
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxAutocompleteRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
673
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxAutocompleteRoot, isStandalone: true, selector: "[rdxAutocompleteRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaultOpen: { classPropertyName: "defaultOpen", publicName: "defaultOpen", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, dirInput: { classPropertyName: "dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readOnly: { classPropertyName: "readOnly", publicName: "readOnly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, loopFocus: { classPropertyName: "loopFocus", publicName: "loopFocus", isSignal: true, isRequired: false, transformFunction: null }, autoHighlight: { classPropertyName: "autoHighlight", publicName: "autoHighlight", isSignal: true, isRequired: false, transformFunction: null }, highlightItemOnHover: { classPropertyName: "highlightItemOnHover", publicName: "highlightItemOnHover", isSignal: true, isRequired: false, transformFunction: null }, keepHighlight: { classPropertyName: "keepHighlight", publicName: "keepHighlight", isSignal: true, isRequired: false, transformFunction: null }, openOnInputClick: { classPropertyName: "openOnInputClick", publicName: "openOnInputClick", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, submitOnItemClick: { classPropertyName: "submitOnItemClick", publicName: "submitOnItemClick", isSignal: true, isRequired: false, transformFunction: null }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, virtualized: { classPropertyName: "virtualized", publicName: "virtualized", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToStringValue: { classPropertyName: "itemToStringValue", publicName: "itemToStringValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", open: "openChange", onValueChange: "onValueChange", onOpenChange: "onOpenChange", onItemHighlighted: "onItemHighlighted", onOpenChangeComplete: "onOpenChangeComplete" }, host: { properties: { "attr.data-disabled": "disabledState() ? \"\" : undefined" } }, providers: [
714
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxAutocompleteRoot, isStandalone: true, selector: "[rdxAutocompleteRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaultOpen: { classPropertyName: "defaultOpen", publicName: "defaultOpen", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, dirInput: { classPropertyName: "dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readOnly: { classPropertyName: "readOnly", publicName: "readOnly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: false, transformFunction: null }, loopFocus: { classPropertyName: "loopFocus", publicName: "loopFocus", isSignal: true, isRequired: false, transformFunction: null }, autoHighlight: { classPropertyName: "autoHighlight", publicName: "autoHighlight", isSignal: true, isRequired: false, transformFunction: null }, highlightItemOnHover: { classPropertyName: "highlightItemOnHover", publicName: "highlightItemOnHover", isSignal: true, isRequired: false, transformFunction: null }, keepHighlight: { classPropertyName: "keepHighlight", publicName: "keepHighlight", isSignal: true, isRequired: false, transformFunction: null }, openOnInputClick: { classPropertyName: "openOnInputClick", publicName: "openOnInputClick", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, submitOnItemClick: { classPropertyName: "submitOnItemClick", publicName: "submitOnItemClick", isSignal: true, isRequired: false, transformFunction: null }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, virtualized: { classPropertyName: "virtualized", publicName: "virtualized", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToStringValue: { classPropertyName: "itemToStringValue", publicName: "itemToStringValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", open: "openChange", onValueChange: "onValueChange", onOpenChange: "onOpenChange", onItemHighlighted: "onItemHighlighted", onOpenChangeComplete: "onOpenChangeComplete" }, host: { properties: { "attr.data-disabled": "disabledState() ? \"\" : undefined" } }, providers: [
674
715
  provideComboboxRootContext(context),
675
716
  { provide: NG_VALUE_ACCESSOR, useExisting: RdxAutocompleteRoot, multi: true },
676
717
  // New floating foundation (ADR 0015/0017) — the dismissal capability reads this shared context.
@@ -695,7 +736,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
695
736
  '[attr.data-disabled]': 'disabledState() ? "" : undefined'
696
737
  }
697
738
  }]
698
- }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], defaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultOpen", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], loopFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "loopFocus", required: false }] }], autoHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoHighlight", required: false }] }], highlightItemOnHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightItemOnHover", required: false }] }], keepHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepHighlight", required: false }] }], openOnInputClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnInputClick", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], submitOnItemClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitOnItemClick", required: false }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], limit: [{ type: i0.Input, args: [{ isSignal: true, alias: "limit", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], virtualized: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualized", required: false }] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToStringValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringValue", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onItemHighlighted: [{ type: i0.Output, args: ["onItemHighlighted"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
739
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], defaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultOpen", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: false }] }], loopFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "loopFocus", required: false }] }], autoHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoHighlight", required: false }] }], highlightItemOnHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightItemOnHover", required: false }] }], keepHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepHighlight", required: false }] }], openOnInputClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnInputClick", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], submitOnItemClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitOnItemClick", required: false }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], limit: [{ type: i0.Input, args: [{ isSignal: true, alias: "limit", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], virtualized: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualized", required: false }] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToStringValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringValue", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onItemHighlighted: [{ type: i0.Output, args: ["onItemHighlighted"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
699
740
 
700
741
  /**
701
742
  * Clears the input value. Hidden when there is nothing to clear.
@@ -905,18 +946,21 @@ class RdxAutocompleteInput {
905
946
  // `aria-autocomplete` tokens. Must reflect the static mechanism, not the transient inline preview.
906
947
  this.ariaAutocomplete = computed(() => this.root.mode(), /* @ts-ignore */
907
948
  ...(ngDevMode ? [{ debugName: "ariaAutocomplete" }] : /* istanbul ignore next */ []));
908
- this.invalidState = computed(() => this.invalid() || this.root.invalidState() || Boolean(this.fieldRootContext?.invalidState()), /* @ts-ignore */
909
- ...(ngDevMode ? [{ debugName: "invalidState" }] : /* istanbul ignore next */ []));
910
949
  /**
911
950
  * Tri-state *displayed* validity: the enclosing Field's gated `validState` when inside a `rdxFieldRoot`
912
951
  * (so a field whose `validationMode` defers display (e.g. `onBlur`) keeps the input neutral until revealed), else the input's
913
952
  * own binary invalidity.
914
953
  */
915
- this.displayValid = computed(() => this.fieldRootContext
916
- ? this.fieldRootContext.validState()
917
- : this.invalid() || this.root.invalidState()
918
- ? false
919
- : true, /* @ts-ignore */
954
+ this.displayValid = computed(() => {
955
+ if (this.fieldRootContext) {
956
+ return this.fieldRootContext.validState();
957
+ }
958
+ const rootValid = this.root.validState();
959
+ if (rootValid === null) {
960
+ return null;
961
+ }
962
+ return this.invalid() ? false : rootValid;
963
+ }, /* @ts-ignore */
920
964
  ...(ngDevMode ? [{ debugName: "displayValid" }] : /* istanbul ignore next */ []));
921
965
  this.disabledState = computed(() => this.root.disabledState() || Boolean(this.fieldRootContext?.disabledState()), /* @ts-ignore */
922
966
  ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
@@ -934,10 +978,7 @@ class RdxAutocompleteInput {
934
978
  if (!this.fieldRootContext) {
935
979
  return undefined;
936
980
  }
937
- const ids = [
938
- ...this.fieldRootContext.descriptionIds(),
939
- ...(this.fieldRootContext.invalidState() ? this.fieldRootContext.errorIds() : [])
940
- ];
981
+ const ids = [...this.fieldRootContext.descriptionIds(), ...this.fieldRootContext.errorIds()];
941
982
  return ids.length ? ids.join(' ') : undefined;
942
983
  }, /* @ts-ignore */
943
984
  ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
@@ -980,11 +1021,20 @@ class RdxAutocompleteInput {
980
1021
  this.commitInput(event.target.value, event);
981
1022
  }
982
1023
  commitInput(value, event) {
1024
+ // The edit itself is authoritative. Mobile keyboards, context-menu cut/paste, drag edits and
1025
+ // IME commits may have no useful keydown, so carrying suppression from the previous key makes
1026
+ // inline completion stale in either direction.
1027
+ const suppressInline = shouldSuppressInlineCompletion(event, value, this.root.value());
983
1028
  // Base UI opens on input only for a non-empty trimmed value — whitespace alone won't open it.
984
1029
  if (!this.root.open() && value.trim() !== '') {
985
1030
  this.root.setOpen(true, 'input-change', event);
986
1031
  }
987
1032
  this.root.setQuery(value);
1033
+ // `onValueChange` is cancelable. Only attach this edit's suppression state if the requested
1034
+ // value actually committed; a vetoed edit must leave the previous preview semantics intact.
1035
+ if (this.root.value() === value) {
1036
+ this.root.setSuppressInline(suppressInline);
1037
+ }
988
1038
  }
989
1039
  onClick(event) {
990
1040
  if (this.root.openOnInputClick()) {
@@ -1004,8 +1054,6 @@ class RdxAutocompleteInput {
1004
1054
  if (event.isComposing || this.composing) {
1005
1055
  return;
1006
1056
  }
1007
- // Backspace / Delete must never re-add an inline completion for the resulting edit.
1008
- this.root.setSuppressInline(event.key === 'Backspace' || event.key === 'Delete');
1009
1057
  if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) {
1010
1058
  return;
1011
1059
  }
@@ -1100,7 +1148,7 @@ class RdxAutocompleteInput {
1100
1148
  }
1101
1149
  }
1102
1150
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxAutocompleteInput, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1103
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxAutocompleteInput, isStandalone: true, selector: "input[rdxAutocompleteInput]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "combobox", "autocomplete": "off" }, listeners: { "input": "onInput($event)", "click": "onClick($event)", "focus": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)", "compositionstart": "composing = true", "compositionend": "onCompositionEnd($event)" }, properties: { "attr.aria-autocomplete": "ariaAutocomplete()", "attr.id": "id()", "attr.aria-haspopup": "root.grid() ? \"grid\" : \"listbox\"", "attr.aria-expanded": "root.open()", "attr.aria-controls": "root.listId", "attr.aria-labelledby": "root.labelId()", "attr.aria-activedescendant": "root.activeId()", "attr.aria-describedby": "describedBy()", "attr.aria-invalid": "displayValid() === false ? \"true\" : undefined", "attr.aria-required": "requiredState() ? \"true\" : undefined", "attr.aria-disabled": "disabledState() ? \"true\" : undefined", "attr.disabled": "disabledState() ? \"\" : undefined", "attr.readonly": "root.readOnly() ? \"\" : undefined", "attr.required": "requiredState() ? \"\" : undefined", "value": "root.displayValue()", "attr.data-popup-open": "dataAttr(root.open())", "attr.data-list-empty": "dataAttr(root.visibleCount() === 0)", "attr.data-invalid": "dataAttr(displayValid() === false)", "attr.data-valid": "dataAttr(displayValid() === true)", "attr.data-disabled": "dataAttr(disabledState())", "attr.data-required": "dataAttr(requiredState())", "attr.data-touched": "dataAttr(touchedState())", "attr.data-dirty": "dataAttr(dirtyState())", "attr.data-filled": "dataAttr(filledState())", "attr.data-focused": "dataAttr(focusedState())" } }, exportAs: ["rdxAutocompleteInput"], hostDirectives: [{ directive: i1$1.RdxPopperAnchor }, { directive: i1$2.RdxFloatingInsideElement }], ngImport: i0 }); }
1151
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxAutocompleteInput, isStandalone: true, selector: "input[rdxAutocompleteInput]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "combobox", "autocomplete": "off" }, listeners: { "input": "onInput($event)", "click": "onClick($event)", "focus": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)", "compositionstart": "composing = true", "compositionend": "onCompositionEnd($event)" }, properties: { "attr.aria-autocomplete": "ariaAutocomplete()", "attr.id": "id()", "attr.aria-haspopup": "root.grid() ? \"grid\" : \"listbox\"", "attr.aria-expanded": "root.open()", "attr.aria-controls": "root.listId", "attr.aria-labelledby": "root.labelId()", "attr.aria-activedescendant": "root.activeId()", "attr.aria-describedby": "describedBy()", "attr.aria-invalid": "displayValid() === false ? \"true\" : undefined", "attr.aria-required": "requiredState() ? \"true\" : undefined", "attr.aria-disabled": "disabledState() ? \"true\" : undefined", "attr.disabled": "disabledState() ? \"\" : undefined", "attr.readonly": "root.readOnly() ? \"\" : undefined", "attr.required": "requiredState() ? \"\" : undefined", "attr.name": "root.inputOwnsFormValue() ? root.name() : undefined", "attr.form": "root.form()", "value": "root.displayValue()", "attr.data-popup-open": "dataAttr(root.open())", "attr.data-list-empty": "dataAttr(root.visibleCount() === 0)", "attr.data-invalid": "dataAttr(displayValid() === false)", "attr.data-valid": "dataAttr(displayValid() === true)", "attr.data-disabled": "dataAttr(disabledState())", "attr.data-required": "dataAttr(requiredState())", "attr.data-touched": "dataAttr(touchedState())", "attr.data-dirty": "dataAttr(dirtyState())", "attr.data-filled": "dataAttr(filledState())", "attr.data-focused": "dataAttr(focusedState())" } }, exportAs: ["rdxAutocompleteInput"], hostDirectives: [{ directive: i1$1.RdxPopperAnchor }, { directive: i1$2.RdxFloatingInsideElement }], ngImport: i0 }); }
1104
1152
  }
1105
1153
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxAutocompleteInput, decorators: [{
1106
1154
  type: Directive,
@@ -1125,6 +1173,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
1125
1173
  '[attr.disabled]': 'disabledState() ? "" : undefined',
1126
1174
  '[attr.readonly]': 'root.readOnly() ? "" : undefined',
1127
1175
  '[attr.required]': 'requiredState() ? "" : undefined',
1176
+ '[attr.name]': 'root.inputOwnsFormValue() ? root.name() : undefined',
1177
+ '[attr.form]': 'root.form()',
1128
1178
  '[value]': 'root.displayValue()',
1129
1179
  '[attr.data-popup-open]': 'dataAttr(root.open())',
1130
1180
  '[attr.data-list-empty]': 'dataAttr(root.visibleCount() === 0)',
@@ -1146,6 +1196,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
1146
1196
  }
1147
1197
  }]
1148
1198
  }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }] } });
1199
+ /** Whether this concrete edit should keep inline completion hidden for its resulting value. */
1200
+ function shouldSuppressInlineCompletion(event, nextValue, previousValue) {
1201
+ // A committed composition inserts text even though CompositionEvent has no `inputType`.
1202
+ if (event.type === 'compositionend') {
1203
+ return false;
1204
+ }
1205
+ const inputType = event.inputType;
1206
+ if (inputType?.startsWith('delete')) {
1207
+ return true;
1208
+ }
1209
+ if (inputType?.startsWith('insert')) {
1210
+ return false;
1211
+ }
1212
+ // Synthetic/programmatic input and history edits may omit an inputType. Length is the safest
1213
+ // fallback: shrinking must not immediately restore the suffix; equal/growing replacements may.
1214
+ return nextValue.length < previousValue.length;
1215
+ }
1149
1216
 
1150
1217
  const attr = (value) => (value ? '' : undefined);
1151
1218
  /**