@ng-forge/dynamic-forms-ionic 0.3.1 → 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,8 +64,8 @@ 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()}`", "class": "className()", "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
69
  @let buttonId = key() + '-button';
70
70
 
71
71
  <ion-button
@@ -83,19 +83,16 @@ class IonicButtonFieldComponent {
83
83
  >
84
84
  {{ label() | dynamicText | async }}
85
85
  </ion-button>
86
- `, 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 });
87
87
  }
88
- 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: [{
89
89
  type: Component,
90
- args: [{
91
- selector: 'df-ion-button',
92
- imports: [IonButton, DynamicTextPipe, AsyncPipe],
93
- host: {
90
+ args: [{ selector: 'df-ion-button', imports: [IonButton, DynamicTextPipe, AsyncPipe], host: {
94
91
  '[id]': '`${key()}`',
95
92
  '[class]': 'className()',
96
93
  '[attr.data-testid]': 'key()',
97
- },
98
- template: `
94
+ '[attr.hidden]': 'hidden() || null',
95
+ }, template: `
99
96
  @let buttonId = key() + '-button';
100
97
 
101
98
  <ion-button
@@ -113,9 +110,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
113
110
  >
114
111
  {{ label() | dynamicText | async }}
115
112
  </ion-button>
116
- `,
117
- changeDetection: ChangeDetectionStrategy.OnPush,
118
- }]
113
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host([hidden]){display:none!important}\n"] }]
119
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 }] }] } });
120
115
 
121
116
  var ionicButton_component = /*#__PURE__*/Object.freeze({
@@ -125,6 +120,29 @@ var ionicButton_component = /*#__PURE__*/Object.freeze({
125
120
 
126
121
  // Public API - component
127
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
+
128
146
  class IonicCheckboxFieldComponent {
129
147
  elementRef = inject((ElementRef));
130
148
  field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
@@ -152,17 +170,21 @@ class IonicCheckboxFieldComponent {
152
170
  // ─────────────────────────────────────────────────────────────────────────────
153
171
  /** Base ID for error elements */
154
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" }] : []));
155
175
  /** Whether the field is currently in an invalid state (invalid AND touched) */
156
- isAriaInvalid = computed(() => {
176
+ ariaInvalid = computed(() => {
157
177
  const fieldState = this.field()();
158
178
  return fieldState.invalid() && fieldState.touched();
159
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
179
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
160
180
  /** Whether the field has a required validator */
161
- isRequired = computed(() => {
162
- return this.field()().required?.() === true;
163
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
164
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
165
- 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: `
166
188
  @let f = field();
167
189
  @let checkboxId = key() + '-checkbox';
168
190
 
@@ -174,18 +196,23 @@ class IonicCheckboxFieldComponent {
174
196
  [color]="props()?.color ?? 'primary'"
175
197
  [indeterminate]="props()?.indeterminate ?? false"
176
198
  [attr.tabindex]="tabIndex()"
177
- [attr.aria-invalid]="isAriaInvalid()"
178
- [attr.aria-required]="isRequired() || null"
199
+ [attr.aria-invalid]="ariaInvalid()"
200
+ [attr.aria-required]="ariaRequired()"
201
+ [attr.aria-describedby]="ariaDescribedBy()"
179
202
  >
180
203
  {{ label() | dynamicText | async }}
181
204
  </ion-checkbox>
182
205
 
183
206
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
184
- <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
+ }
185
212
  }
186
- `, 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 });
187
214
  }
188
- 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: [{
189
216
  type: Component,
190
217
  args: [{ selector: 'df-ion-checkbox', imports: [IonCheckbox, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
191
218
  @let f = field();
@@ -199,21 +226,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
199
226
  [color]="props()?.color ?? 'primary'"
200
227
  [indeterminate]="props()?.indeterminate ?? false"
201
228
  [attr.tabindex]="tabIndex()"
202
- [attr.aria-invalid]="isAriaInvalid()"
203
- [attr.aria-required]="isRequired() || null"
229
+ [attr.aria-invalid]="ariaInvalid()"
230
+ [attr.aria-required]="ariaRequired()"
231
+ [attr.aria-describedby]="ariaDescribedBy()"
204
232
  >
205
233
  {{ label() | dynamicText | async }}
206
234
  </ion-checkbox>
207
235
 
208
236
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
209
- <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
+ }
210
242
  }
211
243
  `, host: {
212
244
  '[class]': 'className()',
213
245
  '[id]': '`${key()}`',
214
246
  '[attr.data-testid]': 'key()',
215
247
  '[attr.hidden]': 'field()().hidden() || null',
216
- }, 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"] }]
217
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 }] }] } });
218
250
 
