@ng-forge/dynamic-forms-ionic 0.3.0 → 0.4.0

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,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, input, computed, ChangeDetectionStrategy, Component, ElementRef, signal, InjectionToken, linkedSignal, model, isSignal } from '@angular/core';
3
3
  import { IonButton, IonCheckbox, IonNote, IonInput, IonModal, IonDatetime, IonButtons, IonHeader, IonContent, IonToolbar, IonTitle, IonItem, IonRadioGroup, IonRadio, IonSelect, IonSelectOption, IonRange, IonTextarea, IonToggle } from '@ionic/angular/standalone';
4
- import { EventBus, ARRAY_CONTEXT, resolveTokens, DynamicTextPipe, buildBaseInputs, FIELD_SIGNAL_CONTEXT, resolveSubmitButtonDisabled, resolveNextButtonDisabled, NextPageEvent, PreviousPageEvent, DynamicFormLogger, AddArrayItemEvent, RemoveArrayItemEvent } from '@ng-forge/dynamic-forms';
4
+ import { EventBus, ARRAY_CONTEXT, resolveTokens, DynamicTextPipe, DEFAULT_PROPS, buildBaseInputs, RootFormRegistryService, FORM_OPTIONS, resolveSubmitButtonDisabled, FIELD_SIGNAL_CONTEXT, resolveNextButtonDisabled, NextPageEvent, PreviousPageEvent, DynamicFormLogger, AddArrayItemEvent, RemoveArrayItemEvent } from '@ng-forge/dynamic-forms';
5
5
  import { AsyncPipe } from '@angular/common';
6
6
  import { FormField } from '@angular/forms/signals';
7
- import { createResolvedErrorsSignal, shouldShowErrors, setupMetaTracking, isEqual, ValueInArrayPipe, valueFieldMapper, optionsFieldMapper, checkboxFieldMapper, datepickerFieldMapper } from '@ng-forge/dynamic-forms/integration';
7
+ import { createResolvedErrorsSignal, shouldShowErrors, setupMetaTracking, isEqual, valueFieldMapper, optionsFieldMapper, checkboxFieldMapper, datepickerFieldMapper } from '@ng-forge/dynamic-forms/integration';
8
8
  import { format } from 'date-fns';
9
9
  import { explicitEffect } from 'ngxtension/explicit-effect';
10
10
 
@@ -64,9 +64,12 @@ class IonicButtonFieldComponent {
64
64
  this.eventBus.dispatch(event);
65
65
  }
66
66
  }
67
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicButtonFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
68
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.8", type: IonicButtonFieldComponent, isStandalone: true, selector: "df-ion-button", inputs: { key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, event: { classPropertyName: "event", publicName: "event", isSignal: true, isRequired: false, transformFunction: null }, eventArgs: { classPropertyName: "eventArgs", publicName: "eventArgs", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, eventContext: { classPropertyName: "eventContext", publicName: "eventContext", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
67
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicButtonFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
68
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: IonicButtonFieldComponent, isStandalone: true, selector: "df-ion-button", inputs: { key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, event: { classPropertyName: "event", publicName: "event", isSignal: true, isRequired: false, transformFunction: null }, eventArgs: { classPropertyName: "eventArgs", publicName: "eventArgs", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, eventContext: { classPropertyName: "eventContext", publicName: "eventContext", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "class": "className()", "attr.data-testid": "key()", "attr.hidden": "hidden() || null" } }, ngImport: i0, template: `
69
+ @let buttonId = key() + '-button';
70
+
69
71
  <ion-button
72
+ [id]="buttonId"
70
73
  [type]="buttonType()"
71
74
  [expand]="props()?.expand"
72
75
  [fill]="props()?.fill || 'solid'"
@@ -74,26 +77,26 @@ class IonicButtonFieldComponent {
74
77
  [size]="props()?.size"
75
78
  [color]="props()?.color || 'primary'"
76
79
  [strong]="props()?.strong"
77
- [class]="className() || ''"
78
80
  [disabled]="disabled()"
79
81
  [attr.data-testid]="buttonTestId()"
80
82
  (click)="onClick()"
81
83
  >
82
84
  {{ label() | dynamicText | async }}
83
85
  </ion-button>
84
- `, isInline: true, dependencies: [{ kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
86
+ `, isInline: true, styles: [":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
85
87
  }
86
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicButtonFieldComponent, decorators: [{
88
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicButtonFieldComponent, decorators: [{
87
89
  type: Component,
88
- args: [{
89
- selector: 'df-ion-button',
90
- imports: [IonButton, DynamicTextPipe, AsyncPipe],
91
- host: {
90
+ args: [{ selector: 'df-ion-button', imports: [IonButton, DynamicTextPipe, AsyncPipe], host: {
92
91
  '[id]': '`${key()}`',
92
+ '[class]': 'className()',
93
93
  '[attr.data-testid]': 'key()',
94
- },
95
- template: `
94
+ '[attr.hidden]': 'hidden() || null',
95
+ }, template: `
96
+ @let buttonId = key() + '-button';
97
+
96
98
  <ion-button
99
+ [id]="buttonId"
97
100
  [type]="buttonType()"
98
101
  [expand]="props()?.expand"
99
102
  [fill]="props()?.fill || 'solid'"
@@ -101,16 +104,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
101
104
  [size]="props()?.size"
102
105
  [color]="props()?.color || 'primary'"
103
106
  [strong]="props()?.strong"
104
- [class]="className() || ''"
105
107
  [disabled]="disabled()"
106
108
  [attr.data-testid]="buttonTestId()"
107
109
  (click)="onClick()"
108
110
  >
109
111
  {{ label() | dynamicText | async }}
110
112
  </ion-button>
111
- `,
112
- changeDetection: ChangeDetectionStrategy.OnPush,
113
- }]
113
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host([hidden]){display:none!important}\n"] }]
114
114
  }], propDecorators: { key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], event: [{ type: i0.Input, args: [{ isSignal: true, alias: "event", required: false }] }], eventArgs: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventArgs", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], eventContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventContext", required: false }] }] } });
115
115
 
