Skip to content

Commit 5c11a46

Browse files
committed
feat(material/datepicker): Allow user to jump between start and end dates with arrow keys
1 parent 5b8d521 commit 5c11a46

File tree

3 files changed

+145
-7
lines changed

3 files changed

+145
-7
lines changed

src/material/datepicker/date-range-input-parts.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Directive,
1111
ElementRef,
1212
Optional,
13+
inject,
1314
InjectionToken,
1415
Inject,
1516
OnInit,
@@ -36,7 +37,8 @@ import {
3637
MatDateFormats,
3738
ErrorStateMatcher,
3839
} from '@angular/material/core';
39-
import {BACKSPACE} from '@angular/cdk/keycodes';
40+
import {Directionality} from '@angular/cdk/bidi';
41+
import {BACKSPACE, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
4042
import {MatDatepickerInputBase, DateFilterFn} from './datepicker-input-base';
4143
import {DateRange, DateSelectionModelChange} from './date-selection-model';
4244

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

91+
protected readonly _dir = inject(Directionality, InjectFlags.Optional);
92+
8993
constructor(
9094
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent<D>,
91-
elementRef: ElementRef<HTMLInputElement>,
95+
public override _elementRef: ElementRef<HTMLInputElement>,
9296
public _defaultErrorStateMatcher: ErrorStateMatcher,
9397
private _injector: Injector,
9498
@Optional() public _parentForm: NgForm,
9599
@Optional() public _parentFormGroup: FormGroupDirective,
96100
@Optional() dateAdapter: DateAdapter<D>,
97101
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats,
98102
) {
99-
super(elementRef, dateAdapter, dateFormats);
103+
super(_elementRef, dateAdapter, dateFormats);
100104
}
101105

102106
ngOnInit() {
@@ -284,6 +288,26 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpd
284288
const value = element.value;
285289
return value.length > 0 ? value : element.placeholder;
286290
}
291+
292+
override _onKeydown(event: KeyboardEvent) {
293+
const endInput = this._rangeInput._endInput;
294+
const element = this._elementRef.nativeElement;
295+
const isLtr = this._dir?.value !== 'rtl';
296+
297+
// If the user hits RIGHT (LTR) when at the end of the input (and no
298+
// selection), move the cursor to the start of the end input.
299+
if (
300+
((event.keyCode === RIGHT_ARROW && isLtr) || (event.keyCode === LEFT_ARROW && !isLtr)) &&
301+
element.selectionStart === element.value.length &&
302+
element.selectionEnd === element.value.length
303+
) {
304+
event.preventDefault();
305+
endInput._elementRef.nativeElement.setSelectionRange(0, 0);
306+
endInput.focus();
307+
}
308+
309+
super._onKeydown(event);
310+
}
287311
}
288312

289313
/** Input for entering the end date in a `mat-date-range-input`. */
@@ -370,9 +394,26 @@ export class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdat
370394
}
371395

