-
@@ -101,8 +91,7 @@
[ngClass]="{ 'sky-date-range-picker-last-input': showEndDatePicker }"
>
diff --git a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts
index 7ac0f45983..62310c44bb 100644
--- a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts
+++ b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts
@@ -394,6 +394,48 @@ describe('Date range picker', function () {
).toHaveCssClass('ng-touched');
});
+ it('should mark the underlying control touched if the host control calls markAllAsTouched', () => {
+ fixture.detectChanges();
+
+ component.reactiveForm?.markAllAsTouched();
+
+ fixture.detectChanges();
+
+ expect(
+ fixture.nativeElement.querySelector('sky-date-range-picker'),
+ ).toHaveCssClass('ng-touched');
+ });
+
+ it('should mark only start date input as touched when start date is interacted with', () => {
+ fixture.detectChanges();
+
+ const datepickerInputs = fixture.nativeElement.querySelectorAll(
+ '.sky-input-group input',
+ );
+
+ SkyAppTestUtility.fireDomEvent(datepickerInputs.item(0), 'blur');
+
+ fixture.detectChanges();
+
+ expect(datepickerInputs.item(0)).toHaveCssClass('ng-touched');
+ expect(datepickerInputs.item(1)).toHaveCssClass('ng-untouched');
+ });
+
+ it('should mark only start date input as touched when start date is interacted with', () => {
+ fixture.detectChanges();
+
+ const datepickerInputs = fixture.nativeElement.querySelectorAll(
+ '.sky-input-group input',
+ );
+
+ SkyAppTestUtility.fireDomEvent(datepickerInputs.item(1), 'blur');
+
+ fixture.detectChanges();
+
+ expect(datepickerInputs.item(1)).toHaveCssClass('ng-touched');
+ expect(datepickerInputs.item(0)).toHaveCssClass('ng-untouched');
+ });
+
it('should maintain selected value when calculators change', fakeAsync(function () {
fixture.detectChanges();
@@ -633,16 +675,42 @@ describe('Date range picker', function () {
expect(calculatorIdControl?.errors).toBeFalsy();
}));
+ it('should visually set start and end date datepickers to required and not the calculator select', () => {
+ fixture.componentInstance.required = true;
+ fixture.detectChanges();
+
+ const control = component.dateRange;
+ control?.setValue({
+ calculatorId: SkyDateRangeCalculatorId.SpecificRange,
+ });
+ fixture.detectChanges();
+
+ const datepickerInputLabels =
+ fixture.nativeElement.querySelectorAll('.sky-control-label');
+ const calculatorSelectLabel = datepickerInputLabels.item(0);
+ const startDateLabel = datepickerInputLabels.item(1);
+ const endDateLabel = datepickerInputLabels.item(2);
+
+ expect(calculatorSelectLabel).not.toHaveCssClass(
+ 'sky-control-label-required',
+ );
+ expect(startDateLabel).toHaveCssClass('sky-control-label-required');
+ expect(endDateLabel).toHaveCssClass('sky-control-label-required');
+ });
+
it('should show validation errors when start date is required but not provided', fakeAsync(function () {
- fixture.componentInstance.startDateRequired = true;
+ fixture.componentInstance.required = true;
detectChanges();
+
const control = component.dateRange;
- const calculatorIdControl =
- component.dateRangePicker['formGroup']?.get('calculatorId');
+ const startDateControl =
+ component.dateRangePicker['formGroup']?.get('startDate');
+
control?.setValue({
calculatorId: SkyDateRangeCalculatorId.SpecificRange,
});
detectChanges();
+
const datepickerInputs = fixture.nativeElement.querySelectorAll(
'.sky-input-group input',
);
@@ -654,31 +722,34 @@ describe('Date range picker', function () {
};
expect(control?.errors).toEqual(expectedError);
- expect(calculatorIdControl?.errors).toEqual(expectedError);
+ expect(startDateControl?.errors).toEqual(expectedError);
}));
it('should show validation errors when end date is required but not provided', fakeAsync(function () {
- fixture.componentInstance.endDateRequired = true;
+ fixture.componentInstance.required = true;
detectChanges();
+
const control = component.dateRange;
- const calculatorIdControl =
- component.dateRangePicker['formGroup']?.get('calculatorId');
+ const endDateControl =
+ component.dateRangePicker['formGroup']?.get('endDate');
+
control?.setValue({
calculatorId: SkyDateRangeCalculatorId.SpecificRange,
});
detectChanges();
+
const datepickerInputs = fixture.nativeElement.querySelectorAll(
'.sky-input-group input',
);
-
SkyAppTestUtility.fireDomEvent(datepickerInputs.item(1), 'blur');
detectChanges();
+
const expectedError = {
required: true,
};
expect(control?.errors).toEqual(expectedError);
- expect(calculatorIdControl?.errors).toEqual(expectedError);
+ expect(endDateControl?.errors).toEqual(expectedError);
}));
it('should log a deprecation warning when label input is used', fakeAsync(() => {
@@ -695,12 +766,17 @@ describe('Date range picker', function () {
}));
it('should render date range specific errors only when labelText is provided', fakeAsync(() => {
- component.dateRange?.setValue({
+ const control = component.dateRange;
+
+ control?.setValue({
calculatorId: SkyDateRangeCalculatorId.SpecificRange,
startDate: new Date('1/2/2000'),
endDate: new Date('1/1/2000'),
});
- component.reactiveForm.markAllAsTouched();
+ detectChanges();
+
+ control?.updateValueAndValidity();
+ control?.markAllAsTouched();
detectChanges();
expect(
@@ -708,6 +784,7 @@ describe('Date range picker', function () {
).toBe(undefined);
component.labelText = 'Date range picker';
+ control?.updateValueAndValidity();
detectChanges();
expect(
@@ -730,12 +807,8 @@ describe('Date range picker', function () {
function verifyFormFieldsRequired(expectation: boolean): void {
const inputBoxes =
fixture.nativeElement.querySelectorAll('sky-input-box');
- const selectElement = fixture.nativeElement.querySelector('select');
const inputs = fixture.nativeElement.querySelectorAll('input');
- expect(
- inputBoxes.item(0).querySelector('.sky-control-label-required'),
- ).toEqual(expectation ? jasmine.any(HTMLLabelElement) : null);
expect(
inputBoxes.item(1).querySelector('.sky-control-label-required'),
).toEqual(expectation ? jasmine.any(HTMLLabelElement) : null);
@@ -743,7 +816,6 @@ describe('Date range picker', function () {
inputBoxes.item(2).querySelector('.sky-control-label-required'),
).toEqual(expectation ? jasmine.any(HTMLLabelElement) : null);
- expect(selectElement.hasAttribute('required')).toEqual(expectation);
expect(inputs.item(0).hasAttribute('required')).toEqual(expectation);
expect(inputs.item(1).hasAttribute('required')).toEqual(expectation);
}
diff --git a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts
index 5f17e13d48..5a1bb448d5 100644
--- a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts
+++ b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts
@@ -24,6 +24,7 @@ import {
NG_VALUE_ACCESSOR,
NgControl,
ReactiveFormsModule,
+ TouchedChangeEvent,
ValidationErrors,
Validator,
Validators,
@@ -36,7 +37,7 @@ import {
SkyInputBoxModule,
} from '@skyux/forms';
-import { Subject, distinctUntilChanged, merge, takeUntil } from 'rxjs';
+import { Subject, distinctUntilChanged, filter, merge, takeUntil } from 'rxjs';
import { SkyDatepickerModule } from '../datepicker/datepicker.module';
import { SkyDatetimeResourcesModule } from '../shared/sky-datetime-resources.module';
@@ -192,14 +193,6 @@ export class SkyDateRangePickerComponent
}
}
- /**
- * Whether to require users to specify a end date.
- * @deprecated Use the `required` directive or Angular's `Validators.required`
- * on the form control to mark the date range picker as required.
- */
- @Input({ transform: booleanAttribute })
- public endDateRequired = false;
-
/**
* The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline)
* button is added to date range picker. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover)
@@ -262,14 +255,6 @@ export class SkyDateRangePickerComponent
@HostBinding('class.sky-margin-stacked-lg')
public stacked = false;
- /**
- * Whether to require users to specify a start date.
- * @deprecated Use the `required` directive or Angular's `Validators.required`
- * on the form control to mark the date range picker as required.
- */
- @Input({ transform: booleanAttribute })
- public startDateRequired = false;
-
/**
* A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline)
* button is placed beside the date range picker label. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help)
@@ -278,13 +263,15 @@ export class SkyDateRangePickerComponent
@Input()
public helpKey: string | undefined;
+ protected calculatorIdHasErrors = false;
protected calculators: SkyDateRangeCalculator[] = [];
+ protected endDateHasErrors = false;
protected formGroup: FormGroup;
- protected hasErrors = false;
protected hostControl: AbstractControl | null | undefined;
protected selectedCalculator: SkyDateRangeCalculator;
protected showEndDatePicker = false;
protected showStartDatePicker = false;
+ protected startDateHasErrors = false;
get #calculatorIdControl(): AbstractControl {
return this.formGroup.get(
@@ -407,6 +394,15 @@ export class SkyDateRangePickerComponent
});
});
+ this.hostControl?.events
+ .pipe(
+ filter((event) => event instanceof TouchedChangeEvent),
+ takeUntil(this.#ngUnsubscribe),
+ )
+ .subscribe(() => {
+ this.formGroup.markAllAsTouched();
+ });
+
this.#updatePickerVisibility(this.selectedCalculator);
}
@@ -419,21 +415,25 @@ export class SkyDateRangePickerComponent
*/
public ngDoCheck(): void {
const control = this.hostControl;
- const touched = this.formGroup.touched;
if (control) {
- if (control.touched && !touched) {
- this.formGroup.markAllAsTouched();
- this.#changeDetector.markForCheck();
- } else if (control.untouched && touched) {
- this.formGroup.markAsUntouched();
- this.#changeDetector.markForCheck();
- }
-
- this.hasErrors = !!control.errors && (control.touched || control.dirty);
+ this.startDateHasErrors =
+ this.#controlHasErrors(this.#startDateControl) ||
+ this.#controlHasErrors(this.#calculatorIdControl);
+ this.endDateHasErrors =
+ this.#controlHasErrors(this.#endDateControl) ||
+ this.#controlHasErrors(this.#calculatorIdControl);
+ this.calculatorIdHasErrors = this.#controlHasErrors(
+ this.#calculatorIdControl,
+ );
+ this.#changeDetector.markForCheck();
}
}
+ #controlHasErrors(control: AbstractControl): boolean {
+ return !!control.errors && (control.touched || control.dirty);
+ }
+
public ngOnDestroy(): void {
this.#ngUnsubscribe.next();
this.#ngUnsubscribe.complete();
@@ -471,6 +471,9 @@ export class SkyDateRangePickerComponent
};
}
+ // Set calculator errors on the select so that they appear beneath it.
+ this.#calculatorIdControl.setErrors(errors);
+
if (this.showStartDatePicker && startDateErrors) {
errors ||= {};
errors = { ...errors, ...startDateErrors };
@@ -481,8 +484,6 @@ export class SkyDateRangePickerComponent
errors = { ...errors, ...endDateErrors };
}
- // Set errors on the calculator select so that they appear beneath it.
- this.#calculatorIdControl.setErrors(errors);
this.#changeDetector.markForCheck();
return errors;
diff --git a/libs/components/datetime/src/lib/modules/date-range-picker/fixtures/date-range-picker.component.fixture.html b/libs/components/datetime/src/lib/modules/date-range-picker/fixtures/date-range-picker.component.fixture.html
index 31e55122ba..231a5c7a9e 100644
--- a/libs/components/datetime/src/lib/modules/date-range-picker/fixtures/date-range-picker.component.fixture.html
+++ b/libs/components/datetime/src/lib/modules/date-range-picker/fixtures/date-range-picker.component.fixture.html
@@ -5,7 +5,6 @@
formControlName="dateRange"
[calculatorIds]="calculatorIds"
[dateFormat]="dateFormat"
- [endDateRequired]="endDateRequired"
[helpKey]="helpKey"
[helpPopoverContent]="helpPopoverContent"
[hintText]="hintText"
@@ -13,7 +12,6 @@
[labelText]="labelText"
[required]="required"
[stacked]="stacked"
- [startDateRequired]="startDateRequired"
/>
}
@@ -24,8 +22,6 @@
[calculatorIds]="calculatorIds"
[dateFormat]="dateFormat"
[disabled]="templateDisable"
- [endDateRequired]="endDateRequired"
[label]="label"
- [startDateRequired]="startDateRequired"
/>
}
diff --git a/libs/components/forms/src/lib/modules/input-box/fixtures/input-box.component.fixture.html b/libs/components/forms/src/lib/modules/input-box/fixtures/input-box.component.fixture.html
index db5786bd72..bb4c33a1c6 100644
--- a/libs/components/forms/src/lib/modules/input-box/fixtures/input-box.component.fixture.html
+++ b/libs/components/forms/src/lib/modules/input-box/fixtures/input-box.component.fixture.html
@@ -556,7 +556,6 @@
diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts b/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts
index b3212881f9..b1a8243ab6 100644
--- a/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts
+++ b/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts
@@ -1482,21 +1482,6 @@ describe('Input box component', () => {
expect(errorEl).toHaveText('Error: Easy mode is required.');
});
- it('should allow showing errors only to screen readers', () => {
- const fixture = TestBed.createComponent(InputBoxFixtureComponent);
- fixture.componentInstance.errorsScreenReaderOnly = true;
- fixture.detectChanges();
-
- const inputBoxEl = getInputBoxEl(fixture, 'input-easy-mode');
- const inputEl = inputBoxEl?.querySelector('input');
- inputEl?.dispatchEvent(new Event('blur'));
- fixture.detectChanges();
-
- const errorsEl = inputBoxEl?.querySelector('sky-form-errors');
-
- expect(errorsEl).toHaveClass('sky-screen-reader-only');
- });
-
it('should add required attributes to label and input when required', async () => {
const fixture = TestBed.createComponent(InputBoxFixtureComponent);
fixture.detectChanges();
diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.component.ts b/libs/components/forms/src/lib/modules/input-box/input-box.component.ts
index e884b4075e..1258328630 100644
--- a/libs/components/forms/src/lib/modules/input-box/input-box.component.ts
+++ b/libs/components/forms/src/lib/modules/input-box/input-box.component.ts
@@ -17,9 +17,7 @@ import {
Renderer2,
TemplateRef,
ViewEncapsulation,
- booleanAttribute,
inject,
- input,
} from '@angular/core';
import {
AbstractControlDirective,
@@ -170,13 +168,6 @@ export class SkyInputBoxComponent
return this.#_hintText;
}
- /**
- * @internal
- */
- public errorsScreenReaderOnly = input(false, {
- transform: booleanAttribute,
- });
-
public hostInputTemplate: TemplateRef | undefined;
public hostButtonsTemplate: TemplateRef | undefined;