Skip to content

feat(autocomplete): add the ability to set a different panel connection element #11284

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

Merged
merged 1 commit into from
May 15, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 10 additions & 5 deletions src/lib/autocomplete/autocomplete-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ import {CommonModule} from '@angular/common';
import {OverlayModule} from '@angular/cdk/overlay';
import {MatOptionModule, MatCommonModule} from '@angular/material/core';
import {MatAutocomplete} from './autocomplete';
import {
MatAutocompleteTrigger,
} from './autocomplete-trigger';
import {MatAutocompleteTrigger} from './autocomplete-trigger';
import {MatAutocompleteOrigin} from './autocomplete-origin';

@NgModule({
imports: [MatOptionModule, OverlayModule, MatCommonModule, CommonModule],
exports: [MatAutocomplete, MatOptionModule, MatAutocompleteTrigger, MatCommonModule],
declarations: [MatAutocomplete, MatAutocompleteTrigger],
exports: [
MatAutocomplete,
MatOptionModule,
MatAutocompleteTrigger,
MatAutocompleteOrigin,
MatCommonModule
],
declarations: [MatAutocomplete, MatAutocompleteTrigger, MatAutocompleteOrigin],
})
export class MatAutocompleteModule {}
23 changes: 23 additions & 0 deletions src/lib/autocomplete/autocomplete-origin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, ElementRef} from '@angular/core';

/**
* Directive applied to an element to make it usable
* as a connection point for an autocomplete panel.
*/
@Directive({
selector: '[matAutocompleteOrigin]',
exportAs: 'matAutocompleteOrigin',
})
export class MatAutocompleteOrigin {
constructor(
/** Reference to the element on which the directive is applied. */
public elementRef: ElementRef<HTMLElement>) { }
}
12 changes: 12 additions & 0 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {MatFormField} from '@angular/material/form-field';
import {Subscription, defer, fromEvent, merge, of as observableOf, Subject, Observable} from 'rxjs';
import {MatAutocomplete} from './autocomplete';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {MatAutocompleteOrigin} from './autocomplete-origin';


/**
Expand Down Expand Up @@ -91,6 +92,7 @@ export function getMatAutocompleteMissingPanelError(): Error {
'you\'re attempting to open it after the ngAfterContentInit hook.');
}


@Directive({
selector: `input[matAutocomplete], textarea[matAutocomplete]`,
host: {
Expand Down Expand Up @@ -143,6 +145,12 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
/** The autocomplete panel to be attached to this trigger. */
@Input('matAutocomplete') autocomplete: MatAutocomplete;

/**
* Reference relative to which to position the autocomplete panel.
* Defaults to the autocomplete trigger element.
*/
@Input('matAutocompleteConnectedTo') connectedTo: MatAutocompleteOrigin;