372396
override _onKeydown(event: KeyboardEvent) {
397+
const startInput = this._rangeInput._startInput;
398+
const element = this._elementRef.nativeElement;
399+
const isLtr = this._dir?.value !== 'rtl';
400+
373401
// If the user is pressing backspace on an empty end input, move focus back to the start.
374-
if (event.keyCode === BACKSPACE && !this._elementRef.nativeElement.value) {
375-
this._rangeInput._startInput.focus();
402+
if (event.keyCode === BACKSPACE && !element.value) {
403+
startInput.focus();
404+
}
405+
406+
// If the user hits LEFT (LTR) when at the start of the input (and no
407+
// selection), move the cursor to the end of the start input.
408+
if (
409+
((event.keyCode === LEFT_ARROW && isLtr) || (event.keyCode === RIGHT_ARROW && !isLtr)) &&
410+
element.selectionStart === 0 &&
411+
element.selectionEnd === 0
412+
) {
413+
event.preventDefault();
414+
const endPosition = startInput._elementRef.nativeElement.value.length;
415+
startInput._elementRef.nativeElement.setSelectionRange(endPosition, endPosition);
416+
startInput.focus();
376417
}
377418

378419
super._onKeydown(event);

src/material/datepicker/date-range-input.spec.ts

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Type, Component, ViewChild, ElementRef, Directive} from '@angular/core';
1+
import {Type, Component, ViewChild, ElementRef, Directive, Provider} from '@angular/core';
22
import {ComponentFixture, TestBed, inject, fakeAsync, tick, flush} from '@angular/core/testing';
33
import {
44
FormsModule,
@@ -10,14 +10,15 @@ import {
1010
NgModel,
1111
} from '@angular/forms';
1212
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
13+
import {Directionality} from '@angular/cdk/bidi';
1314
import {OverlayContainer} from '@angular/cdk/overlay';
1415
import {ErrorStateMatcher, MatNativeDateModule} from '@angular/material/core';
1516
import {MatDatepickerModule} from './datepicker-module';
1617
import {MatLegacyFormFieldModule} from '@angular/material/legacy-form-field';
1718
import {MatLegacyInputModule} from '@angular/material/legacy-input';
1819
import {dispatchFakeEvent, dispatchKeyboardEvent} from '../../cdk/testing/private';
1920
import {FocusMonitor} from '@angular/cdk/a11y';
20-
import {BACKSPACE} from '@angular/cdk/keycodes';
21+
import {BACKSPACE, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
2122
import {MatDateRangeInput} from './date-range-input';
2223
import {MatDateRangePicker} from './date-range-picker';
2324
import {MatStartDate, MatEndDate} from './date-range-input-parts';
@@ -27,6 +28,7 @@ describe('MatDateRangeInput', () => {
2728
function createComponent<T>(
2829
component: Type<T>,
2930
declarations: Type<any>[] = [],
31+
providers: Provider[] = [],
3032
): ComponentFixture<T> {
3133
TestBed.configureTestingModule({
3234
imports: [
@@ -38,6 +40,7 @@ describe('MatDateRangeInput', () => {
3840
ReactiveFormsModule,
3941
MatNativeDateModule,
4042
],
43+
providers,
4144
declarations: [component, ...declarations],
4245
});
4346

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

727+
it('moves focus between fields with arrow keys when cursor is at edge (LTR)', () => {
728+
const fixture = createComponent(StandardRangePicker);
729+
fixture.detectChanges();
730+
const {start, end} = fixture.componentInstance;
731+
732+
start.nativeElement.value = '09/10/2020';
733+
end.nativeElement.value = '10/10/2020';
734+
735+
start.nativeElement.focus();
736+
start.nativeElement.setSelectionRange(9, 9);
737+
dispatchKeyboardEvent(start.nativeElement, 'keydown', RIGHT_ARROW);
738+
fixture.detectChanges();
739+
expect(document.activeElement).toBe(start.nativeElement);
740+
741+
start.nativeElement.setSelectionRange(10, 10);
742+
dispatchKeyboardEvent(start.nativeElement, 'keydown', LEFT_ARROW);
743+
fixture.detectChanges();
744+
expect(document.activeElement).toBe(start.nativeElement);
745+
746+
start.nativeElement.setSelectionRange(10, 10);
747+
dispatchKeyboardEvent(start.nativeElement, 'keydown', RIGHT_ARROW);
748+
fixture.detectChanges();
749+
expect(document.activeElement).toBe(end.nativeElement);
750+
751+
end.nativeElement.setSelectionRange(1, 1);
752+
dispatchKeyboardEvent(end.nativeElement, 'keydown', LEFT_ARROW);
753+
fixture.detectChanges();
754+
expect(document.activeElement).toBe(end.nativeElement);
755+
756+
end.nativeElement.setSelectionRange(0, 0);
757+
dispatchKeyboardEvent(end.nativeElement, 'keydown', RIGHT_ARROW);
758+
fixture.detectChanges();
759+
expect(document.activeElement).toBe(end.nativeElement);
760+
761+
end.nativeElement.setSelectionRange(0, 0);
762+
dispatchKeyboardEvent(end.nativeElement, 'keydown', LEFT_ARROW);
763+
fixture.detectChanges();
764+
expect(document.activeElement).toBe(start.nativeElement);
765+
});
766+
767+
it('moves focus between fields with arrow keys when cursor is at edge (RTL)', () => {
768+
class RTL extends Directionality {
769+
override readonly value = 'rtl';
770+
}
771+
const fixture = createComponent(
772+
StandardRangePicker,
773+
[],
774+
[
775+
{
776+
provide: Directionality,
777+
useFactory: () => new RTL(null),
778+
},
779+
],
780+
);
781+
fixture.detectChanges();
782+
const {start, end} = fixture.componentInstance;
783+
784+
start.nativeElement.value = '09/10/2020';
785+
end.nativeElement.value = '10/10/2020';
786+
787+
start.nativeElement.focus();
788+
start.nativeElement.setSelectionRange(9, 9);
789+
dispatchKeyboardEvent(start.nativeElement, 'keydown', LEFT_ARROW);
790+
fixture.detectChanges();
791+
expect(document.activeElement).toBe(start.nativeElement);
792+
793+
start.nativeElement.setSelectionRange(10, 10);
794+
dispatchKeyboardEvent(start.nativeElement, 'keydown', RIGHT_ARROW);
795+
fixture.detectChanges();
796+
expect(document.activeElement).toBe(start.nativeElement);
797+
798+
start.nativeElement.setSelectionRange(10, 10);
799+
dispatchKeyboardEvent(start.nativeElement, 'keydown', LEFT_ARROW);
800+
fixture.detectChanges();
801+
expect(document.activeElement).toBe(end.nativeElement);
802+
803+
end.nativeElement.setSelectionRange(1, 1);
804+
dispatchKeyboardEvent(end.nativeElement, 'keydown', RIGHT_ARROW);
805+
fixture.detectChanges();
806+
expect(document.activeElement).toBe(end.nativeElement);
807+
808+
end.nativeElement.setSelectionRange(0, 0);
809+
dispatchKeyboardEvent(end.nativeElement, 'keydown', LEFT_ARROW);
810+
fixture.detectChanges();
811+
expect(document.activeElement).toBe(end.nativeElement);
812+
813+
end.nativeElement.setSelectionRange(0, 0);
814+
dispatchKeyboardEvent(end.nativeElement, 'keydown', RIGHT_ARROW);
815+
fixture.detectChanges();
816+
expect(document.activeElement).toBe(start.nativeElement);
817+
});
818+
724819
it('should be able to get the input placeholder', () => {
725820
const fixture = createComponent(StandardRangePicker);
726821
fixture.detectChanges();

tools/public_api_guard/material/datepicker.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,8 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpd
893893
// (undocumented)
894894
protected _getValueFromModel(modelValue: DateRange<D>): D | null;
895895
// (undocumented)
896+
_onKeydown(event: KeyboardEvent): void;
897+
// (undocumented)
896898
protected _shouldHandleChangeEvent(change: DateSelectionModelChange<DateRange<D>>): boolean;
897899
// (undocumented)
898900
protected _validator: ValidatorFn | null;

0 commit comments

Comments
 (0)