Skip to content

Commit

Permalink
fix(autocomplete): attach overlay to a more accurate input element (#…
Browse files Browse the repository at this point in the history
…6282)

* fix(autocomplete): attach overlay to a more accurate input element

* Fix issue with transient prefix/suffix

* Remove unneeded position change subscription
  • Loading branch information
willshowell authored and mmalerba committed Aug 7, 2017
1 parent 48d3a0c commit 667a4e4
Show file tree
Hide file tree
Showing 7 changed files with 22 additions and 69 deletions.
2 changes: 1 addition & 1 deletion src/demo-app/autocomplete/autocomplete-demo.html
Expand Up @@ -53,4 +53,4 @@
<md-option *ngFor="let state of tdStates" [value]="state.name">
<span>{{ state.name }}</span>
</md-option>
</md-autocomplete>
</md-autocomplete>
22 changes: 4 additions & 18 deletions src/lib/autocomplete/autocomplete-trigger.ts
Expand Up @@ -119,9 +119,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
private _portal: TemplatePortal;
private _panelOpen: boolean = false;

/** The subscription to positioning changes in the autocomplete panel. */
private _panelPositionSubscription: Subscription;

/** Strategy that is used to position the panel. */
private _positionStrategy: ConnectedPositionStrategy;

Expand Down Expand Up @@ -160,10 +157,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
@Optional() @Inject(DOCUMENT) private _document: any) {}

ngOnDestroy() {
if (this._panelPositionSubscription) {
this._panelPositionSubscription.unsubscribe();
}

this._destroyPanel();
}