/**
* Whether the autocomplete is disabled. When disabled, the element will
* act as a regular input and the user won't be able to open the panel.
Expand Down Expand Up @@ -562,6 +570,10 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
}

private _getConnectedElement(): ElementRef {
if (this.connectedTo) {
return this.connectedTo.elementRef;
}

return this._formField ? this._formField.getConnectedOverlayOrigin() : this._element;
}

Expand Down
42 changes: 31 additions & 11 deletions src/lib/autocomplete/autocomplete.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ Start by adding a regular `matInput` to your template. Let's assume you're using
directive from `ReactiveFormsModule` to track the value of the input.

> Note: It is possible to use template-driven forms instead, if you prefer. We use reactive forms
in this example because it makes subscribing to changes in the input's value easy. For this example, be sure to
import `ReactiveFormsModule` from `@angular/forms` into your `NgModule`. If you are unfamiliar with using reactive
forms, you can read more about the subject in the [Angular documentation](https://angular.io/guide/reactive-forms).
in this example because it makes subscribing to changes in the input's value easy. For this
example, be sure to import `ReactiveFormsModule` from `@angular/forms` into your `NgModule`.
If you are unfamiliar with using reactive forms, you can read more about the subject in the
[Angular documentation](https://angular.io/guide/reactive-forms).

*my-comp.html*
```html
Expand Down Expand Up @@ -41,9 +42,7 @@ to the input's `matAutocomplete` property.
</mat-form-field>

<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let option of options" [value]="option">
{{ option }}
</mat-option>
<mat-option *ngFor="let option of options" [value]="option">{{option}}</mat-option>
</mat-autocomplete>
```

Expand All @@ -61,8 +60,8 @@ option's first letter. We already have access to the built-in `valueChanges` Obs
them through this filter. The resulting Observable, `filteredOptions`, can be added to the
template in place of the `options` property using the `async` pipe.

Below we are also priming our value change stream with an empty string so that the options are filtered by
that value on init (before there are any value changes).
Below we are also priming our value change stream with an empty string so that the options are
filtered by that value on init (before there are any value changes).

\*For optimal accessibility, you may want to consider adding text guidance on the page to explain
filter criteria. This is especially helpful for screenreader users if you're using a non-standard
Expand Down Expand Up @@ -91,6 +90,27 @@ injection token.

<!-- example(autocomplete-auto-active-first-option) -->

### Attaching the autocomplete panel to a different element

By default the autocomplete panel will be attached to your input element, however in some cases you
may want it to attach to a different container element. You can change the element that the
autocomplete is attached to using the `matAutocompleteOrigin` directive together with the
`matAutocompleteConnectedTo` input:

```html
<div class="custom-wrapper-example" matAutocompleteOrigin #origin="matAutocompleteOrigin">
<input
matInput
[formControl]="myControl"
[matAutocomplete]="auto"
[matAutocompleteConnectedTo]="origin">
</div>

<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let option of options" [value]="option">{{option}}</mat-option>
</mat-autocomplete>
```

### Keyboard interaction
- <kbd>DOWN_ARROW</kbd>: Next option becomes active.
- <kbd>UP_ARROW</kbd>: Previous option becomes active.
Expand All @@ -105,7 +125,7 @@ injection token.
<mat-autocomplete #auto="matAutocomplete">
<mat-optgroup *ngFor="let group of filteredGroups | async" [label]="group.name">
<mat-option *ngFor="let option of group.options" [value]="option">
{{ option.name }}
{{option.name}}
</mat-option>
</mat-optgroup>
</mat-autocomplete>
Expand All @@ -115,5 +135,5 @@ injection token.
The input for an autocomplete without text or labels should be given a meaningful label via
`aria-label` or `aria-labelledby`.

The autocomplete trigger is given `role="combobox"`. The trigger sets `aria-owns` to the autocomplete's
id, and sets `aria-activedescendant` to the active option's id.
The autocomplete trigger is given `role="combobox"`. The trigger sets `aria-owns` to the
autocomplete's id, and sets `aria-activedescendant` to the active option's id.
53 changes: 51 additions & 2 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
QueryList,
ViewChild,
ViewChildren,
Type,
} from '@angular/core';
import {
async,
Expand Down Expand Up @@ -54,7 +55,7 @@ describe('MatAutocomplete', () => {
let zone: MockNgZone;

// Creates a test component fixture.
function createComponent(component: any, providers: Provider[] = []): ComponentFixture<any> {
function createComponent<T>(component: Type<T>, providers: Provider[] = []) {
TestBed.configureTestingModule({
imports: [
MatAutocompleteModule,
Expand All @@ -78,7 +79,7 @@ describe('MatAutocomplete', () => {
overlayContainerElement = oc.getContainerElement();
})();

return TestBed.createComponent(component);
return TestBed.createComponent<T>(component);
}

afterEach(inject([OverlayContainer], (currentOverlayContainer: OverlayContainer) => {
Expand Down Expand Up @@ -1978,6 +1979,22 @@ describe('MatAutocomplete', () => {
expect(event.source).toBe(fixture.componentInstance.autocomplete);
expect(event.option.value).toBe('Puerto Rico');
}));

it('should be able to set a custom panel connection element', () => {
const fixture = createComponent(AutocompleteWithDifferentOrigin);

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

const overlayRect =
overlayContainerElement.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
const originRect = fixture.nativeElement.querySelector('.origin').getBoundingClientRect();

expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.bottom),
'Expected autocomplete panel to align with the bottom of the new origin.');
});
});

@Component({
Expand Down Expand Up @@ -2315,3 +2332,35 @@ class AutocompleteWithNumberInputAndNgModel {
selectedValue: number;
values = [1, 2, 3];
}


@Component({
template: `
<div>
<mat-form-field>
<input
matInput
[matAutocomplete]="auto"
[matAutocompleteConnectedTo]="origin"
[(ngModel)]="selectedValue">
</mat-form-field>
</div>

<div
class="origin"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
style="margin-top: 50px">
Connection element
</div>

<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let value of values" [value]="value">{{value}}</mat-option>
</mat-autocomplete>
`
})
class AutocompleteWithDifferentOrigin {
@ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
selectedValue: string;
values = ['one', 'two', 'three'];
}