@radix-ng/primitives 1.0.4 → 1.0.5

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.
@@ -1,29 +1,49 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, ElementRef, model, input, booleanAttribute, output, signal, computed, effect, Directive, Renderer2, DestroyRef } from '@angular/core';
2
+ import { inject, ElementRef, model, input, booleanAttribute, output, signal, computed, effect, untracked, Directive } from '@angular/core';
3
3
  import * as i1 from '@radix-ng/primitives/composite';
4
4
  import { RdxCompositeRoot, RdxCompositeItem } from '@radix-ng/primitives/composite';
5
- import { createCancelableChangeEventDetails, provideValueAccessor, provideToken } from '@radix-ng/primitives/core';
6
- import * as i1$1 from '@radix-ng/primitives/visually-hidden';
7
- import { RdxVisuallyHiddenDirective } from '@radix-ng/primitives/visually-hidden';
8
-
9
- const RDX_RADIO_GROUP = new InjectionToken('RdxRadioGroup');
5
+ import { createContext, createCancelableChangeEventDetails, provideValueAccessor } from '@radix-ng/primitives/core';
10
6
 
7
+ const rootContext = () => {
8
+ const root = inject(RdxRadioGroupDirective);
9
+ return {
10
+ value: root.value,
11
+ disabledState: root.disabledState,
12
+ readonly: root.readonly,
13
+ required: root.required,
14
+ name: root.name,
15
+ form: root.form,
16
+ select: (value, event, reason) => root.select(value, event, reason),
17
+ setArrowNavigation: (value) => root.setArrowNavigation(value),
18
+ isArrowNavigation: () => root.isArrowNavigation()
19
+ };
20
+ };
21
+ const [injectRadioRootContext, provideRadioRootContext] = createContext('RadioRootContext', 'components/radio');
11
22
  class RdxRadioGroupDirective {
12
23
  constructor() {
13
24
  this.elementRef = inject(ElementRef);
14
25
  this.compositeRoot = inject(RdxCompositeRoot, { self: true });
26
+ /**
27
+ * The selected value. Deliberately typed as `string` (not Base UI's generic `Value`):
28
+ * a radio group maps onto native radio inputs whose form value is a string, so values are
29
+ * the option identifiers. Use the string key of your option as the value.
30
+ */
15
31
  this.value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
16
32
  this.defaultValue = input(...(ngDevMode ? [undefined, { debugName: "defaultValue" }] : /* istanbul ignore next */ []));
17
33
  this.name = input(...(ngDevMode ? [undefined, { debugName: "name" }] : /* istanbul ignore next */ []));
18
34
  this.form = input(...(ngDevMode ? [undefined, { debugName: "form" }] : /* istanbul ignore next */ []));
19
35
  this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
20
- this.readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
36
+ /**
37
+ * Whether the user should be unable to select a different radio button. Bound in templates as
38
+ * `readOnly` (Base UI spelling); the TS member stays `readonly` for cross-primitive consistency.
39
+ */
40
+ this.readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), alias: 'readOnly', transform: booleanAttribute });
21
41
  this.required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
22
42
  /**
23
43
  * Event handler called when the value changes.
24
44
  */
25
45
  this.onValueChange = output();