219
251
  var ionicCheckbox_component = /*#__PURE__*/Object.freeze({
@@ -251,15 +283,19 @@ class IonicDatepickerFieldComponent {
251
283
  // ─────────────────────────────────────────────────────────────────────────────
252
284
  /** Base ID for error elements */
253
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" }] : []));
254
288
  /** Whether the field is currently in an invalid state (invalid AND touched) */
255
- isAriaInvalid = computed(() => {
289
+ ariaInvalid = computed(() => {
256
290
  const fieldState = this.field()();
257
291
  return fieldState.invalid() && fieldState.touched();
258
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
292
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
259
293
  /** Whether the field has a required validator */
260
- isRequired = computed(() => {
261
- return this.field()().required?.() === true;
262
- }, ...(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);
263
299
  openModal() {
264
300
  this.isModalOpen.set(true);
265
301
  }
@@ -304,11 +340,10 @@ class IonicDatepickerFieldComponent {
304
340
  return format(date, 'MMM d, yyyy');
305
341
  }
306
342
  }
307
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicDatepickerFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
308
- 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: `
309
- @let f = field(); @let dateValue = f().value();
310
- @let ariaInvalid = isAriaInvalid();
311
- @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();
312
347
  @let inputId = key() + '-input';
313
348
 
314
349
  <ion-input
@@ -321,12 +356,17 @@ class IonicDatepickerFieldComponent {
321
356
  [readonly]="true"
322
357
  [fill]="'outline'"
323
358
  [attr.tabindex]="tabIndex()"
324
- [attr.aria-invalid]="ariaInvalid"
325
- [attr.aria-required]="ariaRequired"
359
+ [attr.aria-invalid]="ariaInvalid()"
360
+ [attr.aria-required]="ariaRequired()"
361
+ [attr.aria-describedby]="ariaDescribedBy()"
326
362
  (click)="!f().disabled() && openModal()"
327
363
  />
328
364
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
329
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
+ }
330
370
  }
331
371
 
332
372
  <ion-modal [isOpen]="isModalOpen()" (didDismiss)="closeModal()">
@@ -358,9 +398,9 @@ class IonicDatepickerFieldComponent {
358
398
  </ion-content>
359
399
  </ng-template>
360
400
  </ion-modal>
361
- `, 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 });
362
402
  }
363
- 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: [{
364
404
  type: Component,
365
405
  args: [{ selector: 'df-ion-datepicker', imports: [
366
406
  IonInput,
@@ -376,9 +416,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
376
416
  DynamicTextPipe,
377
417
  AsyncPipe,
378
418
  ], template: `
379
- @let f = field(); @let dateValue = f().value();
380
- @let ariaInvalid = isAriaInvalid();
381
- @let ariaRequired = isRequired() || null;
419
+ @let f = field();
420
+ @let dateValue = f().value();
382
421
  @let inputId = key() + '-input';
383
422
 
384
423
  <ion-input
@@ -391,12 +430,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
391
430
  [readonly]="true"
392
431
  [fill]="'outline'"
393
432
  [attr.tabindex]="tabIndex()"
394
- [attr.aria-invalid]="ariaInvalid"
395
- [attr.aria-required]="ariaRequired"
433
+ [attr.aria-invalid]="ariaInvalid()"
434
+ [attr.aria-required]="ariaRequired()"
435
+ [attr.aria-describedby]="ariaDescribedBy()"
396
436
  (click)="!f().disabled() && openModal()"
397
437
  />
398
438
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
399
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
+ }
400
444
  }
401
445
 
402
446
  <ion-modal [isOpen]="isModalOpen()" (didDismiss)="closeModal()">
@@ -433,7 +477,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
433
477
  '[attr.data-testid]': 'key()',
434
478
  '[class]': 'className()',
435
479
  '[attr.hidden]': 'field()().hidden() || null',
436
- }, 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"] }]
437
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 }] }] } });
438
482
 
439
483
  var ionicDatepicker_component = /*#__PURE__*/Object.freeze({
@@ -489,26 +533,48 @@ class IonicInputFieldComponent {
489
533
  setupMetaTracking(this.elementRef, this.meta, {
490
534
  selector: 'ion-input',
491
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
+ });
492
556
  }
493
557
  // ─────────────────────────────────────────────────────────────────────────────
494
558
  // Accessibility
495
559
  // ─────────────────────────────────────────────────────────────────────────────
496
560
  /** Base ID for error elements */
497
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" }] : []));
498
564
  /** Whether the field is currently in an invalid state (invalid AND touched) */
499
- isAriaInvalid = computed(() => {
565
+ ariaInvalid = computed(() => {
500
566
  const fieldState = this.field()();
501
567
  return fieldState.invalid() && fieldState.touched();
502
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
568
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
503
569
  /** Whether the field has a required validator */
504
- isRequired = computed(() => {
505
- return this.field()().required?.() === true;
506
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
507
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicInputFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
508
- 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: `
509
577
  @let f = field();
510
- @let ariaInvalid = isAriaInvalid();
511
- @let ariaRequired = isRequired() || null;
512
578
  @let inputId = key() + '-input';
513
579
 
514
580
  <ion-input
@@ -524,22 +590,21 @@ class IonicInputFieldComponent {
524
590
  [fill]="effectiveFill()"
525
591
  [shape]="effectiveShape()"
526
592
  [readonly]="f().readonly()"
527
- [helperText]="(props()?.helperText | dynamicText | async) ?? undefined"
593
+ [helperText]="errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined"
528
594
  [attr.tabindex]="tabIndex()"
529
- [attr.aria-invalid]="ariaInvalid"
530
- [attr.aria-required]="ariaRequired"
595
+ [attr.aria-invalid]="ariaInvalid()"
596
+ [attr.aria-required]="ariaRequired()"
597
+ [attr.aria-describedby]="ariaDescribedBy()"
531
598
  />
532
599
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
533
600
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
534
601
  }
535
- `, 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 });
536
603
  }
537
- 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: [{
538
605
  type: Component,
539
606
  args: [{ selector: 'df-ion-input', imports: [IonInput, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
540
607
  @let f = field();
541
- @let ariaInvalid = isAriaInvalid();
542
- @let ariaRequired = isRequired() || null;
543
608
  @let inputId = key() + '-input';
544
609
 
545
610
  <ion-input
@@ -555,10 +620,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
555
620
  [fill]="effectiveFill()"
556
621
  [shape]="effectiveShape()"
557
622
  [readonly]="f().readonly()"
558
- [helperText]="(props()?.helperText | dynamicText | async) ?? undefined"
623
+ [helperText]="errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined"
559
624
  [attr.tabindex]="tabIndex()"
560
- [attr.aria-invalid]="ariaInvalid"
561
- [attr.aria-required]="ariaRequired"
625
+ [attr.aria-invalid]="ariaInvalid()"
626
+ [attr.aria-required]="ariaRequired()"
627
+ [attr.aria-describedby]="ariaDescribedBy()"
562
628
  />
563
629
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
564
630
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
@@ -568,7 +634,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
568
634
  '[attr.data-testid]': 'key()',
569
635
  '[class]': 'className()',
570
636
  '[attr.hidden]': 'field()().hidden() || null',
571
- }, 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"] }]
572
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 }] }] } });
573
639
 
574
640
  var ionicInput_component = /*#__PURE__*/Object.freeze({
@@ -597,19 +663,31 @@ class IonicMultiCheckboxFieldComponent {
597
663
  // ─────────────────────────────────────────────────────────────────────────────
598
664
  /** Base ID for error elements */
599
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" }] : []));
600
668
  /** Whether the field is currently in an invalid state (invalid AND touched) */
601
- isAriaInvalid = computed(() => {
669
+ ariaInvalid = computed(() => {
602
670
  const fieldState = this.field()();
603
671
  return fieldState.invalid() && fieldState.touched();
604
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
672
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
605
673
  /** Whether the field has a required validator */
606
- isRequired = computed(() => {
607
- return this.field()().required?.() === true;
608
- }, ...(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);
609
679
  valueViewModel = linkedSignal(() => {
610
680
  const currentValues = this.field()().value();
611
681
  return this.options().filter((option) => currentValues.includes(option.value));
612
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" }] : []));
613
691
  constructor() {
614
692
  // Shadow DOM - apply meta to ion-checkbox elements, re-apply when options change
615
693
  setupMetaTracking(this.elementRef, this.meta, {
@@ -641,9 +719,10 @@ class IonicMultiCheckboxFieldComponent {
641
719
  }
642
720
  });
643
721
  }
644
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicMultiCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
645
- 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: `
646
724
  @let f = field();
725
+ @let checked = checkedValuesMap();
647
726
  @if (label(); as label) {
648
727
  <div class="checkbox-group-label">{{ label | dynamicText | async }}</div>
649
728
  }
@@ -651,15 +730,16 @@ class IonicMultiCheckboxFieldComponent {
651
730
  <div
652
731
  class="checkbox-group"
653
732
  role="group"
654
- [attr.aria-invalid]="isAriaInvalid()"
655
- [attr.aria-required]="isRequired() || null"
733
+ [attr.aria-invalid]="ariaInvalid()"
734
+ [attr.aria-required]="ariaRequired()"
735
+ [attr.aria-describedby]="ariaDescribedBy()"
656
736
  [class.ion-invalid]="showErrors()"
657
737
  [class.ion-touched]="field()().touched()"
658
738
  >
659
739
  @for (option of options(); track option.value) {
660
740
  <ion-item lines="none">
661
741
  <ion-checkbox
662
- [checked]="option | inArray: valueViewModel()"
742
+ [checked]="checked['' + option.value]"
663
743
  [disabled]="f().disabled() || option.disabled"
664
744
  [labelPlacement]="props()?.labelPlacement ?? 'end'"
665
745
  [justify]="props()?.justify ?? 'start'"
@@ -673,14 +753,19 @@ class IonicMultiCheckboxFieldComponent {
673
753
  </div>
674
754
 
675
755
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
676
- <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
+ }
677
761
  }
678
- `, 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 });
679
763
  }
680
- 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: [{
681
765
  type: Component,
682
- 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: `
683
767
  @let f = field();
768
+ @let checked = checkedValuesMap();
684
769
  @if (label(); as label) {
685
770
  <div class="checkbox-group-label">{{ label | dynamicText | async }}</div>
686
771
  }
@@ -688,15 +773,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
688
773
  <div
689
774
  class="checkbox-group"
690
775
  role="group"
691
- [attr.aria-invalid]="isAriaInvalid()"
692
- [attr.aria-required]="isRequired() || null"
776
+ [attr.aria-invalid]="ariaInvalid()"
777
+ [attr.aria-required]="ariaRequired()"
778
+ [attr.aria-describedby]="ariaDescribedBy()"
693
779
  [class.ion-invalid]="showErrors()"
694
780
  [class.ion-touched]="field()().touched()"
695
781
  >
696
782
  @for (option of options(); track option.value) {
697
783
  <ion-item lines="none">
698
784
  <ion-checkbox
699
- [checked]="option | inArray: valueViewModel()"
785
+ [checked]="checked['' + option.value]"
700
786
  [disabled]="f().disabled() || option.disabled"
701
787
  [labelPlacement]="props()?.labelPlacement ?? 'end'"
702
788
  [justify]="props()?.justify ?? 'start'"
@@ -710,14 +796,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
710
796
  </div>
711
797
 
712
798
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
713
- <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
+ }
714
804
  }
715
805
  `, host: {
716
806
  '[class]': 'className() || ""',
717
807
  '[id]': '`${key()}`',
718
808
  '[attr.data-testid]': 'key()',
719
809
  '[attr.hidden]': 'field()().hidden() || null',
720
- }, 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"] }]
721
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 }] }] } });
722
812
 
723
813
  var ionicMultiCheckbox_component = /*#__PURE__*/Object.freeze({
@@ -755,17 +845,21 @@ class IonicRadioFieldComponent {
755
845
  // ─────────────────────────────────────────────────────────────────────────────
756
846
  /** Base ID for error elements */
757
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" }] : []));
758
850
  /** Whether the field is currently in an invalid state (invalid AND touched) */
759
- isAriaInvalid = computed(() => {
851
+ ariaInvalid = computed(() => {
760
852
  const fieldState = this.field()();
761
853
  return fieldState.invalid() && fieldState.touched();
762
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
854
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
763
855
  /** Whether the field has a required validator */
764
- isRequired = computed(() => {
765
- return this.field()().required?.() === true;
766
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
767
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicRadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
768
- 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: `
769
863
  @let f = field();
770
864
  @let radioGroupId = key() + '-radio-group';
771
865
  @if (label(); as label) {
@@ -776,8 +870,9 @@ class IonicRadioFieldComponent {
776
870
  [id]="radioGroupId"
777
871
  [formField]="f"
778
872
  [compareWith]="props()?.compareWith || defaultCompare"
779
- [attr.aria-invalid]="isAriaInvalid()"
780
- [attr.aria-required]="isRequired() || null"
873
+ [attr.aria-invalid]="ariaInvalid()"
874
+ [attr.aria-required]="ariaRequired()"
875
+ [attr.aria-describedby]="ariaDescribedBy()"
781
876
  >
782
877
  @for (option of options(); track option.value) {
783
878
  <ion-item [lines]="'none'">
@@ -795,11 +890,15 @@ class IonicRadioFieldComponent {
795
890
  </ion-radio-group>
796
891
 
797
892
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
798
- <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
+ }
799
898
  }
800
- `, 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 });
801
900
  }
802
- 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: [{
803
902
  type: Component,
804
903
  args: [{ selector: 'df-ion-radio', imports: [IonRadioGroup, IonRadio, IonItem, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
805
904
  @let f = field();
@@ -812,8 +911,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
812
911
  [id]="radioGroupId"
813
912
  [formField]="f"
814
913
  [compareWith]="props()?.compareWith || defaultCompare"
815
- [attr.aria-invalid]="isAriaInvalid()"
816
- [attr.aria-required]="isRequired() || null"
914
+ [attr.aria-invalid]="ariaInvalid()"
915
+ [attr.aria-required]="ariaRequired()"
916
+ [attr.aria-describedby]="ariaDescribedBy()"
817
917
  >
818
918
  @for (option of options(); track option.value) {
819
919
  <ion-item [lines]="'none'">
@@ -831,14 +931,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
831
931
  </ion-radio-group>
832
932
 
833
933
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
834
- <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
+ }
835
939
  }
836
940
  `, host: {
837
941
  '[class]': 'className()',
838
942
  '[id]': '`${key()}`',
839
943
  '[attr.data-testid]': 'key()',
840
944
  '[attr.hidden]': 'field()().hidden() || null',
841
- }, 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"] }]
842
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 }] }] } });
843
947
 
844
948
  var ionicRadio_component = /*#__PURE__*/Object.freeze({
@@ -871,20 +975,22 @@ class IonicSelectFieldComponent {
871
975
  // ─────────────────────────────────────────────────────────────────────────────
872
976
  /** Base ID for error elements */
873
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" }] : []));
874
980
  /** Whether the field is currently in an invalid state (invalid AND touched) */
875
- isAriaInvalid = computed(() => {
981
+ ariaInvalid = computed(() => {
876
982
  const fieldState = this.field()();
877
983
  return fieldState.invalid() && fieldState.touched();
878
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
984
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
879
985
  /** Whether the field has a required validator */
880
- isRequired = computed(() => {
881
- return this.field()().required?.() === true;
882
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
883
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
884
- 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: `
885
993
  @let f = field();
886
- @let ariaInvalid = isAriaInvalid();
887
- @let ariaRequired = isRequired() || null;
888
994
  @let selectId = key() + '-select';
889
995
 
890
996
  <ion-select
@@ -903,8 +1009,9 @@ class IonicSelectFieldComponent {
903
1009
  [fill]="props()?.fill ?? 'outline'"
904
1010
  [shape]="props()?.shape"
905
1011
  [attr.tabindex]="tabIndex()"
906
- [attr.aria-invalid]="ariaInvalid"
907
- [attr.aria-required]="ariaRequired"
1012
+ [attr.aria-invalid]="ariaInvalid()"
1013
+ [attr.aria-required]="ariaRequired()"
1014
+ [attr.aria-describedby]="ariaDescribedBy()"
908
1015
  >
909
1016
  @for (option of options(); track option.value) {
910
1017
  <ion-select-option [value]="option.value" [disabled]="option.disabled || false">
@@ -914,15 +1021,17 @@ class IonicSelectFieldComponent {
914
1021
  </ion-select>
915
1022
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
916
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
+ }
917
1028
  }
918
- `, 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 });
919
1030
  }
920
- 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: [{
921
1032
  type: Component,
922
1033
  args: [{ selector: 'df-ion-select', imports: [IonSelect, IonSelectOption, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
923
1034
  @let f = field();
924
- @let ariaInvalid = isAriaInvalid();
925
- @let ariaRequired = isRequired() || null;
926
1035
  @let selectId = key() + '-select';
927
1036
 
928
1037
  <ion-select
@@ -941,8 +1050,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
941
1050
  [fill]="props()?.fill ?? 'outline'"
942
1051
  [shape]="props()?.shape"
943
1052
  [attr.tabindex]="tabIndex()"
944
- [attr.aria-invalid]="ariaInvalid"
945
- [attr.aria-required]="ariaRequired"
1053
+ [attr.aria-invalid]="ariaInvalid()"
1054
+ [attr.aria-required]="ariaRequired()"
1055
+ [attr.aria-describedby]="ariaDescribedBy()"
946
1056
  >
947
1057
  @for (option of options(); track option.value) {
948
1058
  <ion-select-option [value]="option.value" [disabled]="option.disabled || false">
@@ -952,13 +1062,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
952
1062
  </ion-select>
953
1063
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
954
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
+ }
955
1069
  }
956
1070
  `, changeDetection: ChangeDetectionStrategy.OnPush, host: {
957
1071
  '[id]': '`${key()}`',
958
1072
  '[attr.data-testid]': 'key()',
959
1073
  '[class]': 'className()',
960
1074
  '[attr.hidden]': 'field()().hidden() || null',
961
- }, 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"] }]
962
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 }] }] } });
963
1077
 
964
1078
  var ionicSelect_component = /*#__PURE__*/Object.freeze({
@@ -990,17 +1104,21 @@ class IonicSliderFieldComponent {
990
1104
  // ─────────────────────────────────────────────────────────────────────────────
991
1105
  /** Base ID for error elements */
992
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" }] : []));
993
1109
  /** Whether the field is currently in an invalid state (invalid AND touched) */
994
- isAriaInvalid = computed(() => {
1110
+ ariaInvalid = computed(() => {
995
1111
  const fieldState = this.field()();
996
1112
  return fieldState.invalid() && fieldState.touched();
997
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
1113
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
998
1114
  /** Whether the field has a required validator */
999
- isRequired = computed(() => {
1000
- return this.field()().required?.() === true;
1001
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
1002
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicSliderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1003
- 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: `
1004
1122
  @let f = field();
1005
1123
  @let inputId = key() + '-input';
1006
1124
 
@@ -1009,6 +1127,8 @@ class IonicSliderFieldComponent {
1009
1127
  [formField]="f"
1010
1128
  [label]="(label() | dynamicText | async) ?? undefined"
1011
1129
  [labelPlacement]="props()?.labelPlacement ?? 'stacked'"
1130
+ [min]="props()?.min ?? 0"
1131
+ [max]="props()?.max ?? 100"
1012
1132
  [step]="props()?.step ?? 1"
1013
1133
  [dualKnobs]="props()?.dualKnobs ?? false"
1014
1134
  [pin]="props()?.pin ?? false"
@@ -1017,16 +1137,21 @@ class IonicSliderFieldComponent {
1017
1137
  [snaps]="props()?.snaps ?? false"
1018
1138
  [color]="props()?.color ?? 'primary'"
1019
1139
  [attr.tabindex]="tabIndex()"
1020
- [attr.aria-invalid]="isAriaInvalid()"
1021
- [attr.aria-required]="isRequired() || null"
1140
+ [attr.aria-invalid]="ariaInvalid()"
1141
+ [attr.aria-required]="ariaRequired()"
1142
+ [attr.aria-describedby]="ariaDescribedBy()"
1022
1143
  />
1023
1144
 
1024
1145
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1025
- <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
+ }
1026
1151
  }
1027
- `, 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 });
1028
1153
  }
1029
- 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: [{
1030
1155
  type: Component,
1031
1156
  args: [{ selector: 'df-ion-slider', imports: [IonRange, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
1032
1157
  @let f = field();
@@ -1037,6 +1162,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1037
1162
  [formField]="f"
1038
1163
  [label]="(label() | dynamicText | async) ?? undefined"
1039
1164
  [labelPlacement]="props()?.labelPlacement ?? 'stacked'"
1165
+ [min]="props()?.min ?? 0"
1166
+ [max]="props()?.max ?? 100"
1040
1167
  [step]="props()?.step ?? 1"
1041
1168
  [dualKnobs]="props()?.dualKnobs ?? false"
1042
1169
  [pin]="props()?.pin ?? false"
@@ -1045,19 +1172,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1045
1172
  [snaps]="props()?.snaps ?? false"
1046
1173
  [color]="props()?.color ?? 'primary'"
1047
1174
  [attr.tabindex]="tabIndex()"
1048
- [attr.aria-invalid]="isAriaInvalid()"
1049
- [attr.aria-required]="isRequired() || null"
1175
+ [attr.aria-invalid]="ariaInvalid()"
1176
+ [attr.aria-required]="ariaRequired()"
1177
+ [attr.aria-describedby]="ariaDescribedBy()"
1050
1178
  />
1051
1179
 
1052
1180
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1053
- <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
+ }
1054
1186
  }
1055
1187
  `, host: {
1056
1188
  '[class]': 'className()',
1057
1189
  '[id]': '`${key()}`',
1058
1190
  '[attr.data-testid]': 'key()',
1059
1191
  '[attr.hidden]': 'field()().hidden() || null',
1060
- }, 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"] }]
1061
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 }] }] } });
1062
1194
 
1063
1195
  var ionicSlider_component = /*#__PURE__*/Object.freeze({
@@ -1085,26 +1217,48 @@ class IonicTextareaFieldComponent {
1085
1217
  setupMetaTracking(this.elementRef, this.meta, {
1086
1218
  selector: 'ion-textarea',
1087
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
+ });
1088
1240
  }
1089
1241
  // ─────────────────────────────────────────────────────────────────────────────
1090
1242
  // Accessibility
1091
1243
  // ─────────────────────────────────────────────────────────────────────────────
1092
1244
  /** Base ID for error elements */
1093
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" }] : []));
1094
1248
  /** Whether the field is currently in an invalid state (invalid AND touched) */
1095
- isAriaInvalid = computed(() => {
1249
+ ariaInvalid = computed(() => {
1096
1250
  const fieldState = this.field()();
1097
1251
  return fieldState.invalid() && fieldState.touched();
1098
- }, ...(ngDevMode ? [{ debugName: "isAriaInvalid" }] : []));
1252
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : []));
1099
1253
  /** Whether the field has a required validator */
1100
- isRequired = computed(() => {
1101
- return this.field()().required?.() === true;
1102
- }, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
1103
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicTextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1104
- 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: `
1105
1261
  @let f = field();
1106
- @let ariaInvalid = isAriaInvalid();
1107
- @let ariaRequired = isRequired() || null;
1108
1262
  @let textareaId = key() + '-textarea';
1109
1263
 
1110
1264
  <ion-textarea
@@ -1120,22 +1274,21 @@ class IonicTextareaFieldComponent {
1120
1274
  [fill]="props()?.fill ?? 'outline'"
1121
1275
  [shape]="props()?.shape"
1122
1276
  [readonly]="f().readonly()"
1123
- [helperText]="(props()?.helperText | dynamicText | async) ?? undefined"
1277
+ [helperText]="errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined"
1124
1278
  [attr.tabindex]="tabIndex()"
1125
- [attr.aria-invalid]="ariaInvalid"
1126
- [attr.aria-required]="ariaRequired"
1279
+ [attr.aria-invalid]="ariaInvalid()"
1280
+ [attr.aria-required]="ariaRequired()"
1281
+ [attr.aria-describedby]="ariaDescribedBy()"
1127
1282
  />
1128
1283
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1129
1284
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
1130
1285
  }
1131
- `, 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 });
1132
1287
  }
1133
- 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: [{
1134
1289
  type: Component,
1135
1290
  args: [{ selector: 'df-ion-textarea', imports: [IonTextarea, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
1136
1291
  @let f = field();
1137
- @let ariaInvalid = isAriaInvalid();
1138
- @let ariaRequired = isRequired() || null;
1139
1292
  @let textareaId = key() + '-textarea';
1140
1293
 
1141
1294
  <ion-textarea
@@ -1151,10 +1304,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1151
1304
  [fill]="props()?.fill ?? 'outline'"
1152
1305
  [shape]="props()?.shape"
1153
1306
  [readonly]="f().readonly()"
1154
- [helperText]="(props()?.helperText | dynamicText | async) ?? undefined"
1307
+ [helperText]="errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined"
1155
1308
  [attr.tabindex]="tabIndex()"
1156
- [attr.aria-invalid]="ariaInvalid"
1157
- [attr.aria-required]="ariaRequired"
1309
+ [attr.aria-invalid]="ariaInvalid()"
1310
+ [attr.aria-required]="ariaRequired()"
1311
+ [attr.aria-describedby]="ariaDescribedBy()"
1158
1312
  />
1159
1313
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1160
1314
  <ion-note color="danger" class="df-ion-error" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</ion-note>
@@ -1164,7 +1318,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1164
1318
  '[attr.data-testid]': 'key()',
1165
1319
  '[class]': 'className()',
1166
1320
  '[attr.hidden]': 'field()().hidden() || null',
1167
- }, 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"] }]
1168
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 }] }] } });
1169
1323
 
1170
1324
  var ionicTextarea_component = /*#__PURE__*/Object.freeze({
@@ -1205,6 +1359,7 @@ class IonicToggleControlComponent {
1205
1359
  enableOnOffLabels = input(false, ...(ngDevMode ? [{ debugName: "enableOnOffLabels" }] : []));
1206
1360
  tabIndex = input(undefined, ...(ngDevMode ? [{ debugName: "tabIndex" }] : []));
1207
1361
  meta = input(...(ngDevMode ? [undefined, { debugName: "meta" }] : []));
1362
+ ariaDescribedBy = input(null, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1208
1363
  constructor() {
1209
1364
  // Shadow DOM - apply meta to ion-toggle element
1210
1365
  setupMetaTracking(this.elementRef, this.meta, {
@@ -1248,8 +1403,8 @@ class IonicToggleControlComponent {
1248
1403
  onBlur() {
1249
1404
  this.touched.set(true);
1250
1405
  }
1251
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicToggleControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1252
- 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: `
1253
1408
  <ion-toggle
1254
1409
  [checked]="isChecked()"
1255
1410
  (ionChange)="onToggleChange($event)"
@@ -1263,12 +1418,13 @@ class IonicToggleControlComponent {
1263
1418
  [attr.aria-invalid]="ariaInvalid()"
1264
1419
  [attr.aria-required]="ariaRequired()"
1265
1420
  [attr.aria-readonly]="ariaReadonly()"
1421
+ [attr.aria-describedby]="ariaDescribedBy()"
1266
1422
  >
1267
1423
  <ng-content />
1268
1424
  </ion-toggle>
1269
- `, 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 });
1270
1426
  }
1271
- 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: [{
1272
1428
  type: Component,
1273
1429
  args: [{ selector: 'df-ion-toggle-control', imports: [IonToggle], template: `
1274
1430
  <ion-toggle
@@ -1284,11 +1440,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1284
1440
  [attr.aria-invalid]="ariaInvalid()"
1285
1441
  [attr.aria-required]="ariaRequired()"
1286
1442
  [attr.aria-readonly]="ariaReadonly()"
1443
+ [attr.aria-describedby]="ariaDescribedBy()"
1287
1444
  >
1288
1445
  <ng-content />
1289
1446
  </ion-toggle>
1290
- `, 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"] }]
1291
- }], 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 }] }] } });
1292
1451
 
1293
1452
  class IonicToggleFieldComponent {
1294
1453
  elementRef = inject((ElementRef));
@@ -1314,8 +1473,12 @@ class IonicToggleFieldComponent {
1314
1473
  // ─────────────────────────────────────────────────────────────────────────────
1315
1474
  /** Base ID for error elements */
1316
1475
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1317
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: IonicToggleFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1318
- 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: `
1319
1482
  @let f = field();
1320
1483
  @let toggleId = key() + '-toggle';
1321
1484
 
@@ -1328,16 +1491,21 @@ class IonicToggleFieldComponent {
1328
1491
  [color]="props()?.color ?? 'primary'"
1329
1492
  [enableOnOffLabels]="props()?.enableOnOffLabels ?? false"
1330
1493
  [tabIndex]="tabIndex()"
1494
+ [ariaDescribedBy]="ariaDescribedBy()"
1331
1495
  >
1332
1496
  {{ label() | dynamicText | async }}
1333
1497
  </df-ion-toggle-control>
1334
1498
 
1335
1499
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1336
- <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
+ }
1337
1505
  }
1338
- `, 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 });
1339
1507
  }
1340
- 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: [{
1341
1509
  type: Component,
1342
1510
  args: [{ selector: 'df-ion-toggle', imports: [IonicToggleControlComponent, IonNote, FormField, DynamicTextPipe, AsyncPipe], template: `
1343
1511
  @let f = field();
@@ -1352,19 +1520,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1352
1520
  [color]="props()?.color ?? 'primary'"
1353
1521
  [enableOnOffLabels]="props()?.enableOnOffLabels ?? false"
1354
1522
  [tabIndex]="tabIndex()"
1523
+ [ariaDescribedBy]="ariaDescribedBy()"
1355
1524
  >
1356
1525
  {{ label() | dynamicText | async }}
1357
1526
  </df-ion-toggle-control>
1358
1527
 
1359
1528
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1360
- <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
+ }
1361
1534
  }
1362
1535
  `, host: {
1363
1536
  '[class]': 'className()',
1364
1537
  '[id]': '`${key()}`',
1365
1538
  '[attr.data-testid]': 'key()',
1366
1539
  '[attr.hidden]': 'field()().hidden() || null',
1367
- }, 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"] }]
1368
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 }] }] } });
1369
1542
 
