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

Commit d0a7765

Browse files
crisbetohansl
authored andcommitted
fix(datepicker): improved overlay positioning (#9432)
The current approach for positioning the datepicker's calendar works by expanding the input container in order to fill a gap in the floating calendar pane and using negative margins to prevent the UI from jumping around. This comes with some drawbacks: * It causes the input element to shift down slightly when the calendar gets opened. * It can shift all of the elements surrounding the datepicker, despite the negative margins. * It makes it hard to refactor the positioning. This new approach solves all of the above-mentioned issues by using an element with a box-shadow to create the overlay, avoiding the need for expanding the container. Referencing #9342.
1 parent 14fa477 commit d0a7765

File tree

4 files changed

+43
-52
lines changed

4 files changed

+43
-52
lines changed

src/components/datepicker/datePicker-theme.scss

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,18 @@
6060
}
6161
}
6262

63-
.md-datepicker-open .md-datepicker-input-container,
63+
.md-datepicker-calendar {
64+
background: '{{background-A100}}';
65+
}
66+
67+
$mask-color: '{{background-hue-1}}';
68+
69+
// The box-shadow acts as the background for the overlay.
6470
.md-datepicker-input-mask-opaque {
65-
background: '{{background-hue-1}}';
71+
box-shadow: 0 0 0 9999px $mask-color;
6672
}
6773

68-
.md-datepicker-calendar {
69-
background: '{{background-A100}}';
74+
.md-datepicker-open .md-datepicker-input-container {
75+
background: $mask-color;
7076
}
7177
}

src/components/datepicker/datePicker.scss

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ md-datepicker {
5353
@include md-flat-input();
5454
min-width: 120px;
5555
max-width: $md-calendar-width - $md-datepicker-button-gap;
56+
padding: 0 0 $md-datepicker-border-bottom-gap;
5657
}
5758

