@ng-forge/dynamic-forms-bootstrap 0.3.1 → 0.5.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,7 +1,7 @@
1
1
  import { AsyncPipe } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
3
  import { inject, input, computed, ChangeDetectionStrategy, Component, ElementRef, viewChild, effect, Directive, InjectionToken, afterRenderEffect, linkedSignal, model, isSignal } from '@angular/core';
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 { FormField } from '@angular/forms/signals';
6
6
  import { createResolvedErrorsSignal, shouldShowErrors, setupMetaTracking, isEqual, valueFieldMapper, optionsFieldMapper, checkboxFieldMapper, datepickerFieldMapper } from '@ng-forge/dynamic-forms/integration';
7
7
  import { explicitEffect } from 'ngxtension/explicit-effect';
@@ -78,8 +78,8 @@ class BsButtonFieldComponent {
78
78
  this.eventBus.dispatch(event);
79
79
  }
80
80
  }
81
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsButtonFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
82
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.8", type: BsButtonFieldComponent, isStandalone: true, selector: "df-bs-button", inputs: { key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, event: { classPropertyName: "event", publicName: "event", isSignal: true, isRequired: false, transformFunction: null }, eventArgs: { classPropertyName: "eventArgs", publicName: "eventArgs", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, eventContext: { classPropertyName: "eventContext", publicName: "eventContext", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "hidden": "hidden()" } }, ngImport: i0, template: `
81
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsButtonFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
82
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: BsButtonFieldComponent, isStandalone: true, selector: "df-bs-button", inputs: { key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, event: { classPropertyName: "event", publicName: "event", isSignal: true, isRequired: false, transformFunction: null }, eventArgs: { classPropertyName: "eventArgs", publicName: "eventArgs", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, eventContext: { classPropertyName: "eventContext", publicName: "eventContext", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "hidden() || null" } }, ngImport: i0, template: `
83
83
  @let buttonId = key() + '-button';
84
84
  <button
85
85
  [id]="buttonId"
@@ -94,13 +94,13 @@ class BsButtonFieldComponent {
94
94
  </button>
95
95
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
96
96
  }
97
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsButtonFieldComponent, decorators: [{
97
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsButtonFieldComponent, decorators: [{
98
98
  type: Component,
99
99
  args: [{ selector: 'df-bs-button', imports: [DynamicTextPipe, AsyncPipe], host: {
100
100
  '[id]': '`${key()}`',
101
101
  '[attr.data-testid]': 'key()',
102
102
  '[class]': 'className()',
103
- '[hidden]': 'hidden()',
103
+ '[attr.hidden]': 'hidden() || null',
104
104
  }, template: `
105
105
  @let buttonId = key() + '-button';
106
106
  <button
@@ -124,6 +124,29 @@ var bsButton_component = /*#__PURE__*/Object.freeze({
124
124
 
125
125
  // Public API - component
126
126
 
127
+ /**
128
+ * Creates a signal that computes the aria-describedby value based on errors and hint state.
129
+ * Errors take precedence over hints - when errors are displayed, the hint is hidden.
130
+ * Only the first error is displayed (single error ID, not indexed).
131
+ *
132
+ * @param errorsToDisplay Signal containing the array of errors currently being displayed
133
+ * @param errorId Signal containing the ID for the error element (single error only)
134
+ * @param hintId Signal containing the ID for the hint element
135
+ * @param hasHint Function that returns true if a hint is configured
136
+ * @returns Signal containing the aria-describedby value or null
137
+ */
138
+ function createAriaDescribedBySignal(errorsToDisplay, errorId, hintId, hasHint) {
139
+ return computed(() => {
140
+ if (errorsToDisplay().length > 0) {
141
+ return errorId();
142
+ }
143
+ if (hasHint()) {
144
+ return hintId();
145
+ }
146
+ return null;
147
+ });
148
+ }
149
+
127
150
  class BsCheckboxFieldComponent {
128
151
  elementRef = inject((ElementRef));
129
152
  field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
@@ -154,7 +177,7 @@ class BsCheckboxFieldComponent {
154
177
  // ─────────────────────────────────────────────────────────────────────────────
155
178
  // Accessibility
156
179
  // ─────────────────────────────────────────────────────────────────────────────
157
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
180
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
158
181
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
159
182
  ariaInvalid = computed(() => {
160
183
  const fieldState = this.field()();
@@ -163,21 +186,10 @@ class BsCheckboxFieldComponent {
163
186
  ariaRequired = computed(() => {
164
187
  return this.field()().required?.() === true ? true : null;
165
188
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
166
- ariaDescribedBy = computed(() => {
167
- const ids = [];
168
- if (this.props()?.helpText) {
169
- ids.push(this.helpTextId());
170
- }
171
- const errors = this.errorsToDisplay();
172
- errors.forEach((_, i) => {
173
- ids.push(`${this.errorId()}-${i}`);
174
- });
175
- return ids.length > 0 ? ids.join(' ') : null;
176
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
177
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
178
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsCheckboxFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, viewQueries: [{ propertyName: "checkboxInput", first: true, predicate: ["checkboxInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
179
- @let f = field(); @let checkboxId = key() + '-checkbox'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
180
- @let ariaDescribedBy = this.ariaDescribedBy();
189
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
190
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
191
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsCheckboxFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, viewQueries: [{ propertyName: "checkboxInput", first: true, predicate: ["checkboxInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
192
+ @let f = field(); @let checkboxId = key() + '-checkbox';
181
193
 
182
194
  <div
183
195
  class="form-check"
@@ -194,30 +206,26 @@ class BsCheckboxFieldComponent {
194
206
  class="form-check-input"
195
207
  [class.is-invalid]="f().invalid() && f().touched()"
196
208
  [attr.tabindex]="tabIndex()"
197
- [attr.aria-invalid]="ariaInvalid"
198
- [attr.aria-required]="ariaRequired"
199
- [attr.aria-describedby]="ariaDescribedBy"
209
+ [attr.aria-invalid]="ariaInvalid()"
210
+ [attr.aria-required]="ariaRequired()"
211
+ [attr.aria-describedby]="ariaDescribedBy()"
200
212
  />
201
213
  <label [for]="checkboxId" class="form-check-label">
202
214
  {{ label() | dynamicText | async }}
203
215
  </label>
204
216
  </div>
205
217
 
206
- @if (props()?.helpText; as helpText) {
207
- <div class="form-text" [id]="helpTextId()" [attr.hidden]="f().hidden() || null">
208
- {{ helpText | dynamicText | async }}
209
- </div>
210
- }
211
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
212
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
218
+ @if (errorsToDisplay()[0]; as error) {
219
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
220
+ } @else if (props()?.hint; as hint) {
221
+ <div class="form-text" [id]="hintId()" [attr.hidden]="f().hidden() || null">{{ hint | dynamicText | async }}</div>
213
222
  }
214
223
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
215
224
  }
216
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsCheckboxFieldComponent, decorators: [{
225
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsCheckboxFieldComponent, decorators: [{
217
226
  type: Component,
218
227
  args: [{ selector: 'df-bs-checkbox', imports: [FormField, DynamicTextPipe, AsyncPipe], template: `
219
- @let f = field(); @let checkboxId = key() + '-checkbox'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
220
- @let ariaDescribedBy = this.ariaDescribedBy();
228
+ @let f = field(); @let checkboxId = key() + '-checkbox';
221
229
 
222
230
  <div
223
231
  class="form-check"
@@ -234,22 +242,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
234
242
  class="form-check-input"
235
243
  [class.is-invalid]="f().invalid() && f().touched()"
236
244
  [attr.tabindex]="tabIndex()"
237
- [attr.aria-invalid]="ariaInvalid"
238
- [attr.aria-required]="ariaRequired"
239
- [attr.aria-describedby]="ariaDescribedBy"
245
+ [attr.aria-invalid]="ariaInvalid()"
246
+ [attr.aria-required]="ariaRequired()"
247
+ [attr.aria-describedby]="ariaDescribedBy()"
240
248
  />
241
249
  <label [for]="checkboxId" class="form-check-label">
242
250
  {{ label() | dynamicText | async }}
243
251
  </label>
244
252
  </div>
245
253
 
246
- @if (props()?.helpText; as helpText) {
247
- <div class="form-text" [id]="helpTextId()" [attr.hidden]="f().hidden() || null">
248
- {{ helpText | dynamicText | async }}
249
- </div>
250
- }
251
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
252
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
254
+ @if (errorsToDisplay()[0]; as error) {
255
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
256
+ } @else if (props()?.hint; as hint) {
257
+ <div class="form-text" [id]="hintId()" [attr.hidden]="f().hidden() || null">{{ hint | dynamicText | async }}</div>
253
258
  }
254
259
  `, host: {
255
260
  '[class]': 'className()',
@@ -299,10 +304,10 @@ class InputConstraintsDirective {
299
304
  nativeElement.removeAttribute('step');
300
305
  }
301
306
  });
302
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: InputConstraintsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
303
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.8", type: InputConstraintsDirective, isStandalone: true, selector: "[dfBsInputConstraints]", inputs: { dfMin: { classPropertyName: "dfMin", publicName: "dfMin", isSignal: true, isRequired: false, transformFunction: null }, dfMax: { classPropertyName: "dfMax", publicName: "dfMax", isSignal: true, isRequired: false, transformFunction: null }, dfStep: { classPropertyName: "dfStep", publicName: "dfStep", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
307
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: InputConstraintsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
308
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: InputConstraintsDirective, isStandalone: true, selector: "[dfBsInputConstraints]", inputs: { dfMin: { classPropertyName: "dfMin", publicName: "dfMin", isSignal: true, isRequired: false, transformFunction: null }, dfMax: { classPropertyName: "dfMax", publicName: "dfMax", isSignal: true, isRequired: false, transformFunction: null }, dfStep: { classPropertyName: "dfStep", publicName: "dfStep", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
304
309
  }
305
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: InputConstraintsDirective, decorators: [{
310
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: InputConstraintsDirective, decorators: [{
306
311
  type: Directive,
307
312
  args: [{
308
313
  selector: '[dfBsInputConstraints]',
@@ -342,7 +347,7 @@ class BsDatepickerFieldComponent {
342
347
  // ─────────────────────────────────────────────────────────────────────────────
343
348
  // Accessibility
344
349
  // ─────────────────────────────────────────────────────────────────────────────
345
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
350
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
346
351
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
347
352
  ariaInvalid = computed(() => {
348
353
  const fieldState = this.field()();
@@ -351,21 +356,10 @@ class BsDatepickerFieldComponent {
351
356
  ariaRequired = computed(() => {
352
357
  return this.field()().required?.() === true ? true : null;
353
358
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
354
- ariaDescribedBy = computed(() => {
355
- const ids = [];
356
- if (this.props()?.helpText) {
357
- ids.push(this.helpTextId());
358
- }
359
- const errors = this.errorsToDisplay();
360
- errors.forEach((_, i) => {
361
- ids.push(`${this.errorId()}-${i}`);
362
- });
363
- return ids.length > 0 ? ids.join(' ') : null;
364
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
365
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsDatepickerFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
366
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsDatepickerFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
367
- @let f = field(); @let p = props(); @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
368
- @let ariaDescribedBy = this.ariaDescribedBy(); @let inputId = key() + '-input';
359
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
360
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsDatepickerFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
361
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsDatepickerFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
362
+ @let f = field(); @let p = props(); @let inputId = key() + '-input';
369
363
  @if (p?.floatingLabel) {
370
364
  <!-- Floating label variant -->
371
365
  <div class="form-floating mb-3">
@@ -378,9 +372,9 @@ class BsDatepickerFieldComponent {
378
372
  [dfMin]="minAsString()"
379
373
  [dfMax]="maxAsString()"
380
374
  [attr.tabindex]="tabIndex()"
381
- [attr.aria-invalid]="ariaInvalid"
382
- [attr.aria-required]="ariaRequired"
383
- [attr.aria-describedby]="ariaDescribedBy"
375
+ [attr.aria-invalid]="ariaInvalid()"
376
+ [attr.aria-required]="ariaRequired()"
377
+ [attr.aria-describedby]="ariaDescribedBy()"
384
378
  class="form-control"
385
379
  [class.form-control-sm]="p?.size === 'sm'"
386
380
  [class.form-control-lg]="p?.size === 'lg'"
@@ -395,8 +389,8 @@ class BsDatepickerFieldComponent {
395
389
  {{ p?.validFeedback | dynamicText | async }}
396
390
  </div>
397
391
  }
398
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
399
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
392
+ @if (errorsToDisplay()[0]; as error) {
393
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
400
394
  }
401
395
  </div>
402
396
  } @else {
@@ -415,9 +409,9 @@ class BsDatepickerFieldComponent {
415
409
  [dfMin]="minAsString()"
416
410
  [dfMax]="maxAsString()"
417
411
  [attr.tabindex]="tabIndex()"
418
- [attr.aria-invalid]="ariaInvalid"
419
- [attr.aria-required]="ariaRequired"
420
- [attr.aria-describedby]="ariaDescribedBy"
412
+ [attr.aria-invalid]="ariaInvalid()"
413
+ [attr.aria-required]="ariaRequired()"
414
+ [attr.aria-describedby]="ariaDescribedBy()"
421
415
  class="form-control"
422
416
  [class.form-control-sm]="p?.size === 'sm'"
423
417
  [class.form-control-lg]="p?.size === 'lg'"
@@ -425,28 +419,24 @@ class BsDatepickerFieldComponent {
425
419
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
426
420
  />
427
421
 
428
- @if (p?.helpText) {
429
- <div class="form-text" [id]="helpTextId()">
430
- {{ p?.helpText | dynamicText | async }}
431
- </div>
432
- }
433
422
  @if (p?.validFeedback && f().valid() && f().touched()) {
434
423
  <div class="valid-feedback d-block">
435
424
  {{ p?.validFeedback | dynamicText | async }}
436
425
  </div>
437
426
  }
438
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
439
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
427
+ @if (errorsToDisplay()[0]; as error) {
428
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
429
+ } @else if (p?.hint) {
430
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
440
431
  }
441
432
  </div>
442
433
  }
443
434
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "directive", type: InputConstraintsDirective, selector: "[dfBsInputConstraints]", inputs: ["dfMin", "dfMax", "dfStep"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
444
435
  }
445
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsDatepickerFieldComponent, decorators: [{
436
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsDatepickerFieldComponent, decorators: [{
446
437
  type: Component,
447
438
  args: [{ selector: 'df-bs-datepicker', imports: [FormField, DynamicTextPipe, AsyncPipe, InputConstraintsDirective], template: `
448
- @let f = field(); @let p = props(); @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
449
- @let ariaDescribedBy = this.ariaDescribedBy(); @let inputId = key() + '-input';
439
+ @let f = field(); @let p = props(); @let inputId = key() + '-input';
450
440
  @if (p?.floatingLabel) {
451
441
  <!-- Floating label variant -->
452
442
  <div class="form-floating mb-3">
@@ -459,9 +449,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
459
449
  [dfMin]="minAsString()"
460
450
  [dfMax]="maxAsString()"
461
451
  [attr.tabindex]="tabIndex()"
462
- [attr.aria-invalid]="ariaInvalid"
463
- [attr.aria-required]="ariaRequired"
464
- [attr.aria-describedby]="ariaDescribedBy"
452
+ [attr.aria-invalid]="ariaInvalid()"
453
+ [attr.aria-required]="ariaRequired()"
454
+ [attr.aria-describedby]="ariaDescribedBy()"
465
455
  class="form-control"
466
456
  [class.form-control-sm]="p?.size === 'sm'"
467
457
  [class.form-control-lg]="p?.size === 'lg'"
@@ -476,8 +466,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
476
466
  {{ p?.validFeedback | dynamicText | async }}
477
467
  </div>
478
468
  }
479
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
480
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
469
+ @if (errorsToDisplay()[0]; as error) {
470
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
481
471
  }
482
472
  </div>
483
473
  } @else {
@@ -496,9 +486,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
496
486
  [dfMin]="minAsString()"
497
487
  [dfMax]="maxAsString()"
498
488
  [attr.tabindex]="tabIndex()"
499
- [attr.aria-invalid]="ariaInvalid"
500
- [attr.aria-required]="ariaRequired"
501
- [attr.aria-describedby]="ariaDescribedBy"
489
+ [attr.aria-invalid]="ariaInvalid()"
490
+ [attr.aria-required]="ariaRequired()"
491
+ [attr.aria-describedby]="ariaDescribedBy()"
502
492
  class="form-control"
503
493
  [class.form-control-sm]="p?.size === 'sm'"
504
494
  [class.form-control-lg]="p?.size === 'lg'"
@@ -506,18 +496,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
506
496
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
507
497
  />
508
498
 
509
- @if (p?.helpText) {
510
- <div class="form-text" [id]="helpTextId()">
511
- {{ p?.helpText | dynamicText | async }}
512
- </div>
513
- }
514
499
  @if (p?.validFeedback && f().valid() && f().touched()) {
515
500
  <div class="valid-feedback d-block">
516
501
  {{ p?.validFeedback | dynamicText | async }}
517
502
  </div>
518
503
  }
519
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
520
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
504
+ @if (errorsToDisplay()[0]; as error) {
505
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
506
+ } @else if (p?.hint) {
507
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
521
508
  }
522
509
  </div>
523
510
  }
@@ -613,8 +600,8 @@ class BsInputFieldComponent {
613
600
  // ─────────────────────────────────────────────────────────────────────────────
614
601
  // Accessibility
615
602
  // ─────────────────────────────────────────────────────────────────────────────
616
- /** Unique ID for the help text element, used for aria-describedby */
617
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
603
+ /** Unique ID for the hint element, used for aria-describedby */
604
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
618
605
  /** Base ID for error elements, used for aria-describedby */
619
606
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
620
607
  /** aria-invalid: true when field is invalid AND touched, false otherwise */
@@ -626,26 +613,12 @@ class BsInputFieldComponent {
626
613
  ariaRequired = computed(() => {
627
614
  return this.field()().required?.() === true ? true : null;
628
615
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
629
- /** aria-describedby: links to help text and error messages for screen readers */
630
- ariaDescribedBy = computed(() => {
631
- const ids = [];
632
- if (this.props()?.helpText) {
633
- ids.push(this.helpTextId());
634
- }
635
- const errors = this.errorsToDisplay();
636
- errors.forEach((_, i) => {
637
- ids.push(`${this.errorId()}-${i}`);
638
- });
639
- return ids.length > 0 ? ids.join(' ') : null;
640
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
641
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsInputFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
642
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsInputFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
643
- @let f = field(); @let p = props(); @let effectiveSize = this.effectiveSize();
644
- @let effectiveFloatingLabel = this.effectiveFloatingLabel();
645
- @let inputId = key() + '-input';
646
- @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
647
- @let ariaDescribedBy = this.ariaDescribedBy();
648
- @if (effectiveFloatingLabel) {
616
+ /** aria-describedby: links to hint and error messages for screen readers */
617
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
618
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsInputFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
619
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsInputFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
620
+ @let f = field(); @let p = props(); @let inputId = key() + '-input';
621
+ @if (effectiveFloatingLabel()) {
649
622
  <!-- Floating label variant -->
650
623
  <div class="form-floating mb-3">
651
624
  <input
@@ -655,12 +628,12 @@ class BsInputFieldComponent {
655
628
  [type]="p?.type ?? 'text'"
656
629
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
657
630
  [attr.tabindex]="tabIndex()"
658
- [attr.aria-invalid]="ariaInvalid"
659
- [attr.aria-required]="ariaRequired"
660
- [attr.aria-describedby]="ariaDescribedBy"
631
+ [attr.aria-invalid]="ariaInvalid()"
632
+ [attr.aria-required]="ariaRequired()"
633
+ [attr.aria-describedby]="ariaDescribedBy()"
661
634
  class="form-control"
662
- [class.form-control-sm]="effectiveSize === 'sm'"
663
- [class.form-control-lg]="effectiveSize === 'lg'"
635
+ [class.form-control-sm]="effectiveSize() === 'sm'"
636
+ [class.form-control-lg]="effectiveSize() === 'lg'"
664
637
  [class.form-control-plaintext]="p?.plaintext"
665
638
  [class.is-invalid]="f().invalid() && f().touched()"
666
639
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
@@ -673,8 +646,8 @@ class BsInputFieldComponent {
673
646
  {{ p?.validFeedback | dynamicText | async }}
674
647
  </div>
675
648
  }
676
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
677
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
649
+ @if (errorsToDisplay()[0]; as error) {
650
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
678
651
  }
679
652
  </div>
680
653
  } @else {
@@ -690,42 +663,35 @@ class BsInputFieldComponent {
690
663
  [type]="p?.type ?? 'text'"
691
664
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
692
665
  [attr.tabindex]="tabIndex()"
693
- [attr.aria-invalid]="ariaInvalid"
694
- [attr.aria-required]="ariaRequired"
695
- [attr.aria-describedby]="ariaDescribedBy"
666
+ [attr.aria-invalid]="ariaInvalid()"
667
+ [attr.aria-required]="ariaRequired()"
668
+ [attr.aria-describedby]="ariaDescribedBy()"
696
669
  class="form-control"
697
- [class.form-control-sm]="effectiveSize === 'sm'"
698
- [class.form-control-lg]="effectiveSize === 'lg'"
670
+ [class.form-control-sm]="effectiveSize() === 'sm'"
671
+ [class.form-control-lg]="effectiveSize() === 'lg'"
699
672
  [class.form-control-plaintext]="p?.plaintext"
700
673
  [class.is-invalid]="f().invalid() && f().touched()"
701
674
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
702
675
  />
703
- @if (p?.helpText) {
704
- <div class="form-text" [id]="helpTextId()">
705
- {{ p?.helpText | dynamicText | async }}
706
- </div>
707
- }
708
676
  @if (p?.validFeedback && f().valid() && f().touched()) {
709
677
  <div class="valid-feedback d-block">
710
678
  {{ p?.validFeedback | dynamicText | async }}
711
679
  </div>
712
680
  }
713
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
714
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
681
+ @if (errorsToDisplay()[0]; as error) {
682
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
683
+ } @else if (p?.hint) {
684
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
715
685
  }
716
686
  </div>
717
687
  }
718
688
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
719
689
  }
720
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsInputFieldComponent, decorators: [{
690
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsInputFieldComponent, decorators: [{
721
691
  type: Component,
722
692
  args: [{ selector: 'df-bs-input', imports: [FormField, DynamicTextPipe, AsyncPipe], template: `
723
- @let f = field(); @let p = props(); @let effectiveSize = this.effectiveSize();
724
- @let effectiveFloatingLabel = this.effectiveFloatingLabel();
725
- @let inputId = key() + '-input';
726
- @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
727
- @let ariaDescribedBy = this.ariaDescribedBy();
728
- @if (effectiveFloatingLabel) {
693
+ @let f = field(); @let p = props(); @let inputId = key() + '-input';
694
+ @if (effectiveFloatingLabel()) {
729
695
  <!-- Floating label variant -->
730
696
  <div class="form-floating mb-3">
731
697
  <input
@@ -735,12 +701,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
735
701
  [type]="p?.type ?? 'text'"
736
702
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
737
703
  [attr.tabindex]="tabIndex()"
738
- [attr.aria-invalid]="ariaInvalid"
739
- [attr.aria-required]="ariaRequired"
740
- [attr.aria-describedby]="ariaDescribedBy"
704
+ [attr.aria-invalid]="ariaInvalid()"
705
+ [attr.aria-required]="ariaRequired()"
706
+ [attr.aria-describedby]="ariaDescribedBy()"
741
707
  class="form-control"
742
- [class.form-control-sm]="effectiveSize === 'sm'"
743
- [class.form-control-lg]="effectiveSize === 'lg'"
708
+ [class.form-control-sm]="effectiveSize() === 'sm'"
709
+ [class.form-control-lg]="effectiveSize() === 'lg'"
744
710
  [class.form-control-plaintext]="p?.plaintext"
745
711
  [class.is-invalid]="f().invalid() && f().touched()"
746
712
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
@@ -753,8 +719,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
753
719
  {{ p?.validFeedback | dynamicText | async }}
754
720
  </div>
755
721
  }
756
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
757
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
722
+ @if (errorsToDisplay()[0]; as error) {
723
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
758
724
  }
759
725
  </div>
760
726
  } @else {
@@ -770,28 +736,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
770
736
  [type]="p?.type ?? 'text'"
771
737
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
772
738
  [attr.tabindex]="tabIndex()"
773
- [attr.aria-invalid]="ariaInvalid"
774
- [attr.aria-required]="ariaRequired"
775
- [attr.aria-describedby]="ariaDescribedBy"
739
+ [attr.aria-invalid]="ariaInvalid()"
740
+ [attr.aria-required]="ariaRequired()"
741
+ [attr.aria-describedby]="ariaDescribedBy()"
776
742
  class="form-control"
777
- [class.form-control-sm]="effectiveSize === 'sm'"
778
- [class.form-control-lg]="effectiveSize === 'lg'"
743
+ [class.form-control-sm]="effectiveSize() === 'sm'"
744
+ [class.form-control-lg]="effectiveSize() === 'lg'"
779
745
  [class.form-control-plaintext]="p?.plaintext"
780
746
  [class.is-invalid]="f().invalid() && f().touched()"
781
747
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
782
748
  />
783
- @if (p?.helpText) {
784
- <div class="form-text" [id]="helpTextId()">
785
- {{ p?.helpText | dynamicText | async }}
786
- </div>
787
- }
788
749
  @if (p?.validFeedback && f().valid() && f().touched()) {
789
750
  <div class="valid-feedback d-block">
790
751
  {{ p?.validFeedback | dynamicText | async }}
791
752
  </div>
792
753
  }
793
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
794
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
754
+ @if (errorsToDisplay()[0]; as error) {
755
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
756
+ } @else if (p?.hint) {
757
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
795
758
  }
796
759
  </div>
797
760
  }
@@ -824,6 +787,14 @@ class BsMultiCheckboxFieldComponent {
824
787
  resolvedErrors = createResolvedErrorsSignal(this.field, this.validationMessages, this.defaultValidationMessages);
825
788
  showErrors = shouldShowErrors(this.field);
826
789
  errorsToDisplay = computed(() => (this.showErrors() ? this.resolvedErrors() : []), ...(ngDevMode ? [{ debugName: "errorsToDisplay" }] : []));
790
+ /** Computed map of checked option values for O(1) lookup in template */
791
+ checkedValuesMap = computed(() => {
792
+ const map = {};
793
+ for (const opt of this.valueViewModel()) {
794
+ map[String(opt.value)] = true;
795
+ }
796
+ return map;
797
+ }, ...(ngDevMode ? [{ debugName: "checkedValuesMap" }] : []));
827
798
  valueViewModel = linkedSignal(() => {
828
799
  const currentValues = this.field()().value();
829
800
  return this.options().filter((option) => currentValues.includes(option.value));
@@ -845,23 +816,23 @@ class BsMultiCheckboxFieldComponent {
845
816
  }
846
817
  });
847
818
  }
848
- onCheckboxChange(option, checked) {
819
+ onCheckboxChange(option, event) {
820
+ const checked = event.target.checked;
849
821
  this.valueViewModel.update((currentOptions) => {
850
822
  if (checked) {
851
- return currentOptions.some((opt) => opt.value === option.value) ? currentOptions : [...currentOptions, option];
823
+ return currentOptions.some((opt) => opt.value === option.value)
824
+ ? currentOptions
825
+ : [...currentOptions, option];
852
826
  }
853
827
  else {
854
828
  return currentOptions.filter((opt) => opt.value !== option.value);
855
829
  }
856
830
  });
857
831
  }
858
- isChecked(option) {
859
- return this.valueViewModel().some((opt) => opt.value === option.value);
860
- }
861
832
  // ─────────────────────────────────────────────────────────────────────────────
862
833
  // Accessibility
863
834
  // ─────────────────────────────────────────────────────────────────────────────
864
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
835
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
865
836
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
866
837
  ariaInvalid = computed(() => {
867
838
  const fieldState = this.field()();
@@ -870,22 +841,11 @@ class BsMultiCheckboxFieldComponent {
870
841
  ariaRequired = computed(() => {
871
842
  return this.field()().required?.() === true ? true : null;
872
843
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
873
- ariaDescribedBy = computed(() => {
874
- const ids = [];
875
- if (this.props()?.helpText) {
876
- ids.push(this.helpTextId());
877
- }
878
- const errors = this.errorsToDisplay();
879
- errors.forEach((_, i) => {
880
- ids.push(`${this.errorId()}-${i}`);
881
- });
882
- return ids.length > 0 ? ids.join(' ') : null;
883
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
884
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsMultiCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
885
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsMultiCheckboxFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className() || \"\"", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
844
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
845
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsMultiCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
846
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsMultiCheckboxFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className() || \"\"", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
886
847
  @let f = field();
887
- @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
888
- @let ariaDescribedBy = this.ariaDescribedBy();
848
+ @let checked = checkedValuesMap();
889
849
  @if (label(); as label) {
890
850
  <div class="form-label">{{ label | dynamicText | async }}</div>
891
851
  }
@@ -901,15 +861,15 @@ class BsMultiCheckboxFieldComponent {
901
861
  <input
902
862
  type="checkbox"
903
863
  [id]="key() + '_' + i"
904
- [checked]="isChecked(option)"
864
+ [checked]="checked['' + option.value]"
905
865
  [disabled]="f().disabled() || option.disabled"
906
- (change)="onCheckboxChange(option, $any($event.target).checked)"
866
+ (change)="onCheckboxChange(option, $event)"
907
867
  class="form-check-input"
908
868
  [class.is-invalid]="f().invalid() && f().touched()"
909
869
  [attr.tabindex]="tabIndex()"
910
- [attr.aria-invalid]="ariaInvalid"
911
- [attr.aria-required]="ariaRequired"
912
- [attr.aria-describedby]="ariaDescribedBy"
870
+ [attr.aria-invalid]="ariaInvalid()"
871
+ [attr.aria-required]="ariaRequired()"
872
+ [attr.aria-describedby]="ariaDescribedBy()"
913
873
  />
914
874
  <label [for]="key() + '_' + i" class="form-check-label">
915
875
  {{ option.label | dynamicText | async }}
@@ -918,22 +878,18 @@ class BsMultiCheckboxFieldComponent {
918
878
  }
919
879
  </div>
920
880
 
921
- @if (props()?.helpText; as helpText) {
922
- <div class="form-text" [id]="helpTextId()">
923
- {{ helpText | dynamicText | async }}
924
- </div>
925
- }
926
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
927
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
881
+ @if (errorsToDisplay()[0]; as error) {
882
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
883
+ } @else if (props()?.hint; as hint) {
884
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
928
885
  }
929
886
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host{display:block}.checkbox-group{margin-bottom:.5rem}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
930
887
  }
931
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsMultiCheckboxFieldComponent, decorators: [{
888
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsMultiCheckboxFieldComponent, decorators: [{
932
889
  type: Component,
933
890
  args: [{ selector: 'df-bs-multi-checkbox', imports: [DynamicTextPipe, AsyncPipe], template: `
934
891
  @let f = field();
935
- @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
936
- @let ariaDescribedBy = this.ariaDescribedBy();
892
+ @let checked = checkedValuesMap();
937
893
  @if (label(); as label) {
938
894
  <div class="form-label">{{ label | dynamicText | async }}</div>
939
895
  }
@@ -949,15 +905,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
949
905
  <input
950
906
  type="checkbox"
951
907
  [id]="key() + '_' + i"
952
- [checked]="isChecked(option)"
908
+ [checked]="checked['' + option.value]"
953
909
  [disabled]="f().disabled() || option.disabled"
954
- (change)="onCheckboxChange(option, $any($event.target).checked)"
910
+ (change)="onCheckboxChange(option, $event)"
955
911
  class="form-check-input"
956
912
  [class.is-invalid]="f().invalid() && f().touched()"
957
913
  [attr.tabindex]="tabIndex()"
958
- [attr.aria-invalid]="ariaInvalid"
959
- [attr.aria-required]="ariaRequired"
960
- [attr.aria-describedby]="ariaDescribedBy"
914
+ [attr.aria-invalid]="ariaInvalid()"
915
+ [attr.aria-required]="ariaRequired()"
916
+ [attr.aria-describedby]="ariaDescribedBy()"
961
917
  />
962
918
  <label [for]="key() + '_' + i" class="form-check-label">
963
919
  {{ option.label | dynamicText | async }}
@@ -966,13 +922,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
966
922
  }
967
923
  </div>
968
924
 
969
- @if (props()?.helpText; as helpText) {
970
- <div class="form-text" [id]="helpTextId()">
971
- {{ helpText | dynamicText | async }}
972
- </div>
973
- }
974
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
975
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
925
+ @if (errorsToDisplay()[0]; as error) {
926
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
927
+ } @else if (props()?.hint; as hint) {
928
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
976
929
  }
977
930
  `, host: {
978
931
  '[class]': 'className() || ""',
@@ -1013,10 +966,9 @@ class BsRadioGroupComponent {
1013
966
  this.value.set(newValue);
1014
967
  }
1015
968
  }
1016
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsRadioGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1017
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsRadioGroupComponent, isStandalone: true, selector: "df-bs-radio-group", inputs: { value: { classPropertyName: "value", publicName: "value", 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 }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", 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: { value: "valueChange" }, ngImport: i0, template: `
969
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsRadioGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
970
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsRadioGroupComponent, isStandalone: true, selector: "df-bs-radio-group", inputs: { value: { classPropertyName: "value", publicName: "value", 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 }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", 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: { value: "valueChange" }, ngImport: i0, template: `
1018
971
  @let props = properties();
1019
- @let ariaDescribedBy = this.ariaDescribedBy();
1020
972
  @if (props?.buttonGroup) {
1021
973
  <div class="btn-group" role="group" [attr.aria-label]="label() | dynamicText | async">
1022
974
  @for (option of options(); track option.value; let i = $index) {
@@ -1027,7 +979,7 @@ class BsRadioGroupComponent {
1027
979
  [checked]="value() === option.value"
1028
980
  (change)="onRadioChange(option.value)"
1029
981
  [disabled]="disabled() || option.disabled || false"
1030
- [attr.aria-describedby]="ariaDescribedBy"
982
+ [attr.aria-describedby]="ariaDescribedBy()"
1031
983
  class="btn-check"
1032
984
  [id]="name() + '_' + i"
1033
985
  autocomplete="off"
@@ -1052,7 +1004,7 @@ class BsRadioGroupComponent {
1052
1004
  [checked]="value() === option.value"
1053
1005
  (change)="onRadioChange(option.value)"
1054
1006
  [disabled]="disabled() || option.disabled || false"
1055
- [attr.aria-describedby]="ariaDescribedBy"
1007
+ [attr.aria-describedby]="ariaDescribedBy()"
1056
1008
  class="form-check-input"
1057
1009
  [id]="name() + '_' + i"
1058
1010
  />
@@ -1064,11 +1016,10 @@ class BsRadioGroupComponent {
1064
1016
  }
1065
1017
  `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1066
1018
  }
1067
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsRadioGroupComponent, decorators: [{
1019
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsRadioGroupComponent, decorators: [{
1068
1020
  type: Component,
1069
1021
  args: [{ selector: 'df-bs-radio-group', imports: [DynamicTextPipe, AsyncPipe], template: `
1070
1022
  @let props = properties();
1071
- @let ariaDescribedBy = this.ariaDescribedBy();
1072
1023
  @if (props?.buttonGroup) {
1073
1024
  <div class="btn-group" role="group" [attr.aria-label]="label() | dynamicText | async">
1074
1025
  @for (option of options(); track option.value; let i = $index) {
@@ -1079,7 +1030,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1079
1030
  [checked]="value() === option.value"
1080
1031
  (change)="onRadioChange(option.value)"
1081
1032
  [disabled]="disabled() || option.disabled || false"
1082
- [attr.aria-describedby]="ariaDescribedBy"
1033
+ [attr.aria-describedby]="ariaDescribedBy()"
1083
1034
  class="btn-check"
1084
1035
  [id]="name() + '_' + i"
1085
1036
  autocomplete="off"
@@ -1104,7 +1055,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1104
1055
  [checked]="value() === option.value"
1105
1056
  (change)="onRadioChange(option.value)"
1106
1057
  [disabled]="disabled() || option.disabled || false"
1107
- [attr.aria-describedby]="ariaDescribedBy"
1058
+ [attr.aria-describedby]="ariaDescribedBy()"
1108
1059
  class="form-check-input"
1109
1060
  [id]="name() + '_' + i"
1110
1061
  />
@@ -1142,7 +1093,7 @@ class BsRadioFieldComponent {
1142
1093
  // ─────────────────────────────────────────────────────────────────────────────
1143
1094
  // Accessibility
1144
1095
  // ─────────────────────────────────────────────────────────────────────────────
1145
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
1096
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1146
1097
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1147
1098
  ariaInvalid = computed(() => {
1148
1099
  const fieldState = this.field()();
@@ -1151,21 +1102,10 @@ class BsRadioFieldComponent {
1151
1102
  ariaRequired = computed(() => {
1152
1103
  return this.field()().required?.() === true ? true : null;
1153
1104
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1154
- ariaDescribedBy = computed(() => {
1155
- const ids = [];
1156
- if (this.props()?.helpText) {
1157
- ids.push(this.helpTextId());
1158
- }
1159
- const errors = this.errorsToDisplay();
1160
- errors.forEach((_, i) => {
1161
- ids.push(`${this.errorId()}-${i}`);
1162
- });
1163
- return ids.length > 0 ? ids.join(' ') : null;
1164
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1165
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsRadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1166
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsRadioFieldComponent, isStandalone: true, selector: "df-bs-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: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1105
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1106
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsRadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1107
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsRadioFieldComponent, isStandalone: true, selector: "df-bs-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: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1167
1108
  @let f = field();
1168
- @let ariaDescribedBy = this.ariaDescribedBy();
1169
1109
 
1170
1110
  <div class="mb-3">
1171
1111
  @if (label(); as label) {
@@ -1173,27 +1113,25 @@ class BsRadioFieldComponent {
1173
1113
  }
1174
1114
 
1175
1115
  <df-bs-radio-group
1176
- [formField]="$any(f)"
1116
+ [formField]="f"
1177
1117
  [label]="label()"
1178
1118
  [options]="options()"
1179
1119
  [properties]="props()"
1180
- [ariaDescribedBy]="ariaDescribedBy"
1120
+ [ariaDescribedBy]="ariaDescribedBy()"
1181
1121
  />
1182
1122
 
1183
- @if (props()?.helpText; as helpText) {
1184
- <div class="form-text" [id]="helpTextId()">{{ helpText | dynamicText | async }}</div>
1185
- }
1186
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1187
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1123
+ @if (errorsToDisplay()[0]; as error) {
1124
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1125
+ } @else if (props()?.hint; as hint) {
1126
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1188
1127
  }
1189
1128
  </div>
1190
1129
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: BsRadioGroupComponent, selector: "df-bs-radio-group", inputs: ["value", "disabled", "readonly", "name", "label", "options", "properties", "meta", "ariaDescribedBy"], outputs: ["valueChange"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1191
1130
  }
1192
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsRadioFieldComponent, decorators: [{
1131
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsRadioFieldComponent, decorators: [{
1193
1132
  type: Component,
1194
1133
  args: [{ selector: 'df-bs-radio', imports: [BsRadioGroupComponent, FormField, DynamicTextPipe, AsyncPipe], template: `
1195
1134
  @let f = field();
1196
- @let ariaDescribedBy = this.ariaDescribedBy();
1197
1135
 
1198
1136
  <div class="mb-3">
1199
1137
  @if (label(); as label) {
@@ -1201,18 +1139,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1201
1139
  }
1202
1140
 
1203
1141
  <df-bs-radio-group
1204
- [formField]="$any(f)"
1142
+ [formField]="f"
1205
1143
  [label]="label()"
1206
1144
  [options]="options()"
1207
1145
  [properties]="props()"
1208
- [ariaDescribedBy]="ariaDescribedBy"
1146
+ [ariaDescribedBy]="ariaDescribedBy()"
1209
1147
  />
1210
1148
 
1211
- @if (props()?.helpText; as helpText) {
1212
- <div class="form-text" [id]="helpTextId()">{{ helpText | dynamicText | async }}</div>
1213
- }
1214
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1215
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1149
+ @if (errorsToDisplay()[0]; as error) {
1150
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1151
+ } @else if (props()?.hint; as hint) {
1152
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1216
1153
  }
1217
1154
  </div>
1218
1155
  `, changeDetection: ChangeDetectionStrategy.OnPush, host: {
@@ -1258,7 +1195,7 @@ class BsSelectFieldComponent {
1258
1195
  // ─────────────────────────────────────────────────────────────────────────────
1259
1196
  // Accessibility
1260
1197
  // ─────────────────────────────────────────────────────────────────────────────
1261
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
1198
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1262
1199
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1263
1200
  ariaInvalid = computed(() => {
1264
1201
  const fieldState = this.field()();
@@ -1267,21 +1204,10 @@ class BsSelectFieldComponent {
1267
1204
  ariaRequired = computed(() => {
1268
1205
  return this.field()().required?.() === true ? true : null;
1269
1206
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1270
- ariaDescribedBy = computed(() => {
1271
- const ids = [];
1272
- if (this.props()?.helpText) {
1273
- ids.push(this.helpTextId());
1274
- }
1275
- const errors = this.errorsToDisplay();
1276
- errors.forEach((_, i) => {
1277
- ids.push(`${this.errorId()}-${i}`);
1278
- });
1279
- return ids.length > 0 ? ids.join(' ') : null;
1280
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1281
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1282
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsSelectFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1283
- @let f = field(); @let selectId = key() + '-select'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1284
- @let ariaDescribedBy = this.ariaDescribedBy();
1207
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1208
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1209
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsSelectFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1210
+ @let f = field(); @let selectId = key() + '-select';
1285
1211
 
1286
1212
  <div class="mb-3">
1287
1213
  @if (label(); as label) {
@@ -1296,9 +1222,9 @@ class BsSelectFieldComponent {
1296
1222
  [class.is-invalid]="f().invalid() && f().touched()"
1297
1223
  [multiple]="props()?.multiple || false"
1298
1224
  [size]="props()?.htmlSize"
1299
- [attr.aria-invalid]="ariaInvalid"
1300
- [attr.aria-required]="ariaRequired"
1301
- [attr.aria-describedby]="ariaDescribedBy"
1225
+ [attr.aria-invalid]="ariaInvalid()"
1226
+ [attr.aria-required]="ariaRequired()"
1227
+ [attr.aria-describedby]="ariaDescribedBy()"
1302
1228
  >
1303
1229
  @if (placeholder(); as placeholder) {
1304
1230
  <option value="" disabled [selected]="!f().value()">{{ placeholder | dynamicText | async }}</option>
@@ -1310,20 +1236,18 @@ class BsSelectFieldComponent {
1310
1236
  }
1311
1237
  </select>
1312
1238
 
1313
- @if (props()?.helpText; as helpText) {
1314
- <div class="form-text" [id]="helpTextId()">{{ helpText | dynamicText | async }}</div>
1315
- }
1316
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1317
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1239
+ @if (errorsToDisplay()[0]; as error) {
1240
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1241
+ } @else if (props()?.hint; as hint) {
1242
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1318
1243
  }
1319
1244
  </div>
1320
1245
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1321
1246
  }
1322
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsSelectFieldComponent, decorators: [{
1247
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsSelectFieldComponent, decorators: [{
1323
1248
  type: Component,
1324
1249
  args: [{ selector: 'df-bs-select', imports: [FormField, DynamicTextPipe, AsyncPipe], template: `
1325
- @let f = field(); @let selectId = key() + '-select'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1326
- @let ariaDescribedBy = this.ariaDescribedBy();
1250
+ @let f = field(); @let selectId = key() + '-select';
1327
1251
 
1328
1252
  <div class="mb-3">
1329
1253
  @if (label(); as label) {
@@ -1338,9 +1262,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1338
1262
  [class.is-invalid]="f().invalid() && f().touched()"
1339
1263
  [multiple]="props()?.multiple || false"
1340
1264
  [size]="props()?.htmlSize"
1341
- [attr.aria-invalid]="ariaInvalid"
1342
- [attr.aria-required]="ariaRequired"
1343
- [attr.aria-describedby]="ariaDescribedBy"
1265
+ [attr.aria-invalid]="ariaInvalid()"
1266
+ [attr.aria-required]="ariaRequired()"
1267
+ [attr.aria-describedby]="ariaDescribedBy()"
1344
1268
  >
1345
1269
  @if (placeholder(); as placeholder) {
1346
1270
  <option value="" disabled [selected]="!f().value()">{{ placeholder | dynamicText | async }}</option>
@@ -1352,11 +1276,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1352
1276
  }
1353
1277
  </select>
1354
1278
 
1355
- @if (props()?.helpText; as helpText) {
1356
- <div class="form-text" [id]="helpTextId()">{{ helpText | dynamicText | async }}</div>
1357
- }
1358
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1359
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1279
+ @if (errorsToDisplay()[0]; as error) {
1280
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1281
+ } @else if (props()?.hint; as hint) {
1282
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1360
1283
  }
1361
1284
  </div>
1362
1285
  `, changeDetection: ChangeDetectionStrategy.OnPush, host: {
@@ -1396,7 +1319,7 @@ class BsSliderFieldComponent {
1396
1319
  // ─────────────────────────────────────────────────────────────────────────────
1397
1320
  // Accessibility
1398
1321
  // ─────────────────────────────────────────────────────────────────────────────
1399
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
1322
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1400
1323
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1401
1324
  ariaInvalid = computed(() => {
1402
1325
  const fieldState = this.field()();
@@ -1405,21 +1328,10 @@ class BsSliderFieldComponent {
1405
1328
  ariaRequired = computed(() => {
1406
1329
  return this.field()().required?.() === true ? true : null;
1407
1330
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1408
- ariaDescribedBy = computed(() => {
1409
- const ids = [];
1410
- if (this.props()?.helpText) {
1411
- ids.push(this.helpTextId());
1412
- }
1413
- const errors = this.errorsToDisplay();
1414
- errors.forEach((_, i) => {
1415
- ids.push(`${this.errorId()}-${i}`);
1416
- });
1417
- return ids.length > 0 ? ids.join(' ') : null;
1418
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1419
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsSliderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1420
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsSliderFieldComponent, isStandalone: true, selector: "df-bs-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 }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", 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 }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1421
- @let f = field(); @let inputId = key() + '-input'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1422
- @let ariaDescribedBy = this.ariaDescribedBy();
1331
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1332
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsSliderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1333
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsSliderFieldComponent, isStandalone: true, selector: "df-bs-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 }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", 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 }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1334
+ @let f = field(); @let inputId = key() + '-input';
1423
1335
 
1424
1336
  <div class="mb-3">
1425
1337
  @if (label(); as label) {
@@ -1440,28 +1352,24 @@ class BsSliderFieldComponent {
1440
1352
  [dfMax]="props()?.max ?? max()"
1441
1353
  [dfStep]="props()?.step ?? step()"
1442
1354
  [attr.tabindex]="tabIndex()"
1443
- [attr.aria-invalid]="ariaInvalid"
1444
- [attr.aria-required]="ariaRequired"
1445
- [attr.aria-describedby]="ariaDescribedBy"
1355
+ [attr.aria-invalid]="ariaInvalid()"
1356
+ [attr.aria-required]="ariaRequired()"
1357
+ [attr.aria-describedby]="ariaDescribedBy()"
1446
1358
  class="form-range"
1447
1359
  />
1448
1360
 
1449
- @if (props()?.helpText; as helpText) {
1450
- <div class="form-text" [id]="helpTextId()">
1451
- {{ helpText | dynamicText | async }}
1452
- </div>
1453
- }
1454
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1455
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1361
+ @if (errorsToDisplay()[0]; as error) {
1362
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1363
+ } @else if (props()?.hint; as hint) {
1364
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1456
1365
  }
1457
1366
  </div>
1458
1367
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "directive", type: InputConstraintsDirective, selector: "[dfBsInputConstraints]", inputs: ["dfMin", "dfMax", "dfStep"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1459
1368
  }
1460
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsSliderFieldComponent, decorators: [{
1369
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsSliderFieldComponent, decorators: [{
1461
1370
  type: Component,
1462
1371
  args: [{ selector: 'df-bs-slider', imports: [FormField, DynamicTextPipe, AsyncPipe, InputConstraintsDirective], template: `
1463
- @let f = field(); @let inputId = key() + '-input'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1464
- @let ariaDescribedBy = this.ariaDescribedBy();
1372
+ @let f = field(); @let inputId = key() + '-input';
1465
1373
 
1466
1374
  <div class="mb-3">
1467
1375
  @if (label(); as label) {
@@ -1482,19 +1390,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1482
1390
  [dfMax]="props()?.max ?? max()"
1483
1391
  [dfStep]="props()?.step ?? step()"
1484
1392
  [attr.tabindex]="tabIndex()"
1485
- [attr.aria-invalid]="ariaInvalid"
1486
- [attr.aria-required]="ariaRequired"
1487
- [attr.aria-describedby]="ariaDescribedBy"
1393
+ [attr.aria-invalid]="ariaInvalid()"
1394
+ [attr.aria-required]="ariaRequired()"
1395
+ [attr.aria-describedby]="ariaDescribedBy()"
1488
1396
  class="form-range"
1489
1397
  />
1490
1398
 
1491
- @if (props()?.helpText; as helpText) {
1492
- <div class="form-text" [id]="helpTextId()">
1493
- {{ helpText | dynamicText | async }}
1494
- </div>
1495
- }
1496
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1497
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1399
+ @if (errorsToDisplay()[0]; as error) {
1400
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1401
+ } @else if (props()?.hint; as hint) {
1402
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1498
1403
  }
1499
1404
  </div>
1500
1405
  `, host: {
@@ -1531,7 +1436,7 @@ class BsTextareaFieldComponent {
1531
1436
  // ─────────────────────────────────────────────────────────────────────────────
1532
1437
  // Accessibility
1533
1438
  // ─────────────────────────────────────────────────────────────────────────────
1534
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
1439
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1535
1440
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1536
1441
  ariaInvalid = computed(() => {
1537
1442
  const fieldState = this.field()();
@@ -1540,21 +1445,10 @@ class BsTextareaFieldComponent {
1540
1445
  ariaRequired = computed(() => {
1541
1446
  return this.field()().required?.() === true ? true : null;
1542
1447
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1543
- ariaDescribedBy = computed(() => {
1544
- const ids = [];
1545
- if (this.props()?.helpText) {
1546
- ids.push(this.helpTextId());
1547
- }
1548
- const errors = this.errorsToDisplay();
1549
- errors.forEach((_, i) => {
1550
- ids.push(`${this.errorId()}-${i}`);
1551
- });
1552
- return ids.length > 0 ? ids.join(' ') : null;
1553
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1554
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsTextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1555
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsTextareaFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1556
- @let f = field(); @let p = props(); @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1557
- @let ariaDescribedBy = this.ariaDescribedBy(); @let textareaId = key() + '-textarea';
1448
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1449
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsTextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1450
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsTextareaFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1451
+ @let f = field(); @let p = props(); @let textareaId = key() + '-textarea';
1558
1452
  @if (p?.floatingLabel) {
1559
1453
  <!-- Floating label variant -->
1560
1454
  <div class="form-floating mb-3">
@@ -1563,9 +1457,9 @@ class BsTextareaFieldComponent {
1563
1457
  [id]="textareaId"
1564
1458
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
1565
1459
  [attr.tabindex]="tabIndex()"
1566
- [attr.aria-invalid]="ariaInvalid"
1567
- [attr.aria-required]="ariaRequired"
1568
- [attr.aria-describedby]="ariaDescribedBy"
1460
+ [attr.aria-invalid]="ariaInvalid()"
1461
+ [attr.aria-required]="ariaRequired()"
1462
+ [attr.aria-describedby]="ariaDescribedBy()"
1569
1463
  class="form-control"
1570
1464
  [class.form-control-sm]="p?.size === 'sm'"
1571
1465
  [class.form-control-lg]="p?.size === 'lg'"
@@ -1581,8 +1475,8 @@ class BsTextareaFieldComponent {
1581
1475
  {{ p?.validFeedback | dynamicText | async }}
1582
1476
  </div>
1583
1477
  }
1584
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1585
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1478
+ @if (errorsToDisplay()[0]; as error) {
1479
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1586
1480
  }
1587
1481
  </div>
1588
1482
  } @else {
@@ -1597,9 +1491,9 @@ class BsTextareaFieldComponent {
1597
1491
  [id]="textareaId"
1598
1492
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
1599
1493
  [attr.tabindex]="tabIndex()"
1600
- [attr.aria-invalid]="ariaInvalid"
1601
- [attr.aria-required]="ariaRequired"
1602
- [attr.aria-describedby]="ariaDescribedBy"
1494
+ [attr.aria-invalid]="ariaInvalid()"
1495
+ [attr.aria-required]="ariaRequired()"
1496
+ [attr.aria-describedby]="ariaDescribedBy()"
1603
1497
  class="form-control"
1604
1498
  [class.form-control-sm]="p?.size === 'sm'"
1605
1499
  [class.form-control-lg]="p?.size === 'lg'"
@@ -1607,28 +1501,24 @@ class BsTextareaFieldComponent {
1607
1501
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
1608
1502
  ></textarea>
1609
1503
 
1610
- @if (p?.helpText) {
1611
- <div class="form-text" [id]="helpTextId()">
1612
- {{ p?.helpText | dynamicText | async }}
1613
- </div>
1614
- }
1615
1504
  @if (p?.validFeedback && f().valid() && f().touched()) {
1616
1505
  <div class="valid-feedback d-block">
1617
1506
  {{ p?.validFeedback | dynamicText | async }}
1618
1507
  </div>
1619
1508
  }
1620
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1621
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1509
+ @if (errorsToDisplay()[0]; as error) {
1510
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1511
+ } @else if (p?.hint) {
1512
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
1622
1513
  }
1623
1514
  </div>
1624
1515
  }
1625
1516
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1626
1517
  }
1627
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsTextareaFieldComponent, decorators: [{
1518
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsTextareaFieldComponent, decorators: [{
1628
1519
  type: Component,
1629
1520
  args: [{ selector: 'df-bs-textarea', imports: [FormField, DynamicTextPipe, AsyncPipe], template: `
1630
- @let f = field(); @let p = props(); @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1631
- @let ariaDescribedBy = this.ariaDescribedBy(); @let textareaId = key() + '-textarea';
1521
+ @let f = field(); @let p = props(); @let textareaId = key() + '-textarea';
1632
1522
  @if (p?.floatingLabel) {
1633
1523
  <!-- Floating label variant -->
1634
1524
  <div class="form-floating mb-3">
@@ -1637,9 +1527,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1637
1527
  [id]="textareaId"
1638
1528
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
1639
1529
  [attr.tabindex]="tabIndex()"
1640
- [attr.aria-invalid]="ariaInvalid"
1641
- [attr.aria-required]="ariaRequired"
1642
- [attr.aria-describedby]="ariaDescribedBy"
1530
+ [attr.aria-invalid]="ariaInvalid()"
1531
+ [attr.aria-required]="ariaRequired()"
1532
+ [attr.aria-describedby]="ariaDescribedBy()"
1643
1533
  class="form-control"
1644
1534
  [class.form-control-sm]="p?.size === 'sm'"
1645
1535
  [class.form-control-lg]="p?.size === 'lg'"
@@ -1655,8 +1545,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1655
1545
  {{ p?.validFeedback | dynamicText | async }}
1656
1546
  </div>
1657
1547
  }
1658
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1659
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1548
+ @if (errorsToDisplay()[0]; as error) {
1549
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1660
1550
  }
1661
1551
  </div>
1662
1552
  } @else {
@@ -1671,9 +1561,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1671
1561
  [id]="textareaId"
1672
1562
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
1673
1563
  [attr.tabindex]="tabIndex()"
1674
- [attr.aria-invalid]="ariaInvalid"
1675
- [attr.aria-required]="ariaRequired"
1676
- [attr.aria-describedby]="ariaDescribedBy"
1564
+ [attr.aria-invalid]="ariaInvalid()"
1565
+ [attr.aria-required]="ariaRequired()"
1566
+ [attr.aria-describedby]="ariaDescribedBy()"
1677
1567
  class="form-control"
1678
1568
  [class.form-control-sm]="p?.size === 'sm'"
1679
1569
  [class.form-control-lg]="p?.size === 'lg'"
@@ -1681,18 +1571,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1681
1571
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
1682
1572
  ></textarea>
1683
1573
 
1684
- @if (p?.helpText) {
1685
- <div class="form-text" [id]="helpTextId()">
1686
- {{ p?.helpText | dynamicText | async }}
1687
- </div>
1688
- }
1689
1574
  @if (p?.validFeedback && f().valid() && f().touched()) {
1690
1575
  <div class="valid-feedback d-block">
1691
1576
  {{ p?.validFeedback | dynamicText | async }}
1692
1577
  </div>
1693
1578
  }
1694
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1695
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1579
+ @if (errorsToDisplay()[0]; as error) {
1580
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1581
+ } @else if (p?.hint) {
1582
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
1696
1583
  }
1697
1584
  </div>
1698
1585
  }
@@ -1730,7 +1617,7 @@ class BsToggleFieldComponent {
1730
1617
  // ─────────────────────────────────────────────────────────────────────────────
1731
1618
  // Accessibility
1732
1619
  // ─────────────────────────────────────────────────────────────────────────────
1733
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
1620
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1734
1621
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1735
1622
  ariaInvalid = computed(() => {
1736
1623
  const fieldState = this.field()();
@@ -1739,21 +1626,10 @@ class BsToggleFieldComponent {
1739
1626
  ariaRequired = computed(() => {
1740
1627
  return this.field()().required?.() === true ? true : null;
1741
1628
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1742
- ariaDescribedBy = computed(() => {
1743
- const ids = [];
1744
- if (this.props()?.helpText) {
1745
- ids.push(this.helpTextId());
1746
- }
1747
- const errors = this.errorsToDisplay();
1748
- errors.forEach((_, i) => {
1749
- ids.push(`${this.errorId()}-${i}`);
1750
- });
1751
- return ids.length > 0 ? ids.join(' ') : null;
1752
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1753
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsToggleFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1754
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsToggleFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1755
- @let f = field(); @let inputId = key() + '-input'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1756
- @let ariaDescribedBy = this.ariaDescribedBy();
1629
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1630
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsToggleFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1631
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsToggleFieldComponent, isStandalone: true, selector: "df-bs-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 }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1632
+ @let f = field(); @let inputId = key() + '-input';
1757
1633
 
1758
1634
  <div
1759
1635
  class="form-check form-switch"
@@ -1770,30 +1646,26 @@ class BsToggleFieldComponent {
1770
1646
  class="form-check-input"
1771
1647
  [class.is-invalid]="f().invalid() && f().touched()"
1772
1648
  [attr.tabindex]="tabIndex()"
1773
- [attr.aria-invalid]="ariaInvalid"
1774
- [attr.aria-required]="ariaRequired"
1775
- [attr.aria-describedby]="ariaDescribedBy"
1649
+ [attr.aria-invalid]="ariaInvalid()"
1650
+ [attr.aria-required]="ariaRequired()"
1651
+ [attr.aria-describedby]="ariaDescribedBy()"
1776
1652
  />
1777
1653
  <label [for]="inputId" class="form-check-label">
1778
1654
  {{ label() | dynamicText | async }}
1779
1655
  </label>
1780
1656
  </div>
1781
1657
 
1782
- @if (props()?.helpText; as helpText) {
1783
- <div class="form-text" [id]="helpTextId()" [attr.hidden]="f().hidden() || null">
1784
- {{ helpText | dynamicText | async }}
1785
- </div>
1786
- }
1787
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1788
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1658
+ @if (errorsToDisplay()[0]; as error) {
1659
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1660
+ } @else if (props()?.hint; as hint) {
1661
+ <div class="form-text" [id]="hintId()" [attr.hidden]="f().hidden() || null">{{ hint | dynamicText | async }}</div>
1789
1662
  }
1790
1663
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host{display:block}:host([hidden]){display:none!important}.form-switch-sm .form-check-input{width:1.75rem;height:1rem;font-size:.875rem}.form-switch-lg .form-check-input{width:3rem;height:1.75rem;font-size:1.125rem}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1791
1664
  }
1792
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsToggleFieldComponent, decorators: [{
1665
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsToggleFieldComponent, decorators: [{
1793
1666
  type: Component,
1794
1667
  args: [{ selector: 'df-bs-toggle', imports: [FormField, DynamicTextPipe, AsyncPipe], template: `
1795
- @let f = field(); @let inputId = key() + '-input'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1796
- @let ariaDescribedBy = this.ariaDescribedBy();
1668
+ @let f = field(); @let inputId = key() + '-input';
1797
1669
 
1798
1670
  <div
1799
1671
  class="form-check form-switch"
@@ -1810,22 +1682,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1810
1682
  class="form-check-input"
1811
1683
  [class.is-invalid]="f().invalid() && f().touched()"
1812
1684
  [attr.tabindex]="tabIndex()"
1813
- [attr.aria-invalid]="ariaInvalid"
1814
- [attr.aria-required]="ariaRequired"
1815
- [attr.aria-describedby]="ariaDescribedBy"
1685
+ [attr.aria-invalid]="ariaInvalid()"
1686
+ [attr.aria-required]="ariaRequired()"
1687
+ [attr.aria-describedby]="ariaDescribedBy()"
1816
1688
  />
1817
1689
  <label [for]="inputId" class="form-check-label">
1818
1690
  {{ label() | dynamicText | async }}
1819
1691
  </label>
1820
1692
  </div>
1821
1693
 
1822
- @if (props()?.helpText; as helpText) {
1823
- <div class="form-text" [id]="helpTextId()" [attr.hidden]="f().hidden() || null">
1824
- {{ helpText | dynamicText | async }}
1825
- </div>
1826
- }
1827
- @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1828
- <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1694
+ @if (errorsToDisplay()[0]; as error) {
1695
+ <div class="invalid-feedback d-block" [id]="errorId()" role="alert">{{ error.message }}</div>
1696
+ } @else if (props()?.hint; as hint) {
1697
+ <div class="form-text" [id]="hintId()" [attr.hidden]="f().hidden() || null">{{ hint | dynamicText | async }}</div>
1829
1698
  }
1830
1699
  `, host: {
1831
1700
  '[class]': 'className()',
@@ -1873,9 +1742,9 @@ const BsField = {
1873
1742
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1874
1743
  */
1875
1744
  function buttonFieldMapper(fieldDef) {
1876
- // Build base inputs (static, from field definition)
1877
- const baseInputs = buildBaseInputs(fieldDef);
1745
+ const defaultProps = inject(DEFAULT_PROPS);
1878
1746
  return computed(() => {
1747
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1879
1748
  const inputs = {
1880
1749
  ...baseInputs,
1881
1750
  };
@@ -1913,20 +1782,21 @@ function buttonFieldMapper(fieldDef) {
1913
1782
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1914
1783
  */
1915
1784
  function submitButtonFieldMapper(fieldDef) {
1916
- // Inject field signal context to access form state and options
1917
- const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
1918
- // Build base inputs (static, from field definition)
1919
- const baseInputs = buildBaseInputs(fieldDef);
1920
- // Use button-logic-resolver to compute disabled state
1785
+ const rootFormRegistry = inject(RootFormRegistryService);
1786
+ const defaultProps = inject(DEFAULT_PROPS);
1787
+ const formOptions = inject(FORM_OPTIONS);
1921
1788
  const fieldWithLogic = fieldDef;
1922
- const disabledSignal = resolveSubmitButtonDisabled({
1923
- form: fieldSignalContext.form,
1924
- formOptions: fieldSignalContext.formOptions,
1925
- fieldLogic: fieldWithLogic.logic,
1926
- explicitlyDisabled: fieldDef.disabled,
1927
- });
1928
- // Return computed signal - evaluates disabledSignal inside for reactivity
1929
1789
  return computed(() => {
1790
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1791
+ // Use rootFormRegistry instead of fieldSignalContext.form because when the submit button
1792
+ // is inside a group/array, fieldSignalContext.form points to the nested form tree,
1793
+ // not the root form. We need root form validity for submit button disabled state (#157).
1794
+ const disabledSignal = resolveSubmitButtonDisabled({
1795
+ form: rootFormRegistry.getRootForm(),
1796
+ formOptions: formOptions(),
1797
+ fieldLogic: fieldWithLogic.logic,
1798
+ explicitlyDisabled: fieldDef.disabled,
1799
+ });
1930
1800
  const inputs = {
1931
1801
  ...baseInputs,
1932
1802
  // No event - native form submit handles it via form's onNativeSubmit
@@ -1953,21 +1823,19 @@ function submitButtonFieldMapper(fieldDef) {
1953
1823
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1954
1824
  */
1955
1825
  function nextButtonFieldMapper(fieldDef) {
1956
- // Inject field signal context to access form state and options
1957
1826
  const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
1958
- // Build base inputs (static, from field definition)
1959
- const baseInputs = buildBaseInputs(fieldDef);
1960
- // Use button-logic-resolver to compute disabled state
1827
+ const defaultProps = inject(DEFAULT_PROPS);
1828
+ const formOptions = inject(FORM_OPTIONS);
1961
1829
  const fieldWithLogic = fieldDef;
1962
- const disabledSignal = resolveNextButtonDisabled({
1963
- form: fieldSignalContext.form,
1964
- formOptions: fieldSignalContext.formOptions,
1965
- fieldLogic: fieldWithLogic.logic,
1966
- explicitlyDisabled: fieldDef.disabled,
1967
- currentPageValid: fieldSignalContext.currentPageValid,
1968
- });
1969
- // Return computed signal - evaluates disabledSignal inside for reactivity
1970
1830
  return computed(() => {
1831
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1832
+ const disabledSignal = resolveNextButtonDisabled({
1833
+ form: fieldSignalContext.form,
1834
+ formOptions: formOptions(),
1835
+ fieldLogic: fieldWithLogic.logic,
1836
+ explicitlyDisabled: fieldDef.disabled,
1837
+ currentPageValid: fieldSignalContext.currentPageValid,
1838
+ });
1971
1839
  const inputs = {
1972
1840
  ...baseInputs,
1973
1841
  event: NextPageEvent,
@@ -1988,9 +1856,9 @@ function nextButtonFieldMapper(fieldDef) {
1988
1856
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1989
1857
  */
1990
1858
  function previousButtonFieldMapper(fieldDef) {
1991
- // Build base inputs (static, from field definition)
1992
- const baseInputs = buildBaseInputs(fieldDef);
1859
+ const defaultProps = inject(DEFAULT_PROPS);
1993
1860
  return computed(() => {
1861
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1994
1862
  const inputs = {
1995
1863
  ...baseInputs,
1996
1864
  event: PreviousPageEvent,
@@ -2016,10 +1884,9 @@ function previousButtonFieldMapper(fieldDef) {
2016
1884
  * @returns Signal containing Record of input names to values for ngComponentOutlet
2017
1885
  */
2018
1886
  function addArrayItemButtonFieldMapper(fieldDef) {
2019
- // Try to get array context (available when inside an array)
2020
- // Use optional injection so it doesn't fail when outside an array
2021
1887
  const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
2022
1888
  const logger = inject(DynamicFormLogger);
1889
+ const defaultProps = inject(DEFAULT_PROPS);
2023
1890
  // Determine the target array key
2024
1891
  // Priority: explicit arrayKey from fieldDef > arrayKey from context
2025
1892
  const targetArrayKey = fieldDef.arrayKey ?? arrayContext?.arrayKey;
@@ -2027,13 +1894,12 @@ function addArrayItemButtonFieldMapper(fieldDef) {
2027
1894
  logger.warn(`addArrayItem button "${fieldDef.key}" has no array context. ` +
2028
1895
  'Either place it inside an array field, or provide an explicit arrayKey property.');
2029
1896
  }
2030
- // Build base inputs (static, from field definition)
2031
- const baseInputs = buildBaseInputs(fieldDef);
2032
1897
  // Set default eventArgs for AddArrayItemEvent (arrayKey)
2033
1898
  // User can override by providing eventArgs in field definition
2034
1899
  const defaultEventArgs = ['$arrayKey'];
2035
1900
  const eventArgs = 'eventArgs' in fieldDef && fieldDef.eventArgs !== undefined ? fieldDef.eventArgs : defaultEventArgs;
2036
1901
  return computed(() => {
1902
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
2037
1903
  // Read signal value if index is a signal (supports differential updates)
2038
1904
  const getIndex = () => {
2039
1905
  if (!arrayContext)
@@ -2072,10 +1938,9 @@ function addArrayItemButtonFieldMapper(fieldDef) {
2072
1938
  * @returns Signal containing Record of input names to values for ngComponentOutlet
2073
1939
  */
2074
1940
  function removeArrayItemButtonFieldMapper(fieldDef) {
2075
- // Try to get array context (available when inside an array)
2076
- // Use optional injection so it doesn't fail when outside an array
2077
1941
  const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
2078
1942
  const logger = inject(DynamicFormLogger);
1943
+ const defaultProps = inject(DEFAULT_PROPS);
2079
1944
  // Determine the target array key
2080
1945
  // Priority: explicit arrayKey from fieldDef > arrayKey from context
2081
1946
  const targetArrayKey = fieldDef.arrayKey ?? arrayContext?.arrayKey;
@@ -2083,14 +1948,13 @@ function removeArrayItemButtonFieldMapper(fieldDef) {
2083
1948
  logger.warn(`removeArrayItem button "${fieldDef.key}" has no array context. ` +
2084
1949
  'Either place it inside an array field, or provide an explicit arrayKey property.');
2085
1950
  }
2086
- // Build base inputs (static, from field definition)
2087
- const baseInputs = buildBaseInputs(fieldDef);
2088
1951
  // Set default eventArgs for RemoveArrayItemEvent (arrayKey, index if inside array)
2089
1952
  // When outside array, only pass arrayKey (removes last by default)
2090
1953
  // User can override by providing eventArgs in field definition
2091
1954
  const defaultEventArgs = arrayContext ? ['$arrayKey', '$index'] : ['$arrayKey'];
2092
1955
  const eventArgs = 'eventArgs' in fieldDef && fieldDef.eventArgs !== undefined ? fieldDef.eventArgs : defaultEventArgs;
2093
1956
  return computed(() => {
1957
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
2094
1958
  // Read signal value if index is a signal (supports differential updates)
2095
1959
  const getIndex = () => {
2096
1960
  if (!arrayContext)