Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Aug 7, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/demo-app/autocomplete/autocomplete-demo.html
Original file line number Diff line number Diff line change
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>
13 changes: 7 additions & 6 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ 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'}
Expand All @@ -476,10 +476,11 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
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 _getConnectedElement(): ElementRef {
return this._inputContainer ? this._inputContainer._connectionContainerRef : this._element;
}

/** This method subscribes to position changes in the autocomplete panel. */
private _subscribeToPositionChanges(strategy: ConnectedPositionStrategy) {
this._panelPositionSubscription = strategy.onPositionChange.subscribe(change => {
this.autocomplete.positionY = change.connectionPair.originY === 'top' ? 'above' : 'below';
Expand All @@ -488,7 +489,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {

/** 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
Original file line number Diff line number Diff line change
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
29 changes: 14 additions & 15 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,24 +1055,25 @@ 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.`);
Expand All @@ -1091,39 +1092,38 @@ 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,12 +1132,11 @@ 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
4 changes: 1 addition & 3 deletions src/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC

/** Reference to the input's underline element. */
@ViewChild('underline') underlineRef: ElementRef;
@ViewChild('connectionContainer') _connectionContainerRef: ElementRef;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not use the underlineRef? The datepicker is using it for a similar purpose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#4914 (comment) You still have to add an offset for when the fallback is above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, the fallback can't just cover the input in this case like with the datepicker, since the user still needs to be able to see what they're typing. Makes sense...

@ContentChild(MdInputDirective) _mdInputChild: MdInputDirective;
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
@ContentChildren(MdErrorDirective) _errorChildren: QueryList<MdErrorDirective>;
Expand Down