26
- this.disable = signal(this.disabled(), ...(ngDevMode ? [{ debugName: "disable" }] : /* istanbul ignore next */ []));
46
+ this.disable = signal(false, ...(ngDevMode ? [{ debugName: "disable" }] : /* istanbul ignore next */ []));
27
47
  this.disabledState = computed(() => this.disable() || this.disabled(), ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
28
48
  this.arrowNavigation = signal(false, ...(ngDevMode ? [{ debugName: "arrowNavigation" }] : /* istanbul ignore next */ []));
29
49
  this.itemMetadata = computed(() => Array.from(this.compositeRoot.itemMap().values()).filter(isRadioItemMetadata), ...(ngDevMode ? [{ debugName: "itemMetadata" }] : /* istanbul ignore next */ []));
@@ -50,9 +70,15 @@ class RdxRadioGroupDirective {
50
70
  this.onTouched = () => {
51
71
  /* Empty */
52
72
  };
73
+ let hasAppliedDefault = false;
53
74
  effect(() => {
54
- if (this.value() === null && this.defaultValue() !== undefined) {
55
- this.value.set(this.defaultValue());
75
+ const defaultValue = this.defaultValue();
76
+ if (hasAppliedDefault || defaultValue === undefined) {
77
+ return;
78
+ }
79
+ hasAppliedDefault = true;
80
+ if (untracked(this.value) === null) {
81
+ this.value.set(defaultValue);
56
82
  }
57
83
  });
58
84
  effect(() => {
@@ -133,21 +159,25 @@ class RdxRadioGroupDirective {
133
159
  if (this.disabledState())
134
160
  return;
135
161
  }
162
+ /**
163
+ * Marks the control touched when focus leaves the whole group (Base UI `RadioGroup` parity and the
164
+ * ADR 0004 CVA strategy) — moving focus between items stays inside, so `relatedTarget` is checked.
165
+ */
166
+ onFocusOut(event) {
167
+ const next = event.relatedTarget;
168
+ if (!this.elementRef.nativeElement.contains(next)) {
169
+ this.onTouched();
170
+ }
171
+ }
136
172
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxRadioGroupDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
137
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxRadioGroupDirective, isStandalone: true, selector: "[rdxRadioRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", 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 }, 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 } }, outputs: { value: "valueChange", onValueChange: "onValueChange" }, host: { attributes: { "role": "radiogroup" }, listeners: { "keydown": "onKeydown()" }, properties: { "attr.aria-required": "required() ? \"true\" : undefined", "attr.aria-disabled": "disabledState() ? \"true\" : undefined", "attr.aria-readonly": "readonly() ? \"true\" : undefined", "attr.data-disabled": "disabledState() ? \"\" : undefined", "attr.data-readonly": "readonly() ? \"\" : undefined", "attr.data-required": "required() ? \"\" : undefined" } }, providers: [
138
- provideValueAccessor(RdxRadioGroupDirective),
139
- { provide: RDX_RADIO_GROUP, useExisting: RdxRadioGroupDirective }
140
- ], exportAs: ["rdxRadioRoot"], hostDirectives: [{ directive: i1.RdxCompositeRoot }], ngImport: i0 }); }
173
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxRadioGroupDirective, isStandalone: true, selector: "[rdxRadioRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", 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 }, 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 } }, outputs: { value: "valueChange", onValueChange: "onValueChange" }, host: { attributes: { "role": "radiogroup" }, listeners: { "keydown": "onKeydown()", "focusout": "onFocusOut($event)" }, properties: { "attr.aria-required": "required() ? \"true\" : undefined", "attr.aria-disabled": "disabledState() ? \"true\" : undefined", "attr.aria-readonly": "readonly() ? \"true\" : undefined", "attr.data-disabled": "disabledState() ? \"\" : undefined" } }, providers: [provideValueAccessor(RdxRadioGroupDirective), provideRadioRootContext(rootContext)], exportAs: ["rdxRadioRoot"], hostDirectives: [{ directive: i1.RdxCompositeRoot }], ngImport: i0 }); }
141
174
  }
