Skip to content
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

fix(datepicker): allow disabling calendar popup #5305

Merged
merged 9 commits into from
Jul 9, 2017
36 changes: 32 additions & 4 deletions src/demo-app/datepicker/datepicker-demo.html
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you'll need to also add a host binding: '[disabled]': 'disabled', to keep the native disabled attribute in sync with this

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,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 @@ -94,9 +94,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 @@ -110,7 +137,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 @@ -129,7 +156,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 @@ -144,9 +171,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 @@ -164,7 +191,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 @@ -443,6 +470,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 @@ -734,11 +774,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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should add tests for cascade as well

`,
})
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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 @@ -116,6 +117,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 @@ -206,7 +217,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