Skip to content

Commit 60aa620

Browse files
committed
feat(datepicker): set up input for date range picker
Sets up the UI and most of the boilerplate that we'll need for the input that is associated with a date range picker. Doesn't include any interactions with the datepicker itself yet.
1 parent 2ce2dbc commit 60aa620

File tree

9 files changed

+632
-1
lines changed

9 files changed

+632
-1
lines changed

src/dev-app/datepicker/datepicker-demo.html

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,37 @@ <h2>Datepicker with custom header extending the default header</h2>
169169
<mat-datepicker #customHeaderNgContentPicker [calendarHeaderComponent]="customHeaderNgContent"></mat-datepicker>
170170
</mat-form-field>
171171
</p>
172+
173+
<h2>Range picker</h2>
174+
<p>
175+
<mat-form-field>
176+
<mat-label>Enter a date range</mat-label>
177+
<mat-date-range-input>
178+
<input matStartDate placeholder="Start date">
179+
<input matEndDate placeholder="End date">
180+
</mat-date-range-input>
181+
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
182+
</mat-form-field>
183+
</p>
184+
185+
<p>
186+
<mat-form-field appearance="fill">
187+
<mat-label>Enter a date range</mat-label>
188+
<mat-date-range-input>
189+
<input matStartDate placeholder="Start date">
190+
<input matEndDate placeholder="End date">
191+
</mat-date-range-input>
192+
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
193+
</mat-form-field>
194+
</p>
195+
196+
<p>
197+
<mat-form-field appearance="outline">
198+
<mat-label>Enter a date range</mat-label>
199+
<mat-date-range-input>
200+
<input matStartDate placeholder="Start date">
201+
<input matEndDate placeholder="End date">
202+
</mat-date-range-input>
203+
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
204+
</mat-form-field>
205+
</p>