142
175
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxRadioGroupDirective, decorators: [{
143
176
  type: Directive,
144
177
  args: [{
145
178
  selector: '[rdxRadioRoot]',
146
179
  exportAs: 'rdxRadioRoot',
147
- providers: [
148
- provideValueAccessor(RdxRadioGroupDirective),
149
- { provide: RDX_RADIO_GROUP, useExisting: RdxRadioGroupDirective }
150
- ],
180
+ providers: [provideValueAccessor(RdxRadioGroupDirective), provideRadioRootContext(rootContext)],
151
181
  hostDirectives: [RdxCompositeRoot],
152
182
  host: {
153
183
  role: 'radiogroup',
@@ -155,65 +185,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
155
185
  '[attr.aria-disabled]': 'disabledState() ? "true" : undefined',
156
186
  '[attr.aria-readonly]': 'readonly() ? "true" : undefined',
157
187
  '[attr.data-disabled]': 'disabledState() ? "" : undefined',
158
- '[attr.data-readonly]': 'readonly() ? "" : undefined',
159
- '[attr.data-required]': 'required() ? "" : undefined',
160
- '(keydown)': 'onKeydown()'
188
+ '(keydown)': 'onKeydown()',
189
+ '(focusout)': 'onFocusOut($event)'
161
190
  }
162
191
  }]
163
- }], 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 }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", 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 }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }] } });
192
+ }], 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 }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", 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 }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }] } });
164
193
  function isRadioItemMetadata(metadata) {
165
194
  return typeof metadata['disabled'] === 'boolean' && typeof metadata['value'] === 'string';
166
195
  }
167
196
 
