mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(datepicker): allow disabling calendar popup (#5305)
* Add ability to disable datepicker pop-up in read-only mode. * Change readOnly to disabled * Added ability to disable datepicker toggle when the input is disabled. * Lint edits * Failed test fixes * Allow disabled input to disable popup unless specified otherwise for datepicker. * Minor comment edits * Added host bindings and changed delegation logic. * Added tests for cascade and removed async testing.
This commit is contained in:
@@ -3,25 +3,26 @@
|
||||
<md-checkbox [(ngModel)]="touch">Use touch UI</md-checkbox>
|
||||
<md-checkbox [(ngModel)]="filterOdd">Filter odd months and dates</md-checkbox>
|
||||
<md-checkbox [(ngModel)]="yearView">Start in year view</md-checkbox>
|
||||
<md-checkbox [(ngModel)]="datepickerDisabled">Disable datepicker</md-checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<md-input-container>
|
||||
<input mdInput [mdDatepicker]="minDatePicker" [(ngModel)]="minDate" placeholder="Min date">
|
||||
<button mdSuffix [mdDatepickerToggle]="minDatePicker"></button>
|
||||
</md-input-container>
|
||||
<md-datepicker #minDatePicker [touchUi]="touch"></md-datepicker>
|
||||
<md-datepicker #minDatePicker [touchUi]="touch" [disabled]="datepickerDisabled"></md-datepicker>
|
||||
<md-input-container>
|
||||
<input mdInput [mdDatepicker]="maxDatePicker" [(ngModel)]="maxDate" placeholder="Max date">
|
||||
<button mdSuffix [mdDatepickerToggle]="maxDatePicker"></button>
|
||||
</md-input-container>
|
||||
<md-datepicker #maxDatePicker [touchUi]="touch"></md-datepicker>
|
||||
<md-datepicker #maxDatePicker [touchUi]="touch" [disabled]="datepickerDisabled"></md-datepicker>
|
||||
</p>
|
||||
<p>
|
||||
<md-input-container>
|
||||
<input mdInput [mdDatepicker]="startAtPicker" [(ngModel)]="startAt" placeholder="Start at date">
|
||||
<button mdSuffix [mdDatepickerToggle]="startAtPicker"></button>
|
||||
</md-input-container>
|
||||
<md-datepicker #startAtPicker [touchUi]="touch"></md-datepicker>
|
||||
<md-datepicker #startAtPicker [touchUi]="touch" [disabled]="datepickerDisabled"></md-datepicker>
|
||||
</p>
|
||||
|
||||
<h2>Result</h2>
|
||||
@@ -44,12 +45,14 @@
|
||||
<md-datepicker
|
||||
#resultPicker
|
||||
[touchUi]="touch"
|
||||
[disabled]="datepickerDisabled"
|
||||
[startAt]="startAt"
|
||||
[startView]="yearView ? 'year' : 'month'">
|
||||
</md-datepicker>
|
||||
</p>
|
||||
<p>
|
||||
<input [mdDatepicker]="resultPicker2"
|
||||
<input #resultPickerModel2
|
||||
[mdDatepicker]="resultPicker2"
|
||||
[(ngModel)]="date"
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
@@ -59,7 +62,32 @@
|
||||
<md-datepicker
|
||||
#resultPicker2
|
||||
[touchUi]="touch"
|
||||
[disabled]="datepickerDisabled"
|
||||
[startAt]="startAt"
|
||||
[startView]="yearView ? 'year' : 'month'">
|
||||
</md-datepicker>
|
||||
</p>
|
||||
|
||||
<h2>Input disabled datepicker</h2>
|
||||
<p>
|
||||
<button [mdDatepickerToggle]="datePicker1"></button>
|
||||
<md-input-container>
|
||||
<input mdInput [mdDatepicker]="datePicker1" [(ngModel)]="date" [min]="minDate" [max]="maxDate"
|
||||
[mdDatepickerFilter]="filterOdd ? dateFilter : null" [disabled]="true"
|
||||
placeholder="Input disabled">
|
||||
</md-input-container>
|
||||
<md-datepicker #datePicker1 [touchUi]="touch" [startAt]="startAt"
|
||||
[startView]="yearView ? 'year' : 'month'"></md-datepicker>
|
||||
</p>
|
||||
|
||||
<h2>Input disabled, datepicker popup enabled</h2>
|
||||
<p>
|
||||
<button [mdDatepickerToggle]="datePicker2"></button>
|
||||
<md-input-container>
|
||||
<input mdInput disabled [mdDatepicker]="datePicker2" [(ngModel)]="date" [min]="minDate" [max]="maxDate"
|
||||
[mdDatepickerFilter]="filterOdd ? dateFilter : null"
|
||||
placeholder="Input disabled, datepicker enabled">
|
||||
</md-input-container>
|
||||
<md-datepicker #datePicker2 [touchUi]="touch" [disabled]="false" [startAt]="startAt"
|
||||
[startView]="yearView ? 'year' : 'month'"></md-datepicker>
|
||||
</p>
|
||||
|
||||
@@ -11,6 +11,8 @@ export class DatepickerDemo {
|
||||
touch: boolean;
|
||||
filterOdd: boolean;
|
||||
yearView: boolean;
|
||||
inputDisabled: boolean;
|
||||
datepickerDisabled: boolean;
|
||||
minDate: Date;
|
||||
maxDate: Date;
|
||||
startAt: Date;
|
||||
|
||||
@@ -35,6 +35,7 @@ import {DOWN_ARROW} from '../core/keyboard/keycodes';
|
||||
import {DateAdapter} from '../core/datetime/index';
|
||||
import {createMissingDateImplError} from './datepicker-errors';
|
||||
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
|
||||
import {coerceBooleanProperty} from '@angular/cdk';
|
||||
|
||||
|
||||
export const MD_DATEPICKER_VALUE_ACCESSOR: any = {
|
||||
@@ -61,6 +62,7 @@ export const MD_DATEPICKER_VALIDATORS: any = {
|
||||
'[attr.aria-owns]': '_datepicker?.id',
|
||||
'[attr.min]': 'min ? _dateAdapter.getISODateString(min) : null',
|
||||
'[attr.max]': 'max ? _dateAdapter.getISODateString(max) : null',
|
||||
'[disabled]': 'disabled',
|
||||
'(input)': '_onInput($event.target.value)',
|
||||
'(blur)': '_onTouched()',
|
||||
'(keydown)': '_onKeydown($event)',
|
||||
@@ -124,6 +126,14 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
|
||||
}
|
||||
private _max: D;
|
||||
|
||||
/** Whether the datepicker-input is disabled. */
|
||||
@Input()
|
||||
get disabled() { return this._disabled; }
|
||||
set disabled(value: any) {
|
||||
this._disabled = coerceBooleanProperty(value);
|
||||
}
|
||||
private _disabled: boolean;
|
||||
|
||||
/** Emits when the value changes (either due to user input or programmatic change). */
|
||||
_valueChange = new EventEmitter<D|null>();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core';
|
||||
import {MdDatepicker} from './datepicker';
|
||||
import {MdDatepickerIntl} from './datepicker-intl';
|
||||
import {coerceBooleanProperty} from '@angular/cdk';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -20,6 +21,7 @@ import {MdDatepickerIntl} from './datepicker-intl';
|
||||
'type': 'button',
|
||||
'class': 'mat-datepicker-toggle',
|
||||
'[attr.aria-label]': '_intl.openCalendarLabel',
|
||||
'[disabled]': 'disabled',
|
||||
'(click)': '_open($event)',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
@@ -33,10 +35,20 @@ export class MdDatepickerToggle<D> {
|
||||
get _datepicker() { return this.datepicker; }
|
||||
set _datepicker(v: MdDatepicker<D>) { this.datepicker = v; }
|
||||
|
||||
/** Whether the toggle button is disabled. */
|
||||
@Input()
|
||||
get disabled() {
|
||||
return this._disabled === undefined ? this.datepicker.disabled : this._disabled;
|
||||
}
|
||||
set disabled(value) {
|
||||
this._disabled = coerceBooleanProperty(value);
|
||||
}
|
||||
private _disabled: boolean;
|
||||
|
||||
constructor(public _intl: MdDatepickerIntl) {}
|
||||
|
||||
_open(event: Event): void {
|
||||
if (this.datepicker) {
|
||||
if (this.datepicker && !this.disabled) {
|
||||
this.datepicker.open();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@@ -65,16 +65,16 @@ describe('MdDatepicker', () => {
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('open non-touch should open popup', async(() => {
|
||||
it('open non-touch should open popup', () => {
|
||||
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
|
||||
|
||||
testComponent.datepicker.open();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
|
||||
}));
|
||||
});
|
||||
|
||||
it('open touch should open dialog', async(() => {
|
||||
it('open touch should open dialog', () => {
|
||||
testComponent.touch = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -84,9 +84,36 @@ describe('MdDatepicker', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('md-dialog-container')).not.toBeNull();
|
||||
}));
|
||||
});
|
||||
|
||||
it('close should close popup', async(() => {
|
||||
it('open in disabled mode should not open the calendar', () => {
|
||||
testComponent.disabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
|
||||
expect(document.querySelector('md-dialog-container')).toBeNull();
|
||||
|
||||
testComponent.datepicker.open();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
|
||||
expect(document.querySelector('md-dialog-container')).toBeNull();
|
||||
});
|
||||
|
||||
it('disabled datepicker input should open the calendar if datepicker is enabled', () => {
|
||||
testComponent.datepicker.disabled = false;
|
||||
testComponent.datepickerInput.disabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
|
||||
|
||||
testComponent.datepicker.open();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('close should close popup', () => {
|
||||
testComponent.datepicker.open();
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -100,7 +127,7 @@ describe('MdDatepicker', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(parseInt(getComputedStyle(popup).height as string)).toBe(0);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('should close the popup when pressing ESCAPE', () => {
|
||||
testComponent.datepicker.open();
|
||||
@@ -119,7 +146,7 @@ describe('MdDatepicker', () => {
|
||||
.toBe(true, 'Expected default ESCAPE action to be prevented.');
|
||||
});
|
||||
|
||||
it('close should close dialog', async(() => {
|
||||
it('close should close dialog', () => {
|
||||
testComponent.touch = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -134,9 +161,9 @@ describe('MdDatepicker', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(document.querySelector('md-dialog-container')).toBeNull();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('setting selected should update input and close calendar', async(() => {
|
||||
it('setting selected should update input and close calendar', () => {
|
||||
testComponent.touch = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -154,7 +181,7 @@ describe('MdDatepicker', () => {
|
||||
expect(document.querySelector('md-dialog-container')).toBeNull();
|
||||
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('startAt should fallback to input value', () => {
|
||||
expect(testComponent.datepicker.startAt).toEqual(new Date(2020, JAN, 1));
|
||||
@@ -433,6 +460,19 @@ describe('MdDatepicker', () => {
|
||||
expect(document.querySelector('md-dialog-container')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should not open calendar when toggle clicked if datepicker is disabled', () => {
|
||||
testComponent.datepicker.disabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('md-dialog-container')).toBeNull();
|
||||
|
||||
let toggle = fixture.debugElement.query(By.css('button'));
|
||||
dispatchMouseEvent(toggle.nativeElement, 'click');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('md-dialog-container')).toBeNull();
|
||||
});
|
||||
|
||||
it('should set the `button` type on the trigger to prevent form submissions', () => {
|
||||
let toggle = fixture.debugElement.query(By.css('button')).nativeElement;
|
||||
expect(toggle.getAttribute('type')).toBe('button');
|
||||
@@ -724,11 +764,12 @@ describe('MdDatepicker', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<input [mdDatepicker]="d" [value]="date">
|
||||
<md-datepicker #d [touchUi]="touch"></md-datepicker>
|
||||
<md-datepicker #d [touchUi]="touch" [disabled]="disabled"></md-datepicker>
|
||||
`,
|
||||
})
|
||||
class StandardDatepicker {
|
||||
touch = false;
|
||||
disabled = false;
|
||||
date = new Date(2020, JAN, 1);
|
||||
@ViewChild('d') datepicker: MdDatepicker<Date>;
|
||||
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput<Date>;
|
||||
|
||||
@@ -38,6 +38,7 @@ import {createMissingDateImplError} from './datepicker-errors';
|
||||
import {ESCAPE} from '../core/keyboard/keycodes';
|
||||
import {MdCalendar} from './calendar';
|
||||
import {first} from '../core/rxjs/index';
|
||||
import {coerceBooleanProperty} from '@angular/cdk';
|
||||
|
||||
|
||||
/** Used to generate a unique ID for each datepicker instance. */
|
||||
@@ -115,6 +116,16 @@ export class MdDatepicker<D> implements OnDestroy {
|
||||
*/
|
||||
@Input() touchUi = false;
|
||||
|
||||
/** Whether the datepicker pop-up should be disabled. */
|
||||
@Input()
|
||||
get disabled() {
|
||||
return this._disabled === undefined ? this._datepickerInput.disabled : this._disabled;
|
||||
}
|
||||
set disabled(value: any) {
|
||||
this._disabled = coerceBooleanProperty(value);
|
||||
}
|
||||
private _disabled: boolean;
|
||||
|
||||
/** Emits new selected date when selected date changes. */
|
||||
@Output() selectedChanged = new EventEmitter<D>();
|
||||
|
||||
@@ -205,7 +216,7 @@ export class MdDatepicker<D> implements OnDestroy {
|
||||
|
||||
/** Open the calendar. */
|
||||
open(): void {
|
||||
if (this.opened) {
|
||||
if (this.opened || this.disabled) {
|
||||
return;
|
||||
}
|
||||
if (!this._datepickerInput) {
|
||||
|
||||
Reference in New Issue
Block a user