1370
1543
  var ionicToggle_component = /*#__PURE__*/Object.freeze({
@@ -1406,9 +1579,9 @@ const IonicField = {
1406
1579
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1407
1580
  */
1408
1581
  function buttonFieldMapper(fieldDef) {
1409
- // Build base inputs (static, from field definition)
1410
- const baseInputs = buildBaseInputs(fieldDef);
1582
+ const defaultProps = inject(DEFAULT_PROPS);
1411
1583
  return computed(() => {
1584
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1412
1585
  const inputs = {
1413
1586
  ...baseInputs,
1414
1587
  };
@@ -1446,20 +1619,21 @@ function buttonFieldMapper(fieldDef) {
1446
1619
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1447
1620
  */
1448
1621
  function submitButtonFieldMapper(fieldDef) {
1449
- // Inject field signal context to access form state and options
1450
- const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
1451
- // Build base inputs (static, from field definition)
1452
- const baseInputs = buildBaseInputs(fieldDef);
1453
- // 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);
1454
1625
  const fieldWithLogic = fieldDef;
1455
- const disabledSignal = resolveSubmitButtonDisabled({
1456
- form: fieldSignalContext.form,
1457
- formOptions: fieldSignalContext.formOptions,
1458
- fieldLogic: fieldWithLogic.logic,
1459
- explicitlyDisabled: fieldDef.disabled,
1460
- });
1461
- // Return computed signal - evaluates disabledSignal inside for reactivity
1462
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
+ });
1463
1637
  const inputs = {
1464
1638
  ...baseInputs,
1465
1639
  // No event - native form submit handles it via form's onNativeSubmit
@@ -1486,21 +1660,19 @@ function submitButtonFieldMapper(fieldDef) {
1486
1660
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1487
1661
  */
1488
1662
  function nextButtonFieldMapper(fieldDef) {
1489
- // Inject field signal context to access form state and options
1490
1663
  const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
1491
- // Build base inputs (static, from field definition)
1492
- const baseInputs = buildBaseInputs(fieldDef);
1493
- // Use button-logic-resolver to compute disabled state
1664
+ const defaultProps = inject(DEFAULT_PROPS);
1665
+ const formOptions = inject(FORM_OPTIONS);
1494
1666
  const fieldWithLogic = fieldDef;
1495
- const disabledSignal = resolveNextButtonDisabled({
1496
- form: fieldSignalContext.form,
1497
- formOptions: fieldSignalContext.formOptions,
1498
- fieldLogic: fieldWithLogic.logic,
1499
- explicitlyDisabled: fieldDef.disabled,
1500
- currentPageValid: fieldSignalContext.currentPageValid,
1501
- });
1502
- // Return computed signal - evaluates disabledSignal inside for reactivity
1503
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
+ });
1504
1676
  const inputs = {
1505
1677
  ...baseInputs,
1506
1678
  event: NextPageEvent,
@@ -1521,9 +1693,9 @@ function nextButtonFieldMapper(fieldDef) {
1521
1693
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1522
1694
  */
1523
1695
  function previousButtonFieldMapper(fieldDef) {
1524
- // Build base inputs (static, from field definition)
1525
- const baseInputs = buildBaseInputs(fieldDef);
1696
+ const defaultProps = inject(DEFAULT_PROPS);
1526
1697
  return computed(() => {
1698
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1527
1699
  const inputs = {
1528
1700
  ...baseInputs,
1529
1701
  event: PreviousPageEvent,
@@ -1549,10 +1721,9 @@ function previousButtonFieldMapper(fieldDef) {
1549
1721
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1550
1722
  */
1551
1723
  function addArrayItemButtonFieldMapper(fieldDef) {
1552
- // Try to get array context (available when inside an array)
1553
- // Use optional injection so it doesn't fail when outside an array
1554
1724
  const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
1555
1725
  const logger = inject(DynamicFormLogger);
1726
+ const defaultProps = inject(DEFAULT_PROPS);
1556
1727
  // Determine the target array key
1557
1728
  // Priority: explicit arrayKey from fieldDef > arrayKey from context
1558
1729
  const targetArrayKey = fieldDef.arrayKey ?? arrayContext?.arrayKey;
@@ -1560,13 +1731,12 @@ function addArrayItemButtonFieldMapper(fieldDef) {
1560
1731
  logger.warn(`addArrayItem button "${fieldDef.key}" has no array context. ` +
1561
1732
  'Either place it inside an array field, or provide an explicit arrayKey property.');
1562
1733
  }
1563
- // Build base inputs (static, from field definition)
1564
- const baseInputs = buildBaseInputs(fieldDef);
1565
1734
  // Set default eventArgs for AddArrayItemEvent (arrayKey)
1566
1735
  // User can override by providing eventArgs in field definition
1567
1736
  const defaultEventArgs = ['$arrayKey'];
1568
1737
  const eventArgs = 'eventArgs' in fieldDef && fieldDef.eventArgs !== undefined ? fieldDef.eventArgs : defaultEventArgs;
1569
1738
  return computed(() => {
1739
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1570
1740
  // Read signal value if index is a signal (supports differential updates)
1571
1741
  const getIndex = () => {
1572
1742
  if (!arrayContext)
@@ -1605,10 +1775,9 @@ function addArrayItemButtonFieldMapper(fieldDef) {
1605
1775
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1606
1776
  */
1607
1777
  function removeArrayItemButtonFieldMapper(fieldDef) {
1608
- // Try to get array context (available when inside an array)
1609
- // Use optional injection so it doesn't fail when outside an array
1610
1778
  const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
1611
1779
  const logger = inject(DynamicFormLogger);
1780
+ const defaultProps = inject(DEFAULT_PROPS);
1612
1781
  // Determine the target array key
1613
1782
  // Priority: explicit arrayKey from fieldDef > arrayKey from context
1614
1783
  const targetArrayKey = fieldDef.arrayKey ?? arrayContext?.arrayKey;
@@ -1616,14 +1785,13 @@ function removeArrayItemButtonFieldMapper(fieldDef) {
1616
1785
  logger.warn(`removeArrayItem button "${fieldDef.key}" has no array context. ` +
1617
1786
  'Either place it inside an array field, or provide an explicit arrayKey property.');
1618
1787
  }
1619
- // Build base inputs (static, from field definition)
1620
- const baseInputs = buildBaseInputs(fieldDef);
1621
1788
  // Set default eventArgs for RemoveArrayItemEvent (arrayKey, index if inside array)
1622
1789
  // When outside array, only pass arrayKey (removes last by default)
1623
1790
  // User can override by providing eventArgs in field definition
1624
1791
  const defaultEventArgs = arrayContext ? ['$arrayKey', '$index'] : ['$arrayKey'];
1625
1792
  const eventArgs = 'eventArgs' in fieldDef && fieldDef.eventArgs !== undefined ? fieldDef.eventArgs : defaultEventArgs;
1626
1793
  return computed(() => {
1794
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1627
1795
  // Read signal value if index is a signal (supports differential updates)
1628
1796
  const getIndex = () => {
1629
1797
  if (!arrayContext)