Skip to content

feat(material/datepicker): Allow user to jump between start and end d… #25359

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions src/material/datepicker/date-range-input-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Directive,
ElementRef,
Optional,
inject,
InjectionToken,
Inject,
OnInit,
Expand All @@ -36,7 +37,8 @@ import {
MatDateFormats,
ErrorStateMatcher,
} from '@angular/material/core';
import {BACKSPACE} from '@angular/cdk/keycodes';
import {Directionality} from '@angular/cdk/bidi';
import {BACKSPACE, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
import {MatDatepickerInputBase, DateFilterFn} from './datepicker-input-base';
import {DateRange, DateSelectionModelChange} from './date-selection-model';

Expand Down Expand Up @@ -86,17 +88,19 @@ abstract class MatDateRangeInputPartBase<D>
protected abstract override _assignValueToModel(value: D | null): void;
protected abstract override _getValueFromModel(modelValue: DateRange<D>): D | null;

protected readonly _dir = inject(Directionality, InjectFlags.Optional);

constructor(
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent<D>,
elementRef: ElementRef<HTMLInputElement>,
public override _elementRef: ElementRef<HTMLInputElement>,
public _defaultErrorStateMatcher: ErrorStateMatcher,
private _injector: Injector,
@Optional() public _parentForm: NgForm,
@Optional() public _parentFormGroup: FormGroupDirective,
@Optional() dateAdapter: DateAdapter<D>,
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats,
) {
super(elementRef, dateAdapter, dateFormats);
super(_elementRef, dateAdapter, dateFormats);
}

ngOnInit() {
Expand Down Expand Up @@ -284,6 +288,26 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpd
const value = element.value;
return value.length > 0 ? value : element.placeholder;
}

override _onKeydown(event: KeyboardEvent) {
const endInput = this._rangeInput._endInput;
const element = this._elementRef.nativeElement;
const isLtr = this._dir?.value !== 'rtl';

// If the user hits RIGHT (LTR) when at the end of the input (and no
// selection), move the cursor to the start of the end input.
if (
((event.keyCode === RIGHT_ARROW && isLtr) || (event.keyCode === LEFT_ARROW && !isLtr)) &&
element.selectionStart === element.value.length &&
element.selectionEnd === element.value.length
) {
event.preventDefault();
endInput._elementRef.nativeElement.setSelectionRange(0, 0);
endInput.focus();
} else {
super._onKeydown(event);
}
}
}

/** Input for entering the end date in a `mat-date-range-input`. */
Expand Down Expand Up @@ -370,11 +394,27 @@ export class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdat
}