168
- const RdxRadioItemToken = new InjectionToken('RadioItemToken');
169
- function injectRadioItem() {
170
- return inject(RdxRadioItemToken);
171
- }
197
+ const itemContext = () => {
198
+ const item = inject(RdxRadioItemDirective);
199
+ return {
200
+ value: item.value,
201
+ checkedState: item.checkedState,
202
+ disabledState: item.disabledState,
203
+ readonlyState: item.readonlyState,
204
+ requiredState: item.requiredState
205
+ };
206
+ };
207
+ const [injectRadioItemContext, provideRadioItemContext] = createContext('RadioItemContext', 'components/radio');
172
208
  class RdxRadioItemDirective {
173
209
  constructor() {
174
- this.radioGroup = inject(RDX_RADIO_GROUP);
175
- this.elementRef = inject(ElementRef);
176
- this.renderer = inject(Renderer2);
210
+ this.rootContext = injectRadioRootContext();
177
211
  this.compositeItem = inject(RdxCompositeItem, { self: true });
178
- this.destroyRef = inject(DestroyRef);
179
- this.inputElement = this.renderer.createElement('input');
212
+ this.elementRef = inject(ElementRef);
180
213
  this.value = input.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
181
214
  this.id = input(...(ngDevMode ? [undefined, { debugName: "id" }] : /* istanbul ignore next */ []));
182
215
  this.required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
183
216
  this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
184
- this.readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
185
- this.nativeButton = input(false, { ...(ngDevMode ? { debugName: "nativeButton" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
186
- this.nativeButtonState = computed(() => this.nativeButton(), ...(ngDevMode ? [{ debugName: "nativeButtonState" }] : /* istanbul ignore next */ []));
187
- this.disabledState = computed(() => this.radioGroup.disabledState() || this.disabled(), ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
188
- this.readonlyState = computed(() => this.radioGroup.readonly() || this.readonly(), ...(ngDevMode ? [{ debugName: "readonlyState" }] : /* istanbul ignore next */ []));
189
- this.requiredState = computed(() => this.radioGroup.required() || this.required(), ...(ngDevMode ? [{ debugName: "requiredState" }] : /* istanbul ignore next */ []));
190
- this.checkedState = computed(() => this.radioGroup.value() === this.value(), ...(ngDevMode ? [{ debugName: "checkedState" }] : /* istanbul ignore next */ []));
217
+ /**
218
+ * Whether the user should be unable to select this radio button. Bound in templates as
219
+ * `readOnly` (Base UI spelling); the TS member stays `readonly` for cross-primitive consistency.
220
+ */
221
+ this.readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), alias: 'readOnly', transform: booleanAttribute });
222
+ /**
223
+ * Whether the host is a native `<button>`. Detected from the host tag (unlike Base UI's explicit
224
+ * `nativeButton` prop, the rendered element is statically known in Angular templates), and used to
225
+ * apply `type="button"` and the native `disabled` attribute.
226
+ */
227
+ this.isNativeButton = this.elementRef.nativeElement.tagName === 'BUTTON';
228
+ this.disabledState = computed(() => this.rootContext.disabledState() || this.disabled(), ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
229
+ this.readonlyState = computed(() => this.rootContext.readonly() || this.readonly(), ...(ngDevMode ? [{ debugName: "readonlyState" }] : /* istanbul ignore next */ []));
230
+ this.requiredState = computed(() => this.rootContext.required() || this.required(), ...(ngDevMode ? [{ debugName: "requiredState" }] : /* istanbul ignore next */ []));
231
+ this.checkedState = computed(() => this.rootContext.value() === this.value(), ...(ngDevMode ? [{ debugName: "checkedState" }] : /* istanbul ignore next */ []));
191
232
  this.ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
192
- this.createHiddenInput();
193
- const unlistenInputChange = this.renderer.listen(this.inputElement, 'change', (event) => {
194
- if (this.inputElement.checked) {
195
- this.radioGroup.select(this.value(), event);
196
- }
197
- });
198
- this.destroyRef.onDestroy(() => {
199
- unlistenInputChange();
200
- const parent = this.inputElement.parentNode;
201
- if (parent) {
202
- this.renderer.removeChild(parent, this.inputElement);
203
- }
204
- });
205
233
  effect(() => {
206
234
  this.compositeItem.setMetadata({
207
235
  disabled: this.disabledState(),
208
236
  value: this.value()
209
237
  });
210
- this.syncHiddenInput();
211
238
  });
212
239
  }
213
240
  /** @ignore */
214
241
  onClick(event) {
215
242
  if (!this.disabledState() && !this.readonlyState()) {
216
- this.radioGroup.select(this.value(), event);
243
+ this.rootContext.select(this.value(), event);
217
244
  }
218
245
  }
219
246
  /** @ignore */
@@ -228,91 +255,37 @@ class RdxRadioItemDirective {
228
255
  return;
229
256
  }
230
257
  if (this.isAllowedArrowKey(keyEvent.key)) {
231
- this.radioGroup.setArrowNavigation(true);
258
+ this.rootContext.setArrowNavigation(true);
232
259
  }
233
260
  }
234
261
  /** @ignore */
235
262
  onKeyUp() {
236
- this.radioGroup.setArrowNavigation(false);
263
+ this.rootContext.setArrowNavigation(false);
237
264
  }
238
265
  /** @ignore */
239
266
  onFocus(event) {
240
267
  queueMicrotask(() => {
241
- if (this.radioGroup.isArrowNavigation()) {
242
- this.radioGroup.select(this.value(), event);
243
- this.radioGroup.setArrowNavigation(false);
268
+ if (this.rootContext.isArrowNavigation()) {
269
+ this.rootContext.select(this.value(), event);
270
+ this.rootContext.setArrowNavigation(false);
244
271
  }
245
272
  });
246
273
  }
247
274
  isAllowedArrowKey(key) {
248
- if (!this.ARROW_KEYS.includes(key)) {
249
- return false;
250
- }
251
- return true;
252
- }
253
- createHiddenInput() {
254
- const host = this.elementRef.nativeElement;
255
- const parent = host.parentNode;
256
- this.renderer.setAttribute(this.inputElement, 'type', 'radio');
257
- this.renderer.setAttribute(this.inputElement, 'tabindex', '-1');
258
- this.renderer.setAttribute(this.inputElement, 'aria-hidden', 'true');
259
- this.renderer.setStyle(this.inputElement, 'position', 'absolute');
260
- this.renderer.setStyle(this.inputElement, 'pointer-events', 'none');
261
- this.renderer.setStyle(this.inputElement, 'opacity', '0');
262
- this.renderer.setStyle(this.inputElement, 'margin', '0');
263
- this.renderer.setStyle(this.inputElement, 'inset', '0');
264
- this.renderer.setStyle(this.inputElement, 'transform', 'translateX(-100%)');
265
- if (parent) {
266
- this.renderer.insertBefore(parent, this.inputElement, host.nextSibling);
267
- }
268
- }
269
- syncHiddenInput() {
270
- const checked = this.checkedState();
271
- this.inputElement.name = this.radioGroup.name() ?? '';
272
- this.inputElement.value = this.value();
273
- this.inputElement.checked = checked;
274
- this.inputElement.required = this.requiredState();
275
- this.inputElement.disabled = this.disabledState();
276
- this.setOptionalAttribute('name', this.radioGroup.name());
277
- this.setOptionalAttribute('form', this.radioGroup.form());
278
- this.setBooleanAttribute('checked', checked);
279
- this.setBooleanAttribute('required', this.requiredState());
280
- this.setBooleanAttribute('disabled', this.disabledState());
281
- this.renderer.setAttribute(this.inputElement, 'value', this.value());
282
- if (this.previousCheckedState === false && checked) {
283
- this.inputElement.dispatchEvent(new Event('input', { bubbles: true }));
284
- this.inputElement.dispatchEvent(new Event('change', { bubbles: true }));
285
- }
286
- this.previousCheckedState = checked;
287
- }
288
- setOptionalAttribute(name, value) {
289
- if (value) {
290
- this.renderer.setAttribute(this.inputElement, name, value);
291
- }
292
- else {
293
- this.renderer.removeAttribute(this.inputElement, name);
294
- }
295
- }
296
- setBooleanAttribute(name, value) {
297
- if (value) {
298
- this.renderer.setAttribute(this.inputElement, name, '');
299
- }
300
- else {
301
- this.renderer.removeAttribute(this.inputElement, name);
302
- }
275
+ return this.ARROW_KEYS.includes(key);
303
276
  }
304
277
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxRadioItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
305
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxRadioItemDirective, isStandalone: true, selector: "[rdxRadioItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", 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 }, nativeButton: { classPropertyName: "nativeButton", publicName: "nativeButton", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "radio" }, listeners: { "click": "onClick($event)", "keydown": "onKeyDown($event)", "keyup": "onKeyUp()", "focus": "onFocus($event)" }, properties: { "attr.type": "nativeButtonState() ? \"button\" : undefined", "attr.aria-checked": "checkedState()", "attr.aria-disabled": "disabledState() ? \"true\" : undefined", "attr.aria-readonly": "readonlyState() ? \"true\" : undefined", "attr.aria-required": "requiredState() ? \"true\" : undefined", "attr.data-composite-item-active": "checkedState() ? \"\" : undefined", "attr.data-checked": "checkedState() ? \"\" : undefined", "attr.data-unchecked": "!checkedState() ? \"\" : undefined", "attr.data-disabled": "disabledState() ? \"\" : undefined", "attr.data-readonly": "readonlyState() ? \"\" : undefined", "attr.data-required": "requiredState() ? \"\" : undefined", "attr.data-state": "checkedState() ? \"checked\" : \"unchecked\"", "attr.disabled": "nativeButtonState() && disabledState() ? \"\" : undefined" } }, providers: [provideToken(RdxRadioItemToken, RdxRadioItemDirective)], exportAs: ["rdxRadioItem"], hostDirectives: [{ directive: i1.RdxCompositeItem }], ngImport: i0 }); }
278
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxRadioItemDirective, isStandalone: true, selector: "[rdxRadioItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", 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 } }, host: { attributes: { "role": "radio" }, listeners: { "click": "onClick($event)", "keydown": "onKeyDown($event)", "keyup": "onKeyUp()", "focus": "onFocus($event)" }, properties: { "attr.type": "isNativeButton ? \"button\" : undefined", "attr.aria-checked": "checkedState()", "attr.aria-disabled": "disabledState() ? \"true\" : undefined", "attr.aria-readonly": "readonlyState() ? \"true\" : undefined", "attr.aria-required": "requiredState() ? \"true\" : undefined", "attr.data-composite-item-active": "checkedState() ? \"\" : undefined", "attr.data-checked": "checkedState() ? \"\" : undefined", "attr.data-unchecked": "!checkedState() ? \"\" : undefined", "attr.data-disabled": "disabledState() ? \"\" : undefined", "attr.data-readonly": "readonlyState() ? \"\" : undefined", "attr.data-required": "requiredState() ? \"\" : undefined", "attr.disabled": "isNativeButton && disabledState() ? \"\" : undefined" } }, providers: [provideRadioItemContext(itemContext)], exportAs: ["rdxRadioItem"], hostDirectives: [{ directive: i1.RdxCompositeItem }], ngImport: i0 }); }
306
279
  }