src/material/datepicker/BUILD.bazel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ ng_module(
1919
assets = [
2020
":datepicker-content.css",
2121
":datepicker-toggle.css",
22+
":date-range-input.css",
2223
":calendar-body.css",
2324
":calendar.css",
2425
] + glob(["**/*.html"]),
@@ -71,6 +72,12 @@ sass_binary(
7172
deps = ["//src/cdk/a11y:a11y_scss_lib"],
7273
)
7374

75+
sass_binary(
76+
name = "date_range_input_scss",
77+
src = "date-range-input.scss",
78+
deps = ["//src/material/core:core_scss_lib"],
79+
)
80+
7481
ng_test_library(
7582
name = "unit_test_sources",
7683
srcs = glob(
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Directive, ElementRef, Optional, Self, InjectionToken, Inject} from '@angular/core';
10+
import {
11+
NG_VALUE_ACCESSOR,
12+
NG_VALIDATORS,
13+
ControlValueAccessor,
14+
Validator,
15+
AbstractControl,
16+
ValidationErrors,
17+
NgForm,
18+
FormGroupDirective,
19+
NgControl,
20+
} from '@angular/forms';
21+
import {
22+
CanUpdateErrorState,
23+
CanDisable,
24+
ErrorStateMatcher,
25+
CanDisableCtor,
26+
CanUpdateErrorStateCtor,
27+
mixinErrorState,
28+
mixinDisabled,
29+
} from '@angular/material/core';
30+
import {BooleanInput} from '@angular/cdk/coercion';
31+
32+
/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */
33+
export interface MatDateRangeInputParent {
34+
id: string;
35+
_ariaDescribedBy: string | null;
36+
_ariaLabelledBy: string | null;
37+
_handleChildValueChange: () => void;
38+
}
39+
40+
/**
41+
* Used to provide the date range input wrapper component
42+
* to the parts without circular dependencies.
43+
*/
44+
export const MAT_DATE_RANGE_INPUT_PARENT =
45+
new InjectionToken<MatDateRangeInputParent>('MAT_DATE_RANGE_INPUT_PARENT');
46+
47+
// Boilerplate for applying mixins to MatDateRangeInput.
48+
/** @docs-private */
49+
class MatDateRangeInputPartMixinBase {
50+
constructor(public _defaultErrorStateMatcher: ErrorStateMatcher,
51+
public _parentForm: NgForm,
52+
public _parentFormGroup: FormGroupDirective,
53+
/** @docs-private */
54+
public ngControl: NgControl) {}
55+
}
56+
const _MatDateRangeInputMixinBase: CanDisableCtor &
57+
CanUpdateErrorStateCtor & typeof MatDateRangeInputPartMixinBase =
58+
mixinErrorState(mixinDisabled(MatDateRangeInputPartMixinBase));
59+
60+
/**
61+
* Base class for the individual inputs that can be projected inside a `mat-date-range-input`.
62+
*/
63+
@Directive()
64+
abstract class MatDateRangeInputPartBase<D> extends _MatDateRangeInputMixinBase implements
65+
ControlValueAccessor, Validator, CanUpdateErrorState, CanDisable, CanUpdateErrorState {
66+
67+
private _onTouched = () => {};
68+
69+
constructor(
70+
protected _elementRef: ElementRef<HTMLInputElement>,
71+
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent,
72+
defaultErrorStateMatcher: ErrorStateMatcher,
73+
@Optional() parentForm: NgForm,
74+
@Optional() parentFormGroup: FormGroupDirective,
75+
@Optional() @Self() ngControl: NgControl) {
76+
super(defaultErrorStateMatcher, parentForm, parentFormGroup, ngControl);
77+
}
78+
79+
/** @docs-private */
80+
writeValue(_value: D | null): void {
81+
// TODO(crisbeto): implement
82+
}
83+
84+
/** @docs-private */
85+
registerOnChange(_fn: () => void): void {
86+
// TODO(crisbeto): implement
87+
}
88+
89+
/** @docs-private */
90+
registerOnTouched(fn: () => void): void {
91+
this._onTouched = fn;
92+
}
93+
94+
/** @docs-private */
95+
setDisabledState(isDisabled: boolean): void {
96+
this.disabled = isDisabled;
97+
}
98+
99+
/** @docs-private */
100+
validate(_control: AbstractControl): ValidationErrors | null {
101+
// TODO(crisbeto): implement
102+
return null;
103+
}
104+
105+
/** @docs-private */
106+
registerOnValidatorChange(_fn: () => void): void {
107+
// TODO(crisbeto): implement
108+
}
109+
110+
/** Gets whether the input is empty. */
111+
isEmpty(): boolean {
112+
// TODO(crisbeto): should look at the CVA value.
113+
return this._elementRef.nativeElement.value.length === 0;
114+
}
115+
116+
/** Focuses the input. */
117+
focus(): void {
118+
this._elementRef.nativeElement.focus();
119+
}
120+
121+
/** Handles blur events on the input. */
122+
_handleBlur(): void {
123+
this._onTouched();
124+
}
125+
126+
static ngAcceptInputType_disabled: BooleanInput;
127+
}
128+
129+
130+
/** Input for entering the start date in a `mat-date-range-input`. */
131+
@Directive({
132+
selector: 'input[matStartDate]',
133+
inputs: ['disabled'],
134+
host: {
135+
'[id]': '_rangeInput.id',
136+
'[attr.aria-labelledby]': '_rangeInput._ariaLabelledBy',
137+
'[attr.aria-describedby]': '_rangeInput._ariaDescribedBy',
138+
'class': 'mat-date-range-input-inner',
139+
'type': 'text',
140+
'(blur)': '_handleBlur()',
141+
'(input)': '_rangeInput._handleChildValueChange()'
142+
},
143+
providers: [
144+
{provide: NG_VALUE_ACCESSOR, useExisting: MatStartDate, multi: true},
145+
{provide: NG_VALIDATORS, useExisting: MatStartDate, multi: true}
146+
]
147+
})
148+
export class MatStartDate<D> extends MatDateRangeInputPartBase<D> {
149+
/** Gets the value that should be used when mirroring the input's size. */
150+
getMirrorValue(): string {
151+
const element = this._elementRef.nativeElement;
152+
const value = element.value;
153+
return value.length > 0 ? value : element.placeholder;
154+
}
155+
156+
static ngAcceptInputType_disabled: BooleanInput;
157+
}
158+
159+
160+
/** Input for entering the end date in a `mat-date-range-input`. */
161+
@Directive({
162+
selector: 'input[matEndDate]',
163+
inputs: ['disabled'],
164+
host: {
165+
'class': 'mat-date-range-input-inner',
166+
'[attr.aria-labelledby]': '_rangeInput._ariaLabelledBy',
167+
'[attr.aria-describedby]': '_rangeInput._ariaDescribedBy',
168+
'(focus)': '_handleFocusEvent(true)',
169+
'(blur)': '_handleFocusEvent(false)',
170+
'type': 'text',
171+
},
172+
providers: [
173+
{provide: NG_VALUE_ACCESSOR, useExisting: MatEndDate, multi: true},
174+
{provide: NG_VALIDATORS, useExisting: MatEndDate, multi: true}
175+
]
176+
})
177+
export class MatEndDate<D> extends MatDateRangeInputPartBase<D> {
178+
/** Whether the input is currently focused. */
179+
_hasFocus = false;
180+
181+
/** Handles focus/blur events on the input. */
182+
_handleFocusEvent(hasFocus: boolean) {
183+
this._hasFocus = hasFocus;
184+
185+
if (!hasFocus) {
186+
this._handleBlur();
187+
}
188+
}
189+
190+
static ngAcceptInputType_disabled: BooleanInput;
191+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<div
2+
class="mat-date-range-input-container"
3+
cdkMonitorSubtreeFocus
4+
(cdkFocusChange)="focused = $event !== null">
5+
<div class="mat-date-range-input-start-wrapper">
6+
<ng-content select="input[matStartDate]"></ng-content>
7+
<span
8+
class="mat-date-range-input-mirror"
9+
aria-hidden="true">{{_getInputMirrorValue()}}</span>
10+
</div>
11+
12+
<span class="mat-date-range-input-separator">{{separator}}</span>
13+
14+
<div class="mat-date-range-input-end-wrapper">
15+
<ng-content select="input[matEndDate]"></ng-content>
16+
</div>
17+
</div>
18+
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
@import '../core/style/variables';
2+
@import '../core/style/vendor-prefixes';
3+
4+
$mat-date-range-input-separator-spacing: 4px;
5+
$mat-date-range-input-part-max-width: calc(50% - #{$mat-date-range-input-separator-spacing});
6+
$mat-date-range-input-placeholder-transition:
7+
color $swift-ease-out-duration $swift-ease-out-duration / 3 $swift-ease-out-timing-function;
8+
9+
// Host of the date range input.
10+
.mat-date-range-input {
11+
display: block;
12+
width: 100%;
13+
}
14+
15+
// Inner container that wraps around all the content.
16+
.mat-date-range-input-container {
17+
display: flex;
18+
align-items: center;
19+
}
20+
21+
// Text shown between the two inputs.
22+
.mat-date-range-input-separator {
23+
margin: 0 $mat-date-range-input-separator-spacing;
24+
transition: $mat-date-range-input-placeholder-transition;
25+
26+
.mat-form-field-hide-placeholder &,
27+
.mat-date-range-input-hide-separator & {
28+
color: transparent;
29+
transition: none;
30+
}
31+
}
32+
33+
// Underlying input inside the range input.
34+
.mat-date-range-input-inner {
35+
// Reset the input so it's just a transparent rectangle.
36+
font: inherit;
37+
background: transparent;
38+
color: currentColor;
39+
border: none;
40+
outline: none;
41+
padding: 0;
42+
margin: 0;
43+
vertical-align: bottom;
44+
text-align: inherit;
45+
-webkit-appearance: none;
46+
width: 100%;
47+
48+
// Remove IE's default clear and reveal icons.
49+
&::-ms-clear,
50+
&::-ms-reveal {
51+
display: none;
52+
}
53+
54+
@include input-placeholder {
55+
transition: $mat-date-range-input-placeholder-transition;
56+
}
57+
58+
.mat-form-field-hide-placeholder &,
59+
.mat-date-range-input-hide-placeholders & {
60+
@include input-placeholder {
61+
// Needs to be !important, because the placeholder will end up inheriting the
62+
// input color in IE, if the consumer overrides it with a higher specificity.
63+
color: transparent !important;
64+
-webkit-text-fill-color: transparent;
65+
transition: none;
66+
}
67+
}
68+
}
69+
70+
// We want the start input to be flush against the separator, no matter how much text it has, but
71+
// the problem is that inputs have a fixed width. We work around the issue by implementing an
72+
// auto-resizing input that stretches based on its text, up to a point. It works by having
73+
// a relatively-positioned wrapper (`.mat-date-range-input-start-wrapper` below) and an absolutely-
74+
// positioned `input`, as well as a `span` inside the wrapper which mirrors the input's value and
75+
// placeholder. As the user is typing, the value gets mirrored in the span which causes the wrapper
76+
// to stretch and the input with it.
77+
.mat-date-range-input-mirror {
78+
// Disable user selection so users don't accidentally copy the text via ctrl + A.
79+
@include user-select(none);
80+
81+
// Hide the element so it doesn't get read out by screen
82+
// readers and it doesn't show up behind the input.
83+
visibility: hidden;
84+
85+
// Text inside inputs never wraps so the one in the span shouldn't either.
86+
white-space: nowrap;
87+
display: inline-block;
88+
89+
// Prevent the container from collapsing. Make it more
90+
// than 1px so the input caret doesn't get clipped.
91+
min-width: 2px;
92+
}
93+
94+
// Wrapper around the start input. Used to facilitate the auto-resizing input.
95+
.mat-date-range-input-start-wrapper {
96+
position: relative;
97+
overflow: hidden;
98+
max-width: $mat-date-range-input-part-max-width;
99+
100+
.mat-date-range-input-inner {
101+
position: absolute;
102+
top: 0;
103+
left: 0;
104+
}
105+
}
106+
107+
// Wrapper around the end input that makes sure that it has the proper size.
108+
.mat-date-range-input-end-wrapper {
109+
flex-grow: 1;
110+
max-width: $mat-date-range-input-part-max-width;
111+
}
112+
113+
.mat-form-field-type-mat-date-range-input .mat-form-field-infix {
114+
// Bump the default width slightly since it's somewhat cramped with two inputs and a separator.
115+
width: 200px;
116+
}

0 commit comments

Comments
 (0)