@radix-ng/primitives 1.1.0 → 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 (31) hide show
  1. package/fesm2022/radix-ng-primitives-autocomplete.mjs +66 -7
  2. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -1
  3. package/fesm2022/radix-ng-primitives-checkbox.mjs +147 -125
  4. package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
  5. package/fesm2022/radix-ng-primitives-combobox.mjs +171 -37
  6. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  7. package/fesm2022/radix-ng-primitives-core.mjs +173 -5
  8. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  9. package/fesm2022/radix-ng-primitives-number-field.mjs +38 -7
  10. package/fesm2022/radix-ng-primitives-number-field.mjs.map +1 -1
  11. package/fesm2022/radix-ng-primitives-radio.mjs +49 -2
  12. package/fesm2022/radix-ng-primitives-radio.mjs.map +1 -1
  13. package/fesm2022/radix-ng-primitives-select.mjs +35 -3
  14. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  15. package/fesm2022/radix-ng-primitives-slider.mjs +17 -2
  16. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  17. package/fesm2022/radix-ng-primitives-switch.mjs +60 -7
  18. package/fesm2022/radix-ng-primitives-switch.mjs.map +1 -1
  19. package/fesm2022/radix-ng-primitives-toggle-group.mjs +25 -3
  20. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  21. package/package.json +1 -1
  22. package/types/radix-ng-primitives-autocomplete.d.ts +10 -1
  23. package/types/radix-ng-primitives-checkbox.d.ts +56 -43
  24. package/types/radix-ng-primitives-combobox.d.ts +46 -2
  25. package/types/radix-ng-primitives-core.d.ts +62 -4
  26. package/types/radix-ng-primitives-number-field.d.ts +12 -5
  27. package/types/radix-ng-primitives-radio.d.ts +6 -1
  28. package/types/radix-ng-primitives-select.d.ts +8 -1
  29. package/types/radix-ng-primitives-slider.d.ts +2 -1
  30. package/types/radix-ng-primitives-switch.d.ts +19 -3
  31. package/types/radix-ng-primitives-toggle-group.d.ts +5 -1
@@ -4,7 +4,7 @@ import * as i1 from '@radix-ng/primitives/popper';
4
4
  import { RdxPopperAnchor, RdxPopperArrow, RdxPopper, RdxPopperContentWrapper, provideRdxPopperContentWrapper, provideRdxPopperContentConfig, DROPDOWN_COLLISION_AVOIDANCE, injectPopperContentWrapperContext, RdxPopperContent } from '@radix-ng/primitives/popper';
5
5
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
6
6
  import * as i2 from '@radix-ng/primitives/core';
7
- import { useFilter, injectId, useListHighlight, useTransitionStatus, createContext, RdxFormUiControlBase, createFloatingRootContext, isItemEqualToValue, isNullish, createCancelableChangeEventDetails, itemToStringLabel, provideFloatingTree, provideFloatingRootContext, setupInternalBackdrop, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, useAnchoredScrollLock, RdxFloatingNodeRegistration, rdxDevError } from '@radix-ng/primitives/core';
7
+ import { useFilter, injectId, useListHighlight, useTransitionStatus, createContext, RdxFormUiControlBase, createFloatingRootContext, useNativeFormControl, serializeNativeFormValue, isItemEqualToValue, isNullish, createCancelableChangeEventDetails, itemToStringLabel, provideFloatingTree, provideFloatingRootContext, setupInternalBackdrop, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, useAnchoredScrollLock, RdxFloatingNodeRegistration, rdxDevError } from '@radix-ng/primitives/core';
8
8
  import { injectDirection } from '@radix-ng/primitives/direction-provider';
9
9
  import * as i1$1 from '@radix-ng/primitives/dismissable-layer';
10
10
  import { RdxFloatingInsideElement, RdxDismiss } from '@radix-ng/primitives/dismissable-layer';