307
280
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxRadioItemDirective, decorators: [{
308
281
  type: Directive,
309
282
  args: [{
310
283
  selector: '[rdxRadioItem]',
311
284
  exportAs: 'rdxRadioItem',
312
- providers: [provideToken(RdxRadioItemToken, RdxRadioItemDirective)],
285
+ providers: [provideRadioItemContext(itemContext)],
313
286
  hostDirectives: [RdxCompositeItem],
314
287
  host: {
315
- '[attr.type]': 'nativeButtonState() ? "button" : undefined',
288
+ '[attr.type]': 'isNativeButton ? "button" : undefined',
316
289
  role: 'radio',
317
290
  '[attr.aria-checked]': 'checkedState()',
318
291
  '[attr.aria-disabled]': 'disabledState() ? "true" : undefined',
@@ -324,23 +297,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
324
297
  '[attr.data-disabled]': 'disabledState() ? "" : undefined',
325
298
  '[attr.data-readonly]': 'readonlyState() ? "" : undefined',
326
299
  '[attr.data-required]': 'requiredState() ? "" : undefined',
327
- '[attr.data-state]': 'checkedState() ? "checked" : "unchecked"',
328
- '[attr.disabled]': 'nativeButtonState() && disabledState() ? "" : undefined',
300
+ '[attr.disabled]': 'isNativeButton && disabledState() ? "" : undefined',
329
301
  '(click)': 'onClick($event)',
330
302
  '(keydown)': 'onKeyDown($event)',
331
303
  '(keyup)': 'onKeyUp()',
332
304
  '(focus)': 'onFocus($event)'
333
305
  }
334
306
  }]
335
- }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], nativeButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "nativeButton", required: false }] }] } });
307
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnly", required: false }] }] } });
336
308
 
