Skip to content

Commit

Permalink
feat(datepicker): add support for md-input-container
Browse files Browse the repository at this point in the history
Makes the datepicker compatible with the md-input-container directive.

Closes angular#4233.
  • Loading branch information
crisbeto committed Jun 11, 2016
1 parent 62bf00c commit 4e4de24
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 30 deletions.
66 changes: 55 additions & 11 deletions src/components/datepicker/datePicker.scss
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
/** Styles for mdDatepicker. */
$md-datepicker-button-gap: 12px !default; // Space between the text input and the calendar-icon button.
$md-datepicker-border-bottom-gap: 5px !default; // Space between input and the grey underline.
$md-date-arrow-size: 5px !default; // Size of the triangle on the right side of the input.
$md-datepicker-open-animation-duration: 0.2s !default;
$md-datepicker-triangle-button-width: 36px !default;
$md-datepicker-input-mask-height: 40px !default;

md-datepicker {
// Don't let linebreaks happen between the open icon-button and the input.
white-space: nowrap;
overflow: hidden;

// Leave room for the down-triangle button to "overflow" it's parent without modifying scrollLeft
// Leave room for the down-triangle button to "overflow" it's parent without modifying scrollLeft.
// This prevents the element from shifting right when opening via the triangle button.
@include rtl-prop(padding-right, padding-left, $md-datepicker-triangle-button-width / 2);
@include rtl-prop(margin-right, margin-left, -$md-datepicker-triangle-button-width / 2);

Expand All @@ -31,12 +34,37 @@ md-datepicker {
}

// The input into which the user can type the date.
.md-datepicker-input {
.md-datepicker-input, label:not(.md-no-float):not(._md-container-ignore) {
@include md-flat-input();
min-width: 120px;
max-width: $md-calendar-width - $md-datepicker-button-gap;
}

// If the datepicker is inside of a md-input-container
._md-datepicker-floating-label {
> label:not(.md-no-float):not(._md-container-ignore) {
$offset: $md-datepicker-triangle-button-width + $md-datepicker-button-gap + $icon-button-margin + $baseline-grid;
@include rtl-prop(left, right, $offset);
width: calc(100% - #{$offset + $md-datepicker-triangle-button-width});
}

> md-datepicker {
// Prevents the ripple on the triangle from being clipped.
overflow: visible;

.md-datepicker-input-container {
border: none;
}

.md-datepicker-button {
// Prevents the button from wrapping around.
@include rtl(float, left, right);
margin-top: -$md-datepicker-border-bottom-gap / 2;
}
}
}


// Container for the datepicker input.
.md-datepicker-input-container {
// Position relative in order to absolutely position the down-triangle button within.
Expand All @@ -61,11 +89,15 @@ md-datepicker {

// Floating pane that contains the calendar at the bottom of the input.
.md-datepicker-calendar-pane {
// On most browsers the `scale(0)` below prevents this element from
// overflowing it's parent, however IE and Edge seem to disregard it.
// The `left: -100%` pulls the element back in order to ensure that
// it doesn't cause an overflow.
left: -100%;
position: absolute;
top: 0;
left: 0;
z-index: $z-index-calendar-pane;

border-width: 1px;
border-style: solid;
background: transparent;
Expand All @@ -81,7 +113,9 @@ md-datepicker {

// Portion of the floating panel that sits, invisibly, on top of the input.
.md-datepicker-input-mask {
height: 40px;
// It needs to be 1px shorter because of the datepicker-input-container's bottom border,
// which can cause a slight hole in the mask when it's inside a md-input-container.
height: $md-datepicker-input-mask-height - 1px;
width: $md-calendar-width;
position: relative;

Expand Down Expand Up @@ -122,7 +156,6 @@ md-datepicker {
// Down triangle/arrow indicating that the datepicker can be opened.
// We can do this entirely with CSS without needing to load an icon.
// See https://css-tricks.com/snippets/css/css-triangle/
$md-date-arrow-size: 5px !default;
.md-datepicker-expand-triangle {
// Center the triangle inside of the button so that the
// ink ripple origin looks correct.
Expand All @@ -142,7 +175,7 @@ $md-date-arrow-size: 5px !default;
.md-datepicker-triangle-button {
position: absolute;
@include rtl-prop(right, left, 0);
top: 0;
top: $md-date-arrow-size;

// TODO(jelbourn): This position isn't great on all platforms.
@include rtl(transform, translateY(-25%) translateX(45%), translateY(-25%) translateX(-45%));
Expand All @@ -151,7 +184,7 @@ $md-date-arrow-size: 5px !default;
// Need crazy specificity to override .md-button.md-icon-button.
// Only apply this high specifiy to the property we need to override.
.md-datepicker-triangle-button.md-button.md-icon-button {
height: 100%;
height: $md-datepicker-triangle-button-width;
width: $md-datepicker-triangle-button-width;
position: absolute;
}
Expand All @@ -169,21 +202,32 @@ md-datepicker[disabled] {

// Open state for all of the elements of the picker.
.md-datepicker-open {
overflow: hidden;

.md-datepicker-input-container {
@include rtl-prop(margin-left, margin-right, -$md-datepicker-button-gap);

// The negative bottom margin prevents the content around the datepicker
// from jumping when it gets opened.
margin-bottom: -$md-datepicker-border-bottom-gap;
border: none;
}

.md-datepicker-input {
.md-datepicker-input,
label:not(.md-no-float):not(._md-container-ignore) {
margin-bottom: -$md-datepicker-border-bottom-gap;
}

// This needs some extra specificity in order to override
// the focused/invalid border colors.
input.md-datepicker-input {
@include rtl-prop(margin-left, margin-right, 24px);
height: 40px;
height: $md-datepicker-input-mask-height;
border-bottom-color: transparent;
}

.md-datepicker-triangle-button {
.md-datepicker-triangle-button,
&.md-input-has-value > label,
&.md-input-has-placeholder > label {
display: none;
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/components/datepicker/demoBasicUsage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,21 @@ <h4>With ngMessages</h4>
</div>
</form>

<h4>Inside a md-input-container</h4>
<form name="myOtherForm">
<md-input-container>
<label>Enter date</label>

<md-datepicker ng-model="myDate" name="dateField" md-min-date="minDate"
md-max-date="maxDate"></md-datepicker>

<div class="validation-messages" ng-messages="myOtherForm.dateField.$error">
<div ng-message="valid">The entered value is not a date!</div>
<div ng-message="required">This date is required!</div>
<div ng-message="mindate">Date is too early!</div>
<div ng-message="maxdate">Date is too late!</div>
</div>
</md-input-container>
</form>
</md-content>
</div>
2 changes: 1 addition & 1 deletion src/components/datepicker/demoBasicUsage/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ md-content {
.validation-messages {
font-size: 12px;
color: #dd2c00;
margin: 10px 0 0 25px;
margin-left: 15px;
}
54 changes: 46 additions & 8 deletions src/components/datepicker/js/datepickerDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
// TODO(jelbourn): UTC mode
// TODO(jelbourn): RTL


angular.module('material.components.datepicker')
.directive('mdDatepicker', datePickerDirective);
.directive('mdDatepicker', datePickerDirective);

/**
* @ngdoc directive
Expand Down Expand Up @@ -96,13 +97,36 @@
link: function(scope, element, attr, controllers) {
var ngModelCtrl = controllers[0];
var mdDatePickerCtrl = controllers[1];

var mdInputContainer = controllers[2];

mdDatePickerCtrl.configureNgModel(ngModelCtrl, mdInputContainer);

if (mdInputContainer) {
throw Error('md-datepicker should not be placed inside md-input-container.');
// We need to move the spacer after the datepicker itself,
// because md-input-container adds it after the
// md-datepicker-input by default. The spacer gets wrapped in a
// div, because it floats and gets aligned next to the datepicker.
// There are easier ways of working around this with CSS (making the
// datepicker 100% wide, change the `display` etc.), however they
// break the alignment with any other form controls.
var spacer = element[0].querySelector('.md-errors-spacer');

if (spacer) {
element.after(angular.element('<div>').append(spacer));
}

mdInputContainer.setHasPlaceholder(attr.mdPlaceholder);
mdInputContainer.element.addClass(INPUT_CONTAINER_CLASS);
mdInputContainer.input = element;

if (!mdInputContainer.label) {
$mdAria.expect(element, 'aria-label', attr.mdPlaceholder);
}

scope.$watch(mdInputContainer.isErrorGetter || function() {
return ngModelCtrl.$invalid && ngModelCtrl.$touched;
}, mdInputContainer.setInvalid);
}

mdDatePickerCtrl.configureNgModel(ngModelCtrl);
}
};
}
Expand All @@ -113,6 +137,12 @@
/** Class applied to the container if the date is invalid. */
var INVALID_CLASS = 'md-datepicker-invalid';

/** Class applied to the datepicker when it's open. */
var OPEN_CLASS = 'md-datepicker-open';

/** Class applied to the md-input-container, if a datepicker is placed inside it */
var INPUT_CONTAINER_CLASS = '_md-datepicker-floating-label';

/** Default time in ms to debounce input event by. */
var DEFAULT_DEBOUNCE_INTERVAL = 500;

Expand Down Expand Up @@ -225,6 +255,9 @@
/** @type {boolean} Whether the calendar should open when the input is focused. */
this.openOnFocus = $attrs.hasOwnProperty('mdOpenOnFocus');

/** @final */
this.mdInputContainer = null;

/**
* Element from which the calendar pane was opened. Keep track of this so that we can return
* focus to it when the pane is closed.
Expand Down Expand Up @@ -263,8 +296,9 @@
* Sets up the controller's reference to ngModelController.
* @param {!angular.NgModelController} ngModelCtrl
*/
DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl) {
DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl, mdInputContainer) {
this.ngModelCtrl = ngModelCtrl;
this.mdInputContainer = mdInputContainer;

var self = this;
ngModelCtrl.$render = function() {
Expand All @@ -277,6 +311,7 @@

self.date = value;
self.inputElement.value = self.dateLocale.formatDate(value);
self.mdInputContainer && self.mdInputContainer.setHasValue(!!value);
self.resizeInputElement();
self.updateErrorState();
};
Expand All @@ -294,6 +329,7 @@
self.ngModelCtrl.$setViewValue(date);
self.date = date;
self.inputElement.value = self.dateLocale.formatDate(date);
self.mdInputContainer && self.mdInputContainer.setHasValue(!!date);
self.closeCalendarPane();
self.resizeInputElement();
self.updateErrorState();
Expand Down Expand Up @@ -466,7 +502,8 @@
var body = document.body;

calendarPane.style.transform = '';
this.$element.addClass('md-datepicker-open');
this.$element.addClass(OPEN_CLASS);
this.mdInputContainer && this.mdInputContainer.element.addClass(OPEN_CLASS);
angular.element(body).addClass('md-datepicker-is-showing');

var elementRect = this.inputContainer.getBoundingClientRect();
Expand Down Expand Up @@ -534,7 +571,8 @@

/** Detach the floating calendar pane from the document. */
DatePickerCtrl.prototype.detachCalendarPane = function() {
this.$element.removeClass('md-datepicker-open');
this.$element.removeClass(OPEN_CLASS);
this.mdInputContainer && this.mdInputContainer.element.removeClass(OPEN_CLASS);
angular.element(document.body).removeClass('md-datepicker-is-showing');
this.calendarPane.classList.remove('md-pane-open');
this.calendarPane.classList.remove('md-datepicker-pos-adjusted');
Expand Down
11 changes: 3 additions & 8 deletions src/components/datepicker/js/datepickerDirective.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,7 @@ describe('md-date-picker', function() {
'ng-disabled="isDisabled">' +
'</md-datepicker>';

var fakeInputModule = angular.module('fakeInputModule', [])
.directive('mdInputContainer', function() {
return {controller: angular.noop};
});

beforeEach(module('material.components.datepicker', 'ngAnimateMock', 'fakeInputModule'));
beforeEach(module('material.components.datepicker', 'ngAnimateMock'));

beforeEach(inject(function($rootScope, $injector) {
$compile = $injector.get('$compile');
Expand Down Expand Up @@ -120,15 +115,15 @@ describe('md-date-picker', function() {
}).not.toThrow();
});

it('should throw an error when inside of md-input-container', function() {
it('should work inside of md-input-container', function() {
var template =
'<md-input-container>' +
'<md-datepicker ng-model="myDate"></md-datepicker>' +
'</md-input-container>';

expect(function() {
$compile(template)(pageScope);
}).toThrowError('md-datepicker should not be placed inside md-input-container.');
}).not.toThrow();
});

describe('ngMessages suport', function() {
Expand Down
6 changes: 4 additions & 2 deletions src/components/input/demoBasicUsage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
<input ng-model="user.company" disabled>
</md-input-container>

<md-datepicker ng-model="user.submissionDate" md-placeholder="Enter date">
</md-datepicker>
<md-input-container>
<label>Enter date</label>
<md-datepicker ng-model="user.submissionDate"></md-datepicker>
</md-input-container>
</div>

<div layout-gt-sm="row">
Expand Down

0 comments on commit 4e4de24

Please sign in to comment.