@@ -98,34 +98,58 @@ function useComboboxEngine(config) {
98
98
  // Tracks whether the last interaction was the keyboard, so the highlight doesn't jump to an item
99
99
  // the cursor happens to rest on when arrow-key navigation scrolls the list under a still pointer.
100
100
  let keyboardActive = false;
101
- const _items = signal([], /* @ts-ignore */
102
- ...(ngDevMode ? [{ debugName: "_items" }] : /* istanbul ignore next */ []));
101
+ // Keep registrations in a stable map. Copying the growing array on every item mount/unmount makes
102
+ // registering N options O(n²) before filtering or DOM-order sorting even begins. The numeric tick is
103
+ // only the reactive invalidation channel; Angular batches the registrations from one render, so
104
+ // `orderedItems` materializes and sorts the final collection once for that batch. This is the same
105
+ // stable-map pattern used by Base UI and our CompositeList.
106
+ const itemRegistry = new Map();
107
+ const itemRegistryTick = signal(0, /* @ts-ignore */
108
+ ...(ngDevMode ? [{ debugName: "itemRegistryTick" }] : /* istanbul ignore next */ []));
103
109
  const orderedItems = computed(() => {
104
- const items = [..._items()];
105
- return items.sort((a, b) => domOrder(a.element, b.element));
110
+ itemRegistryTick();
111
+ return [...itemRegistry.values()]
112
+ .filter((item) => item.element.isConnected)
113
+ .sort((a, b) => domOrder(a.element, b.element));
106
114
  }, /* @ts-ignore */
107
115
  ...(ngDevMode ? [{ debugName: "orderedItems" }] : /* istanbul ignore next */ []));
108
- const matchesFilter = (item) => {
116
+ const visibleItems = computed(() => {
117
+ const items = orderedItems();
118
+ const limit = config.limit();
119
+ if (limit === 0) {
120
+ return [];
121
+ }
122
+ // Read root signals once per query, not once per item. The early paths also avoid
123
+ // allocating/filtering when every registered item is known to be visible.
109
124
  if (!config.filteringEnabled()) {
110
- return true;
125
+ return limit >= 0 ? items.slice(0, limit) : items;
111
126
  }
112
127
  const filter = config.filter();
113
128
  if (filter === null) {
114
- return true;
129
+ return limit >= 0 ? items.slice(0, limit) : items;
115
130
  }
116
131
  const query = config.query();
117
- // Custom filter: Base UI shape `(value, query, itemToString)`. Default: locale-aware contains
118
- // on the item's own text (the element content), no value→text round-trip.
119
- return filter
120
- ? filter(item.value(), query, config.itemToString)
121
- : defaultFilter().contains(item.textValue(), query);
122
- };
123
- const visibleItems = computed(() => {
124
- const matching = orderedItems().filter((item) => matchesFilter(item));
125
- const limit = config.limit();
126
- return limit >= 0 ? matching.slice(0, limit) : matching;
127
- }, /* @ts-ignore */
128
- ...(ngDevMode ? [{ debugName: "visibleItems" }] : /* istanbul ignore next */ []));
132
+ if (!filter && query.length === 0) {
133
+ return limit >= 0 ? items.slice(0, limit) : items;
134
+ }
135
+ const matching = [];
136
+ const contains = filter ? null : defaultFilter().contains;
137
+ for (const item of items) {
138
+ // Custom filter: Base UI shape `(value, query, itemToString)`. Default: locale-aware
139
+ // contains on the item's own text (the element content), no value→text round-trip.
140
+ const matches = filter
141
+ ? filter(item.value(), query, config.itemToString)
142
+ : contains(item.textValue(), query);
143
+ if (!matches) {
144
+ continue;
145
+ }
146
+ matching.push(item);
147
+ if (limit >= 0 && matching.length >= limit) {
148
+ break;
149
+ }
150
+ }
151
+ return matching;
152
+ }, { ...(ngDevMode ? { debugName: "visibleItems" } : /* istanbul ignore next */ {}), equal: sameItemOrder });
129
153
  const visibleSet = computed(() => new Set(visibleItems()), /* @ts-ignore */
130
154
  ...(ngDevMode ? [{ debugName: "visibleSet" }] : /* istanbul ignore next */ []));
131
155
  const isVisible = (item) => visibleSet().has(item);
@@ -224,7 +248,13 @@ function useComboboxEngine(config) {
224
248
  };
225
249
  const recomputeInlinePreview = (label, query, reason) => {
226
250
  // Pointer hover must not rewrite the input (matches Base UI); only typing / keyboard nav complete it.
227
- if (!config.inlineMode() || suppressInline || !label || reason === 'pointer') {
251
+ if (!config.inlineMode() || !label || reason === 'pointer') {
252
+ inlinePreview.set(null);
253
+ return;
254
+ }
255
+ // Suppression belongs to the current edit only. Explicit keyboard navigation starts a new
256
+ // interaction and must still mirror the navigated label after a delete/cut edit.
257
+ if (suppressInline && reason !== 'keyboard') {
228
258
  inlinePreview.set(null);
229
259
  return;
230
260
  }
@@ -316,11 +346,14 @@ function useComboboxEngine(config) {
316
346
  pendingHighlightEdge.set(null);
317
347
  });
318
348
  }, { injector });
