Skip to content

Commit 2294ea2

Browse files
jwshinjwshinmmalerba
authored andcommitted
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.
1 parent c9956a5 commit 2294ea2

File tree

6 files changed

+121
-17
lines changed

6 files changed

+121
-17
lines changed

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,26 @@ <h2>Options</h2>
33
<md-checkbox [(ngModel)]="touch">Use touch UI</md-checkbox>
44
<md-checkbox [(ngModel)]="filterOdd">Filter odd months and dates</md-checkbox>
55
<md-checkbox [(ngModel)]="yearView">Start in year view</md-checkbox>
6+
<md-checkbox [(ngModel)]="datepickerDisabled">Disable datepicker</md-checkbox>
67
</p>
78
<p>
89
<md-input-container>
910
<input mdInput [mdDatepicker]="minDatePicker" [(ngModel)]="minDate" placeholder="Min date">
1011
<button mdSuffix [mdDatepickerToggle]="minDatePicker"></button>
1112
</md-input-container>
12-
<md-datepicker #minDatePicker [touchUi]="touch"></md-datepicker>
13+
<md-datepicker #minDatePicker [touchUi]="touch" [disabled]="datepickerDisabled"></md-datepicker>
1314
<md-input-container>
1415
<input mdInput [mdDatepicker]="maxDatePicker" [(ngModel)]="maxDate" placeholder="Max date">
1516
<button mdSuffix [mdDatepickerToggle]="maxDatePicker"></button>
1617
</md-input-container>
17-
<md-datepicker #maxDatePicker [touchUi]="touch"></md-datepicker>
18+
<md-datepicker #maxDatePicker [touchUi]="touch" [disabled]="datepickerDisabled"></md-datepicker>
1819
</p>
1920
<p>
2021
<md-input-container>
2122
<input mdInput [mdDatepicker]="startAtPicker" [(ngModel)]="startAt" placeholder="Start at date">
2223
<button mdSuffix [mdDatepickerToggle]="startAtPicker"></button>
2324
</md-input-container>
24-
<md-datepicker #startAtPicker [touchUi]="touch"></md-datepicker>
25+
<md-datepicker #startAtPicker [touchUi]="touch" [disabled]="datepickerDisabled"></md-datepicker>
2526
</p>
2627

2728
<h2>Result</h2>
@@ -44,12 +45,14 @@ <h2>Result</h2>
4445
<md-datepicker
4546
#resultPicker
4647
[touchUi]="touch"
48+
[disabled]="datepickerDisabled"
4749
[startAt]="startAt"
4850
[startView]="yearView ? 'year' : 'month'">
4951
</md-datepicker>
5052
</p>
5153
<p>
52-
<input [mdDatepicker]="resultPicker2"
54+
<input #resultPickerModel2
55+
[mdDatepicker]="resultPicker2"
5356
[(ngModel)]="date"
5457
[min]="minDate"
5558
[max]="maxDate"
@@ -59,7 +62,32 @@ <h2>Result</h2>
5962
<md-datepicker
6063
#resultPicker2
6164
[touchUi]="touch"
65+
[disabled]="datepickerDisabled"
6266
[startAt]="startAt"
6367
[startView]="yearView ? 'year' : 'month'">
6468
</md-datepicker>
6569
</p>
70+
71+
<h2>Input disabled datepicker</h2>
72+
<p>
73+
<button [mdDatepickerToggle]="datePicker1"></button>
74+
<md-input-container>
75+
<input mdInput [mdDatepicker]="datePicker1" [(ngModel)]="date" [min]="minDate" [max]="maxDate"
76+
[mdDatepickerFilter]="filterOdd ? dateFilter : null" [disabled]="true"
77+
placeholder="Input disabled">
78+
</md-input-container>
79+
<md-datepicker #datePicker1 [touchUi]="touch" [startAt]="startAt"
80+
[startView]="yearView ? 'year' : 'month'"></md-datepicker>
81+
</p>
82+
83+
<h2>Input disabled, datepicker popup enabled</h2>
84+
<p>
85+
<button [mdDatepickerToggle]="datePicker2"></button>
86+
<md-input-container>
87+
<input mdInput disabled [mdDatepicker]="datePicker2" [(ngModel)]="date" [min]="minDate" [max]="maxDate"
88+
[mdDatepickerFilter]="filterOdd ? dateFilter : null"
89+
placeholder="Input disabled, datepicker enabled">
90+
</md-input-container>
91+
<md-datepicker #datePicker2 [touchUi]="touch" [disabled]="false" [startAt]="startAt"
92+
[startView]="yearView ? 'year' : 'month'"></md-datepicker>
93+
</p>