337
309
  class RdxRadioIndicatorDirective {
338
310
  constructor() {
339
- this.radioGroup = inject(RDX_RADIO_GROUP);
340
- this.radioItem = inject(RdxRadioItemDirective);
311
+ this.itemContext = injectRadioItemContext();
312
+ /** Keep the indicator in the DOM when unchecked so CSS exit animations can play. */
313
+ this.keepMounted = input(false, { ...(ngDevMode ? { debugName: "keepMounted" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
314
+ this.isVisible = computed(() => this.itemContext.checkedState(), ...(ngDevMode ? [{ debugName: "isVisible" }] : /* istanbul ignore next */ []));
341
315
  }
342
316
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxRadioIndicatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
343
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxRadioIndicatorDirective, isStandalone: true, selector: "[rdxRadioIndicator]", host: { properties: { "attr.data-checked": "radioItem.checkedState() ? \"\" : undefined", "attr.data-unchecked": "!radioItem.checkedState() ? \"\" : undefined", "attr.data-state": "radioItem.checkedState() ? \"checked\" : \"unchecked\"", "attr.data-disabled": "radioItem.disabledState() ? \"\" : undefined", "attr.data-readonly": "radioItem.readonlyState() ? \"\" : undefined", "attr.data-required": "radioItem.requiredState() ? \"\" : undefined", "hidden": "!radioItem.checkedState()", "style.pointer-events": "\"none\"" } }, exportAs: ["rdxRadioIndicator"], ngImport: i0 }); }
317
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxRadioIndicatorDirective, isStandalone: true, selector: "[rdxRadioIndicator]", inputs: { keepMounted: { classPropertyName: "keepMounted", publicName: "keepMounted", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-checked": "itemContext.checkedState() ? \"\" : undefined", "attr.data-unchecked": "!itemContext.checkedState() ? \"\" : undefined", "attr.data-disabled": "itemContext.disabledState() ? \"\" : undefined", "attr.data-readonly": "itemContext.readonlyState() ? \"\" : undefined", "attr.data-required": "itemContext.requiredState() ? \"\" : undefined", "attr.data-starting-style": "isVisible() ? \"\" : undefined", "attr.data-ending-style": "!isVisible() ? \"\" : undefined", "style.display": "!keepMounted() && !isVisible() ? \"none\" : null", "style.pointer-events": "\"none\"" } }, exportAs: ["rdxRadioIndicator"], ngImport: i0 }); }
344
318
  }
