Skip to content

Commit

Permalink
fix(datepicker): allow disabling calendar popup (#5305)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
jwshinjwshin authored and mmalerba committed Jul 9, 2017
1 parent c9956a5 commit 2294ea2
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 17 deletions.
36 changes: 32 additions & 4 deletions src/demo-app/datepicker/datepicker-demo.html
Expand Up @@ -3,25 +3,26 @@ <h2>Options</h2>
<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>
Expand All @@ -44,12 +45,14 @@ <h2>Result</h2>
<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"
Expand All @@ -59,7 +62,32 @@ <h2>Result</h2>
<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>
2 changes: 2 additions & 0 deletions src/demo-app/datepicker/datepicker-demo.ts
Expand Up @@ -11,6 +11,8 @@ export class DatepickerDemo {
touch: boolean;
filterOdd: boolean;
yearView: boolean;
inputDisabled: boolean;
datepickerDisabled: boolean;
minDate: Date;
maxDate: Date;
startAt: Date;
Expand Down
10 changes: 10 additions & 0 deletions src/lib/datepicker/datepicker-input.ts
Expand Up @@ -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 = {
Expand All @@ -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)',
Expand Down Expand Up @@ -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>();

Expand Down
14 changes: 13 additions & 1 deletion src/lib/datepicker/datepicker-toggle.ts
Expand Up @@ -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({
Expand All @@ -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,
Expand All @@ -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();
}
Expand Down
63 changes: 52 additions & 11 deletions src/lib/datepicker/datepicker.spec.ts
Expand Up @@ -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();

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

expect(document.querySelector('md-dialog-container')).not.toBeNull();
}));
});

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();

it('close should close popup', async(() => {
testComponent.datepicker.open();
fixture.detectChanges();

expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
});

it('close should close popup', () => {
testComponent.datepicker.open();
fixture.detectChanges();

Expand All @@ -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();
Expand All @@ -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();

Expand All @@ -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();

Expand All @@ -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));
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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>;
Expand Down
13 changes: 12 additions & 1 deletion src/lib/datepicker/datepicker.ts
Expand Up @@ -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. */
Expand Down Expand Up @@ -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>();

Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 2294ea2

Please sign in to comment.