-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(datepicker): set up input for date range picker #18159
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {Directive, ElementRef, Optional, Self, InjectionToken, Inject} from '@angular/core'; | ||
import { | ||
NG_VALUE_ACCESSOR, | ||
NG_VALIDATORS, | ||
ControlValueAccessor, | ||
Validator, | ||
AbstractControl, | ||
ValidationErrors, | ||
NgForm, | ||
FormGroupDirective, | ||
NgControl, | ||
} from '@angular/forms'; | ||
import { | ||
CanUpdateErrorState, | ||
CanDisable, | ||
ErrorStateMatcher, | ||
CanDisableCtor, | ||
CanUpdateErrorStateCtor, | ||
mixinErrorState, | ||
mixinDisabled, | ||
} from '@angular/material/core'; | ||
import {BooleanInput} from '@angular/cdk/coercion'; | ||
|
||
/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */ | ||
export interface MatDateRangeInputParent { | ||
id: string; | ||
_ariaDescribedBy: string | null; | ||
_ariaLabelledBy: string | null; | ||
_handleChildValueChange: () => void; | ||
} | ||
|
||
/** | ||
* Used to provide the date range input wrapper component | ||
* to the parts without circular dependencies. | ||
*/ | ||
export const MAT_DATE_RANGE_INPUT_PARENT = | ||
new InjectionToken<MatDateRangeInputParent>('MAT_DATE_RANGE_INPUT_PARENT'); | ||
|
||
// Boilerplate for applying mixins to MatDateRangeInput. | ||
/** @docs-private */ | ||
class MatDateRangeInputPartMixinBase { | ||
constructor(public _defaultErrorStateMatcher: ErrorStateMatcher, | ||
public _parentForm: NgForm, | ||
public _parentFormGroup: FormGroupDirective, | ||
/** @docs-private */ | ||
public ngControl: NgControl) {} | ||
} | ||
const _MatDateRangeInputMixinBase: CanDisableCtor & | ||
CanUpdateErrorStateCtor & typeof MatDateRangeInputPartMixinBase = | ||
mixinErrorState(mixinDisabled(MatDateRangeInputPartMixinBase)); | ||
|
||
/** | ||
* Base class for the individual inputs that can be projected inside a `mat-date-range-input`. | ||
*/ | ||
@Directive() | ||
abstract class MatDateRangeInputPartBase<D> extends _MatDateRangeInputMixinBase implements | ||
ControlValueAccessor, Validator, CanUpdateErrorState, CanDisable, CanUpdateErrorState { | ||
|
||
private _onTouched = () => {}; | ||
|
||
constructor( | ||
protected _elementRef: ElementRef<HTMLInputElement>, | ||
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent, | ||
defaultErrorStateMatcher: ErrorStateMatcher, | ||
@Optional() parentForm: NgForm, | ||
@Optional() parentFormGroup: FormGroupDirective, | ||
@Optional() @Self() ngControl: NgControl) { | ||
super(defaultErrorStateMatcher, parentForm, parentFormGroup, ngControl); | ||
} | ||
|
||
/** @docs-private */ | ||
writeValue(_value: D | null): void { | ||
// TODO(crisbeto): implement | ||
} | ||
|
||
/** @docs-private */ | ||
registerOnChange(_fn: () => void): void { | ||
// TODO(crisbeto): implement | ||
} | ||
|
||
/** @docs-private */ | ||
registerOnTouched(fn: () => void): void { | ||
this._onTouched = fn; | ||
} | ||
|
||
/** @docs-private */ | ||
setDisabledState(isDisabled: boolean): void { | ||
this.disabled = isDisabled; | ||
} | ||
|
||
/** @docs-private */ | ||
validate(_control: AbstractControl): ValidationErrors | null { | ||
// TODO(crisbeto): implement | ||
return null; | ||
} | ||
|
||
/** @docs-private */ | ||
registerOnValidatorChange(_fn: () => void): void { | ||
// TODO(crisbeto): implement | ||
} | ||
|
||
/** Gets whether the input is empty. */ | ||
isEmpty(): boolean { | ||
// TODO(crisbeto): should look at the CVA value. | ||
return this._elementRef.nativeElement.value.length === 0; | ||
} | ||
|
||
/** Focuses the input. */ | ||
focus(): void { | ||
this._elementRef.nativeElement.focus(); | ||
} | ||
|
||
/** Handles blur events on the input. */ | ||
_handleBlur(): void { | ||
this._onTouched(); | ||
} | ||
|
||
static ngAcceptInputType_disabled: BooleanInput; | ||
} | ||
|
||
|
||
/** Input for entering the start date in a `mat-date-range-input`. */ | ||
@Directive({ | ||
selector: 'input[matStartDate]', | ||
inputs: ['disabled'], | ||
host: { | ||
'[id]': '_rangeInput.id', | ||
'[attr.aria-labelledby]': '_rangeInput._ariaLabelledBy', | ||
'[attr.aria-describedby]': '_rangeInput._ariaDescribedBy', | ||
'class': 'mat-date-range-input-inner', | ||
'type': 'text', | ||
'(blur)': '_handleBlur()', | ||
'(input)': '_rangeInput._handleChildValueChange()' | ||
}, | ||
providers: [ | ||
{provide: NG_VALUE_ACCESSOR, useExisting: MatStartDate, multi: true}, | ||
{provide: NG_VALIDATORS, useExisting: MatStartDate, multi: true} | ||
] | ||
}) | ||
export class MatStartDate<D> extends MatDateRangeInputPartBase<D> { | ||
/** Gets the value that should be used when mirroring the input's size. */ | ||
getMirrorValue(): string { | ||
crisbeto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const element = this._elementRef.nativeElement; | ||
const value = element.value; | ||
return value.length > 0 ? value : element.placeholder; | ||
} | ||
|
||
static ngAcceptInputType_disabled: BooleanInput; | ||
} | ||
|
||
|
||
/** Input for entering the end date in a `mat-date-range-input`. */ | ||
@Directive({ | ||
selector: 'input[matEndDate]', | ||
inputs: ['disabled'], | ||
host: { | ||
'class': 'mat-date-range-input-inner', | ||
'[attr.aria-labelledby]': '_rangeInput._ariaLabelledBy', | ||
'[attr.aria-describedby]': '_rangeInput._ariaDescribedBy', | ||
'(blur)': '_handleBlur', | ||
'type': 'text', | ||
}, | ||
providers: [ | ||
{provide: NG_VALUE_ACCESSOR, useExisting: MatEndDate, multi: true}, | ||
{provide: NG_VALIDATORS, useExisting: MatEndDate, multi: true} | ||
] | ||
}) | ||
export class MatEndDate<D> extends MatDateRangeInputPartBase<D> { | ||
static ngAcceptInputType_disabled: BooleanInput; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<div | ||
class="mat-date-range-input-container" | ||
cdkMonitorSubtreeFocus | ||
(cdkFocusChange)="focused = $event !== null"> | ||
<div class="mat-date-range-input-start-wrapper"> | ||
<ng-content select="input[matStartDate]"></ng-content> | ||
<span | ||
class="mat-date-range-input-mirror" | ||
jelbourn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
aria-hidden="true">{{_getInputMirrorValue()}}</span> | ||
</div> | ||
|
||
<span class="mat-date-range-input-separator">{{separator}}</span> | ||
|
||
<div class="mat-date-range-input-end-wrapper"> | ||
<ng-content select="input[matEndDate]"></ng-content> | ||
</div> | ||
</div> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
@import '../core/style/variables'; | ||
@import '../core/style/vendor-prefixes'; | ||
|
||
$mat-date-range-input-separator-spacing: 4px; | ||
$mat-date-range-input-part-max-width: calc(50% - #{$mat-date-range-input-separator-spacing}); | ||
$mat-date-range-input-placeholder-transition: | ||
color $swift-ease-out-duration $swift-ease-out-duration / 3 $swift-ease-out-timing-function; | ||
|
||
// Host of the date range input. | ||
.mat-date-range-input { | ||
display: block; | ||
width: 100%; | ||
} | ||
|
||
// Inner container that wraps around all the content. | ||
.mat-date-range-input-container { | ||
display: flex; | ||
align-items: center; | ||
} | ||
|
||
// Text shown between the two inputs. | ||
.mat-date-range-input-separator { | ||
margin: 0 $mat-date-range-input-separator-spacing; | ||
transition: $mat-date-range-input-placeholder-transition; | ||
|
||
.mat-form-field-hide-placeholder & { | ||
color: transparent; | ||
transition: none; | ||
} | ||
} | ||
|
||
// Underlying input inside the range input. | ||
.mat-date-range-input-inner { | ||
// Reset the input so it's just a transparent rectangle. | ||
crisbeto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
font: inherit; | ||
background: transparent; | ||
color: currentColor; | ||
border: none; | ||
outline: none; | ||
padding: 0; | ||
margin: 0; | ||
vertical-align: bottom; | ||
text-align: inherit; | ||
-webkit-appearance: none; | ||
width: 100%; | ||
|
||
// Remove IE's default clear and reveal icons. | ||
&::-ms-clear, | ||
&::-ms-reveal { | ||
display: none; | ||
} | ||
|
||
@include input-placeholder { | ||
transition: $mat-date-range-input-placeholder-transition; | ||
} | ||
|
||
.mat-form-field-hide-placeholder &, | ||
.mat-date-range-input-hide-placeholders & { | ||
@include input-placeholder { | ||
// Needs to be !important, because the placeholder will end up inheriting the | ||
// input color in IE, if the consumer overrides it with a higher specificity. | ||
color: transparent !important; | ||
crisbeto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
-webkit-text-fill-color: transparent; | ||
transition: none; | ||
} | ||
} | ||
} | ||
|
||
// We want the start input to be flush against the separator, no matter how much text it has, but | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does the input need to be flush against the separator? Looking at the gif of your demo, I think it would be better if the separate and end-date input didn't shift as you were typing into the start date. Would it be better to give the input a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it looks weird if you typed something shorter and then the space around the separator was uneven, e.g.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we right-align the the input? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which one? The start date input or the date range input in general? I think we'd just the same problem but in a different place. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I pulled down the PR and tried tweaking a few things locally. I concluded that everything I suggested looks worse than what you ultimately did. I have one more idea, though, which is to hide the end-date input placeholder once you start typing into the start-date input. I tried this out locally and I think it eliminates the appearance of the input getting pushed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please leave TODO's for anything that you will handle in a follow-up PR. I feel we've been getting a little lax with the "will handle in a follow-up PR" lately, and I'm worried that things might slip off our radar and never actually get done There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I ended up just doing it now. Can you take one more look @mmalerba? The way it behaves now is that it hides the placeholders when one of these criteria is true:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The way I was imagining it, the dash doesn't disappear. @jelbourn should take a look too, since he felt more strongly about the placeholder stuff There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The way I was reading it the dash should disappear, otherwise you'll see it jump to the beginning when you start typing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've updated it to keep the separator visible while the start has a value. |
||
// the problem is that inputs have a fixed width. We work around the issue by implementing an | ||
// auto-resizing input that stretches based on its text, up to a point. It works by having | ||
// a relatively-positioned wrapper (`.mat-date-range-input-start-wrapper` below) and an absolutely- | ||
// positioned `input`, as well as a `span` inside the wrapper which mirrors the input's value and | ||
jelbourn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// placeholder. As the user is typing, the value gets mirrored in the span which causes the wrapper | ||
// to stretch and the input with it. | ||
.mat-date-range-input-mirror { | ||
// Disable user selection so users don't accidentally copy the text via ctrl + A. | ||
@include user-select(none); | ||
|
||
// Hide the element so it doesn't get read out by screen | ||
// readers and it doesn't show up behind the input. | ||
visibility: hidden; | ||
|
||
// Text inside inputs never wraps so the one in the span shouldn't either. | ||
white-space: nowrap; | ||
display: inline-block; | ||
|
||
// Prevent the container from collapsing. Make it more | ||
// than 1px so the input caret doesn't get clipped. | ||
min-width: 2px; | ||
} | ||
|
||
// Wrapper around the start input. Used to facilitate the auto-resizing input. | ||
.mat-date-range-input-start-wrapper { | ||
position: relative; | ||
overflow: hidden; | ||
max-width: $mat-date-range-input-part-max-width; | ||
|
||
.mat-date-range-input-inner { | ||
position: absolute; | ||
top: 0; | ||
left: 0; | ||
} | ||
crisbeto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
// Wrapper around the end input that makes sure that it has the proper size. | ||
.mat-date-range-input-end-wrapper { | ||
flex-grow: 1; | ||
max-width: $mat-date-range-input-part-max-width; | ||
} | ||
|
||
.mat-form-field-type-mat-date-range-input .mat-form-field-infix { | ||
// Bump the default width slightly since it's somewhat cramped with two inputs and a separator. | ||
width: 200px; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.