319
- // Inline completion: mirror the active item's label into the input. Tracks the DOM-ref highlight,
320
- // the virtualized index, the query, the reason, and `inlineMode` so virtualized navigation and a
321
- // `both → list` mode switch both recompute (and clear) the preview. No-op (null) when off (combobox).
349
+ // Inline completion: mirror the active item's label into the input. When inline mode is off, stop
350
+ // before reading query/highlight state so Combobox and list-mode Autocomplete do not subscribe this
351
+ // effect to the typing hot path. A `both → list` switch still clears the previous preview.
322
352
  effect(() => {
323
- config.inlineMode();
353
+ if (!config.inlineMode()) {
354
+ untracked(() => inlinePreview.set(null));
355
+ return;
356
+ }
324
357
  highlightedItem();
325
358
  highlightedIndex();
326
359
  const query = config.query();
@@ -459,10 +492,19 @@ function useComboboxEngine(config) {
459
492
  filteredItems,
460
493
  isVisible,
461
494
  registerItem(item) {
462
- _items.update((items) => [...items, item]);
495
+ if (itemRegistry.get(item.element) === item) {
496
+ return;
497
+ }
498
+ itemRegistry.set(item.element, item);
499
+ itemRegistryTick.update((tick) => tick + 1);
463
500
  },
464
501
  unregisterItem(item) {
465
- _items.update((items) => items.filter((i) => i !== item));
502
+ // A stale cleanup must not delete a newer registration for the same host element.
503
+ if (itemRegistry.get(item.element) !== item) {
504
+ return;
505
+ }
506
+ itemRegistry.delete(item.element);
507
+ itemRegistryTick.update((tick) => tick + 1);
466
508
  },
467
509
  highlight,
468
510
  highlightedItem,
@@ -603,6 +645,21 @@ function domOrder(a, b) {
603
645
  }
604
646
  return 0;
605
647
  }
648
+ /** Referential item-sequence equality used to keep unchanged filtering results stable across queries. */
649
+ function sameItemOrder(previous, next) {
650
+ if (previous === next) {
651
+ return true;
652
+ }
653
+ if (previous.length !== next.length) {
654
+ return false;
655
+ }
656
+ for (let index = 0; index < previous.length; index++) {
657
+ if (previous[index] !== next[index]) {
658
+ return false;
659
+ }
660
+ }
661
+ return true;
662
+ }
606
663
 
607
664
  // The engine stays private to the root (it owns mutable internals); the context factory — a free
608
665
  // function, so it can't reach a `private` field — reads it through this registry instead.
@@ -617,6 +674,9 @@ const context = () => {
617
674
  dir: root.dir,
618
675
  value: root.value,
619
676
  inputValue: root.inputValue,
677
+ name: root.name,
678
+ form: root.form,
679
+ inputOwnsFormValue: root.inputOwnsFormValue,
620
680
  open: root.open,
621
681
  present: root.present,
622
682
  multiple: root.multiple,
@@ -768,6 +828,9 @@ class RdxComboboxRoot extends RdxFormUiControlBase {
768
828
  /** Whether the combobox is in multiple-selection mode. */
769
829
  this.multiple = computed(() => this.mode() === 'multiple', /* @ts-ignore */
770
830
  ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
831
+ /** Whether the visible text input owns native serialization in filter-only mode. */
832
+ this.inputOwnsFormValue = computed(() => this.mode() === 'none' && this.engine.inputLayout() !== 'inside', /* @ts-ignore */
833
+ ...(ngDevMode ? [{ debugName: "inputOwnsFormValue" }] : /* istanbul ignore next */ []));
771
834
  /** In `'none'` mode, whether pressing an item fills the input with its label. */
772
835
  this.fillInputOnItemPress = input(true, { ...(ngDevMode ? { debugName: "fillInputOnItemPress" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
773
836
  /** Text direction. */
@@ -779,6 +842,12 @@ class RdxComboboxRoot extends RdxFormUiControlBase {
779
842
  this.readOnly = input(false, { ...(ngDevMode ? { debugName: "readOnly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
780
843
  /** Whether a value is required (for forms). */
781
844
  this.required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
845
+ /** Name used when serializing the committed selection into native `FormData`. */
846
+ this.name = input(/* @ts-ignore */
847
+ ...(ngDevMode ? [undefined, { debugName: "name" }] : /* istanbul ignore next */ []));
848
+ /** Id of an external form that owns this control. */
849
+ this.form = input(/* @ts-ignore */
850
+ ...(ngDevMode ? [undefined, { debugName: "form" }] : /* istanbul ignore next */ []));
782
851
  /** Whether keyboard navigation wraps at the list boundaries. */
783
852
  this.loopFocus = input(true, { ...(ngDevMode ? { debugName: "loopFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
784
853
  /**
@@ -857,6 +926,9 @@ class RdxComboboxRoot extends RdxFormUiControlBase {
857
926
  /** Converts a value to its display label. Defaults to the matching item's text. */
858
927
  this.itemToStringLabel = input(/* @ts-ignore */
859
928
  ...(ngDevMode ? [undefined, { debugName: "itemToStringLabel" }] : /* istanbul ignore next */ []));
929
+ /** Converts an item value to the string submitted by a native form. */
930
+ this.itemToStringValue = input(/* @ts-ignore */
931
+ ...(ngDevMode ? [undefined, { debugName: "itemToStringValue" }] : /* istanbul ignore next */ []));
860
932
  /** Emits before the selection changes; call `eventDetails.cancel()` to veto it. */
861
933
  this.onValueChange = output();
862
934
  /** Emits before the input text changes; call `eventDetails.cancel()` to veto it. */
@@ -874,6 +946,46 @@ class RdxComboboxRoot extends RdxFormUiControlBase {
874
946
  ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
875
947
  this.disabledState = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
876
948
  ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
949
+ this.nativeFormValue = computed(() => this.mode() === 'none' ? this.inputValue() : this.value(), /* @ts-ignore */
950
+ ...(ngDevMode ? [{ debugName: "nativeFormValue" }] : /* istanbul ignore next */ []));
951
+ this.nativeFormControl = useNativeFormControl({
952
+ name: this.name,
953
+ form: this.form,
954
+ disabled: this.disabledState,
955
+ value: this.nativeFormValue,
956
+ serialize: (value) => {
957
+ const itemToStringValue = this.itemToStringValue();
958
+ return itemToStringValue
959
+ ? serializeNativeFormValue(value, (entry) => itemToStringValue(entry))
960
+ : serializeNativeFormValue(value);
961
+ },
962
+ hasNativeControl: this.inputOwnsFormValue,
963
+ syncNativeControl: () => {
964
+ const input = this.engine.inputElement();
965
+ if (input && this.inputOwnsFormValue()) {
966
+ input.value = this.inputValue();
967
+ }
968
+ },
969
+ defaultValue: () => {
970
+ if (this.mode() === 'none') {
971
+ return this.inputValue();
972
+ }
973
+ const defaultValue = this.defaultValue();
974
+ return defaultValue === undefined ? this.value() : defaultValue;
975
+ },
976
+ onReset: (value) => {
977
+ if (this.mode() === 'none') {
978
+ this.setLabel(typeof value === 'string' ? value : '');
979
+ this.formUi.resetInteractionState?.();
980
+ return;
981
+ }
982
+ this.writeValue(value);
983
+ if (!this.resetNgControl(value)) {
984
+ this.onChange?.(value);
985
+ }
986
+ this.formUi.resetInteractionState?.();
987
+ }
988
+ });
877
989
  this.requiredState = computed(() => this.required(), /* @ts-ignore */
878
990
  ...(ngDevMode ? [{ debugName: "requiredState" }] : /* istanbul ignore next */ []));
879
991
  /** @ignore */
@@ -1157,7 +1269,7 @@ class RdxComboboxRoot extends RdxFormUiControlBase {
1157
1269
  /** Requests submit of the closest form when `submitOnItemClick` is enabled. */
1158
1270
  maybeSubmit() {
1159
1271
  if (this.submitOnItemClick()) {
1160
- this.engine.inputElement()?.form?.requestSubmit?.();
1272
+ this.nativeFormControl.requestSubmit();
1161
1273
  }
1162
1274
  }
1163
1275
  selectHighlighted(event = new Event('combobox.item-press')) {
@@ -1281,7 +1393,7 @@ class RdxComboboxRoot extends RdxFormUiControlBase {
1281
1393
  this.cvaDisabled.set(isDisabled);
1282
1394
  }
1283
1395
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxComboboxRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1284
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxComboboxRoot, isStandalone: true, selector: "[rdxComboboxRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, inputValue: { classPropertyName: "inputValue", publicName: "inputValue", 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 }, multipleInput: { classPropertyName: "multipleInput", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, fillInputOnItemPress: { classPropertyName: "fillInputOnItemPress", publicName: "fillInputOnItemPress", 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 }, 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 }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToStringLabel: { classPropertyName: "itemToStringLabel", publicName: "itemToStringLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", inputValue: "inputValueChange", open: "openChange", onValueChange: "onValueChange", onInputValueChange: "onInputValueChange", onOpenChange: "onOpenChange", onItemHighlighted: "onItemHighlighted", onOpenChangeComplete: "onOpenChangeComplete" }, host: { properties: { "attr.data-disabled": "disabledState() ? \"\" : undefined" } }, providers: [
1396
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxComboboxRoot, isStandalone: true, selector: "[rdxComboboxRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, inputValue: { classPropertyName: "inputValue", publicName: "inputValue", 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 }, multipleInput: { classPropertyName: "multipleInput", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, fillInputOnItemPress: { classPropertyName: "fillInputOnItemPress", publicName: "fillInputOnItemPress", 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 }, 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 }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToStringLabel: { classPropertyName: "itemToStringLabel", publicName: "itemToStringLabel", isSignal: true, isRequired: false, transformFunction: null }, itemToStringValue: { classPropertyName: "itemToStringValue", publicName: "itemToStringValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", inputValue: "inputValueChange", open: "openChange", onValueChange: "onValueChange", onInputValueChange: "onInputValueChange", onOpenChange: "onOpenChange", onItemHighlighted: "onItemHighlighted", onOpenChangeComplete: "onOpenChangeComplete" }, host: { properties: { "attr.data-disabled": "disabledState() ? \"\" : undefined" } }, providers: [
1285
1397
  provideComboboxRootContext(context),
1286
1398
  { provide: NG_VALUE_ACCESSOR, useExisting: RdxComboboxRoot, multi: true },
1287
1399
  // New floating foundation (ADR 0015/0017) — the dismissal capability reads this shared context.
@@ -1306,7 +1418,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
1306
1418
  '[attr.data-disabled]': 'disabledState() ? "" : undefined'
1307
1419
  }
1308
1420
  }]
1309
- }], 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 }] }], inputValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputValue", required: false }] }, { type: i0.Output, args: ["inputValueChange"] }], 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 }] }], multipleInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], fillInputOnItemPress: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillInputOnItemPress", 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 }] }], 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 }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", required: false }] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToStringLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringLabel", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onInputValueChange: [{ type: i0.Output, args: ["onInputValueChange"] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onItemHighlighted: [{ type: i0.Output, args: ["onItemHighlighted"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
1421
+ }], 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 }] }], inputValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputValue", required: false }] }, { type: i0.Output, args: ["inputValueChange"] }], 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 }] }], multipleInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], fillInputOnItemPress: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillInputOnItemPress", 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 }] }], 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 }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", required: false }] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToStringLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringLabel", required: false }] }], itemToStringValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringValue", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onInputValueChange: [{ type: i0.Output, args: ["onInputValueChange"] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onItemHighlighted: [{ type: i0.Output, args: ["onItemHighlighted"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
1310
1422
 