Expand Down Expand Up @@ -467,28 +460,21 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {

private _getOverlayPosition(): PositionStrategy {
this._positionStrategy = this._overlay.position().connectedTo(
this._element,
this._getConnectedElement(),
{originX: 'start', originY: 'bottom'}, {overlayX: 'start', overlayY: 'top'})
.withFallbackPosition(
{originX: 'start', originY: 'top'}, {overlayX: 'start', overlayY: 'bottom'}
);
this._subscribeToPositionChanges(this._positionStrategy);
return this._positionStrategy;
}

/**
* This method subscribes to position changes in the autocomplete panel, so the panel's
* y-offset can be adjusted to match the new position.
*/
private _subscribeToPositionChanges(strategy: ConnectedPositionStrategy) {
this._panelPositionSubscription = strategy.onPositionChange.subscribe(change => {
this.autocomplete.positionY = change.connectionPair.originY === 'top' ? 'above' : 'below';
});
private _getConnectedElement(): ElementRef {
return this._inputContainer ? this._inputContainer._connectionContainerRef : this._element;
}

/** Returns the width of the input element, so the panel width can match it. */
private _getHostWidth(): number {
return this._element.nativeElement.getBoundingClientRect().width;
return this._getConnectedElement().nativeElement.getBoundingClientRect().width;
}

/** Reset active item to -1 so arrow events will activate the correct options.*/
Expand Down
20 changes: 0 additions & 20 deletions src/lib/autocomplete/autocomplete.scss
Expand Up @@ -6,18 +6,6 @@
*/
$mat-autocomplete-panel-max-height: 256px !default;

/**
* When in "below" position, the panel needs a slight
* y-offset to ensure the input underline displays.
*/
$mat-autocomplete-panel-below-offset: 6px !default;

/**
* When in "above" position, the panel needs a larger
* y-offset to ensure the label has room to display.
*/
$mat-autocomplete-panel-above-offset: -24px !default;

.mat-autocomplete-panel {
@include mat-menu-base(8);
visibility: hidden;
Expand All @@ -26,14 +14,6 @@ $mat-autocomplete-panel-above-offset: -24px !default;
max-height: $mat-autocomplete-panel-max-height;
position: relative;

&.mat-autocomplete-panel-below {
top: $mat-autocomplete-panel-below-offset;
}

&.mat-autocomplete-panel-above {
top: $mat-autocomplete-panel-above-offset;
}

&.mat-autocomplete-visible {
visibility: visible;
}
Expand Down
35 changes: 14 additions & 21 deletions src/lib/autocomplete/autocomplete.spec.ts
Expand Up @@ -1055,27 +1055,26 @@ describe('MdAutocomplete', () => {
describe('Fallback positions', () => {
let fixture: ComponentFixture<SimpleAutocomplete>;
let input: HTMLInputElement;
let inputReference: HTMLInputElement;

beforeEach(() => {
fixture = TestBed.createComponent(SimpleAutocomplete);
fixture.detectChanges();

input = fixture.debugElement.query(By.css('input')).nativeElement;
inputReference = fixture.debugElement.query(By.css('.mat-input-flex')).nativeElement;
});

it('should use below positioning by default', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();

const inputBottom = input.getBoundingClientRect().bottom;
const inputBottom = inputReference.getBoundingClientRect().bottom;
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!;
const panelTop = panel.getBoundingClientRect().top;

// Panel is offset by 6px in styles so that the underline has room to display.
expect(Math.floor(inputBottom + 6))
expect(Math.floor(inputBottom))
.toEqual(Math.floor(panelTop), `Expected panel top to match input bottom by default.`);
expect(fixture.componentInstance.trigger.autocomplete.positionY)
.toEqual('below', `Expected autocomplete positionY to default to below.`);
});

it('should reposition the panel on scroll', () => {
Expand All @@ -1091,39 +1090,36 @@ describe('MdAutocomplete', () => {
scrolledSubject.next();
fixture.detectChanges();

const inputBottom = input.getBoundingClientRect().bottom;
const inputBottom = inputReference.getBoundingClientRect().bottom;
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!;
const panelTop = panel.getBoundingClientRect().top;

expect(Math.floor(inputBottom + 6)).toEqual(Math.floor(panelTop),
expect(Math.floor(inputBottom)).toEqual(Math.floor(panelTop),
'Expected panel top to match input bottom after scrolling.');

document.body.removeChild(spacer);
});

it('should fall back to above position if panel cannot fit below', () => {
// Push the autocomplete trigger down so it won't have room to open "below"
input.style.top = '600px';
input.style.position = 'relative';
inputReference.style.top = '600px';
inputReference.style.position = 'relative';

fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();

const inputTop = input.getBoundingClientRect().top;
const inputTop = inputReference.getBoundingClientRect().top;
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!;
const panelBottom = panel.getBoundingClientRect().bottom;

// Panel is offset by 24px in styles so that the label has room to display.
expect(Math.floor(inputTop - 24))
expect(Math.floor(inputTop))
.toEqual(Math.floor(panelBottom), `Expected panel to fall back to above position.`);
expect(fixture.componentInstance.trigger.autocomplete.positionY)
.toEqual('above', `Expected autocomplete positionY to be "above" if panel won't fit.`);
});

it('should align panel properly when filtering in "above" position', async(() => {
// Push the autocomplete trigger down so it won't have room to open "below"
input.style.top = '600px';
input.style.position = 'relative';
inputReference.style.top = '600px';
inputReference.style.position = 'relative';

fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
Expand All @@ -1132,15 +1128,12 @@ describe('MdAutocomplete', () => {
typeInElement('f', input);
fixture.detectChanges();

const inputTop = input.getBoundingClientRect().top;
const inputTop = inputReference.getBoundingClientRect().top;
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!;
const panelBottom = panel.getBoundingClientRect().bottom;

// Panel is offset by 24px in styles so that the label has room to display.
expect(Math.floor(inputTop - 24))
expect(Math.floor(inputTop))
.toEqual(Math.floor(panelBottom), `Expected panel to stay aligned after filtering.`);
expect(fixture.componentInstance.trigger.autocomplete.positionY)
.toEqual('above', `Expected autocomplete positionY to be "above" if panel won't fit.`);
});
}));

Expand Down
9 changes: 1 addition & 8 deletions src/lib/autocomplete/autocomplete.ts
Expand Up @@ -28,8 +28,6 @@ import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-mana
*/
let _uniqueAutocompleteIdCounter = 0;

export type AutocompletePositionY = 'above' | 'below';

@Component({
moduleId: module.id,
selector: 'md-autocomplete, mat-autocomplete',
Expand All @@ -47,9 +45,6 @@ export class MdAutocomplete implements AfterContentInit {
/** Manages active item in option list based on key events. */
_keyManager: ActiveDescendantKeyManager;

/** Whether the autocomplete panel displays above or below its trigger. */
positionY: AutocompletePositionY = 'below';

/** Whether the autocomplete panel should be visible, depending on option length. */
showPanel = false;

Expand Down Expand Up @@ -97,11 +92,9 @@ export class MdAutocomplete implements AfterContentInit {
});
}

/** Sets a class on the panel based on its position (used to set y-offset). */
/** Sets a class on the panel based on whether it is visible. */
_getClassList() {
return {
'mat-autocomplete-panel-below': this.positionY === 'below',
'mat-autocomplete-panel-above': this.positionY === 'above',
'mat-autocomplete-visible': this.showPanel,
'mat-autocomplete-hidden': !this.showPanel
};
Expand Down
2 changes: 1 addition & 1 deletion src/lib/input/input-container.html
@@ -1,5 +1,5 @@
<div class="mat-input-wrapper">
<div class="mat-input-flex">
<div class="mat-input-flex" #connectionContainer>
<div class="mat-input-prefix" *ngIf="_prefixChildren.length">
<ng-content select="[mdPrefix], [matPrefix]"></ng-content>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/lib/input/input-container.ts
Expand Up @@ -462,6 +462,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC

/** Reference to the input's underline element. */
@ViewChild('underline') underlineRef: ElementRef;
@ViewChild('connectionContainer') _connectionContainerRef: ElementRef;
@ContentChild(MdInputDirective) _mdInputChild: MdInputDirective;
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
@ContentChildren(MdErrorDirective) _errorChildren: QueryList<MdErrorDirective>;
Expand Down

0 comments on commit 667a4e4

Please sign in to comment.