Skip to content

Commit 0c5fc4c

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

File tree

1 file changed

+66
-5
lines changed

1 file changed

+66
-5
lines changed

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

Lines changed: 66 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,9 +37,12 @@ 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';
44+
import {Subject} from 'rxjs';
45+
import {filter, take} from 'rxjs/operators';
4246

4347
/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */
4448
export interface MatDateRangeInputParent<D> {
@@ -86,17 +90,20 @@ abstract class MatDateRangeInputPartBase<D>
8690
protected abstract override _assignValueToModel(value: D | null): void;
8791
protected abstract override _getValueFromModel(modelValue: DateRange<D>): D | null;
8892

93+
protected readonly _dir = inject(Directionality, InjectFlags.Optional);
94+
private readonly _onKeyupSubject = new Subject<KeyboardEvent>();
95+
8996
constructor(
9097
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent<D>,
91-
elementRef: ElementRef<HTMLInputElement>,
98+
public override _elementRef: ElementRef<HTMLInputElement>,
9299
public _defaultErrorStateMatcher: ErrorStateMatcher,
93100
private _injector: Injector,
94101
@Optional() public _parentForm: NgForm,
95102
@Optional() public _parentFormGroup: FormGroupDirective,
96103
@Optional() dateAdapter: DateAdapter<D>,
97104
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats,
98105
) {
99-
super(elementRef, dateAdapter, dateFormats);
106+
super(_elementRef, dateAdapter, dateFormats);
100107
}
101108

102109
ngOnInit() {
@@ -181,6 +188,26 @@ abstract class MatDateRangeInputPartBase<D>
181188
) as MatDateRangeInputPartBase<D> | undefined;
182189
opposite?._validatorOnChange();
183190
}
191+
192+
protected _onKeyup(event: KeyboardEvent) {
193+
this._onKeyupSubject.next(event);
194+
}
195+
196+
protected _moveFocusOnKeyup(
197+
target: MatDateRangeInputPartBase<D>,
198+
downEvent: KeyboardEvent,
199+
cursorPosition: number,
200+
) {
201+
this._onKeyupSubject
202+
.pipe(
203+
filter(upEvent => upEvent.keyCode === downEvent.keyCode),
204+
take(1),
205+
)
206+
.subscribe(() => {
207+
target._elementRef.nativeElement.setSelectionRange(cursorPosition, cursorPosition);
208+
target.focus();
209+
});
210+
}
184211
}
185212

186213
const _MatDateRangeInputBase = mixinErrorState(MatDateRangeInputPartBase);
@@ -194,6 +221,7 @@ const _MatDateRangeInputBase = mixinErrorState(MatDateRangeInputPartBase);
194221
'(input)': '_onInput($event.target.value)',
195222
'(change)': '_onChange()',
196223
'(keydown)': '_onKeydown($event)',
224+
'(keyup)': '_onKeyup($event)',
197225
'[attr.id]': '_rangeInput.id',
198226
'[attr.aria-haspopup]': '_rangeInput.rangePicker ? "dialog" : null',
199227
'[attr.aria-owns]': '(_rangeInput.rangePicker?.opened && _rangeInput.rangePicker.id) || null',
@@ -284,6 +312,24 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpd
284312
const value = element.value;
285313
return value.length > 0 ? value : element.placeholder;
286314
}
315+
316+
override _onKeydown(event: KeyboardEvent) {
317+
const endInput = this._rangeInput._endInput;
318+
const element = this._elementRef.nativeElement;
319+
const isLtr = this._dir?.value !== 'rtl';
320+
321+
// If the user hits RIGHT (LTR) when at the end of the input (and no
322+
// selection), move the cursor to the start of the end input.
323+
if (
324+
((event.keyCode === RIGHT_ARROW && isLtr) || (event.keyCode === LEFT_ARROW && !isLtr)) &&
325+
element.selectionStart === element.value.length &&
326+
element.selectionEnd === element.value.length
327+
) {
328+
this._moveFocusOnKeyup(endInput, event, 0);
329+
}
330+
331+
super._onKeydown(event);
332+
}
287333
}
288334

289335
/** Input for entering the end date in a `mat-date-range-input`. */
@@ -295,6 +341,7 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpd
295341
'(input)': '_onInput($event.target.value)',
296342
'(change)': '_onChange()',
297343
'(keydown)': '_onKeydown($event)',
344+
'(keyup)': '_onKeyup($event)',
298345
'[attr.aria-haspopup]': '_rangeInput.rangePicker ? "dialog" : null',
299346
'[attr.aria-owns]': '(_rangeInput.rangePicker?.opened && _rangeInput.rangePicker.id) || null',
300347
'[attr.min]': '_getMinDate() ? _dateAdapter.toIso8601(_getMinDate()) : null',
@@ -370,9 +417,23 @@ export class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdat
370417
}
371418

372419
override _onKeydown(event: KeyboardEvent) {
420+
const startInput = this._rangeInput._startInput;
421+
const element = this._elementRef.nativeElement;
422+
const isLtr = this._dir?.value !== 'rtl';
423+
373424
// 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();
425+
if (event.keyCode === BACKSPACE && !element.value) {
426+
startInput.focus();
427+
}
428+
429+
// If the user hits LEFT (LTR) when at the start of the input (and no
430+
// selection), move the cursor to the end of the start input.
431+
if (
432+
((event.keyCode === LEFT_ARROW && isLtr) || (event.keyCode === RIGHT_ARROW && !isLtr)) &&
433+
element.selectionStart === 0 &&
434+
element.selectionEnd === 0
435+
) {
436+
this._moveFocusOnKeyup(startInput, event, startInput._elementRef.nativeElement.value.length);
376437
}
377438

378439
super._onKeydown(event);

0 commit comments

Comments
 (0)