116
116
  var ionicButton_component = /*#__PURE__*/Object.freeze({
@@ -120,6 +120,29 @@ var ionicButton_component = /*#__PURE__*/Object.freeze({
120
120
 
121
121
  // Public API - component
122
122
 
123
+ /**
124
+ * Creates a signal that computes the aria-describedby value based on errors and hint state.
125
+ * Errors take precedence over hints - when errors are displayed, the hint is hidden.
126
+ *
127
+ * @param errorsToDisplay Signal containing the array of errors currently being displayed
128
+ * @param errorId Signal containing the base ID for error elements
129
+ * @param hintId Signal containing the ID for the hint element
130
+ * @param hasHint Function that returns true if a hint is configured
131
+ * @returns Signal containing the aria-describedby value (space-separated IDs) or null
132
+ */
133
+ function createAriaDescribedBySignal(errorsToDisplay, errorId, hintId, hasHint) {
134
+ return computed(() => {
135
+ const errors = errorsToDisplay();
136
+ if (errors.length > 0) {
137
+ return errors.map((_, i) => `${errorId()}-${i}`).join(' ');
138
+ }
139
+ if (hasHint()) {
140
+ return hintId();
141
+ }
142
+ return null;
143
+ });
144
+ }
145
+
123
146
  class IonicCheckboxFieldComponent {
124
147
  elementRef = inject((ElementRef));
125
148
  field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
@@ -147,64 +170,82 @@ class IonicCheckboxFieldComponent {
147
170
  // ─────────────────────────────────────────────────────────────────────────────
148
171
  /** Base ID for error elements */
149
172
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
173
+ /** Unique ID for the helper text element */
174
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
150
175
  /** Whether the field is currently in an invalid state (invalid AND touched) */
151
- isAriaInvalid = computed(() => {
176
+ ariaInvalid = computed(() => {
152
177
  const fieldState = this.field()();
153
178
  return fieldState.invalid() && fieldState.touched();
154
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
179
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
155
180
  /** Whether the field has a required validator */
156
- isRequired = computed(() => {
157
- return this.field()().required?.() === true;
158
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
159
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
160
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: IonicCheckboxFieldComponent, isStandalone: true, selector: "df-ion-checkbox", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
181
+ ariaRequired = computed(() => {
182
+ return this.field()().required?.() === true ? true : null;
183
+ }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
184
+ /** aria-describedby linking to hint OR error elements (mutually exclusive) */
185
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
186
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
187
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: IonicCheckboxFieldComponent, isStandalone: true, selector: "df-ion-checkbox", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
161
188
  @let f = field();
189
+ @let checkboxId = key() + '-checkbox';
162
190
 
163
191
  <ion-checkbox
192
+ [id]="checkboxId"
164
193
  [formField]="f"
165
194
  [labelPlacement]="props()?.labelPlacement ?? 'end'"
166
195
  [justify]="props()?.justify"
167
196
  [color]="props()?.color ?? 'primary'"
168
197
  [indeterminate]="props()?.indeterminate ?? false"
169
198
  [attr.tabindex]="tabIndex()"
170
- [attr.aria-invalid]="isAriaInvalid()"
171
- [attr.aria-required]="isRequired() || null"
199
+ [attr.aria-invalid]="ariaInvalid()"
200
+ [attr.aria-required]="ariaRequired()"
201
+ [attr.aria-describedby]="ariaDescribedBy()"
172
202
  >
173
203
  {{ label() | dynamicText | async }}
174
204
  </ion-checkbox>
175
205
 
176
206
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
177
- <ion-note color="danger" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
207
+ <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
208
+ } @empty {
209
+ @if (props()?.hint; as hint) {
210
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
211
+ }
178
212
  }
179
- `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
213
+ `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
180
214
  }
181
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicCheckboxFieldComponent, decorators: [{
215
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicCheckboxFieldComponent, decorators: [{
182
216
  type: Component,
183
217
  args: [{ selector: 'df-ion-checkbox', imports: [IonCheckbox, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
184
218
  @let f = field();
219
+ @let checkboxId = key() + '-checkbox';
185
220
 
186
221
  <ion-checkbox
222
+ [id]="checkboxId"
187
223
  [formField]="f"
188
224
  [labelPlacement]="props()?.labelPlacement ?? 'end'"
189
225
  [justify]="props()?.justify"
190
226
  [color]="props()?.color ?? 'primary'"
191
227
  [indeterminate]="props()?.indeterminate ?? false"
192
228
  [attr.tabindex]="tabIndex()"
193
- [attr.aria-invalid]="isAriaInvalid()"
194
- [attr.aria-required]="isRequired() || null"
229
+ [attr.aria-invalid]="ariaInvalid()"
230
+ [attr.aria-required]="ariaRequired()"
231
+ [attr.aria-describedby]="ariaDescribedBy()"
195
232
  >
196
233
  {{ label() | dynamicText | async }}
197
234
  </ion-checkbox>
198
235
 
199
236
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
200
- <ion-note color="danger" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
237
+ <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
238
+ } @empty {
239
+ @if (props()?.hint; as hint) {
240
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
241
+ }
201
242
  }
202
243
  `, host: {
203
244
  '[class]': 'className()',
204
245
  '[id]': '`${key()}`',
205
246
  '[attr.data-testid]': 'key()',
206
247
  '[attr.hidden]': 'field()().hidden() || null',
207
- }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"] }]
248
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"] }]
208
249
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }], defaultValidationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValidationMessages", required: false }] }] } });
209
250
 
210
251
  var ionicCheckbox_component = /*#__PURE__*/Object.freeze({
@@ -242,15 +283,19 @@ class IonicDatepickerFieldComponent {
242
283
  // ─────────────────────────────────────────────────────────────────────────────
243
284
  /** Base ID for error elements */
244
285
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
286
+ /** Unique ID for the helper text element */
287
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
245
288
  /** Whether the field is currently in an invalid state (invalid AND touched) */
246
- isAriaInvalid = computed(() => {
289
+ ariaInvalid = computed(() => {
247
290
  const fieldState = this.field()();
248
291
  return fieldState.invalid() && fieldState.touched();
249
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
292
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
250
293
  /** Whether the field has a required validator */
251
- isRequired = computed(() => {
252
- return this.field()().required?.() === true;
253
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
294
+ ariaRequired = computed(() => {
295
+ return this.field()().required?.() === true ? true : null;
296
+ }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
297
+ /** aria-describedby linking to hint OR error elements (mutually exclusive) */
298
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
254
299
  openModal() {
255
300
  this.isModalOpen.set(true);
256
301
  }
@@ -295,13 +340,14 @@ class IonicDatepickerFieldComponent {
295
340
  return format(date, 'MMM d, yyyy');
296
341
  }
297
342
  }
298
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicDatepickerFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
299
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: IonicDatepickerFieldComponent, isStandalone: true, selector: "df-ion-datepicker", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, startAt: { classPropertyName: "startAt", publicName: "startAt", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
300
- @let f = field(); @let dateValue = f().value();
301
- @let ariaInvalid = isAriaInvalid();
302
- @let ariaRequired = isRequired() || null;
343
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicDatepickerFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
344
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: IonicDatepickerFieldComponent, isStandalone: true, selector: "df-ion-datepicker", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, startAt: { classPropertyName: "startAt", publicName: "startAt", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
345
+ @let f = field();
346
+ @let dateValue = f().value();
347
+ @let inputId = key() + '-input';
303
348
 
304
349
  <ion-input
350
+ [id]="inputId"
305
351
  [label]="(label() | dynamicText | async) ?? undefined"
306
352
  [labelPlacement]="'stacked'"
307
353
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
@@ -310,12 +356,17 @@ class IonicDatepickerFieldComponent {
310
356
  [readonly]="true"
311
357
  [fill]="'outline'"
312
358
  [attr.tabindex]="tabIndex()"
313
- [attr.aria-invalid]="ariaInvalid"
314
- [attr.aria-required]="ariaRequired"
359
+ [attr.aria-invalid]="ariaInvalid()"
360
+ [attr.aria-required]="ariaRequired()"
361
+ [attr.aria-describedby]="ariaDescribedBy()"
315
362
  (click)="!f().disabled() && openModal()"
316
363
  />
317
364
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
318
365
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
366
+ } @empty {
367
+ @if (props()?.hint; as hint) {
368
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
369
+ }
319
370
  }
320
371
 
321
372
  <ion-modal [isOpen]="isModalOpen()" (didDismiss)="closeModal()">
@@ -347,9 +398,9 @@ class IonicDatepickerFieldComponent {
347
398
  </ion-content>
348
399
  </ng-template>
349
400
  </ion-modal>
350
- `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", "ion-input{cursor:pointer}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonDatetime, selector: "ion-datetime", inputs: ["cancelText", "clearText", "color", "dayValues", "disabled", "doneText", "firstDayOfWeek", "formatOptions", "highlightedDates", "hourCycle", "hourValues", "isDateEnabled", "locale", "max", "min", "minuteValues", "mode", "monthValues", "multiple", "name", "preferWheel", "presentation", "readonly", "showAdjacentDays", "showClearButton", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "titleSelectedDatesFormatter", "value", "yearValues"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
401
+ `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", "ion-input{cursor:pointer}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonDatetime, selector: "ion-datetime", inputs: ["cancelText", "clearText", "color", "dayValues", "disabled", "doneText", "firstDayOfWeek", "formatOptions", "highlightedDates", "hourCycle", "hourValues", "isDateEnabled", "locale", "max", "min", "minuteValues", "mode", "monthValues", "multiple", "name", "preferWheel", "presentation", "readonly", "showAdjacentDays", "showClearButton", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "titleSelectedDatesFormatter", "value", "yearValues"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
351
402
  }
352
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicDatepickerFieldComponent, decorators: [{
403
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicDatepickerFieldComponent, decorators: [{
353
404
  type: Component,
354
405
  args: [{ selector: 'df-ion-datepicker', imports: [
355
406
  IonInput,
@@ -365,11 +416,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
365
416
  DynamicTextPipe,
366
417
  AsyncPipe,
367
418
  ], template: `
368
- @let f = field(); @let dateValue = f().value();
369
- @let ariaInvalid = isAriaInvalid();
370
- @let ariaRequired = isRequired() || null;
419
+ @let f = field();
420
+ @let dateValue = f().value();
421
+ @let inputId = key() + '-input';
371
422
 
372
423
  <ion-input
424
+ [id]="inputId"
373
425
  [label]="(label() | dynamicText | async) ?? undefined"
374
426
  [labelPlacement]="'stacked'"
375
427
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
@@ -378,12 +430,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
378
430
  [readonly]="true"
379
431
  [fill]="'outline'"
380
432
  [attr.tabindex]="tabIndex()"
381
- [attr.aria-invalid]="ariaInvalid"
382
- [attr.aria-required]="ariaRequired"
433
+ [attr.aria-invalid]="ariaInvalid()"
434
+ [attr.aria-required]="ariaRequired()"
435
+ [attr.aria-describedby]="ariaDescribedBy()"
383
436
  (click)="!f().disabled() && openModal()"
384
437
  />
385
438
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
386
439
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
440
+ } @empty {
441
+ @if (props()?.hint; as hint) {
442
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
443
+ }
387
444
  }
388
445
 
389
446
  <ion-modal [isOpen]="isModalOpen()" (didDismiss)="closeModal()">
@@ -420,7 +477,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
420
477
  '[attr.data-testid]': 'key()',
421
478
  '[class]': 'className()',
422
479
  '[attr.hidden]': 'field()().hidden() || null',
423
- }, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", "ion-input{cursor:pointer}:host([hidden]){display:none!important}\n"] }]
480
+ }, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", "ion-input{cursor:pointer}:host([hidden]){display:none!important}\n"] }]
424
481
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], startAt: [{ type: i0.Input, args: [{ isSignal: true, alias: "startAt", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }], defaultValidationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValidationMessages", required: false }] }] } });
425
482
 
426
483
  var ionicDatepicker_component = /*#__PURE__*/Object.freeze({
@@ -476,28 +533,52 @@ class IonicInputFieldComponent {
476
533
  setupMetaTracking(this.elementRef, this.meta, {
477
534
  selector: 'ion-input',
478
535
  });
536
+ // Workaround: Ionic's ion-input does NOT automatically propagate aria-describedby changes
537
+ // to the native input element inside its shadow DOM. This effect imperatively syncs the attribute
538
+ // after a microtask to ensure Ionic has processed the attribute change.
539
+ explicitEffect([this.ariaDescribedBy], ([describedBy]) => {
540
+ queueMicrotask(() => {
541
+ const ionInput = this.elementRef.nativeElement.querySelector('ion-input');
542
+ if (ionInput?.getInputElement) {
543
+ ionInput.getInputElement().then((inputEl) => {
544
+ if (inputEl) {
545
+ if (describedBy) {
546
+ inputEl.setAttribute('aria-describedby', describedBy);
547
+ }
548
+ else {
549
+ inputEl.removeAttribute('aria-describedby');
550
+ }
551
+ }
552
+ });
553
+ }
554
+ });
555
+ });
479
556
  }
480
557
  // ─────────────────────────────────────────────────────────────────────────────
481
558
  // Accessibility
482
559
  // ─────────────────────────────────────────────────────────────────────────────
483
560
  /** Base ID for error elements */
484
561
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
562
+ /** Unique ID for the helper text element */
563
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
485
564
  /** Whether the field is currently in an invalid state (invalid AND touched) */
486
- isAriaInvalid = computed(() => {
565
+ ariaInvalid = computed(() => {
487
566
  const fieldState = this.field()();
488
567
  return fieldState.invalid() && fieldState.touched();
489
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
568
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
490
569
  /** Whether the field has a required validator */
491
- isRequired = computed(() => {
492
- return this.field()().required?.() === true;
493
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
494
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicInputFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
495
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: IonicInputFieldComponent, isStandalone: true, selector: "df-ion-input", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
570
+ ariaRequired = computed(() => {
571
+ return this.field()().required?.() === true ? true : null;
572
+ }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
573
+ /** aria-describedby linking to hint OR error elements (mutually exclusive) */
574
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
575
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicInputFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
576
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: IonicInputFieldComponent, isStandalone: true, selector: "df-ion-input", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
496
577
  @let f = field();
497
- @let ariaInvalid = isAriaInvalid();
498
- @let ariaRequired = isRequired() || null;
578
+ @let inputId = key() + '-input';
499
579
 
500
580
  <ion-input
581
+ [id]="inputId"
501
582
  [type]="props()?.type ?? 'text'"
502
583
  [formField]="f"
503
584
  [label]="(label() | dynamicText | async) ?? undefined"
@@ -509,24 +590,25 @@ class IonicInputFieldComponent {
509
590
  [fill]="effectiveFill()"
510
591
  [shape]="effectiveShape()"
511
592
  [readonly]="f().readonly()"
512
- [helperText]="(props()?.helperText | dynamicText | async) ?? undefined"
593
+ [helperText]="errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined"
513
594
  [attr.tabindex]="tabIndex()"
514
- [attr.aria-invalid]="ariaInvalid"
515
- [attr.aria-required]="ariaRequired"
595
+ [attr.aria-invalid]="ariaInvalid()"
596
+ [attr.aria-required]="ariaRequired()"
597
+ [attr.aria-describedby]="ariaDescribedBy()"
516
598
  />
517
599
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
518
600
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
519
601
  }
520
- `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
602
+ `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
521
603
  }
522
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicInputFieldComponent, decorators: [{
604
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicInputFieldComponent, decorators: [{
523
605
  type: Component,
524
606
  args: [{ selector: 'df-ion-input', imports: [IonInput, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
525
607
  @let f = field();
526
- @let ariaInvalid = isAriaInvalid();
527
- @let ariaRequired = isRequired() || null;
608
+ @let inputId = key() + '-input';
528
609
 
529
610
  <ion-input
611
+ [id]="inputId"
530
612
  [type]="props()?.type ?? 'text'"
531
613
  [formField]="f"
532
614
  [label]="(label() | dynamicText | async) ?? undefined"
@@ -538,10 +620,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
538
620
  [fill]="effectiveFill()"
539
621
  [shape]="effectiveShape()"
540
622
  [readonly]="f().readonly()"
541
- [helperText]="(props()?.helperText | dynamicText | async) ?? undefined"
623
+ [helperText]="errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined"
542
624
  [attr.tabindex]="tabIndex()"
543
- [attr.aria-invalid]="ariaInvalid"
544
- [attr.aria-required]="ariaRequired"
625
+ [attr.aria-invalid]="ariaInvalid()"
626
+ [attr.aria-required]="ariaRequired()"
627
+ [attr.aria-describedby]="ariaDescribedBy()"
545
628
  />
546
629
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
547
630
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
@@ -551,7 +634,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
551
634
  '[attr.data-testid]': 'key()',
552
635
  '[class]': 'className()',
553
636
  '[attr.hidden]': 'field()().hidden() || null',
554
- }, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"] }]
637
+ }, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"] }]
555
638
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }], defaultValidationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValidationMessages", required: false }] }] } });
556
639
 
557
640
  var ionicInput_component = /*#__PURE__*/Object.freeze({
@@ -580,19 +663,31 @@ class IonicMultiCheckboxFieldComponent {
580
663
  // ─────────────────────────────────────────────────────────────────────────────
581
664
  /** Base ID for error elements */
582
665
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
666
+ /** Unique ID for the helper text element */
667
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
583
668
  /** Whether the field is currently in an invalid state (invalid AND touched) */
584
- isAriaInvalid = computed(() => {
669
+ ariaInvalid = computed(() => {
585
670
  const fieldState = this.field()();
586
671
  return fieldState.invalid() && fieldState.touched();
587
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
672
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
588
673
  /** Whether the field has a required validator */
589
- isRequired = computed(() => {
590
- return this.field()().required?.() === true;
591
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
674
+ ariaRequired = computed(() => {
675
+ return this.field()().required?.() === true ? true : null;
676
+ }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
677
+ /** aria-describedby linking to hint OR error elements (mutually exclusive) */
678
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
592
679
  valueViewModel = linkedSignal(() => {
593
680
  const currentValues = this.field()().value();
594
681
  return this.options().filter((option) => currentValues.includes(option.value));
595
682
  }, { ...(ngDevMode ? { debugName: "valueViewModel" } : {}), equal: isEqual });
683
+ /** Computed map of checked option values for O(1) lookup in template */
684
+ checkedValuesMap = computed(() => {
685
+ const map = {};
686
+ for (const opt of this.valueViewModel()) {
687
+ map[String(opt.value)] = true;
688
+ }
689
+ return map;
690
+ }, ...(ngDevMode ? [{ debugName: "checkedValuesMap" }] : []));
596
691
  constructor() {
597
692
  // Shadow DOM - apply meta to ion-checkbox elements, re-apply when options change
598
693
  setupMetaTracking(this.elementRef, this.meta, {
@@ -624,9 +719,10 @@ class IonicMultiCheckboxFieldComponent {
624
719
  }
625
720
  });
626
721
  }
627
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicMultiCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
628
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: IonicMultiCheckboxFieldComponent, isStandalone: true, selector: "df-ion-multi-checkbox", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className() || \"\"", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, providers: [ValueInArrayPipe], ngImport: i0, template: `
722
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicMultiCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
723
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: IonicMultiCheckboxFieldComponent, isStandalone: true, selector: "df-ion-multi-checkbox", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className() || \"\"", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
629
724
  @let f = field();
725
+ @let checked = checkedValuesMap();
630
726
  @if (label(); as label) {
631
727
  <div class="checkbox-group-label">{{ label | dynamicText | async }}</div>
632
728
  }
@@ -634,15 +730,16 @@ class IonicMultiCheckboxFieldComponent {
634
730
  <div
635
731
  class="checkbox-group"
636
732
  role="group"
637
- [attr.aria-invalid]="isAriaInvalid()"
638
- [attr.aria-required]="isRequired() || null"
733
+ [attr.aria-invalid]="ariaInvalid()"
734
+ [attr.aria-required]="ariaRequired()"
735
+ [attr.aria-describedby]="ariaDescribedBy()"
639
736
  [class.ion-invalid]="showErrors()"
640
737
  [class.ion-touched]="field()().touched()"
641
738
  >
642
739
  @for (option of options(); track option.value) {
643
740
  <ion-item lines="none">
644
741
  <ion-checkbox
645
- [checked]="option | inArray: valueViewModel()"
742
+ [checked]="checked['' + option.value]"
646
743
  [disabled]="f().disabled() || option.disabled"
647
744
  [labelPlacement]="props()?.labelPlacement ?? 'end'"
648
745
  [justify]="props()?.justify ?? 'start'"
@@ -656,14 +753,19 @@ class IonicMultiCheckboxFieldComponent {
656
753
  </div>
657
754
 
658
755
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
659
- <ion-note color="danger" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
756
+ <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
757
+ } @empty {
758
+ @if (props()?.hint; as hint) {
759
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
760
+ }
660
761
  }
661
- `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}.checkbox-group-label{margin-bottom:8px;font-weight:500}.checkbox-group{display:flex;flex-direction:column;gap:4px}ion-item{--padding-start: 0;--inner-padding-end: 0}\n"], dependencies: [{ kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "pipe", type: ValueInArrayPipe, name: "inArray" }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
762
+ `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}.checkbox-group-label{margin-bottom:8px;font-weight:500}.checkbox-group{display:flex;flex-direction:column;gap:4px}ion-item{--padding-start: 0;--inner-padding-end: 0}\n"], dependencies: [{ kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
662
763
  }
663
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicMultiCheckboxFieldComponent, decorators: [{
764
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicMultiCheckboxFieldComponent, decorators: [{
664
765
  type: Component,
665
- args: [{ selector: 'df-ion-multi-checkbox', imports: [IonCheckbox, IonItem, IonNote, ValueInArrayPipe, DynamicTextPipe, AsyncPipe], template: `
766
+ args: [{ selector: 'df-ion-multi-checkbox', imports: [IonCheckbox, IonItem, IonNote, DynamicTextPipe, AsyncPipe], template: `
666
767
  @let f = field();
768
+ @let checked = checkedValuesMap();
667
769
  @if (label(); as label) {
668
770
  <div class="checkbox-group-label">{{ label | dynamicText | async }}</div>
669
771
  }
@@ -671,15 +773,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
671
773
  <div
672
774
  class="checkbox-group"
673
775
  role="group"
674
- [attr.aria-invalid]="isAriaInvalid()"
675
- [attr.aria-required]="isRequired() || null"
776
+ [attr.aria-invalid]="ariaInvalid()"
777
+ [attr.aria-required]="ariaRequired()"
778
+ [attr.aria-describedby]="ariaDescribedBy()"
676
779
  [class.ion-invalid]="showErrors()"
677
780
  [class.ion-touched]="field()().touched()"
678
781
  >
679
782
  @for (option of options(); track option.value) {
680
783
  <ion-item lines="none">
681
784
  <ion-checkbox
682
- [checked]="option | inArray: valueViewModel()"
785
+ [checked]="checked['' + option.value]"
683
786
  [disabled]="f().disabled() || option.disabled"
684
787
  [labelPlacement]="props()?.labelPlacement ?? 'end'"
685
788
  [justify]="props()?.justify ?? 'start'"
@@ -693,14 +796,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
693
796
  </div>
694
797
 
695
798
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
696
- <ion-note color="danger" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
799
+ <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
800
+ } @empty {
801
+ @if (props()?.hint; as hint) {
802
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
803
+ }
697
804
  }
698
805
  `, host: {
699
806
  '[class]': 'className() || ""',
700
807
  '[id]': '`${key()}`',
701
808
  '[attr.data-testid]': 'key()',
702
809
  '[attr.hidden]': 'field()().hidden() || null',
703
- }, providers: [ValueInArrayPipe], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}.checkbox-group-label{margin-bottom:8px;font-weight:500}.checkbox-group{display:flex;flex-direction:column;gap:4px}ion-item{--padding-start: 0;--inner-padding-end: 0}\n"] }]
810
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}.checkbox-group-label{margin-bottom:8px;font-weight:500}.checkbox-group{display:flex;flex-direction:column;gap:4px}ion-item{--padding-start: 0;--inner-padding-end: 0}\n"] }]
704
811
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }], defaultValidationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValidationMessages", required: false }] }] } });
705
812
 
706
813
  var ionicMultiCheckbox_component = /*#__PURE__*/Object.freeze({
@@ -738,27 +845,34 @@ class IonicRadioFieldComponent {
738
845
  // ─────────────────────────────────────────────────────────────────────────────
739
846
  /** Base ID for error elements */
740
847
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
848
+ /** Unique ID for the helper text element */
849
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
741
850
  /** Whether the field is currently in an invalid state (invalid AND touched) */
742
- isAriaInvalid = computed(() => {
851
+ ariaInvalid = computed(() => {
743
852
  const fieldState = this.field()();
744
853
  return fieldState.invalid() && fieldState.touched();
745
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
854
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
746
855
  /** Whether the field has a required validator */
747
- isRequired = computed(() => {
748
- return this.field()().required?.() === true;
749
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
750
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicRadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
751
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: IonicRadioFieldComponent, isStandalone: true, selector: "df-ion-radio", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
856
+ ariaRequired = computed(() => {
857
+ return this.field()().required?.() === true ? true : null;
858
+ }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
859
+ /** aria-describedby linking to hint OR error elements (mutually exclusive) */
860
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
861
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicRadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
862
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: IonicRadioFieldComponent, isStandalone: true, selector: "df-ion-radio", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
752
863
  @let f = field();
864
+ @let radioGroupId = key() + '-radio-group';
753
865
  @if (label(); as label) {
754
866
  <div class="radio-label">{{ label | dynamicText | async }}</div>
755
867
  }
756
868
 
757
869
  <ion-radio-group
870
+ [id]="radioGroupId"
758
871
  [formField]="f"
759
872
  [compareWith]="props()?.compareWith || defaultCompare"
760
- [attr.aria-invalid]="isAriaInvalid()"
761
- [attr.aria-required]="isRequired() || null"
873
+ [attr.aria-invalid]="ariaInvalid()"
874
+ [attr.aria-required]="ariaRequired()"
875
+ [attr.aria-describedby]="ariaDescribedBy()"
762
876
  >
763
877
  @for (option of options(); track option.value) {
764
878
  <ion-item [lines]="'none'">
@@ -776,23 +890,30 @@ class IonicRadioFieldComponent {
776
890
  </ion-radio-group>
777
891
 
778
892
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
779
- <ion-note color="danger" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
893
+ <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
894
+ } @empty {
895
+ @if (props()?.hint; as hint) {
896
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
897
+ }
780
898
  }
781
- `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}.radio-label{font-size:.875rem;font-weight:500;margin-bottom:.5rem;color:var(--ion-text-color)}\n"], dependencies: [{ kind: "component", type: IonRadioGroup, selector: "ion-radio-group", inputs: ["allowEmptySelection", "compareWith", "errorText", "helperText", "name", "value"] }, { kind: "component", type: IonRadio, selector: "ion-radio", inputs: ["alignment", "color", "disabled", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
899
+ `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}.radio-label{font-size:.875rem;font-weight:500;margin-bottom:.5rem;color:var(--ion-text-color)}\n"], dependencies: [{ kind: "component", type: IonRadioGroup, selector: "ion-radio-group", inputs: ["allowEmptySelection", "compareWith", "errorText", "helperText", "name", "value"] }, { kind: "component", type: IonRadio, selector: "ion-radio", inputs: ["alignment", "color", "disabled", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
782
900
  }
783
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicRadioFieldComponent, decorators: [{
901
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicRadioFieldComponent, decorators: [{
784
902
  type: Component,
785
903
  args: [{ selector: 'df-ion-radio', imports: [IonRadioGroup, IonRadio, IonItem, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
786
904
  @let f = field();
905
+ @let radioGroupId = key() + '-radio-group';
787
906
  @if (label(); as label) {
788
907
  <div class="radio-label">{{ label | dynamicText | async }}</div>
789
908
  }
790
909
 
791
910
  <ion-radio-group
911
+ [id]="radioGroupId"
792
912
  [formField]="f"
793
913
  [compareWith]="props()?.compareWith || defaultCompare"
794
- [attr.aria-invalid]="isAriaInvalid()"
795
- [attr.aria-required]="isRequired() || null"
914
+ [attr.aria-invalid]="ariaInvalid()"
915
+ [attr.aria-required]="ariaRequired()"
916
+ [attr.aria-describedby]="ariaDescribedBy()"
796
917
  >
797
918
  @for (option of options(); track option.value) {
798
919
  <ion-item [lines]="'none'">
@@ -810,14 +931,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
810
931
  </ion-radio-group>
811
932
 
812
933
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
813
- <ion-note color="danger" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
934
+ <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
935
+ } @empty {
936
+ @if (props()?.hint; as hint) {
937
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
938
+ }
814
939
  }
815
940
  `, host: {
816
941
  '[class]': 'className()',
817
942
  '[id]': '`${key()}`',
818
943
  '[attr.data-testid]': 'key()',
819
944
  '[attr.hidden]': 'field()().hidden() || null',
820
- }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}.radio-label{font-size:.875rem;font-weight:500;margin-bottom:.5rem;color:var(--ion-text-color)}\n"] }]
945
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}.radio-label{font-size:.875rem;font-weight:500;margin-bottom:.5rem;color:var(--ion-text-color)}\n"] }]
821
946
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }], defaultValidationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValidationMessages", required: false }] }] } });
822
947
 
823
948
  var ionicRadio_component = /*#__PURE__*/Object.freeze({
@@ -850,22 +975,26 @@ class IonicSelectFieldComponent {
850
975
  // ─────────────────────────────────────────────────────────────────────────────
851
976
  /** Base ID for error elements */
852
977
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
978
+ /** Unique ID for the helper text element */
979
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
853
980
  /** Whether the field is currently in an invalid state (invalid AND touched) */
854
- isAriaInvalid = computed(() => {
981
+ ariaInvalid = computed(() => {
855
982
  const fieldState = this.field()();
856
983
  return fieldState.invalid() && fieldState.touched();
857
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
984
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
858
985
  /** Whether the field has a required validator */
859
- isRequired = computed(() => {
860
- return this.field()().required?.() === true;
861
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
862
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
863
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: IonicSelectFieldComponent, isStandalone: true, selector: "df-ion-select", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
986
+ ariaRequired = computed(() => {
987
+ return this.field()().required?.() === true ? true : null;
988
+ }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
989
+ /** aria-describedby linking to hint OR error elements (mutually exclusive) */
990
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
991
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
992
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: IonicSelectFieldComponent, isStandalone: true, selector: "df-ion-select", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
864
993
  @let f = field();
865
- @let ariaInvalid = isAriaInvalid();
866
- @let ariaRequired = isRequired() || null;
994
+ @let selectId = key() + '-select';
867
995
 
868
996
  <ion-select
997
+ [id]="selectId"
869
998
  [formField]="f"
870
999
  [label]="(label() | dynamicText | async) ?? undefined"
871
1000
  [labelPlacement]="props()?.labelPlacement ?? 'stacked'"
@@ -880,8 +1009,9 @@ class IonicSelectFieldComponent {
880
1009
  [fill]="props()?.fill ?? 'outline'"
881
1010
  [shape]="props()?.shape"
882
1011
  [attr.tabindex]="tabIndex()"
883
- [attr.aria-invalid]="ariaInvalid"
884
- [attr.aria-required]="ariaRequired"
1012
+ [attr.aria-invalid]="ariaInvalid()"
1013
+ [attr.aria-required]="ariaRequired()"
1014
+ [attr.aria-describedby]="ariaDescribedBy()"
885
1015
  >
886
1016
  @for (option of options(); track option.value) {
887
1017
  <ion-select-option [value]="option.value" [disabled]="option.disabled || false">
@@ -891,17 +1021,21 @@ class IonicSelectFieldComponent {
891
1021
  </ion-select>
892
1022
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
893
1023
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1024
+ } @empty {
1025
+ @if (props()?.hint; as hint) {
1026
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
1027
+ }
894
1028
  }
895
- `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonSelect, selector: "ion-select", inputs: ["cancelText", "color", "compareWith", "disabled", "errorText", "expandedIcon", "fill", "helperText", "interface", "interfaceOptions", "justify", "label", "labelPlacement", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "shape", "toggleIcon", "value"] }, { kind: "component", type: IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1029
+ `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonSelect, selector: "ion-select", inputs: ["cancelText", "color", "compareWith", "disabled", "errorText", "expandedIcon", "fill", "helperText", "interface", "interfaceOptions", "justify", "label", "labelPlacement", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "shape", "toggleIcon", "value"] }, { kind: "component", type: IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
896
1030
  }
897
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicSelectFieldComponent, decorators: [{
1031
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicSelectFieldComponent, decorators: [{
898
1032
  type: Component,
899
1033
  args: [{ selector: 'df-ion-select', imports: [IonSelect, IonSelectOption, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
900
1034
  @let f = field();
901
- @let ariaInvalid = isAriaInvalid();
902
- @let ariaRequired = isRequired() || null;
1035
+ @let selectId = key() + '-select';
903
1036
 
904
1037
  <ion-select
1038
+ [id]="selectId"
905
1039
  [formField]="f"
906
1040
  [label]="(label() | dynamicText | async) ?? undefined"
907
1041
  [labelPlacement]="props()?.labelPlacement ?? 'stacked'"
@@ -916,8 +1050,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
916
1050
  [fill]="props()?.fill ?? 'outline'"
917
1051
  [shape]="props()?.shape"
918
1052
  [attr.tabindex]="tabIndex()"
919
- [attr.aria-invalid]="ariaInvalid"
920
- [attr.aria-required]="ariaRequired"
1053
+ [attr.aria-invalid]="ariaInvalid()"
1054
+ [attr.aria-required]="ariaRequired()"
1055
+ [attr.aria-describedby]="ariaDescribedBy()"
921
1056
  >
922
1057
  @for (option of options(); track option.value) {
923
1058
  <ion-select-option [value]="option.value" [disabled]="option.disabled || false">
@@ -927,13 +1062,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
927
1062
  </ion-select>
928
1063
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
929
1064
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1065
+ } @empty {
1066
+ @if (props()?.hint; as hint) {
1067
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
1068
+ }
930
1069
  }
931
1070
  `, changeDetection: ChangeDetectionStrategy.OnPush, host: {
932
1071
  '[id]': '`${key()}`',
933
1072
  '[attr.data-testid]': 'key()',
934
1073
  '[class]': 'className()',
935
1074
  '[attr.hidden]': 'field()().hidden() || null',
936
- }, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"] }]
1075
+ }, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"] }]
937
1076
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }], defaultValidationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValidationMessages", required: false }] }] } });
938
1077
 
939
1078
  var ionicSelect_component = /*#__PURE__*/Object.freeze({
@@ -965,23 +1104,31 @@ class IonicSliderFieldComponent {
965
1104
  // ─────────────────────────────────────────────────────────────────────────────
966
1105
  /** Base ID for error elements */
967
1106
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1107
+ /** Unique ID for the helper text element */
1108
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
968
1109
  /** Whether the field is currently in an invalid state (invalid AND touched) */
969
- isAriaInvalid = computed(() => {
1110
+ ariaInvalid = computed(() => {
970
1111
  const fieldState = this.field()();
971
1112
  return fieldState.invalid() && fieldState.touched();
972
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
1113
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
973
1114
  /** Whether the field has a required validator */
974
- isRequired = computed(() => {
975
- return this.field()().required?.() === true;
976
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
977
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicSliderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
978
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: IonicSliderFieldComponent, isStandalone: true, selector: "df-ion-slider", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1115
+ ariaRequired = computed(() => {
1116
+ return this.field()().required?.() === true ? true : null;
1117
+ }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1118
+ /** aria-describedby linking to hint OR error elements (mutually exclusive) */
1119
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1120
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicSliderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1121
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: IonicSliderFieldComponent, isStandalone: true, selector: "df-ion-slider", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
979
1122
  @let f = field();
1123
+ @let inputId = key() + '-input';
980
1124
 
981
1125
  <ion-range
1126
+ [id]="inputId"
982
1127
  [formField]="f"
983
1128
  [label]="(label() | dynamicText | async) ?? undefined"
984
1129
  [labelPlacement]="props()?.labelPlacement ?? 'stacked'"
1130
+ [min]="props()?.min ?? 0"
1131
+ [max]="props()?.max ?? 100"
985
1132
  [step]="props()?.step ?? 1"
986
1133
  [dualKnobs]="props()?.dualKnobs ?? false"
987
1134
  [pin]="props()?.pin ?? false"
@@ -990,24 +1137,33 @@ class IonicSliderFieldComponent {
990
1137
  [snaps]="props()?.snaps ?? false"
991
1138
  [color]="props()?.color ?? 'primary'"
992
1139
  [attr.tabindex]="tabIndex()"
993
- [attr.aria-invalid]="isAriaInvalid()"
994
- [attr.aria-required]="isRequired() || null"
1140
+ [attr.aria-invalid]="ariaInvalid()"
1141
+ [attr.aria-required]="ariaRequired()"
1142
+ [attr.aria-describedby]="ariaDescribedBy()"
995
1143
  />
996
1144
 
997
1145
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
998
- <ion-note color="danger" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1146
+ <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1147
+ } @empty {
1148
+ @if (props()?.hint; as hint) {
1149
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
1150
+ }
999
1151
  }
1000
- `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonRange, selector: "ion-range", inputs: ["activeBarStart", "color", "debounce", "disabled", "dualKnobs", "label", "labelPlacement", "max", "min", "mode", "name", "pin", "pinFormatter", "snaps", "step", "ticks", "value"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1152
+ `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonRange, selector: "ion-range", inputs: ["activeBarStart", "color", "debounce", "disabled", "dualKnobs", "label", "labelPlacement", "max", "min", "mode", "name", "pin", "pinFormatter", "snaps", "step", "ticks", "value"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1001
1153
  }
1002
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicSliderFieldComponent, decorators: [{
1154
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicSliderFieldComponent, decorators: [{
1003
1155
  type: Component,
1004
1156
  args: [{ selector: 'df-ion-slider', imports: [IonRange, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
1005
1157
  @let f = field();
1158
+ @let inputId = key() + '-input';
1006
1159
 
1007
1160
  <ion-range
1161
+ [id]="inputId"
1008
1162
  [formField]="f"
1009
1163
  [label]="(label() | dynamicText | async) ?? undefined"
1010
1164
  [labelPlacement]="props()?.labelPlacement ?? 'stacked'"
1165
+ [min]="props()?.min ?? 0"
1166
+ [max]="props()?.max ?? 100"
1011
1167
  [step]="props()?.step ?? 1"
1012
1168
  [dualKnobs]="props()?.dualKnobs ?? false"
1013
1169
  [pin]="props()?.pin ?? false"
@@ -1016,19 +1172,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1016
1172
  [snaps]="props()?.snaps ?? false"
1017
1173
  [color]="props()?.color ?? 'primary'"
1018
1174
  [attr.tabindex]="tabIndex()"
1019
- [attr.aria-invalid]="isAriaInvalid()"
1020
- [attr.aria-required]="isRequired() || null"
1175
+ [attr.aria-invalid]="ariaInvalid()"
1176
+ [attr.aria-required]="ariaRequired()"
1177
+ [attr.aria-describedby]="ariaDescribedBy()"
1021
1178
  />
1022
1179
 
1023
1180
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1024
- <ion-note color="danger" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1181
+ <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1182
+ } @empty {
1183
+ @if (props()?.hint; as hint) {
1184
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
1185
+ }
1025
1186
  }
1026
1187
  `, host: {
1027
1188
  '[class]': 'className()',
1028
1189
  '[id]': '`${key()}`',
1029
1190
  '[attr.data-testid]': 'key()',
1030
1191
  '[attr.hidden]': 'field()().hidden() || null',
1031
- }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"] }]
1192
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"] }]
1032
1193
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }], defaultValidationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValidationMessages", required: false }] }] } });
1033
1194
 
1034
1195
  var ionicSlider_component = /*#__PURE__*/Object.freeze({
@@ -1056,28 +1217,52 @@ class IonicTextareaFieldComponent {
1056
1217
  setupMetaTracking(this.elementRef, this.meta, {
1057
1218
  selector: 'ion-textarea',
1058
1219
  });
1220
+ // Workaround: Ionic's ion-textarea does NOT automatically propagate aria-describedby changes
1221
+ // to the native textarea element inside its shadow DOM. This effect imperatively syncs the attribute
1222
+ // after a microtask to ensure Ionic has processed the attribute change.
1223
+ explicitEffect([this.ariaDescribedBy], ([describedBy]) => {
1224
+ queueMicrotask(() => {
1225
+ const ionTextarea = this.elementRef.nativeElement.querySelector('ion-textarea');
1226
+ if (ionTextarea?.getInputElement) {
1227
+ ionTextarea.getInputElement().then((textareaEl) => {
1228
+ if (textareaEl) {
1229
+ if (describedBy) {
1230
+ textareaEl.setAttribute('aria-describedby', describedBy);
1231
+ }
1232
+ else {
1233
+ textareaEl.removeAttribute('aria-describedby');
1234
+ }
1235
+ }
1236
+ });
1237
+ }
1238
+ });
1239
+ });
1059
1240
  }
1060
1241
  // ─────────────────────────────────────────────────────────────────────────────
1061
1242
  // Accessibility
1062
1243
  // ─────────────────────────────────────────────────────────────────────────────
1063
1244
  /** Base ID for error elements */
1064
1245
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1246
+ /** Unique ID for the helper text element */
1247
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1065
1248
  /** Whether the field is currently in an invalid state (invalid AND touched) */
1066
- isAriaInvalid = computed(() => {
1249
+ ariaInvalid = computed(() => {
1067
1250
  const fieldState = this.field()();
1068
1251
  return fieldState.invalid() && fieldState.touched();
1069
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
1252
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
1070
1253
  /** Whether the field has a required validator */
1071
- isRequired = computed(() => {
1072
- return this.field()().required?.() === true;
1073
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
1074
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicTextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1075
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: IonicTextareaFieldComponent, isStandalone: true, selector: "df-ion-textarea", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1254
+ ariaRequired = computed(() => {
1255
+ return this.field()().required?.() === true ? true : null;
1256
+ }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1257
+ /** aria-describedby linking to hint OR error elements (mutually exclusive) */
1258
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1259
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicTextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1260
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: IonicTextareaFieldComponent, isStandalone: true, selector: "df-ion-textarea", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1076
1261
  @let f = field();
1077
- @let ariaInvalid = isAriaInvalid();
1078
- @let ariaRequired = isRequired() || null;
1262
+ @let textareaId = key() + '-textarea';
1079
1263
 
1080
1264
  <ion-textarea
1265
+ [id]="textareaId"
1081
1266
  [formField]="f"
1082
1267
  [label]="(label() | dynamicText | async) ?? undefined"
1083
1268
  [labelPlacement]="props()?.labelPlacement ?? 'stacked'"
@@ -1089,24 +1274,25 @@ class IonicTextareaFieldComponent {
1089
1274
  [fill]="props()?.fill ?? 'outline'"
1090
1275
  [shape]="props()?.shape"
1091
1276
  [readonly]="f().readonly()"
1092
- [helperText]="(props()?.helperText | dynamicText | async) ?? undefined"
1277
+ [helperText]="errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined"
1093
1278
  [attr.tabindex]="tabIndex()"
1094
- [attr.aria-invalid]="ariaInvalid"
1095
- [attr.aria-required]="ariaRequired"
1279
+ [attr.aria-invalid]="ariaInvalid()"
1280
+ [attr.aria-required]="ariaRequired()"
1281
+ [attr.aria-describedby]="ariaDescribedBy()"
1096
1282
  />
1097
1283
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1098
1284
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1099
1285
  }
1100
- `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1286
+ `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1101
1287
  }
1102
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicTextareaFieldComponent, decorators: [{
1288
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicTextareaFieldComponent, decorators: [{
1103
1289
  type: Component,
1104
1290
  args: [{ selector: 'df-ion-textarea', imports: [IonTextarea, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
1105
1291
  @let f = field();
1106
- @let ariaInvalid = isAriaInvalid();
1107
- @let ariaRequired = isRequired() || null;
1292
+ @let textareaId = key() + '-textarea';
1108
1293
 
1109
1294
  <ion-textarea
1295
+ [id]="textareaId"
1110
1296
  [formField]="f"
1111
1297
  [label]="(label() | dynamicText | async) ?? undefined"
1112
1298
  [labelPlacement]="props()?.labelPlacement ?? 'stacked'"
@@ -1118,10 +1304,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1118
1304
  [fill]="props()?.fill ?? 'outline'"
1119
1305
  [shape]="props()?.shape"
1120
1306
  [readonly]="f().readonly()"
1121
- [helperText]="(props()?.helperText | dynamicText | async) ?? undefined"
1307
+ [helperText]="errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined"
1122
1308
  [attr.tabindex]="tabIndex()"
1123
- [attr.aria-invalid]="ariaInvalid"
1124
- [attr.aria-required]="ariaRequired"
1309
+ [attr.aria-invalid]="ariaInvalid()"
1310
+ [attr.aria-required]="ariaRequired()"
1311
+ [attr.aria-describedby]="ariaDescribedBy()"
1125
1312
  />
1126
1313
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1127
1314
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
@@ -1131,7 +1318,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1131
1318
  '[attr.data-testid]': 'key()',
1132
1319
  '[class]': 'className()',
1133
1320
  '[attr.hidden]': 'field()().hidden() || null',
1134
- }, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"] }]
1321
+ }, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host([hidden]){display:none!important}\n"] }]
1135
1322
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }], defaultValidationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValidationMessages", required: false }] }] } });
1136
1323
 
1137
1324
  var ionicTextarea_component = /*#__PURE__*/Object.freeze({
@@ -1172,6 +1359,7 @@ class IonicToggleControlComponent {
1172
1359
  enableOnOffLabels = input(false, ...(ngDevMode ? [{ debugName: "enableOnOffLabels" }] : []));
1173
1360
  tabIndex = input(undefined, ...(ngDevMode ? [{ debugName: "tabIndex" }] : []));
1174
1361
  meta = input(...(ngDevMode ? [undefined, { debugName: "meta" }] : []));
1362
+ ariaDescribedBy = input(null, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1175
1363
  constructor() {
1176
1364
  // Shadow DOM - apply meta to ion-toggle element
1177
1365
  setupMetaTracking(this.elementRef, this.meta, {
@@ -1215,8 +1403,8 @@ class IonicToggleControlComponent {
1215
1403
  onBlur() {
1216
1404
  this.touched.set(true);
1217
1405
  }
1218
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicToggleControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1219
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.8", type: IonicToggleControlComponent, isStandalone: true, selector: "df-ion-toggle-control", inputs: { checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", 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 }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, labelPlacement: { classPropertyName: "labelPlacement", publicName: "labelPlacement", isSignal: true, isRequired: false, transformFunction: null }, justify: { classPropertyName: "justify", publicName: "justify", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, enableOnOffLabels: { classPropertyName: "enableOnOffLabels", publicName: "enableOnOffLabels", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", touched: "touchedChange" }, ngImport: i0, template: `
1406
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicToggleControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1407
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: IonicToggleControlComponent, isStandalone: true, selector: "df-ion-toggle-control", inputs: { checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", 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 }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, labelPlacement: { classPropertyName: "labelPlacement", publicName: "labelPlacement", isSignal: true, isRequired: false, transformFunction: null }, justify: { classPropertyName: "justify", publicName: "justify", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, enableOnOffLabels: { classPropertyName: "enableOnOffLabels", publicName: "enableOnOffLabels", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", touched: "touchedChange" }, host: { styleAttribute: "display: block" }, ngImport: i0, template: `
1220
1408
  <ion-toggle
1221
1409
  [checked]="isChecked()"
1222
1410
  (ionChange)="onToggleChange($event)"
@@ -1230,12 +1418,13 @@ class IonicToggleControlComponent {
1230
1418
  [attr.aria-invalid]="ariaInvalid()"
1231
1419
  [attr.aria-required]="ariaRequired()"
1232
1420
  [attr.aria-readonly]="ariaReadonly()"
1421
+ [attr.aria-describedby]="ariaDescribedBy()"
1233
1422
  >
1234
1423
  <ng-content />
1235
1424
  </ion-toggle>
1236
- `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n"], dependencies: [{ kind: "component", type: IonToggle, selector: "ion-toggle", inputs: ["checked", "color", "disabled", "enableOnOffLabels", "errorText", "helperText", "justify", "labelPlacement", "mode", "name", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1425
+ `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n"], dependencies: [{ kind: "component", type: IonToggle, selector: "ion-toggle", inputs: ["checked", "color", "disabled", "enableOnOffLabels", "errorText", "helperText", "justify", "labelPlacement", "mode", "name", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1237
1426
  }
1238
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicToggleControlComponent, decorators: [{
1427
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicToggleControlComponent, decorators: [{
1239
1428
  type: Component,
1240
1429
  args: [{ selector: 'df-ion-toggle-control', imports: [IonToggle], template: `
1241
1430
  <ion-toggle
@@ -1251,11 +1440,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1251
1440
  [attr.aria-invalid]="ariaInvalid()"
1252
1441
  [attr.aria-required]="ariaRequired()"
1253
1442
  [attr.aria-readonly]="ariaReadonly()"
1443
+ [attr.aria-describedby]="ariaDescribedBy()"
1254
1444
  >
1255
1445
  <ng-content />
1256
1446
  </ion-toggle>
1257
- `, host: {}, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n"] }]
1258
- }], ctorParameters: () => [], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], labelPlacement: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPlacement", required: false }] }], justify: [{ type: i0.Input, args: [{ isSignal: true, alias: "justify", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], enableOnOffLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableOnOffLabels", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }] } });
1447
+ `, host: {
1448
+ style: 'display: block',
1449
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n"] }]
1450
+ }], ctorParameters: () => [], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], labelPlacement: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPlacement", required: false }] }], justify: [{ type: i0.Input, args: [{ isSignal: true, alias: "justify", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], enableOnOffLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableOnOffLabels", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }] } });
1259
1451
 
1260
1452
  class IonicToggleFieldComponent {
1261
1453
  elementRef = inject((ElementRef));
@@ -1281,11 +1473,17 @@ class IonicToggleFieldComponent {
1281
1473
  // ─────────────────────────────────────────────────────────────────────────────
1282
1474
  /** Base ID for error elements */
1283
1475
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1284
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicToggleFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1285
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: IonicToggleFieldComponent, isStandalone: true, selector: "df-ion-toggle", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1476
+ /** Unique ID for the helper text element */
1477
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1478
+ /** aria-describedby linking to hint OR error elements (mutually exclusive) */
1479
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1480
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicToggleFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1481
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: IonicToggleFieldComponent, isStandalone: true, selector: "df-ion-toggle", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1286
1482
  @let f = field();
1483
+ @let toggleId = key() + '-toggle';
1287
1484
 
1288
1485
  <df-ion-toggle-control
1486
+ [id]="toggleId"
1289
1487
  [formField]="f"
1290
1488
  [meta]="meta()"
1291
1489
  [labelPlacement]="props()?.labelPlacement ?? 'end'"
@@ -1293,21 +1491,28 @@ class IonicToggleFieldComponent {
1293
1491
  [color]="props()?.color ?? 'primary'"
1294
1492
  [enableOnOffLabels]="props()?.enableOnOffLabels ?? false"
1295
1493
  [tabIndex]="tabIndex()"
1494
+ [ariaDescribedBy]="ariaDescribedBy()"
1296
1495
  >
1297
1496
  {{ label() | dynamicText | async }}
1298
1497
  </df-ion-toggle-control>
1299
1498
 
1300
1499
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1301
- <ion-note color="danger" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1500
+ <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1501
+ } @empty {
1502
+ @if (props()?.hint; as hint) {
1503
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
1504
+ }
1302
1505
  }
1303
- `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonicToggleControlComponent, selector: "df-ion-toggle-control", inputs: ["checked", "touched", "disabled", "readonly", "invalid", "required", "labelPlacement", "justify", "color", "enableOnOffLabels", "tabIndex", "meta"], outputs: ["checkedChange", "touchedChange"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1506
+ `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: IonicToggleControlComponent, selector: "df-ion-toggle-control", inputs: ["checked", "touched", "disabled", "readonly", "invalid", "required", "labelPlacement", "justify", "color", "enableOnOffLabels", "tabIndex", "meta", "ariaDescribedBy"], outputs: ["checkedChange", "touchedChange"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1304
1507
  }
1305
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicToggleFieldComponent, decorators: [{
1508
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IonicToggleFieldComponent, decorators: [{
1306
1509
  type: Component,
1307
1510
  args: [{ selector: 'df-ion-toggle', imports: [IonicToggleControlComponent, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
1308
1511
  @let f = field();
1512
+ @let toggleId = key() + '-toggle';
1309
1513
 
1310
1514
  <df-ion-toggle-control
1515
+ [id]="toggleId"
1311
1516
  [formField]="f"
1312
1517
  [meta]="meta()"
1313
1518
  [labelPlacement]="props()?.labelPlacement ?? 'end'"
@@ -1315,19 +1520,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1315
1520
  [color]="props()?.color ?? 'primary'"
1316
1521
  [enableOnOffLabels]="props()?.enableOnOffLabels ?? false"
1317
1522
  [tabIndex]="tabIndex()"
1523
+ [ariaDescribedBy]="ariaDescribedBy()"
1318
1524
  >
1319
1525
  {{ label() | dynamicText | async }}
1320
1526
  </df-ion-toggle-control>
1321
1527
 
1322
1528
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1323
- <ion-note color="danger" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1529
+ <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1530
+ } @empty {
1531
+ @if (props()?.hint; as hint) {
1532
+ <ion-note class="df-ion-hint" [id]="hintId()">{{ hint | dynamicText | async }}</ion-note>
1533
+ }
1324
1534
  }
1325
1535
  `, host: {
1326
1536
  '[class]': 'className()',
1327
1537
  '[id]': '`${key()}`',
1328
1538
  '[attr.data-testid]': 'key()',
1329
1539
  '[attr.hidden]': 'field()().hidden() || null',
1330
- }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"] }]
1540
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--ion-color-medium, #92949c);--df-hint-font-size: .875rem;--df-error-color: var(--ion-color-danger, #eb445a);--df-error-font-size: .875rem}.df-ion-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%}.df-ion-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color)}.df-ion-hint{display:block;color:var(--df-hint-color);font-size:var(--df-hint-font-size)}.df-ion-error{display:block;color:var(--df-error-color);font-size:var(--df-error-font-size)}:host([hidden]){display:none!important}ion-input.ng-invalid.ng-touched,ion-textarea.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-select.ng-invalid.ng-touched{--highlight-color: var(--ion-color-danger);--border-color: var(--ion-color-danger)}ion-checkbox.ng-invalid.ng-touched{--checkbox-background-checked: var(--ion-color-danger);--border-color: var(--ion-color-danger);--border-color-checked: var(--ion-color-danger)}ion-radio.ng-invalid.ng-touched{--color: var(--ion-color-danger);--color-checked: var(--ion-color-danger)}ion-range.ng-invalid.ng-touched{--bar-background: var(--ion-color-danger);--knob-background: var(--ion-color-danger)}ion-toggle.ng-invalid.ng-touched{--track-background: var(--ion-color-danger-tint);--track-background-checked: var(--ion-color-danger);--handle-background-checked: var(--ion-color-danger-contrast)}\n", ":host{display:block}:host([hidden]){display:none!important}\n"] }]
1331
1541
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }], defaultValidationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValidationMessages", required: false }] }] } });
1332
1542
 
1333
1543
  var ionicToggle_component = /*#__PURE__*/Object.freeze({
@@ -1369,9 +1579,9 @@ const IonicField = {
1369
1579
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1370
1580
  */
1371
1581
  function buttonFieldMapper(fieldDef) {
1372
- // Build base inputs (static, from field definition)
1373
- const baseInputs = buildBaseInputs(fieldDef);
1582
+ const defaultProps = inject(DEFAULT_PROPS);
1374
1583
  return computed(() => {
1584
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1375
1585
  const inputs = {
1376
1586
  ...baseInputs,
1377
1587
  };
@@ -1409,20 +1619,21 @@ function buttonFieldMapper(fieldDef) {
1409
1619
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1410
1620
  */
1411
1621
  function submitButtonFieldMapper(fieldDef) {
1412
- // Inject field signal context to access form state and options
1413
- const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
1414
- // Build base inputs (static, from field definition)
1415
- const baseInputs = buildBaseInputs(fieldDef);
1416
- // Use button-logic-resolver to compute disabled state
1622
+ const rootFormRegistry = inject(RootFormRegistryService);
1623
+ const defaultProps = inject(DEFAULT_PROPS);
1624
+ const formOptions = inject(FORM_OPTIONS);
1417
1625
  const fieldWithLogic = fieldDef;
1418
- const disabledSignal = resolveSubmitButtonDisabled({
1419
- form: fieldSignalContext.form,
1420
- formOptions: fieldSignalContext.formOptions,
1421
- fieldLogic: fieldWithLogic.logic,
1422
- explicitlyDisabled: fieldDef.disabled,
1423
- });
1424
- // Return computed signal - evaluates disabledSignal inside for reactivity
1425
1626
  return computed(() => {
1627
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1628
+ // Use rootFormRegistry instead of fieldSignalContext.form because when the submit button
1629
+ // is inside a group/array, fieldSignalContext.form points to the nested form tree,
1630
+ // not the root form. We need root form validity for submit button disabled state (#157).
1631
+ const disabledSignal = resolveSubmitButtonDisabled({
1632
+ form: rootFormRegistry.getRootForm(),
1633
+ formOptions: formOptions(),
1634
+ fieldLogic: fieldWithLogic.logic,
1635
+ explicitlyDisabled: fieldDef.disabled,
1636
+ });
1426
1637
  const inputs = {
1427
1638
  ...baseInputs,
1428
1639
  // No event - native form submit handles it via form's onNativeSubmit
@@ -1449,21 +1660,19 @@ function submitButtonFieldMapper(fieldDef) {
1449
1660
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1450
1661
  */
1451
1662
  function nextButtonFieldMapper(fieldDef) {
1452
- // Inject field signal context to access form state and options
1453
1663
  const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
1454
- // Build base inputs (static, from field definition)
1455
- const baseInputs = buildBaseInputs(fieldDef);
1456
- // Use button-logic-resolver to compute disabled state
1664
+ const defaultProps = inject(DEFAULT_PROPS);
1665
+ const formOptions = inject(FORM_OPTIONS);
1457
1666
  const fieldWithLogic = fieldDef;
1458
- const disabledSignal = resolveNextButtonDisabled({
1459
- form: fieldSignalContext.form,
1460
- formOptions: fieldSignalContext.formOptions,
1461
- fieldLogic: fieldWithLogic.logic,
1462
- explicitlyDisabled: fieldDef.disabled,
1463
- currentPageValid: fieldSignalContext.currentPageValid,
1464
- });
1465
- // Return computed signal - evaluates disabledSignal inside for reactivity
1466
1667
  return computed(() => {
1668
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1669
+ const disabledSignal = resolveNextButtonDisabled({
1670
+ form: fieldSignalContext.form,
1671
+ formOptions: formOptions(),
1672
+ fieldLogic: fieldWithLogic.logic,
1673
+ explicitlyDisabled: fieldDef.disabled,
1674
+ currentPageValid: fieldSignalContext.currentPageValid,
1675
+ });
1467
1676
  const inputs = {
1468
1677
  ...baseInputs,
1469
1678
  event: NextPageEvent,
@@ -1484,9 +1693,9 @@ function nextButtonFieldMapper(fieldDef) {
1484
1693
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1485
1694
  */
1486
1695
  function previousButtonFieldMapper(fieldDef) {
1487
- // Build base inputs (static, from field definition)
1488
- const baseInputs = buildBaseInputs(fieldDef);
1696
+ const defaultProps = inject(DEFAULT_PROPS);
1489
1697
  return computed(() => {
1698
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1490
1699
  const inputs = {
1491
1700
  ...baseInputs,
1492
1701
  event: PreviousPageEvent,
@@ -1512,10 +1721,9 @@ function previousButtonFieldMapper(fieldDef) {
1512
1721
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1513
1722
  */
1514
1723
  function addArrayItemButtonFieldMapper(fieldDef) {
1515
- // Try to get array context (available when inside an array)
1516
- // Use optional injection so it doesn't fail when outside an array
1517
1724
  const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
1518
1725
  const logger = inject(DynamicFormLogger);
1726
+ const defaultProps = inject(DEFAULT_PROPS);
1519
1727
  // Determine the target array key
1520
1728
  // Priority: explicit arrayKey from fieldDef > arrayKey from context
1521
1729
  const targetArrayKey = fieldDef.arrayKey ?? arrayContext?.arrayKey;
@@ -1523,13 +1731,12 @@ function addArrayItemButtonFieldMapper(fieldDef) {
1523
1731
  logger.warn(`addArrayItem button "${fieldDef.key}" has no array context. ` +
1524
1732
  'Either place it inside an array field, or provide an explicit arrayKey property.');
1525
1733
  }
1526
- // Build base inputs (static, from field definition)
1527
- const baseInputs = buildBaseInputs(fieldDef);
1528
1734
  // Set default eventArgs for AddArrayItemEvent (arrayKey)
1529
1735
  // User can override by providing eventArgs in field definition
1530
1736
  const defaultEventArgs = ['$arrayKey'];
1531
1737
  const eventArgs = 'eventArgs' in fieldDef && fieldDef.eventArgs !== undefined ? fieldDef.eventArgs : defaultEventArgs;
1532
1738
  return computed(() => {
1739
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1533
1740
  // Read signal value if index is a signal (supports differential updates)
1534
1741
  const getIndex = () => {
1535
1742
  if (!arrayContext)
@@ -1568,10 +1775,9 @@ function addArrayItemButtonFieldMapper(fieldDef) {
1568
1775
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1569
1776
  */
1570
1777
  function removeArrayItemButtonFieldMapper(fieldDef) {
1571
- // Try to get array context (available when inside an array)
1572
- // Use optional injection so it doesn't fail when outside an array
1573
1778
  const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
1574
1779
  const logger = inject(DynamicFormLogger);
1780
+ const defaultProps = inject(DEFAULT_PROPS);
1575
1781
  // Determine the target array key
1576
1782
  // Priority: explicit arrayKey from fieldDef > arrayKey from context
1577
1783
  const targetArrayKey = fieldDef.arrayKey ?? arrayContext?.arrayKey;
@@ -1579,14 +1785,13 @@ function removeArrayItemButtonFieldMapper(fieldDef) {
1579
1785
  logger.warn(`removeArrayItem button "${fieldDef.key}" has no array context. ` +
1580
1786
  'Either place it inside an array field, or provide an explicit arrayKey property.');
1581
1787
  }
1582
- // Build base inputs (static, from field definition)
1583
- const baseInputs = buildBaseInputs(fieldDef);
1584
1788
  // Set default eventArgs for RemoveArrayItemEvent (arrayKey, index if inside array)
1585
1789
  // When outside array, only pass arrayKey (removes last by default)
1586
1790
  // User can override by providing eventArgs in field definition
1587
1791
  const defaultEventArgs = arrayContext ? ['$arrayKey', '$index'] : ['$arrayKey'];
1588
1792
  const eventArgs = 'eventArgs' in fieldDef && fieldDef.eventArgs !== undefined ? fieldDef.eventArgs : defaultEventArgs;
1589
1793
  return computed(() => {
1794
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1590
1795
  // Read signal value if index is a signal (supports differential updates)
1591
1796
  const getIndex = () => {
1592
1797
  if (!arrayContext)