Skip to content

Commit

Permalink
fix(components/datetime): date range picker visually invalid on custo…
Browse files Browse the repository at this point in the history
…m errors (#2910)
  • Loading branch information
Blackbaud-SandhyaRajasabeson authored Nov 15, 2024
1 parent b85231e commit 357c3eb
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
[calculatorIds]="calculatorIds"
[dateFormat]="dateFormat"
[hintText]="hintText"
/>
>
@if (lastDonationControl.errors?.['dateWeekend']) {
<sky-form-error
errorName="dateWeekend"
errorText="Do not pick a weekend."
/>
}
</sky-date-range-picker>

<button class="sky-btn sky-btn-default" type="submit">Submit</button>
</form>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormControl,
FormsModule,
ReactiveFormsModule,
ValidationErrors,
Validators,
} from '@angular/forms';
import {
Expand All @@ -16,6 +18,25 @@ import { SkyAppLocaleProvider } from '@skyux/i18n';

import { LocaleProvider } from './locale-provider';

function dateRangeExcludesWeekend(
control: AbstractControl,
): ValidationErrors | null {
const startDate = control.value.startDate;
const endDate = control.value.endDate;

const isWeekend = (value: unknown): boolean => {
return (
value instanceof Date && (value.getDay() === 6 || value.getDay() === 0)
);
};

if (isWeekend(startDate) || isWeekend(endDate)) {
return { dateWeekend: true };
}

return null;
}

@Component({
imports: [
CommonModule,
Expand All @@ -37,9 +58,9 @@ export class DateRangePickerComponent {
protected calculatorIds: SkyDateRangeCalculatorId[] | undefined;
protected dateFormat: string | undefined;
protected hintText: string | undefined;
protected lastDonationControl = new FormControl<SkyDateRangeCalculation>({
calculatorId: SkyDateRangeCalculatorId.Today,
});
protected lastDonationControl = new FormControl<
SkyDateRangeCalculation | string
>('', [dateRangeExcludesWeekend]);

protected formGroup = inject(FormBuilder).group({
lastDonation: this.lastDonationControl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,69 @@ describe('Date range picker', function () {
});
});

it('should not mark calculator as invalid when the error is from the datepicker', fakeAsync(() => {
detectChanges();
selectCalculator(SkyDateRangeCalculatorId.SpecificRange);
const control = component.dateRange;
control?.markAllAsTouched();
detectChanges();

const calculatorInput = fixture.nativeElement.querySelector(
'.sky-date-range-picker-select-calculator .sky-input-box-input-group-inner',
);
const startInput: HTMLElement = fixture.nativeElement.querySelector(
'.sky-date-range-picker-start-date .sky-input-box-input-group-inner',
);

expect(
startInput.classList.contains('sky-field-status-invalid'),
).toBeTrue();
expect(
calculatorInput.classList.contains('sky-field-status-invalid'),
).toBeFalse();

const selectedValue: SkyDateRangeCalculation = {
calculatorId: SkyDateRangeCalculatorId.SpecificRange,
startDate: new Date('1/1/2000'),
endDate: new Date('1/2/1997'),
};

control?.setValue(selectedValue);

detectChanges();

expect(
startInput.classList.contains('sky-field-status-invalid'),
).toBeTrue();
expect(
calculatorInput.classList.contains('sky-field-status-invalid'),
).toBeTrue();
}));

it('should display custom form errors and mark all inputs as invalid', fakeAsync(() => {
detectChanges();
selectCalculator(SkyDateRangeCalculatorId.SpecificRange);
detectChanges();
const control = component.dateRange;
control?.setErrors({
customError: true,
});
control?.markAllAsTouched();
detectChanges();

const inputs: HTMLElement[] = fixture.nativeElement.querySelectorAll(
'.sky-input-box-input-group-inner',
);

inputs.forEach((input) => {
expect(input.classList.contains('sky-field-status-invalid')).toBeTrue();
});

expect(
fixture.nativeElement.querySelector('sky-form-error')?.textContent.trim(),
).toBe('Error: This is a custom error.');
}));

describe('accessibility', () => {
function verifyFormFieldsRequired(expectation: boolean): void {
const inputBoxes =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
booleanAttribute,
computed,
inject,
runInInjectionContext,
signal,
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
Expand Down Expand Up @@ -280,6 +281,7 @@ export class SkyDateRangePickerComponent
#startDateControl = new FormControl<DateValue>(this.#getValue().startDate);
#startDateInvalid = this.#createStatusChangeSignal(this.#startDateControl);
#startDateTouched = this.#createTouchedChangeSignal(this.#startDateControl);
#hostHasCustomError: Signal<boolean | undefined> | undefined;

protected formGroup = inject(FormBuilder).group({
calculatorId: this.#calculatorIdControl,
Expand All @@ -289,8 +291,7 @@ export class SkyDateRangePickerComponent

protected readonly calculatorIdHasErrors = computed(() => {
const touched = this.#calculatorIdTouched();
const invalid = this.#calculatorIdInvalid();

const invalid = this.#calculatorIdInvalid() || this.#hostHasCustomError?.();
return touched && invalid;
});

Expand Down Expand Up @@ -320,6 +321,14 @@ export class SkyDateRangePickerComponent
self: true,
})?.control;

runInInjectionContext(this.#injector, () => {
if (this.hostControl) {
this.#hostHasCustomError = this.#createHostCustomErrorChangeSignal(
this.hostControl,
);
}
});

// Set a default value on the control if it's undefined on init.
// We need to use setTimeout to avoid interfering with the first
// validation cycle.
Expand Down Expand Up @@ -554,6 +563,24 @@ export class SkyDateRangePickerComponent
this.showStartDatePicker.set(showStartDatePicker);
}

#createHostCustomErrorChangeSignal(
control: AbstractControl,
): Signal<boolean | undefined> {
return toSignal(
control.events.pipe(
filter((evt) => evt instanceof StatusChangeEvent),
map((evt: StatusChangeEvent) => {
const errors = evt.source.errors ?? [];
const knownErrors = ['required', 'skyDate'];

return Object.keys(errors).some((error) => {
return !knownErrors.includes(error);
});
}),
),
);
}

#createStatusChangeSignal(control: FormControl): Signal<boolean | undefined> {
return toSignal(
control.events.pipe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@
[labelText]="labelText"
[required]="required"
[stacked]="stacked"
/>
>
@if (dateRange?.errors?.['customError']) {
<sky-form-error
errorName="customError"
errorText="This is a custom error."
/>
}
</sky-date-range-picker>
}
</form>

Expand Down

0 comments on commit 357c3eb

Please sign in to comment.