345
319
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxRadioIndicatorDirective, decorators: [{
346
320
  type: Directive,
@@ -348,29 +322,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
348
322
  selector: '[rdxRadioIndicator]',
349
323
  exportAs: 'rdxRadioIndicator',
350
324
  host: {
351
- '[attr.data-checked]': 'radioItem.checkedState() ? "" : undefined',
352
- '[attr.data-unchecked]': '!radioItem.checkedState() ? "" : undefined',
353
- '[attr.data-state]': 'radioItem.checkedState() ? "checked" : "unchecked"',
354
- '[attr.data-disabled]': 'radioItem.disabledState() ? "" : undefined',
355
- '[attr.data-readonly]': 'radioItem.readonlyState() ? "" : undefined',
356
- '[attr.data-required]': 'radioItem.requiredState() ? "" : undefined',
357
- '[hidden]': '!radioItem.checkedState()',
325
+ '[attr.data-checked]': 'itemContext.checkedState() ? "" : undefined',
326
+ '[attr.data-unchecked]': '!itemContext.checkedState() ? "" : undefined',
327
+ '[attr.data-disabled]': 'itemContext.disabledState() ? "" : undefined',
328
+ '[attr.data-readonly]': 'itemContext.readonlyState() ? "" : undefined',
329
+ '[attr.data-required]': 'itemContext.requiredState() ? "" : undefined',
330
+ '[attr.data-starting-style]': 'isVisible() ? "" : undefined',
331
+ '[attr.data-ending-style]': '!isVisible() ? "" : undefined',
332
+ '[style.display]': '!keepMounted() && !isVisible() ? "none" : null',
358
333
  '[style.pointer-events]': '"none"'
359
334
  }
360
335
  }]