src/demo-app/datepicker/datepicker-demo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export class DatepickerDemo {
1111
touch: boolean;
1212
filterOdd: boolean;
1313
yearView: boolean;
14+
inputDisabled: boolean;
15+
datepickerDisabled: boolean;
1416
minDate: Date;
1517
maxDate: Date;
1618
startAt: Date;

src/lib/datepicker/datepicker-input.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {DOWN_ARROW} from '../core/keyboard/keycodes';
3535
import {DateAdapter} from '../core/datetime/index';
3636
import {createMissingDateImplError} from './datepicker-errors';
3737
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
38+
import {coerceBooleanProperty} from '@angular/cdk';
3839

3940

4041
export const MD_DATEPICKER_VALUE_ACCESSOR: any = {
@@ -61,6 +62,7 @@ export const MD_DATEPICKER_VALIDATORS: any = {
6162
'[attr.aria-owns]': '_datepicker?.id',
6263
'[attr.min]': 'min ? _dateAdapter.getISODateString(min) : null',
6364
'[attr.max]': 'max ? _dateAdapter.getISODateString(max) : null',
65+
'[disabled]': 'disabled',
6466
'(input)': '_onInput($event.target.value)',
6567
'(blur)': '_onTouched()',
6668
'(keydown)': '_onKeydown($event)',
@@ -124,6 +126,14 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
124126
}
125127
private _max: D;
126128

129+
/** Whether the datepicker-input is disabled. */
130+
@Input()
131+
get disabled() { return this._disabled; }
132+
set disabled(value: any) {
133+
this._disabled = coerceBooleanProperty(value);
134+
}
135+
private _disabled: boolean;
136+
127137
/** Emits when the value changes (either due to user input or programmatic change). */
128138
_valueChange = new EventEmitter<D|null>();
129139

src/lib/datepicker/datepicker-toggle.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core';
1010
import {MdDatepicker} from './datepicker';
1111
import {MdDatepickerIntl} from './datepicker-intl';
12+
import {coerceBooleanProperty} from '@angular/cdk';
1213

1314

1415
@Component({
@@ -20,6 +21,7 @@ import {MdDatepickerIntl} from './datepicker-intl';
2021
'type': 'button',
2122
'class': 'mat-datepicker-toggle',
2223
'[attr.aria-label]': '_intl.openCalendarLabel',
24+
'[disabled]': 'disabled',
2325
'(click)': '_open($event)',
2426
},
2527
encapsulation: ViewEncapsulation.None,
@@ -33,10 +35,20 @@ export class MdDatepickerToggle<D> {
3335
get _datepicker() { return this.datepicker; }
3436
set _datepicker(v: MdDatepicker<D>) { this.datepicker = v; }
3537

38+
/** Whether the toggle button is disabled. */
39+
@Input()
40+
get disabled() {
41+
return this._disabled === undefined ? this.datepicker.disabled : this._disabled;
42+
}
43+
set disabled(value) {
44+
this._disabled = coerceBooleanProperty(value);
45+
}
46+
private _disabled: boolean;
47+
3648
constructor(public _intl: MdDatepickerIntl) {}
3749

3850
_open(event: Event): void {
39-
if (this.datepicker) {
51+
if (this.datepicker && !this.disabled) {
4052
this.datepicker.open();
4153
event.stopPropagation();
4254
}

src/lib/datepicker/datepicker.spec.ts

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,16 @@ describe('MdDatepicker', () => {
6565
fixture.detectChanges();
6666
}));
6767

68-
it('open non-touch should open popup', async(() => {
68+
it('open non-touch should open popup', () => {
6969
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
7070

7171
testComponent.datepicker.open();
7272
fixture.detectChanges();
7373

7474
expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
75-
}));
75+
});
7676

77-
it('open touch should open dialog', async(() => {
77+
it('open touch should open dialog', () => {
7878
testComponent.touch = true;
7979
fixture.detectChanges();
8080

@@ -84,9 +84,36 @@ describe('MdDatepicker', () => {
8484
fixture.detectChanges();
8585

8686
expect(document.querySelector('md-dialog-container')).not.toBeNull();
87-
}));
87+
});
88+
89+
it('open in disabled mode should not open the calendar', () => {
90+
testComponent.disabled = true;
91+
fixture.detectChanges();
92+
93+
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
94+
expect(document.querySelector('md-dialog-container')).toBeNull();
95+
96+
testComponent.datepicker.open();
97+
fixture.detectChanges();
98+
99+
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
100+
expect(document.querySelector('md-dialog-container')).toBeNull();
101+
});
102+
103+
it('disabled datepicker input should open the calendar if datepicker is enabled', () => {
104+
testComponent.datepicker.disabled = false;
105+
testComponent.datepickerInput.disabled = true;
106+
fixture.detectChanges();
107+
108+
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
88109

89-
it('close should close popup', async(() => {
110+
testComponent.datepicker.open();
111+
fixture.detectChanges();
112+
113+
expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
114+
});
115+
116+
it('close should close popup', () => {
90117
testComponent.datepicker.open();
91118
fixture.detectChanges();
92119

@@ -100,7 +127,7 @@ describe('MdDatepicker', () => {
100127
fixture.whenStable().then(() => {
101128
expect(parseInt(getComputedStyle(popup).height as string)).toBe(0);
102129
});
103-
}));
130+
});
104131

105132
it('should close the popup when pressing ESCAPE', () => {
106133
testComponent.datepicker.open();
@@ -119,7 +146,7 @@ describe('MdDatepicker', () => {
119146
.toBe(true, 'Expected default ESCAPE action to be prevented.');
120147
});
121148

122-
it('close should close dialog', async(() => {
149+
it('close should close dialog', () => {
123150
testComponent.touch = true;
124151
fixture.detectChanges();
125152

@@ -134,9 +161,9 @@ describe('MdDatepicker', () => {
134161
fixture.whenStable().then(() => {
135162
expect(document.querySelector('md-dialog-container')).toBeNull();
136163
});
137-
}));
164+
});
138165

139-
it('setting selected should update input and close calendar', async(() => {
166+
it('setting selected should update input and close calendar', () => {
140167
testComponent.touch = true;
141168
fixture.detectChanges();
142169

@@ -154,7 +181,7 @@ describe('MdDatepicker', () => {
154181
expect(document.querySelector('md-dialog-container')).toBeNull();
155182
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
156183
});
157-
}));
184+
});
158185

159186
it('startAt should fallback to input value', () => {
160187
expect(testComponent.datepicker.startAt).toEqual(new Date(2020, JAN, 1));
@@ -433,6 +460,19 @@ describe('MdDatepicker', () => {
433460
expect(document.querySelector('md-dialog-container')).not.toBeNull();
434461
});
435462

463+
it('should not open calendar when toggle clicked if datepicker is disabled', () => {
464+
testComponent.datepicker.disabled = true;
465+
fixture.detectChanges();
466+
467+
expect(document.querySelector('md-dialog-container')).toBeNull();
468+
469+
let toggle = fixture.debugElement.query(By.css('button'));
470+
dispatchMouseEvent(toggle.nativeElement, 'click');
471+
fixture.detectChanges();
472+
473+
expect(document.querySelector('md-dialog-container')).toBeNull();
474+
});
475+
436476
it('should set the `button` type on the trigger to prevent form submissions', () => {
437477
let toggle = fixture.debugElement.query(By.css('button')).nativeElement;
438478
expect(toggle.getAttribute('type')).toBe('button');
@@ -724,11 +764,12 @@ describe('MdDatepicker', () => {
724764
@Component({
725765
template: `
726766
<input [mdDatepicker]="d" [value]="date">
727-
<md-datepicker #d [touchUi]="touch"></md-datepicker>
767+
<md-datepicker #d [touchUi]="touch" [disabled]="disabled"></md-datepicker>
728768
`,
729769
})
730770
class StandardDatepicker {
731771
touch = false;
772+
disabled = false;
732773
date = new Date(2020, JAN, 1);
733774
@ViewChild('d') datepicker: MdDatepicker<Date>;
734775
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput<Date>;

src/lib/datepicker/datepicker.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {createMissingDateImplError} from './datepicker-errors';
3838
import {ESCAPE} from '../core/keyboard/keycodes';
3939
import {MdCalendar} from './calendar';
4040
import {first} from '../core/rxjs/index';
41+
import {coerceBooleanProperty} from '@angular/cdk';
4142

4243

4344
/** Used to generate a unique ID for each datepicker instance. */
@@ -115,6 +116,16 @@ export class MdDatepicker<D> implements OnDestroy {
115116
*/
116117
@Input() touchUi = false;
117118

119+
/** Whether the datepicker pop-up should be disabled. */
120+
@Input()
121+
get disabled() {
122+
return this._disabled === undefined ? this._datepickerInput.disabled : this._disabled;
123+
}
124+
set disabled(value: any) {
125+
this._disabled = coerceBooleanProperty(value);
126+
}
127+
private _disabled: boolean;
128+
118129
/** Emits new selected date when selected date changes. */
119130
@Output() selectedChanged = new EventEmitter<D>();
120131

@@ -205,7 +216,7 @@ export class MdDatepicker<D> implements OnDestroy {
205216

206217
/** Open the calendar. */
207218
open(): void {
208-
if (this.opened) {
219+
if (this.opened || this.disabled) {
209220
return;
210221
}
211222
if (!this._datepickerInput) {

0 commit comments

Comments
 (0)