5859
// If the datepicker is inside of a md-input-container
@@ -94,7 +95,6 @@ md-datepicker {
9495
// Position relative in order to absolutely position the down-triangle button within.
9596
position: relative;
9697

97-
padding-bottom: $md-datepicker-border-bottom-gap;
9898
border-bottom-width: 1px;
9999
border-bottom-style: solid;
100100

@@ -139,29 +139,16 @@ md-datepicker {
139139

140140
// Portion of the floating panel that sits, invisibly, on top of the input.
141141
.md-datepicker-input-mask {
142-
// It needs to be 1px shorter because of the datepicker-input-container's bottom border,
143-
// which can cause a slight hole in the mask when it's inside a md-input-container.
144-
height: $md-datepicker-input-mask-height - 1px;
142+
height: $md-datepicker-input-mask-height;
145143
width: $md-calendar-width;
146144
position: relative;
145+
overflow: hidden;
147146

148147
background: transparent;
149148
pointer-events: none;
150149
cursor: text;
151150
}
152151

153-
.md-datepicker-input-mask-opaque {
154-
position: absolute;
155-
right: 0;
156-
left: 120px;
157-
height: 100%;
158-
159-
// The margin pulls it an extra pixel to the left, which gives it a slight overlap
160-
// with the input container. This ensures that there are no gaps between the two
161-
// elements.
162-
margin-left: -1px;
163-
}
164-
165152
// The calendar portion of the floating pane (vs. the input mask).
166153
.md-datepicker-calendar {
167154
opacity: 0;
@@ -231,26 +218,8 @@ md-datepicker[disabled] {
231218
.md-datepicker-open {
232219
overflow: hidden;
233220

234-
.md-datepicker-input-container {
235-
// The negative bottom margin prevents the content around the datepicker
236-
// from jumping when it gets opened.
237-
margin-bottom: -$md-datepicker-border-bottom-gap;
238-
}
239-
240-
.md-icon-button + .md-datepicker-input-container {
241-
@include rtl-prop(margin-left, margin-right, -$md-datepicker-button-gap, auto);
242-
}
243-
244-
.md-datepicker-input,
245-
label:not(.md-no-float):not(.md-container-ignore) {
246-
margin-bottom: -$md-datepicker-border-bottom-gap;
247-
}
248-
249-
// This needs some extra specificity in order to override
250-
// the focused/invalid border colors.
251-
input.md-datepicker-input {
252-
@include rtl-prop(margin-left, margin-right, 24px, auto);
253-
height: $md-datepicker-input-mask-height;
221+
.md-datepicker-input-container,
222+
input.md-input {
254223
border-bottom-color: transparent;
255224
}
256225

src/components/datepicker/js/datepickerDirective.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,9 @@
271271

272272
/**
273273
* Element covering everything but the input in the top of the floating calendar pane.
274-
* @type {HTMLElement}
274+
* @type {!angular.JQLite}
275275
*/
276-
this.inputMask = $element[0].querySelector('.md-datepicker-input-mask-opaque');
276+
this.inputMask = angular.element($element[0].querySelector('.md-datepicker-input-mask-opaque'));
277277

278278
/** @final {!angular.JQLite} */
279279
this.$element = $element;
@@ -332,6 +332,12 @@
332332
/** The built-in Angular date filter. */
333333
this.ngDateFilter = $filter('date');
334334

335+
/** @type {Number} Extra margin for the left side of the floating calendar pane. */
336+
this.leftMargin = 20;
337+
338+
/** @type {Number} Extra margin for the top of the floating calendar. Gets determined on the first open. */
339+
this.topMargin = null;
340+
335341
// Unless the user specifies so, the datepicker should not be a tab stop.
336342
// This is necessary because ngAria might add a tabindex to anything with an ng-model
337343
// (based on whether or not the user has turned that particular feature on/off).
@@ -616,10 +622,14 @@
616622
var elementRect = this.inputContainer.getBoundingClientRect();
617623
var bodyRect = body.getBoundingClientRect();
618624

625+
if (!this.topMargin || this.topMargin < 0) {
626+
this.topMargin = (this.inputMask.parent().prop('clientHeight') - this.ngInputElement.prop('clientHeight')) / 2;
627+
}
628+
619629
// Check to see if the calendar pane would go off the screen. If so, adjust position
620630
// accordingly to keep it within the viewport.
621-
var paneTop = elementRect.top - bodyRect.top;
622-
var paneLeft = elementRect.left - bodyRect.left;
631+
var paneTop = elementRect.top - bodyRect.top - this.topMargin;
632+
var paneLeft = elementRect.left - bodyRect.left - this.leftMargin;
623633

624634
// If ng-material has disabled body scrolling (for example, if a dialog is open),
625635
// then it's possible that the already-scrolled body has a negative top/left. In this case,
@@ -636,6 +646,17 @@
636646
var viewportBottom = viewportTop + this.$window.innerHeight;
637647
var viewportRight = viewportLeft + this.$window.innerWidth;
638648

649+
// Creates an overlay with a hole the same size as element. We remove a pixel or two
650+
// on each end to make it overlap slightly. The overlay's background is added in
651+
// the theme in the form of a box-shadow with a huge spread.
652+
this.inputMask.css({
653+
position: 'absolute',
654+
left: this.leftMargin + 'px',
655+
top: this.topMargin + 'px',
656+
width: (elementRect.width - 1) + 'px',
657+
height: (elementRect.height - 2) + 'px'
658+
});
659+
639660
// If the right edge of the pane would be off the screen and shifting it left by the
640661
// difference would not go past the left edge of the screen. If the calendar pane is too
641662
// big to fit on the screen at all, move it to the left of the screen and scale the entire
@@ -664,12 +685,6 @@
664685
calendarPane.style.top = paneTop + 'px';
665686
document.body.appendChild(calendarPane);
666687

667-
// The top of the calendar pane is a transparent box that shows the text input underneath.
668-
// Since the pane is floating, though, the page underneath the pane *adjacent* to the input is
669-
// also shown unless we cover it up. The inputMask does this by filling up the remaining space
670-
// based on the width of the input.
671-
this.inputMask.style.left = elementRect.width + 'px';
672-
673688
// Add CSS class after one frame to trigger open animation.
674689
this.$$rAF(function() {
675690
calendarPane.classList.add('md-pane-open');

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ describe('md-datepicker', function() {
382382
$timeout.flush();
383383

384384
expect(controller.calendarPane.offsetHeight).toBeGreaterThan(0);
385-
expect(controller.inputMask.style.left).toBe(controller.inputContainer.clientWidth + 'px');
385+
expect(controller.inputMask[0].style.left).toBeTruthy();
386386

387387
// Click off of the calendar.
388388
document.body.click();
@@ -499,7 +499,8 @@ describe('md-datepicker', function() {
499499
var triggerRect = controller.inputContainer.getBoundingClientRect();
500500

501501
// We expect the offset to be close to the exact height, because on IE there are some deviations.
502-
expect(paneRect.top).toBeCloseTo(triggerRect.top, 0.5);
502+
expect(controller.topMargin).toBeGreaterThan(0);
503+
expect(paneRect.top).toBeCloseTo(triggerRect.top - controller.topMargin, 0.5);
503504

504505
// Restore body to pre-test state.
505506
body.removeChild(superLongElement);

0 commit comments

Comments
 (0)