361
- }] });
336
+ }], propDecorators: { keepMounted: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepMounted", required: false }] }] } });
362
337
 
338
+ /**
339
+ * The hidden native radio input that mirrors the item state for form submission, native validation,
340
+ * and `<label>` activation. Place it inside an `rdxRadioItem`.
341
+ *
342
+ * @see https://base-ui.com/react/components/radio
343
+ */
363
344
  class RdxRadioItemInputDirective {
364
345
  constructor() {
365
- this.radioItem = injectRadioItem();
366
- this.radioGroup = inject(RDX_RADIO_GROUP);
346
+ this.rootContext = injectRadioRootContext();
347
+ this.itemContext = injectRadioItemContext();
367
348
  this.input = inject(ElementRef).nativeElement;
368
- this.name = computed(() => this.radioGroup.name(), ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
369
- this.form = computed(() => this.radioGroup.form(), ...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
370
- this.value = computed(() => this.radioItem.value() || undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
371
- this.checked = computed(() => this.radioItem.checkedState(), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
372
- this.required = computed(() => this.radioItem.requiredState(), ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
373
- this.disabled = computed(() => this.radioItem.disabledState(), ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
349
+ this.name = computed(() => this.rootContext.name(), ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
350
+ this.form = computed(() => this.rootContext.form(), ...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
351
+ this.value = computed(() => this.itemContext.value() || undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
352
+ this.checked = computed(() => this.itemContext.checkedState(), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
353
+ this.required = computed(() => this.itemContext.requiredState(), ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
354
+ this.disabled = computed(() => this.itemContext.disabledState(), ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
374
355
  let isInitial = true;
375
356
  effect(() => {
376
357
  const checked = this.checked();
@@ -384,15 +365,26 @@ class RdxRadioItemInputDirective {
384
365
  }
385
366
  });
386
367
  }
368
+ /**
369
+ * Selects this item when the native input is checked — covers `<label>` activation,
370
+ * where clicking the label toggles the hidden radio input rather than the visible item.
371
+ * `select()` is a no-op when the value is already current, so the programmatic
372
+ * `change` dispatched above does not re-trigger selection.
373
+ * @ignore
374
+ */
375
+ onInputChange(event) {
376
+ if (this.input.checked) {
377
+ this.rootContext.select(this.itemContext.value(), event);
378
+ }
379
+ }
387
380
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxRadioItemInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
388
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxRadioItemInputDirective, isStandalone: true, selector: "input[rdxRadioItemInput]", host: { attributes: { "type": "radio", "tabindex": "-1", "aria-hidden": "true" }, properties: { "attr.name": "name()", "attr.form": "form()", "attr.required": "required() ? \"\" : undefined", "attr.disabled": "disabled() ? \"\" : undefined", "attr.checked": "checked() ? \"\" : undefined", "checked": "checked()", "attr.value": "value()", "style": "{\n position: 'absolute',\n pointerEvents: 'none',\n opacity: 0,\n margin: 0,\n inset: 0,\n transform: 'translateX(-100%)',\n }" } }, exportAs: ["rdxRadioItemInput"], hostDirectives: [{ directive: i1$1.RdxVisuallyHiddenDirective, inputs: ["feature", "feature"] }], ngImport: i0 }); }
381
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxRadioItemInputDirective, isStandalone: true, selector: "input[rdxRadioItemInput]", host: { attributes: { "type": "radio", "tabindex": "-1", "aria-hidden": "true" }, listeners: { "change": "onInputChange($event)" }, properties: { "attr.name": "name()", "attr.form": "form()", "attr.required": "required() ? \"\" : undefined", "attr.disabled": "disabled() ? \"\" : undefined", "attr.checked": "checked() ? \"\" : undefined", "checked": "checked()", "attr.value": "value()" }, styleAttribute: "transform: translateX(-100%); position: absolute; pointer-events: none; opacity: 0; margin: 0; inset: 0;" }, exportAs: ["rdxRadioItemInput"], ngImport: i0 }); }
389
382
  }
390
383
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxRadioItemInputDirective, decorators: [{
391
384
  type: Directive,
392
385
  args: [{
393
386
  selector: 'input[rdxRadioItemInput]',
394
387
  exportAs: 'rdxRadioItemInput',
395
- hostDirectives: [{ directive: RdxVisuallyHiddenDirective, inputs: ['feature'] }],
396
388
  host: {
397
389
  type: 'radio',
398
390
  tabindex: '-1',
@@ -404,14 +396,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
404
396
  '[attr.checked]': 'checked() ? "" : undefined',
405
397
  '[checked]': 'checked()',
406
398
  '[attr.value]': 'value()',
407
- '[style]': `{
408
- position: 'absolute',
409
- pointerEvents: 'none',
410
- opacity: 0,
411
- margin: 0,
412
- inset: 0,
413
- transform: 'translateX(-100%)',
414
- }`
399
+ '(change)': 'onInputChange($event)',
400
+ style: 'transform: translateX(-100%); position: absolute; pointer-events: none; opacity: 0; margin: 0; inset: 0;'
415
401
  }
416
402
  }]
417
403
  }], ctorParameters: () => [] });
@@ -420,5 +406,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
420
406
  * Generated bundle index. Do not edit.
421
407
  */
422
408
 
423
- export { RdxRadioGroupDirective, RdxRadioIndicatorDirective, RdxRadioItemDirective, RdxRadioItemInputDirective, RdxRadioItemToken, injectRadioItem };
409
+ export { RdxRadioGroupDirective, RdxRadioIndicatorDirective, RdxRadioItemDirective, RdxRadioItemInputDirective, injectRadioItemContext, injectRadioRootContext, provideRadioItemContext, provideRadioRootContext };
424
410
  //# sourceMappingURL=radix-ng-primitives-radio.mjs.map