Skip to content

Commit 63505dc

Browse files
crisbetoandrewseguin
authored andcommitted
fix(connected-position-strategy): position change event not emitting for fallback positions (#5978)
* Fixes the `onPositionChange` event from the `ConnectedPositionStrategy` not emitting if the strategy picks one of the fallback positions. * Fixes the autocomplete panel not having the proper offset when it is in the `above` position. This was tightly-coupled to the `onPositionChange` not firing. * Fixes a couple of tests that were nested in another test, causing them not to be executed. I've removed one of the tests since it was also duplicated with one above. This seems to have been the result of a faulty conflict resolution.
1 parent a51f82f commit 63505dc

File tree

3 files changed

+63
-57
lines changed

3 files changed

+63
-57
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import {Subscription} from 'rxjs/Subscription';
4545
import {merge} from 'rxjs/observable/merge';
4646
import {fromEvent} from 'rxjs/observable/fromEvent';
4747
import {of as observableOf} from 'rxjs/observable/of';
48-
import {RxChain, switchMap, first, filter} from '../core/rxjs/index';
48+
import {RxChain, switchMap, first, filter, map} from '../core/rxjs/index';
4949

5050
/**
5151
* The following style constants are necessary to save here in order
@@ -379,12 +379,17 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
379379
* stream every time the option list changes.
380380
*/
381381
private _subscribeToClosingActions(): Subscription {
382+
const firstStable = first.call(this._zone.onStable);
383+
const optionChanges = map.call(this.autocomplete.options.changes, () =>
384+
this._positionStrategy.recalculateLastPosition());
385+
382386
// When the zone is stable initially, and when the option list changes...
383-
return RxChain.from(merge(first.call(this._zone.onStable), this.autocomplete.options.changes))
387+
return RxChain.from(merge(firstStable, optionChanges))
384388
// create a new stream of panelClosingActions, replacing any previous streams
385389
// that were created, and flatten it so our stream only emits closing events...
386390
.call(switchMap, () => {
387-
this._resetPanel();
391+
this._resetActiveItem();
392+
this.autocomplete._setVisibility();
388393
return this.panelClosingActions;
389394
})
390395
// when the first closing event occurs...
@@ -481,14 +486,4 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
481486
this.autocomplete._keyManager.setActiveItem(-1);
482487
}
483488

484-
/**
485-
* Resets the active item and re-calculates alignment of the panel in case its size
486-
* has changed due to fewer or greater number of options.
487-
*/
488-
private _resetPanel() {
489-
this._resetActiveItem();
490-
this._positionStrategy.recalculateLastPosition();
491-
this.autocomplete._setVisibility();
492-
}
493-
494489
}

src/lib/core/overlay/position/connected-position-strategy.spec.ts

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {OverlayPositionBuilder} from './overlay-position-builder';
66
import {ConnectedOverlayPositionChange} from './connected-position';
77
import {Scrollable} from '../scroll/scrollable';
88
import {Subscription} from 'rxjs/Subscription';
9-
import Spy = jasmine.Spy;
109
import {ScrollDispatchModule} from '../scroll/index';
1110

1211

@@ -343,53 +342,65 @@ describe('ConnectedPositionStrategy', () => {
343342
expect(latestCall.args[0] instanceof ConnectedOverlayPositionChange)
344343
.toBe(true, `Expected strategy to emit an instance of ConnectedOverlayPositionChange.`);
345344

346-
it('should pick the fallback position that shows the largest area of the element', () => {
347-
positionBuilder = new OverlayPositionBuilder(viewportRuler);
345+
// If the strategy is re-applied and the initial position would now fit,
346+
// the position change event should be emitted again.
347+
originElement.style.top = '200px';
348+
originElement.style.left = '200px';
349+
strategy.apply(overlayElement);
348350

349-
originElement.style.top = '200px';
350-
originElement.style.right = '25px';
351-
originRect = originElement.getBoundingClientRect();
351+
expect(positionChangeHandler).toHaveBeenCalledTimes(2);
352352

353-
strategy = positionBuilder.connectedTo(
354-
fakeElementRef,
355-
{originX: 'end', originY: 'center'},
356-
{overlayX: 'start', overlayY: 'center'})
357-
.withFallbackPosition(
358-
{originX: 'end', originY: 'top'},
359-
{overlayX: 'start', overlayY: 'bottom'})
360-
.withFallbackPosition(
361-
{originX: 'end', originY: 'top'},
362-
{overlayX: 'end', overlayY: 'top'});
353+
subscription.unsubscribe();
354+
});
363355

364-
strategy.apply(overlayElement);
356+
it('should emit the onPositionChange event even if none of the positions fit', () => {
357+
positionBuilder = new OverlayPositionBuilder(viewportRuler);
358+
originElement.style.bottom = '25px';
359+
originElement.style.right = '25px';
365360

366-
let overlayRect = overlayElement.getBoundingClientRect();
361+
strategy = positionBuilder.connectedTo(
362+
fakeElementRef,
363+
{originX: 'end', originY: 'bottom'},
364+
{overlayX: 'start', overlayY: 'top'})
365+
.withFallbackPosition(
366+
{originX: 'start', originY: 'bottom'},
367+
{overlayX: 'end', overlayY: 'top'});
367368

368-
expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.top));
369-
expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left));
370-
});
369+
const positionChangeHandler = jasmine.createSpy('positionChangeHandler');
370+
const subscription = strategy.onPositionChange.subscribe(positionChangeHandler);
371371

