diff --git a/src/material/datepicker/date-range-input-parts.ts b/src/material/datepicker/date-range-input-parts.ts index e9254b0e1b88..9f7f460e2a37 100644 --- a/src/material/datepicker/date-range-input-parts.ts +++ b/src/material/datepicker/date-range-input-parts.ts @@ -10,6 +10,7 @@ import { Directive, ElementRef, Optional, + inject, InjectionToken, Inject, OnInit, @@ -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'; @@ -86,9 +88,11 @@ abstract class MatDateRangeInputPartBase protected abstract override _assignValueToModel(value: D | null): void; protected abstract override _getValueFromModel(modelValue: DateRange): D | null; + protected readonly _dir = inject(Directionality, InjectFlags.Optional); + constructor( @Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent, - elementRef: ElementRef, + public override _elementRef: ElementRef, public _defaultErrorStateMatcher: ErrorStateMatcher, private _injector: Injector, @Optional() public _parentForm: NgForm, @@ -96,7 +100,7 @@ abstract class MatDateRangeInputPartBase @Optional() dateAdapter: DateAdapter, @Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats, ) { - super(elementRef, dateAdapter, dateFormats); + super(_elementRef, dateAdapter, dateFormats); } ngOnInit() { @@ -284,6 +288,26 @@ export class MatStartDate extends _MatDateRangeInputBase 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`. */ @@ -370,11 +394,27 @@ export class MatEndDate extends _MatDateRangeInputBase 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); } } diff --git a/src/material/datepicker/date-range-input.spec.ts b/src/material/datepicker/date-range-input.spec.ts index 7869f760369f..6993d2a6d3da 100644 --- a/src/material/datepicker/date-range-input.spec.ts +++ b/src/material/datepicker/date-range-input.spec.ts @@ -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, @@ -10,6 +10,7 @@ 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'; @@ -17,7 +18,7 @@ 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'; @@ -27,6 +28,7 @@ describe('MatDateRangeInput', () => { function createComponent( component: Type, declarations: Type[] = [], + providers: Provider[] = [], ): ComponentFixture { TestBed.configureTestingModule({ imports: [ @@ -38,6 +40,7 @@ describe('MatDateRangeInput', () => { ReactiveFormsModule, MatNativeDateModule, ], + providers, declarations: [component, ...declarations], }); @@ -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(); diff --git a/tools/public_api_guard/material/datepicker.md b/tools/public_api_guard/material/datepicker.md index 543c53a0192f..8f9599e243be 100644 --- a/tools/public_api_guard/material/datepicker.md +++ b/tools/public_api_guard/material/datepicker.md @@ -893,6 +893,8 @@ export class MatStartDate extends _MatDateRangeInputBase implements CanUpd // (undocumented) protected _getValueFromModel(modelValue: DateRange): D | null; // (undocumented) + _onKeydown(event: KeyboardEvent): void; + // (undocumented) protected _shouldHandleChangeEvent(change: DateSelectionModelChange>): boolean; // (undocumented) protected _validator: ValidatorFn | null;