1311
1423
  /**
1312
1424
  * An overlay rendered beneath the popup in `modal` mode. Place it inside the portal/presence; style
@@ -1637,18 +1749,38 @@ class RdxComboboxGroup {
1637
1749
  this.rootContext = injectComboboxRootContext();
1638
1750
  this.labelId = signal(undefined, /* @ts-ignore */
1639
1751
  ...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
1640
- this.items = signal([], /* @ts-ignore */
1641
- ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1642
- this.hasItems = computed(() => this.items().length > 0, /* @ts-ignore */
1752
+ // Group membership is unordered, so a stable Set avoids copying a growing array for every child
1753
+ // registration. The tick keeps the two derived host states reactive.
1754
+ this.items = new Set();
1755
+ this.itemsTick = signal(0, /* @ts-ignore */
1756
+ ...(ngDevMode ? [{ debugName: "itemsTick" }] : /* istanbul ignore next */ []));
1757
+ this.hasItems = computed(() => {
1758
+ this.itemsTick();
1759
+ return this.items.size > 0;
1760
+ }, /* @ts-ignore */
1643
1761
  ...(ngDevMode ? [{ debugName: "hasItems" }] : /* istanbul ignore next */ []));
1644
- this.hasVisibleItems = computed(() => this.items().some((item) => this.rootContext.isVisible(item)), /* @ts-ignore */
1762
+ this.hasVisibleItems = computed(() => {
1763
+ this.itemsTick();
1764
+ for (const item of this.items) {
1765
+ if (this.rootContext.isVisible(item)) {
1766
+ return true;
1767
+ }
1768
+ }
1769
+ return false;
1770
+ }, /* @ts-ignore */
1645
1771
  ...(ngDevMode ? [{ debugName: "hasVisibleItems" }] : /* istanbul ignore next */ []));
1646
1772
  }
1647
1773
  registerItem(item) {
1648
- this.items.update((items) => [...items, item]);
1774
+ if (this.items.has(item)) {
1775
+ return;
1776
+ }
1777
+ this.items.add(item);
1778
+ this.itemsTick.update((tick) => tick + 1);
1649
1779
  }
1650
1780
  unregisterItem(item) {
1651
- this.items.update((items) => items.filter((i) => i !== item));
1781
+ if (this.items.delete(item)) {
1782
+ this.itemsTick.update((tick) => tick + 1);
1783
+ }
1652
1784
  }
1653
1785
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxComboboxGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1654
1786
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: RdxComboboxGroup, isStandalone: true, selector: "[rdxComboboxGroup]", host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelId()", "hidden": "hasItems() && !hasVisibleItems()" } }, providers: [provideComboboxGroupContext(groupContext)], exportAs: ["rdxComboboxGroup"], ngImport: i0 }); }
@@ -2003,7 +2135,7 @@ class RdxComboboxInput {
2003
2135
  }
2004
2136
  }
2005
2137
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxComboboxInput, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2006
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxComboboxInput, isStandalone: true, selector: "input[rdxComboboxInput]", 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", "aria-autocomplete": "list" }, listeners: { "input": "onInput($event)", "click": "onClick($event)", "focus": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)", "compositionstart": "composing = true", "compositionend": "onCompositionEnd($event)" }, properties: { "attr.id": "id()", "attr.aria-haspopup": "rootContext.grid() ? \"grid\" : \"listbox\"", "attr.aria-expanded": "rootContext.open()", "attr.aria-controls": "rootContext.listId", "attr.aria-labelledby": "rootContext.labelId()", "attr.aria-activedescendant": "rootContext.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": "rootContext.readonly() ? \"\" : undefined", "attr.required": "requiredState() ? \"\" : undefined", "value": "rootContext.inputValue()", "attr.data-popup-open": "dataAttr(rootContext.open())", "attr.data-list-empty": "dataAttr(rootContext.visibleCount() === 0)", "attr.data-placeholder": "dataAttr(isEmptyValue())", "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: ["rdxComboboxInput"], hostDirectives: [{ directive: i1.RdxPopperAnchor }, { directive: i1$1.RdxFloatingInsideElement }], ngImport: i0 }); }
2138
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxComboboxInput, isStandalone: true, selector: "input[rdxComboboxInput]", 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", "aria-autocomplete": "list" }, listeners: { "input": "onInput($event)", "click": "onClick($event)", "focus": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)", "compositionstart": "composing = true", "compositionend": "onCompositionEnd($event)" }, properties: { "attr.id": "id()", "attr.aria-haspopup": "rootContext.grid() ? \"grid\" : \"listbox\"", "attr.aria-expanded": "rootContext.open()", "attr.aria-controls": "rootContext.listId", "attr.aria-labelledby": "rootContext.labelId()", "attr.aria-activedescendant": "rootContext.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": "rootContext.readonly() ? \"\" : undefined", "attr.required": "requiredState() ? \"\" : undefined", "attr.name": "rootContext.inputOwnsFormValue() ? rootContext.name() : undefined", "attr.form": "rootContext.form()", "value": "rootContext.inputValue()", "attr.data-popup-open": "dataAttr(rootContext.open())", "attr.data-list-empty": "dataAttr(rootContext.visibleCount() === 0)", "attr.data-placeholder": "dataAttr(isEmptyValue())", "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: ["rdxComboboxInput"], hostDirectives: [{ directive: i1.RdxPopperAnchor }, { directive: i1$1.RdxFloatingInsideElement }], ngImport: i0 }); }
2007
2139
  }
2008
2140
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxComboboxInput, decorators: [{
2009
2141
  type: Directive,
@@ -2028,6 +2160,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
2028
2160
  '[attr.disabled]': 'disabledState() ? "" : undefined',
2029
2161
  '[attr.readonly]': 'rootContext.readonly() ? "" : undefined',
2030
2162
  '[attr.required]': 'requiredState() ? "" : undefined',
2163
+ '[attr.name]': 'rootContext.inputOwnsFormValue() ? rootContext.name() : undefined',
2164
+ '[attr.form]': 'rootContext.form()',
2031
2165
  '[value]': 'rootContext.inputValue()',
2032
2166
  '[attr.data-popup-open]': 'dataAttr(rootContext.open())',
2033
2167
  '[attr.data-list-empty]': 'dataAttr(rootContext.visibleCount() === 0)',