Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 4c65fce

Browse files
crisbetoThomasBurleson
authored andcommitted
feat(datepicker): add support for md-input-container
Makes the datepicker compatible with the md-input-container directive. Closes #4233. Closes #8083
1 parent 3654d72 commit 4c65fce

File tree

6 files changed

+125
-30
lines changed

6 files changed

+125
-30
lines changed

src/components/datepicker/datePicker.scss

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
/** Styles for mdDatepicker. */
22
$md-datepicker-button-gap: 12px !default; // Space between the text input and the calendar-icon button.
33
$md-datepicker-border-bottom-gap: 5px !default; // Space between input and the grey underline.
4+
$md-date-arrow-size: 5px !default; // Size of the triangle on the right side of the input.
45
$md-datepicker-open-animation-duration: 0.2s !default;
56
$md-datepicker-triangle-button-width: 36px !default;
7+
$md-datepicker-input-mask-height: 40px !default;
68

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

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

@@ -31,12 +34,37 @@ md-datepicker {
3134
}
3235

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

43+
// If the datepicker is inside of a md-input-container
44+
._md-datepicker-floating-label {
45+
> label:not(.md-no-float):not(._md-container-ignore) {
46+
$offset: $md-datepicker-triangle-button-width + $md-datepicker-button-gap + $icon-button-margin + $baseline-grid;
47+
@include rtl-prop(left, right, $offset);
48+
width: calc(100% - #{$offset + $md-datepicker-triangle-button-width});
49+
}
50+
51+
> md-datepicker {
52+
// Prevents the ripple on the triangle from being clipped.
53+
overflow: visible;
54+
55+
.md-datepicker-input-container {
56+
border: none;
57+
}
58+
59+
.md-datepicker-button {
60+
// Prevents the button from wrapping around.
61+
@include rtl(float, left, right);
62+
margin-top: -$md-datepicker-border-bottom-gap / 2;
63+
}
64+
}
65+
}
66+
67+
4068
// Container for the datepicker input.
4169
.md-datepicker-input-container {
4270
// Position relative in order to absolutely position the down-triangle button within.
@@ -61,11 +89,15 @@ md-datepicker {
6189

6290
// Floating pane that contains the calendar at the bottom of the input.
6391
.md-datepicker-calendar-pane {
92+
// On most browsers the `scale(0)` below prevents this element from
93+
// overflowing it's parent, however IE and Edge seem to disregard it.
94+
// The `left: -100%` pulls the element back in order to ensure that
95+
// it doesn't cause an overflow.
96+
left: -100%;
6497
position: absolute;
6598
top: 0;
6699
left: 0;
67100
z-index: $z-index-calendar-pane;
68-
69101
border-width: 1px;
70102
border-style: solid;
71103
background: transparent;
@@ -81,7 +113,9 @@ md-datepicker {
81113

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

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

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

170203
// Open state for all of the elements of the picker.
171204
.md-datepicker-open {
205+
overflow: hidden;
206+
172207
.md-datepicker-input-container {
173208
@include rtl-prop(margin-left, margin-right, -$md-datepicker-button-gap);
174209

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

181-
.md-datepicker-input {
215+
.md-datepicker-input,
216+
label:not(.md-no-float):not(._md-container-ignore) {
217+
margin-bottom: -$md-datepicker-border-bottom-gap;
218+
}
219+
220+
// This needs some extra specificity in order to override
221+
// the focused/invalid border colors.
222+
input.md-datepicker-input {
182223
@include rtl-prop(margin-left, margin-right, 24px);
183-
height: 40px;
224+
height: $md-datepicker-input-mask-height;
225+
border-bottom-color: transparent;
184226
}
185227

186-
.md-datepicker-triangle-button {
228+
.md-datepicker-triangle-button,
229+
&.md-input-has-value > label,
230+
&.md-input-has-placeholder > label {
187231
display: none;
188232
}
189233
}

src/components/datepicker/demoBasicUsage/index.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,21 @@ <h4>With ngMessages</h4>
3939
</div>
4040
</form>
4141

42+
<h4>Inside a md-input-container</h4>
43+
<form name="myOtherForm">
44+
<md-input-container>
45+
<label>Enter date</label>
46+
47+
<md-datepicker ng-model="myDate" name="dateField" md-min-date="minDate"
48+
md-max-date="maxDate"></md-datepicker>
49+
50+
<div class="validation-messages" ng-messages="myOtherForm.dateField.$error">
51+
<div ng-message="valid">The entered value is not a date!</div>
52+
<div ng-message="required">This date is required!</div>
53+
<div ng-message="mindate">Date is too early!</div>
54+
<div ng-message="maxdate">Date is too late!</div>
55+
</div>
56+
</md-input-container>
57+
</form>
4258
</md-content>
4359
</div>

src/components/datepicker/demoBasicUsage/style.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ md-content {
66
.validation-messages {
77
font-size: 12px;
88
color: #dd2c00;
9-
margin: 10px 0 0 25px;
9+
margin-left: 15px;
1010
}

src/components/datepicker/js/datepickerDirective.js

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
// TODO(jelbourn): UTC mode
1212
// TODO(jelbourn): RTL
1313

14+
1415
angular.module('material.components.datepicker')
15-
.directive('mdDatepicker', datePickerDirective);
16+
.directive('mdDatepicker', datePickerDirective);
1617

1718
/**
1819
* @ngdoc directive
@@ -96,13 +97,36 @@
9697
link: function(scope, element, attr, controllers) {
9798
var ngModelCtrl = controllers[0];
9899
var mdDatePickerCtrl = controllers[1];
99-
100100
var mdInputContainer = controllers[2];
101+
102+
mdDatePickerCtrl.configureNgModel(ngModelCtrl, mdInputContainer);
103+
101104
if (mdInputContainer) {
102-
throw Error('md-datepicker should not be placed inside md-input-container.');
105+
// We need to move the spacer after the datepicker itself,
106+
// because md-input-container adds it after the
107+
// md-datepicker-input by default. The spacer gets wrapped in a
108+
// div, because it floats and gets aligned next to the datepicker.
109+
// There are easier ways of working around this with CSS (making the
110+
// datepicker 100% wide, change the `display` etc.), however they
111+
// break the alignment with any other form controls.
112+
var spacer = element[0].querySelector('.md-errors-spacer');
113+
114+
if (spacer) {
115+
element.after(angular.element('<div>').append(spacer));
116+
}
117+
118+
mdInputContainer.setHasPlaceholder(attr.mdPlaceholder);
119+
mdInputContainer.element.addClass(INPUT_CONTAINER_CLASS);
120+
mdInputContainer.input = element;
121+
122+
if (!mdInputContainer.label) {
123+
$mdAria.expect(element, 'aria-label', attr.mdPlaceholder);
124+
}
125+
126+
scope.$watch(mdInputContainer.isErrorGetter || function() {
127+
return ngModelCtrl.$invalid && ngModelCtrl.$touched;
128+
}, mdInputContainer.setInvalid);
103129
}
104-
105-
mdDatePickerCtrl.configureNgModel(ngModelCtrl);
106130
}
107131
};
108132
}
@@ -113,6 +137,12 @@
113137
/** Class applied to the container if the date is invalid. */
114138
var INVALID_CLASS = 'md-datepicker-invalid';
115139

140+
/** Class applied to the datepicker when it's open. */
141+
var OPEN_CLASS = 'md-datepicker-open';
142+
143+
/** Class applied to the md-input-container, if a datepicker is placed inside it */
144+
var INPUT_CONTAINER_CLASS = '_md-datepicker-floating-label';
145+
116146
/** Default time in ms to debounce input event by. */
117147
var DEFAULT_DEBOUNCE_INTERVAL = 500;
118148

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

258+
/** @final */
259+
this.mdInputContainer = null;
260+
228261
/**
229262
* Element from which the calendar pane was opened. Keep track of this so that we can return
230263
* focus to it when the pane is closed.
@@ -263,8 +296,9 @@
263296
* Sets up the controller's reference to ngModelController.
264297
* @param {!angular.NgModelController} ngModelCtrl
265298
*/
266-
DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl) {
299+
DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl, mdInputContainer) {
267300
this.ngModelCtrl = ngModelCtrl;
301+
this.mdInputContainer = mdInputContainer;
268302

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

278312
self.date = value;
279313
self.inputElement.value = self.dateLocale.formatDate(value);
314+
self.mdInputContainer && self.mdInputContainer.setHasValue(!!value);
280315
self.resizeInputElement();
281316
self.updateErrorState();
282317
};
@@ -294,6 +329,7 @@
294329
self.ngModelCtrl.$setViewValue(date);
295330
self.date = date;
296331
self.inputElement.value = self.dateLocale.formatDate(date);
332+
self.mdInputContainer && self.mdInputContainer.setHasValue(!!date);
297333
self.closeCalendarPane();
298334
self.resizeInputElement();
299335
self.updateErrorState();
@@ -466,7 +502,8 @@
466502
var body = document.body;
467503

468504
calendarPane.style.transform = '';
469-
this.$element.addClass('md-datepicker-open');
505+
this.$element.addClass(OPEN_CLASS);
506+
this.mdInputContainer && this.mdInputContainer.element.addClass(OPEN_CLASS);
470507
angular.element(body).addClass('md-datepicker-is-showing');
471508

472509
var elementRect = this.inputContainer.getBoundingClientRect();
@@ -534,7 +571,8 @@
534571

535572
/** Detach the floating calendar pane from the document. */
536573
DatePickerCtrl.prototype.detachCalendarPane = function() {
537-
this.$element.removeClass('md-datepicker-open');
574+
this.$element.removeClass(OPEN_CLASS);
575+
this.mdInputContainer && this.mdInputContainer.element.removeClass(OPEN_CLASS);
538576
angular.element(document.body).removeClass('md-datepicker-is-showing');
539577
this.calendarPane.classList.remove('md-pane-open');
540578
this.calendarPane.classList.remove('md-datepicker-pos-adjusted');

src/components/datepicker/js/datepickerDirective.spec.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,7 @@ describe('md-date-picker', function() {
2121
'ng-disabled="isDisabled">' +
2222
'</md-datepicker>';
2323

24-
var fakeInputModule = angular.module('fakeInputModule', [])
25-
.directive('mdInputContainer', function() {
26-
return {controller: angular.noop};
27-
});
28-
29-
beforeEach(module('material.components.datepicker', 'ngAnimateMock', 'fakeInputModule'));
24+
beforeEach(module('material.components.datepicker', 'ngAnimateMock'));
3025

3126
beforeEach(inject(function($rootScope, $injector) {
3227
$compile = $injector.get('$compile');
@@ -120,15 +115,15 @@ describe('md-date-picker', function() {
120115
}).not.toThrow();
121116
});
122117

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

129124
expect(function() {
130125
$compile(template)(pageScope);
131-
}).toThrowError('md-datepicker should not be placed inside md-input-container.');
126+
}).not.toThrow();
132127
});
133128

134129
describe('ngMessages suport', function() {

src/components/input/demoBasicUsage/index.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
<input ng-model="user.company" disabled>
2525
</md-input-container>
2626

27-
<md-datepicker ng-model="user.submissionDate" md-placeholder="Enter date">
28-
</md-datepicker>
27+
<md-input-container>
28+
<label>Enter date</label>
29+
<md-datepicker ng-model="user.submissionDate"></md-datepicker>
30+
</md-input-container>
2931
</div>
3032

3133
<div layout-gt-sm="row">

0 commit comments

Comments
 (0)