override _onKeydown(event: KeyboardEvent) {
const startInput = this._rangeInput._startInput;
const element = this._elementRef.nativeElement;
const isLtr = this._dir?.value !== 'rtl';

// If the user is pressing backspace on an empty end input, move focus back to the start.
if (event.keyCode === BACKSPACE && !this._elementRef.nativeElement.value) {
this._rangeInput._startInput.focus();
if (event.keyCode === BACKSPACE && !element.value) {
startInput.focus();
}
// If the user hits LEFT (LTR) when at the start of the input (and no
// selection), move the cursor to the end of the start input.
else if (
((event.keyCode === LEFT_ARROW && isLtr) || (event.keyCode === RIGHT_ARROW && !isLtr)) &&
element.selectionStart === 0 &&
element.selectionEnd === 0
) {
event.preventDefault();
const endPosition = startInput._elementRef.nativeElement.value.length;
startInput._elementRef.nativeElement.setSelectionRange(endPosition, endPosition);
startInput.focus();
} else {
super._onKeydown(event);
}

super._onKeydown(event);
}
}
99 changes: 97 additions & 2 deletions src/material/datepicker/date-range-input.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Type, Component, ViewChild, ElementRef, Directive} from '@angular/core';
import {Type, Component, ViewChild, ElementRef, Directive, Provider} from '@angular/core';
import {ComponentFixture, TestBed, inject, fakeAsync, tick, flush} from '@angular/core/testing';
import {
FormsModule,
Expand All @@ -10,14 +10,15 @@ import {
NgModel,
} from '@angular/forms';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Directionality} from '@angular/cdk/bidi';
import {OverlayContainer} from '@angular/cdk/overlay';
import {ErrorStateMatcher, MatNativeDateModule} from '@angular/material/core';
import {MatDatepickerModule} from './datepicker-module';
import {MatLegacyFormFieldModule} from '@angular/material/legacy-form-field';
import {MatLegacyInputModule} from '@angular/material/legacy-input';
import {dispatchFakeEvent, dispatchKeyboardEvent} from '../../cdk/testing/private';
import {FocusMonitor} from '@angular/cdk/a11y';
import {BACKSPACE} from '@angular/cdk/keycodes';
import {BACKSPACE, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
import {MatDateRangeInput} from './date-range-input';
import {MatDateRangePicker} from './date-range-picker';
import {MatStartDate, MatEndDate} from './date-range-input-parts';
Expand All @@ -27,6 +28,7 @@ describe('MatDateRangeInput', () => {
function createComponent<T>(
component: Type<T>,
declarations: Type<any>[] = [],
providers: Provider[] = [],
): ComponentFixture<T> {
TestBed.configureTestingModule({
imports: [
Expand All @@ -38,6 +40,7 @@ describe('MatDateRangeInput', () => {
ReactiveFormsModule,
MatNativeDateModule,
],
providers,
declarations: [component, ...declarations],
});

Expand Down Expand Up @@ -721,6 +724,98 @@ describe('MatDateRangeInput', () => {
expect(start.nativeElement.focus).not.toHaveBeenCalled();
});

it('moves focus between fields with arrow keys when cursor is at edge (LTR)', () => {
const fixture = createComponent(StandardRangePicker);
fixture.detectChanges();
const {start, end} = fixture.componentInstance;

start.nativeElement.value = '09/10/2020';
end.nativeElement.value = '10/10/2020';

start.nativeElement.focus();
start.nativeElement.setSelectionRange(9, 9);
dispatchKeyboardEvent(start.nativeElement, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(start.nativeElement);

start.nativeElement.setSelectionRange(10, 10);
dispatchKeyboardEvent(start.nativeElement, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(start.nativeElement);

start.nativeElement.setSelectionRange(10, 10);
dispatchKeyboardEvent(start.nativeElement, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(end.nativeElement);

end.nativeElement.setSelectionRange(1, 1);
dispatchKeyboardEvent(end.nativeElement, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(end.nativeElement);

end.nativeElement.setSelectionRange(0, 0);
dispatchKeyboardEvent(end.nativeElement, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(end.nativeElement);

end.nativeElement.setSelectionRange(0, 0);
dispatchKeyboardEvent(end.nativeElement, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(start.nativeElement);
});

it('moves focus between fields with arrow keys when cursor is at edge (RTL)', () => {
class RTL extends Directionality {
override readonly value = 'rtl';
}
const fixture = createComponent(
StandardRangePicker,
[],
[
{
provide: Directionality,
useFactory: () => new RTL(null),
},
],
);
fixture.detectChanges();
const {start, end} = fixture.componentInstance;

start.nativeElement.value = '09/10/2020';
end.nativeElement.value = '10/10/2020';

start.nativeElement.focus();
start.nativeElement.setSelectionRange(9, 9);
dispatchKeyboardEvent(start.nativeElement, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(start.nativeElement);

start.nativeElement.setSelectionRange(10, 10);
dispatchKeyboardEvent(start.nativeElement, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(start.nativeElement);

start.nativeElement.setSelectionRange(10, 10);
dispatchKeyboardEvent(start.nativeElement, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(end.nativeElement);

end.nativeElement.setSelectionRange(1, 1);
dispatchKeyboardEvent(end.nativeElement, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(end.nativeElement);

end.nativeElement.setSelectionRange(0, 0);
dispatchKeyboardEvent(end.nativeElement, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(end.nativeElement);

end.nativeElement.setSelectionRange(0, 0);
dispatchKeyboardEvent(end.nativeElement, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(start.nativeElement);
});

it('should be able to get the input placeholder', () => {
const fixture = createComponent(StandardRangePicker);
fixture.detectChanges();
Expand Down
2 changes: 2 additions & 0 deletions tools/public_api_guard/material/datepicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,8 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpd
// (undocumented)
protected _getValueFromModel(modelValue: DateRange<D>): D | null;
// (undocumented)
_onKeydown(event: KeyboardEvent): void;
// (undocumented)
protected _shouldHandleChangeEvent(change: DateSelectionModelChange<DateRange<D>>): boolean;
// (undocumented)
protected _validator: ValidatorFn | null;
Expand Down