Skip to content

Commit

Permalink
fix(components/datetime): date range picker emits touched status when…
Browse files Browse the repository at this point in the history
… focus leaves composite control (#2947)
  • Loading branch information
Blackbaud-SteveBrush authored Dec 13, 2024
1 parent a999545 commit 3b84077
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@
('skyux_date_range_picker_default_label' | skyLibResources)
"
>
<select
formControlName="calculatorId"
(blur)="onBlur()"
(change)="onCalculatorIdChange()"
>
<select formControlName="calculatorId" (change)="onCalculatorIdChange()">
@for (calculator of calculators; track calculator.calculatorId) {
<option [value]="calculator.calculatorId">
{{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ describe('Date range picker', function () {
let fixture: ComponentFixture<DateRangePickerTestComponent>;
let component: DateRangePickerTestComponent;

function blurInput(inputEl: Element): void {
inputEl.dispatchEvent(
/**
* Dispatches a focusout event on the specified element.
* @param target The element losing focus.
* @param relatedTarget The element receiving focus.
*/
function blurElement(target?: Element, relatedTarget?: EventTarget): void {
target?.dispatchEvent(
new FocusEvent('focusout', {
bubbles: true,
cancelable: true,
relatedTarget: null,
relatedTarget,
}),
);
}
Expand All @@ -39,6 +44,16 @@ describe('Date range picker', function () {
tick();
}

function getActiveDatepickerCalendar(): HTMLElement {
const calendarEl = document.querySelector('sky-datepicker-calendar');

if (!calendarEl) {
throw new Error('Datepicker calendar not found.');
}

return calendarEl as HTMLElement;
}

function getCalculatorSelect(): HTMLSelectElement {
return fixture.nativeElement.querySelector('select');
}
Expand Down Expand Up @@ -76,6 +91,12 @@ describe('Date range picker', function () {
}
}

function openStartDatepickerCalendar(): void {
fixture.nativeElement
.querySelector('.sky-date-range-picker-start-date button')
?.click();
}

function verifyVisiblePickers(
id: SkyDateRangeCalculatorId,
type: SkyDateRangeCalculatorType,
Expand Down Expand Up @@ -357,16 +378,39 @@ describe('Date range picker', function () {
verifyFormFieldsDisabledStatus(false);
}));

it('should mark the control as touched when select is blurred', fakeAsync(function () {
it('should mark the control as touched when the composite control loses focus', fakeAsync(() => {
detectChanges();

expect(component.reactiveForm?.touched).toEqual(false);

// Show both date pickers.
selectCalculator(SkyDateRangeCalculatorId.SpecificRange);
detectChanges();

const selectElement = fixture.nativeElement.querySelector('select');
SkyAppTestUtility.fireDomEvent(selectElement, 'blur');
const startDateInput = getStartDateInput();
const endDateInput = getEndDateInput();

// Move focus to the start date input.
blurElement(selectElement, startDateInput);
detectChanges();
expect(component.reactiveForm?.touched).toEqual(false);

// Move focus to the opened datepicker calendar.
openStartDatepickerCalendar();
const startDateCalendarEl = getActiveDatepickerCalendar();
blurElement(startDateInput, startDateCalendarEl);
detectChanges();
expect(component.reactiveForm?.touched).toEqual(false);

// Move focus to the end date input.
blurElement(startDateCalendarEl, endDateInput);
detectChanges();
expect(component.reactiveForm?.touched).toEqual(false);

// Blur the end date input and expect touched.
blurElement(endDateInput, document.body);
detectChanges();
expect(component.reactiveForm?.touched).toEqual(true);
}));

Expand Down Expand Up @@ -401,7 +445,7 @@ describe('Date range picker', function () {
fixture.nativeElement as HTMLElement
).querySelectorAll('.sky-input-group input');

blurInput(datepickerInputs.item(0));
blurElement(datepickerInputs.item(0));

fixture.detectChanges();

Expand All @@ -416,7 +460,7 @@ describe('Date range picker', function () {
'.sky-input-group input',
);

blurInput(datepickerInputs.item(1));
blurElement(datepickerInputs.item(1));

fixture.detectChanges();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ChangeDetectionStrategy,
Component,
DestroyRef,
ElementRef,
HostBinding,
Injector,
Input,
Expand All @@ -14,6 +15,7 @@ import {
inject,
runInInjectionContext,
signal,
viewChildren,
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import {
Expand All @@ -40,6 +42,7 @@ import {

import { distinctUntilChanged, filter, map } from 'rxjs';

import { SkyDatepickerComponent } from '../datepicker/datepicker.component';
import { SkyDatepickerModule } from '../datepicker/datepicker.module';
import { SkyDatetimeResourcesModule } from '../shared/sky-datetime-resources.module';

Expand Down Expand Up @@ -100,6 +103,9 @@ function isPartialValue(

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'(focusout)': 'onFocusout($event)',
},
imports: [
CommonModule,
FormsModule,
Expand Down Expand Up @@ -134,6 +140,7 @@ export class SkyDateRangePickerComponent
{
readonly #dateRangeSvc = inject(SkyDateRangeService);
readonly #destroyRef = inject(DestroyRef);
readonly #elementRef = inject(ElementRef);
readonly #injector = inject(Injector);
readonly #logger = inject(SkyLogService);

Expand Down Expand Up @@ -255,6 +262,7 @@ export class SkyDateRangePickerComponent
public helpKey: string | undefined;

protected calculators = this.#dateRangeSvc.calculators;
protected datepickers = viewChildren(SkyDatepickerComponent);
protected hostControl: AbstractControl | null | undefined;
protected selectedCalculator = this.calculators[0];
protected showEndDatePicker = signal<boolean>(false);
Expand Down Expand Up @@ -433,10 +441,6 @@ export class SkyDateRangePickerComponent
}
}

protected onBlur(): void {
this.#notifyTouched?.();
}

/**
* Fires when a user changes the selected calculator ID.
*/
Expand All @@ -458,6 +462,19 @@ export class SkyDateRangePickerComponent
});
}

/**
* Fires when the date range picker loses focus.
*/
protected onFocusout({ relatedTarget }: FocusEvent): void {
if (
relatedTarget &&
!this.#elementRef.nativeElement.contains(relatedTarget) &&
!this.datepickers().some((picker) => picker.containsTarget(relatedTarget))
) {
this.#notifyTouched?.();
}
}

#getCalculator(calculatorId: number): SkyDateRangeCalculator {
const found = this.calculators.find((c) => c.calculatorId === calculatorId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ export class SkyDatepickerComponent
readonly #zIndex: Observable<number> | undefined;

readonly #datepickerHostSvc = inject(SkyDatepickerHostService);
readonly #elementRef = inject(ElementRef);

constructor(
affixService: SkyAffixService,
Expand Down Expand Up @@ -352,6 +353,17 @@ export class SkyDatepickerComponent
}
}

/**
* Whether the datepicker component contains the provided focus event target.
* @internal
*/
public containsTarget(target: EventTarget): boolean {
return (
this.#elementRef.nativeElement.contains(target) ||
this.getPickerRef()?.nativeElement.contains(target)
);
}

/**
* Gets the element reference of the picker overlay.
* @internal
Expand Down

0 comments on commit 3b84077

Please sign in to comment.