mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(autocomplete): attach overlay to a more accurate input element (#6282)
* fix(autocomplete): attach overlay to a more accurate input element * Fix issue with transient prefix/suffix * Remove unneeded position change subscription
This commit is contained in:
@@ -53,4 +53,4 @@ Space above cards: <input type="number" [formControl]="topHeightCtrl">
|
||||
<md-option *ngFor="let state of tdStates" [value]="state.name">
|
||||
<span>{{ state.name }}</span>
|
||||
</md-option>
|
||||
</md-autocomplete>
|
||||
</md-autocomplete>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.*/
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -1091,11 +1090,11 @@ 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);
|
||||
@@ -1103,27 +1102,24 @@ describe('MdAutocomplete', () => {
|
||||
|
||||
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();
|
||||
@@ -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.`);
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
@@ -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',
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user