372-
it('should position a panel properly when rtl', () => {
373-
// must make the overlay longer than the origin to properly test attachment
374-
overlayElement.style.width = `500px`;
375-
originRect = originElement.getBoundingClientRect();
376-
strategy = positionBuilder.connectedTo(
377-
fakeElementRef,
378-
{originX: 'start', originY: 'bottom'},
379-
{overlayX: 'start', overlayY: 'top'})
380-
.withDirection('rtl');
381-
originElement.style.top = '0';
382-
originElement.style.left = '0';
372+
strategy.apply(overlayElement);
383373

384-
// If the strategy is re-applied and the initial position would now fit,
385-
// the position change event should be emitted again.
386-
strategy.apply(overlayElement);
387-
expect(positionChangeHandler).toHaveBeenCalledTimes(2);
374+
expect(positionChangeHandler).toHaveBeenCalled();
388375

389-
subscription.unsubscribe();
390-
});
376+
subscription.unsubscribe();
391377
});
392378

379+
it('should pick the fallback position that shows the largest area of the element', () => {
380+
positionBuilder = new OverlayPositionBuilder(viewportRuler);
381+
382+
originElement.style.top = '200px';
383+
originElement.style.right = '25px';
384+
originRect = originElement.getBoundingClientRect();
385+
386+
strategy = positionBuilder.connectedTo(
387+
fakeElementRef,
388+
{originX: 'end', originY: 'center'},
389+
{overlayX: 'start', overlayY: 'center'})
390+
.withFallbackPosition(
391+
{originX: 'end', originY: 'top'},
392+
{overlayX: 'start', overlayY: 'bottom'})
393+
.withFallbackPosition(
394+
{originX: 'end', originY: 'top'},
395+
{overlayX: 'end', overlayY: 'top'});
396+
397+
strategy.apply(overlayElement);
398+
399+
let overlayRect = overlayElement.getBoundingClientRect();
400+
401+
expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.top));
402+
expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left));
403+
});
393404

394405
/**
395406
* Run all tests for connecting the overlay to the origin such that first preferred
@@ -485,7 +496,7 @@ describe('ConnectedPositionStrategy', () => {
485496
let strategy: ConnectedPositionStrategy;
486497

487498
let scrollable: HTMLDivElement;
488-
let positionChangeHandler: Spy;
499+
let positionChangeHandler: jasmine.Spy;
489500
let onPositionChangeSubscription: Subscription;
490501
let positionChange: ConnectedOverlayPositionChange;
491502

src/lib/core/overlay/position/connected-position-strategy.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,6 @@ export class ConnectedPositionStrategy implements PositionStrategy {
133133
// Save the last connected position in case the position needs to be re-calculated.
134134
this._lastConnectedPosition = pos;
135135

136-
// Notify that the position has been changed along with its change properties.
137-
const scrollableViewProperties = this.getScrollableViewProperties(element);
138-
const positionChange = new ConnectedOverlayPositionChange(pos, scrollableViewProperties);
139-
this._onPositionChange.next(positionChange);
140-
141136
return;
142137
} else if (!fallbackPoint || fallbackPoint.visibleArea < overlayPoint.visibleArea) {
143138
fallbackPoint = overlayPoint;
@@ -395,6 +390,11 @@ export class ConnectedPositionStrategy implements PositionStrategy {
395390

396391
element.style[verticalStyleProperty] = `${y}px`;
397392
element.style[horizontalStyleProperty] = `${x}px`;
393+
394+
// Notify that the position has been changed along with its change properties.
395+
const scrollableViewProperties = this.getScrollableViewProperties(element);
396+
const positionChange = new ConnectedOverlayPositionChange(pos, scrollableViewProperties);
397+
this._onPositionChange.next(positionChange);
398398
}
399399

400400
/** Returns the bounding positions of the provided element with respect to the viewport. */

0 commit comments

Comments
 (0)