@angular/material
Version:
Angular Material
966 lines (963 loc) • 66.3 kB
JavaScript
import { M as MAT_OPTION_PARENT_COMPONENT, a as MatOption, b as MAT_OPTGROUP, c as MatOptionSelectionChange, _ as _countGroupLabelsBeforeOption, d as _getOptionScrollPosition } from './option-acd4abcc.mjs';
export { e as MatOptgroup, a as MatOption } from './option-acd4abcc.mjs';
import * as i0 from '@angular/core';
import { InjectionToken, inject, ChangeDetectorRef, ElementRef, ANIMATION_MODULE_TYPE, EventEmitter, booleanAttribute, TemplateRef, Component, ViewEncapsulation, ChangeDetectionStrategy, ViewChild, ContentChildren, Input, Output, Directive, forwardRef, EnvironmentInjector, ViewContainerRef, NgZone, Renderer2, afterNextRender, NgModule } from '@angular/core';
import { ViewportRuler, CdkScrollableModule } from '@angular/cdk/scrolling';
import { Overlay, OverlayConfig, OverlayModule } from '@angular/cdk/overlay';
import { _IdGenerator, ActiveDescendantKeyManager, removeAriaReferencedId, addAriaReferencedId } from '@angular/cdk/a11y';
import { Platform, _getFocusedElementPierceShadowDom, _getEventTarget } from '@angular/cdk/platform';
import { Subscription, Subject, merge, of, defer, Observable } from 'rxjs';
import { Directionality } from '@angular/cdk/bidi';
import { hasModifierKey, ESCAPE, ENTER, TAB, UP_ARROW, DOWN_ARROW } from '@angular/cdk/keycodes';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { TemplatePortal } from '@angular/cdk/portal';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { filter, map, startWith, switchMap, tap, delay, take } from 'rxjs/operators';
import { M as MAT_FORM_FIELD } from './form-field-7b8fd54e.mjs';
import { M as MatOptionModule } from './index-bea46290.mjs';
import { M as MatCommonModule } from './common-module-43c0ba57.mjs';
import './ripple-acd53c76.mjs';
import '@angular/cdk/coercion';
import '@angular/cdk/private';
import './pseudo-checkbox-b981dcda.mjs';
import './structural-styles-7c66c8fc.mjs';
import '@angular/common';
import '@angular/cdk/observers/private';
import './index-5decf30c.mjs';
import './pseudo-checkbox-module-99bfad5b.mjs';
/** Event object that is emitted when an autocomplete option is selected. */
class MatAutocompleteSelectedEvent {
source;
option;
constructor(
/** Reference to the autocomplete panel that emitted the event. */
source,
/** Option that was selected. */
option) {
this.source = source;
this.option = option;
}
}
/** Injection token to be used to override the default options for `mat-autocomplete`. */
const MAT_AUTOCOMPLETE_DEFAULT_OPTIONS = new InjectionToken('mat-autocomplete-default-options', {
providedIn: 'root',
factory: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY,
});
/**
* @docs-private
* @deprecated No longer used, will be removed.
* @breaking-change 21.0.0
*/
function MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY() {
return {
autoActiveFirstOption: false,
autoSelectActiveOption: false,
hideSingleSelectionIndicator: false,
requireSelection: false,
hasBackdrop: false,
};
}
/** Autocomplete component. */
class MatAutocomplete {
_changeDetectorRef = inject(ChangeDetectorRef);
_elementRef = inject(ElementRef);
_defaults = inject(MAT_AUTOCOMPLETE_DEFAULT_OPTIONS);
_animationsDisabled = inject(ANIMATION_MODULE_TYPE, { optional: true }) === 'NoopAnimations';
_activeOptionChanges = Subscription.EMPTY;
/** Manages active item in option list based on key events. */
_keyManager;
/** Whether the autocomplete panel should be visible, depending on option length. */
showPanel = false;
/** Whether the autocomplete panel is open. */
get isOpen() {
return this._isOpen && this.showPanel;
}
_isOpen = false;
/** Latest trigger that opened the autocomplete. */
_latestOpeningTrigger;
/** @docs-private Sets the theme color of the panel. */
_setColor(value) {
this._color = value;
this._changeDetectorRef.markForCheck();
}
/** @docs-private theme color of the panel */
_color;
// The @ViewChild query for TemplateRef here needs to be static because some code paths
// lead to the overlay being created before change detection has finished for this component.
// Notably, another component may trigger `focus` on the autocomplete-trigger.
/** @docs-private */
template;
/** Element for the panel containing the autocomplete options. */
panel;
/** Reference to all options within the autocomplete. */
options;
/** Reference to all option groups within the autocomplete. */
optionGroups;
/** Aria label of the autocomplete. */
ariaLabel;
/** Input that can be used to specify the `aria-labelledby` attribute. */
ariaLabelledby;
/** Function that maps an option's control value to its display value in the trigger. */
displayWith = null;
/**
* Whether the first option should be highlighted when the autocomplete panel is opened.
* Can be configured globally through the `MAT_AUTOCOMPLETE_DEFAULT_OPTIONS` token.
*/
autoActiveFirstOption;
/** Whether the active option should be selected as the user is navigating. */
autoSelectActiveOption;
/**
* Whether the user is required to make a selection when they're interacting with the
* autocomplete. If the user moves away from the autocomplete without selecting an option from
* the list, the value will be reset. If the user opens the panel and closes it without
* interacting or selecting a value, the initial value will be kept.
*/
requireSelection;
/**
* Specify the width of the autocomplete panel. Can be any CSS sizing value, otherwise it will
* match the width of its host.
*/
panelWidth;
/** Whether ripples are disabled within the autocomplete panel. */
disableRipple;
/** Event that is emitted whenever an option from the list is selected. */
optionSelected = new EventEmitter();
/** Event that is emitted when the autocomplete panel is opened. */
opened = new EventEmitter();
/** Event that is emitted when the autocomplete panel is closed. */
closed = new EventEmitter();
/** Emits whenever an option is activated. */
optionActivated = new EventEmitter();
/**
* Takes classes set on the host mat-autocomplete element and applies them to the panel
* inside the overlay container to allow for easy styling.
*/
set classList(value) {
this._classList = value;
this._elementRef.nativeElement.className = '';
}
_classList;
/** Whether checkmark indicator for single-selection options is hidden. */
get hideSingleSelectionIndicator() {
return this._hideSingleSelectionIndicator;
}
set hideSingleSelectionIndicator(value) {
this._hideSingleSelectionIndicator = value;
this._syncParentProperties();
}
_hideSingleSelectionIndicator;
/** Syncs the parent state with the individual options. */
_syncParentProperties() {
if (this.options) {
for (const option of this.options) {
option._changeDetectorRef.markForCheck();
}
}
}
/** Unique ID to be used by autocomplete trigger's "aria-owns" property. */
id = inject(_IdGenerator).getId('mat-autocomplete-');
/**
* Tells any descendant `mat-optgroup` to use the inert a11y pattern.
* @docs-private
*/
inertGroups;
constructor() {
const platform = inject(Platform);
// TODO(crisbeto): the problem that the `inertGroups` option resolves is only present on
// Safari using VoiceOver. We should occasionally check back to see whether the bug
// wasn't resolved in VoiceOver, and if it has, we can remove this and the `inertGroups`
// option altogether.
this.inertGroups = platform?.SAFARI || false;
this.autoActiveFirstOption = !!this._defaults.autoActiveFirstOption;
this.autoSelectActiveOption = !!this._defaults.autoSelectActiveOption;
this.requireSelection = !!this._defaults.requireSelection;
this._hideSingleSelectionIndicator = this._defaults.hideSingleSelectionIndicator ?? false;
}
ngAfterContentInit() {
this._keyManager = new ActiveDescendantKeyManager(this.options)
.withWrap()
.skipPredicate(this._skipPredicate);
this._activeOptionChanges = this._keyManager.change.subscribe(index => {
if (this.isOpen) {
this.optionActivated.emit({ source: this, option: this.options.toArray()[index] || null });
}
});
// Set the initial visibility state.
this._setVisibility();
}
ngOnDestroy() {
this._keyManager?.destroy();
this._activeOptionChanges.unsubscribe();
}
/**
* Sets the panel scrollTop. This allows us to manually scroll to display options
* above or below the fold, as they are not actually being focused when active.
*/
_setScrollTop(scrollTop) {
if (this.panel) {
this.panel.nativeElement.scrollTop = scrollTop;
}
}
/** Returns the panel's scrollTop. */
_getScrollTop() {
return this.panel ? this.panel.nativeElement.scrollTop : 0;
}
/** Panel should hide itself when the option list is empty. */
_setVisibility() {
this.showPanel = !!this.options?.length;
this._changeDetectorRef.markForCheck();
}
/** Emits the `select` event. */
_emitSelectEvent(option) {
const event = new MatAutocompleteSelectedEvent(this, option);
this.optionSelected.emit(event);
}
/** Gets the aria-labelledby for the autocomplete panel. */
_getPanelAriaLabelledby(labelId) {
if (this.ariaLabel) {
return null;
}
const labelExpression = labelId ? labelId + ' ' : '';
return this.ariaLabelledby ? labelExpression + this.ariaLabelledby : labelId;
}
// `skipPredicate` determines if key manager should avoid putting a given option in the tab
// order. Allow disabled list items to receive focus via keyboard to align with WAI ARIA
// recommendation.
//
// Normally WAI ARIA's instructions are to exclude disabled items from the tab order, but it
// makes a few exceptions for compound widgets.
//
// From [Developing a Keyboard Interface](
// https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/):
// "For the following composite widget elements, keep them focusable when disabled: Options in a
// Listbox..."
//
// The user can focus disabled options using the keyboard, but the user cannot click disabled
// options.
_skipPredicate() {
return false;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatAutocomplete, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "19.2.0", type: MatAutocomplete, isStandalone: true, selector: "mat-autocomplete", inputs: { ariaLabel: ["aria-label", "ariaLabel"], ariaLabelledby: ["aria-labelledby", "ariaLabelledby"], displayWith: "displayWith", autoActiveFirstOption: ["autoActiveFirstOption", "autoActiveFirstOption", booleanAttribute], autoSelectActiveOption: ["autoSelectActiveOption", "autoSelectActiveOption", booleanAttribute], requireSelection: ["requireSelection", "requireSelection", booleanAttribute], panelWidth: "panelWidth", disableRipple: ["disableRipple", "disableRipple", booleanAttribute], classList: ["class", "classList"], hideSingleSelectionIndicator: ["hideSingleSelectionIndicator", "hideSingleSelectionIndicator", booleanAttribute] }, outputs: { optionSelected: "optionSelected", opened: "opened", closed: "closed", optionActivated: "optionActivated" }, host: { classAttribute: "mat-mdc-autocomplete" }, providers: [{ provide: MAT_OPTION_PARENT_COMPONENT, useExisting: MatAutocomplete }], queries: [{ propertyName: "options", predicate: MatOption, descendants: true }, { propertyName: "optionGroups", predicate: MAT_OPTGROUP, descendants: true }], viewQueries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true, static: true }, { propertyName: "panel", first: true, predicate: ["panel"], descendants: true }], exportAs: ["matAutocomplete"], ngImport: i0, template: "<ng-template let-formFieldId=\"id\">\n <div\n class=\"mat-mdc-autocomplete-panel mdc-menu-surface mdc-menu-surface--open\"\n role=\"listbox\"\n [id]=\"id\"\n [class]=\"_classList\"\n [class.mat-mdc-autocomplete-visible]=\"showPanel\"\n [class.mat-mdc-autocomplete-hidden]=\"!showPanel\"\n [class.mat-autocomplete-panel-animations-enabled]=\"!_animationsDisabled\"\n [class.mat-primary]=\"_color === 'primary'\"\n [class.mat-accent]=\"_color === 'accent'\"\n [class.mat-warn]=\"_color === 'warn'\"\n [attr.aria-label]=\"ariaLabel || null\"\n [attr.aria-labelledby]=\"_getPanelAriaLabelledby(formFieldId)\"\n #panel>\n <ng-content></ng-content>\n </div>\n</ng-template>\n", styles: ["div.mat-mdc-autocomplete-panel{width:100%;max-height:256px;visibility:hidden;transform-origin:center top;overflow:auto;padding:8px 0;box-sizing:border-box;position:static;border-radius:var(--mat-autocomplete-container-shape, var(--mat-sys-corner-extra-small));box-shadow:var(--mat-autocomplete-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12));background-color:var(--mat-autocomplete-background-color, var(--mat-sys-surface-container))}@media(forced-colors: active){div.mat-mdc-autocomplete-panel{outline:solid 1px}}.cdk-overlay-pane:not(.mat-mdc-autocomplete-panel-above) div.mat-mdc-autocomplete-panel{border-top-left-radius:0;border-top-right-radius:0}.mat-mdc-autocomplete-panel-above div.mat-mdc-autocomplete-panel{border-bottom-left-radius:0;border-bottom-right-radius:0;transform-origin:center bottom}div.mat-mdc-autocomplete-panel.mat-mdc-autocomplete-visible{visibility:visible}div.mat-mdc-autocomplete-panel.mat-mdc-autocomplete-hidden{visibility:hidden;pointer-events:none}@keyframes _mat-autocomplete-enter{from{opacity:0;transform:scaleY(0.8)}to{opacity:1;transform:none}}.mat-autocomplete-panel-animations-enabled{animation:_mat-autocomplete-enter 120ms cubic-bezier(0, 0, 0.2, 1)}mat-autocomplete{display:none}"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatAutocomplete, decorators: [{
type: Component,
args: [{ selector: 'mat-autocomplete', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'matAutocomplete', host: {
'class': 'mat-mdc-autocomplete',
}, providers: [{ provide: MAT_OPTION_PARENT_COMPONENT, useExisting: MatAutocomplete }], template: "<ng-template let-formFieldId=\"id\">\n <div\n class=\"mat-mdc-autocomplete-panel mdc-menu-surface mdc-menu-surface--open\"\n role=\"listbox\"\n [id]=\"id\"\n [class]=\"_classList\"\n [class.mat-mdc-autocomplete-visible]=\"showPanel\"\n [class.mat-mdc-autocomplete-hidden]=\"!showPanel\"\n [class.mat-autocomplete-panel-animations-enabled]=\"!_animationsDisabled\"\n [class.mat-primary]=\"_color === 'primary'\"\n [class.mat-accent]=\"_color === 'accent'\"\n [class.mat-warn]=\"_color === 'warn'\"\n [attr.aria-label]=\"ariaLabel || null\"\n [attr.aria-labelledby]=\"_getPanelAriaLabelledby(formFieldId)\"\n #panel>\n <ng-content></ng-content>\n </div>\n</ng-template>\n", styles: ["div.mat-mdc-autocomplete-panel{width:100%;max-height:256px;visibility:hidden;transform-origin:center top;overflow:auto;padding:8px 0;box-sizing:border-box;position:static;border-radius:var(--mat-autocomplete-container-shape, var(--mat-sys-corner-extra-small));box-shadow:var(--mat-autocomplete-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12));background-color:var(--mat-autocomplete-background-color, var(--mat-sys-surface-container))}@media(forced-colors: active){div.mat-mdc-autocomplete-panel{outline:solid 1px}}.cdk-overlay-pane:not(.mat-mdc-autocomplete-panel-above) div.mat-mdc-autocomplete-panel{border-top-left-radius:0;border-top-right-radius:0}.mat-mdc-autocomplete-panel-above div.mat-mdc-autocomplete-panel{border-bottom-left-radius:0;border-bottom-right-radius:0;transform-origin:center bottom}div.mat-mdc-autocomplete-panel.mat-mdc-autocomplete-visible{visibility:visible}div.mat-mdc-autocomplete-panel.mat-mdc-autocomplete-hidden{visibility:hidden;pointer-events:none}@keyframes _mat-autocomplete-enter{from{opacity:0;transform:scaleY(0.8)}to{opacity:1;transform:none}}.mat-autocomplete-panel-animations-enabled{animation:_mat-autocomplete-enter 120ms cubic-bezier(0, 0, 0.2, 1)}mat-autocomplete{display:none}"] }]
}], ctorParameters: () => [], propDecorators: { template: [{
type: ViewChild,
args: [TemplateRef, { static: true }]
}], panel: [{
type: ViewChild,
args: ['panel']
}], options: [{
type: ContentChildren,
args: [MatOption, { descendants: true }]
}], optionGroups: [{
type: ContentChildren,
args: [MAT_OPTGROUP, { descendants: true }]
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], ariaLabelledby: [{
type: Input,
args: ['aria-labelledby']
}], displayWith: [{
type: Input
}], autoActiveFirstOption: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], autoSelectActiveOption: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], requireSelection: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], panelWidth: [{
type: Input
}], disableRipple: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], optionSelected: [{
type: Output
}], opened: [{
type: Output
}], closed: [{
type: Output
}], optionActivated: [{
type: Output
}], classList: [{
type: Input,
args: ['class']
}], hideSingleSelectionIndicator: [{
type: Input,
args: [{ transform: booleanAttribute }]
}] } });
/**
* Directive applied to an element to make it usable
* as a connection point for an autocomplete panel.
*/
class MatAutocompleteOrigin {
elementRef = inject(ElementRef);
constructor() { }
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatAutocompleteOrigin, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.0", type: MatAutocompleteOrigin, isStandalone: true, selector: "[matAutocompleteOrigin]", exportAs: ["matAutocompleteOrigin"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatAutocompleteOrigin, decorators: [{
type: Directive,
args: [{
selector: '[matAutocompleteOrigin]',
exportAs: 'matAutocompleteOrigin',
}]
}], ctorParameters: () => [] });
/**
* Provider that allows the autocomplete to register as a ControlValueAccessor.
* @docs-private
*/
const MAT_AUTOCOMPLETE_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MatAutocompleteTrigger),
multi: true,
};
/**
* Creates an error to be thrown when attempting to use an autocomplete trigger without a panel.
* @docs-private
*/
function getMatAutocompleteMissingPanelError() {
return Error('Attempting to open an undefined instance of `mat-autocomplete`. ' +
'Make sure that the id passed to the `matAutocomplete` is correct and that ' +
"you're attempting to open it after the ngAfterContentInit hook.");
}
/** Injection token that determines the scroll handling while the autocomplete panel is open. */
const MAT_AUTOCOMPLETE_SCROLL_STRATEGY = new InjectionToken('mat-autocomplete-scroll-strategy', {
providedIn: 'root',
factory: () => {
const overlay = inject(Overlay);
return () => overlay.scrollStrategies.reposition();
},
});
/**
* @docs-private
* @deprecated No longer used, will be removed.
* @breaking-change 21.0.0
*/
function MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY(overlay) {
return () => overlay.scrollStrategies.reposition();
}
/**
* @docs-private
* @deprecated No longer used, will be removed.
* @breaking-change 21.0.0
*/
const MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY_PROVIDER = {
provide: MAT_AUTOCOMPLETE_SCROLL_STRATEGY,
deps: [Overlay],
useFactory: MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY,
};
/** Base class with all of the `MatAutocompleteTrigger` functionality. */
class MatAutocompleteTrigger {
_environmentInjector = inject(EnvironmentInjector);
_element = inject(ElementRef);
_overlay = inject(Overlay);
_viewContainerRef = inject(ViewContainerRef);
_zone = inject(NgZone);
_changeDetectorRef = inject(ChangeDetectorRef);
_dir = inject(Directionality, { optional: true });
_formField = inject(MAT_FORM_FIELD, { optional: true, host: true });
_viewportRuler = inject(ViewportRuler);
_scrollStrategy = inject(MAT_AUTOCOMPLETE_SCROLL_STRATEGY);
_renderer = inject(Renderer2);
_defaults = inject(MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, { optional: true });
_overlayRef;
_portal;
_componentDestroyed = false;
_initialized = new Subject();
_keydownSubscription;
_outsideClickSubscription;
_cleanupWindowBlur;
/** Old value of the native input. Used to work around issues with the `input` event on IE. */
_previousValue;
/** Value of the input element when the panel was attached (even if there are no options). */
_valueOnAttach;
/** Value on the previous keydown event. */
_valueOnLastKeydown;
/** Strategy that is used to position the panel. */
_positionStrategy;
/** Whether or not the label state is being overridden. */
_manuallyFloatingLabel = false;
/** The subscription for closing actions (some are bound to document). */
_closingActionsSubscription;
/** Subscription to viewport size changes. */
_viewportSubscription = Subscription.EMPTY;
/** Implements BreakpointObserver to be used to detect handset landscape */
_breakpointObserver = inject(BreakpointObserver);
_handsetLandscapeSubscription = Subscription.EMPTY;
/**
* Whether the autocomplete can open the next time it is focused. Used to prevent a focused,
* closed autocomplete from being reopened if the user switches to another browser tab and then
* comes back.
*/
_canOpenOnNextFocus = true;
/** Value inside the input before we auto-selected an option. */
_valueBeforeAutoSelection;
/**
* Current option that we have auto-selected as the user is navigating,
* but which hasn't been propagated to the model value yet.
*/
_pendingAutoselectedOption;
/** Stream of keyboard events that can close the panel. */
_closeKeyEventStream = new Subject();
/**
* Event handler for when the window is blurred. Needs to be an
* arrow function in order to preserve the context.
*/
_windowBlurHandler = () => {
// If the user blurred the window while the autocomplete is focused, it means that it'll be
// refocused when they come back. In this case we want to skip the first focus event, if the
// pane was closed, in order to avoid reopening it unintentionally.
this._canOpenOnNextFocus = this.panelOpen || !this._hasFocus();
};
/** `View -> model callback called when value changes` */
_onChange = () => { };
/** `View -> model callback called when autocomplete has been touched` */
_onTouched = () => { };
/** The autocomplete panel to be attached to this trigger. */
autocomplete;
/**
* Position of the autocomplete panel relative to the trigger element. A position of `auto`
* will render the panel underneath the trigger if there is enough space for it to fit in
* the viewport, otherwise the panel will be shown above it. If the position is set to
* `above` or `below`, the panel will always be shown above or below the trigger. no matter
* whether it fits completely in the viewport.
*/
position = 'auto';
/**
* Reference relative to which to position the autocomplete panel.
* Defaults to the autocomplete trigger element.
*/
connectedTo;
/**
* `autocomplete` attribute to be set on the input element.
* @docs-private
*/
autocompleteAttribute = 'off';
/**
* 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.
*/
autocompleteDisabled;
constructor() { }
/** Class to apply to the panel when it's above the input. */
_aboveClass = 'mat-mdc-autocomplete-panel-above';
ngAfterViewInit() {
this._initialized.next();
this._initialized.complete();
this._cleanupWindowBlur = this._renderer.listen('window', 'blur', this._windowBlurHandler);
}
ngOnChanges(changes) {
if (changes['position'] && this._positionStrategy) {
this._setStrategyPositions(this._positionStrategy);
if (this.panelOpen) {
this._overlayRef.updatePosition();
}
}
}
ngOnDestroy() {
this._cleanupWindowBlur?.();
this._handsetLandscapeSubscription.unsubscribe();
this._viewportSubscription.unsubscribe();
this._componentDestroyed = true;
this._destroyPanel();
this._closeKeyEventStream.complete();
this._clearFromModal();
}
/** Whether or not the autocomplete panel is open. */
get panelOpen() {
return this._overlayAttached && this.autocomplete.showPanel;
}
_overlayAttached = false;
/** Opens the autocomplete suggestion panel. */
openPanel() {
this._openPanelInternal();
}
/** Closes the autocomplete suggestion panel. */
closePanel() {
this._resetLabel();
if (!this._overlayAttached) {
return;
}
if (this.panelOpen) {
// Only emit if the panel was visible.
// `afterNextRender` always runs outside of the Angular zone, so all the subscriptions from
// `_subscribeToClosingActions()` are also outside of the Angular zone.
// We should manually run in Angular zone to update UI after panel closing.
this._zone.run(() => {
this.autocomplete.closed.emit();
});
}
// Only reset if this trigger is the latest one that opened the
// autocomplete since another may have taken it over.
if (this.autocomplete._latestOpeningTrigger === this) {
this.autocomplete._isOpen = false;
this.autocomplete._latestOpeningTrigger = null;
}
this._overlayAttached = false;
this._pendingAutoselectedOption = null;
if (this._overlayRef && this._overlayRef.hasAttached()) {
this._overlayRef.detach();
this._closingActionsSubscription.unsubscribe();
}
this._updatePanelState();
// Note that in some cases this can end up being called after the component is destroyed.
// Add a check to ensure that we don't try to run change detection on a destroyed view.
if (!this._componentDestroyed) {
// We need to trigger change detection manually, because
// `fromEvent` doesn't seem to do it at the proper time.
// This ensures that the label is reset when the
// user clicks outside.
this._changeDetectorRef.detectChanges();
}
// Remove aria-owns attribute when the autocomplete is no longer visible.
if (this._trackedModal) {
removeAriaReferencedId(this._trackedModal, 'aria-owns', this.autocomplete.id);
}
}
/**
* Updates the position of the autocomplete suggestion panel to ensure that it fits all options
* within the viewport.
*/
updatePosition() {
if (this._overlayAttached) {
this._overlayRef.updatePosition();
}
}
/**
* A stream of actions that should close the autocomplete panel, including
* when an option is selected, on blur, and when TAB is pressed.
*/
get panelClosingActions() {
return merge(this.optionSelections, this.autocomplete._keyManager.tabOut.pipe(filter(() => this._overlayAttached)), this._closeKeyEventStream, this._getOutsideClickStream(), this._overlayRef
? this._overlayRef.detachments().pipe(filter(() => this._overlayAttached))
: of()).pipe(
// Normalize the output so we return a consistent type.
map(event => (event instanceof MatOptionSelectionChange ? event : null)));
}
/** Stream of changes to the selection state of the autocomplete options. */
optionSelections = defer(() => {
const options = this.autocomplete ? this.autocomplete.options : null;
if (options) {
return options.changes.pipe(startWith(options), switchMap(() => merge(...options.map(option => option.onSelectionChange))));
}
// If there are any subscribers before `ngAfterViewInit`, the `autocomplete` will be undefined.
// Return a stream that we'll replace with the real one once everything is in place.
return this._initialized.pipe(switchMap(() => this.optionSelections));
});
/** The currently active option, coerced to MatOption type. */
get activeOption() {
if (this.autocomplete && this.autocomplete._keyManager) {
return this.autocomplete._keyManager.activeItem;
}
return null;
}
/** Stream of clicks outside of the autocomplete panel. */
_getOutsideClickStream() {
return new Observable(observer => {
const listener = (event) => {
// If we're in the Shadow DOM, the event target will be the shadow root, so we have to
// fall back to check the first element in the path of the click event.
const clickTarget = _getEventTarget(event);
const formField = this._formField
? this._formField.getConnectedOverlayOrigin().nativeElement
: null;
const customOrigin = this.connectedTo ? this.connectedTo.elementRef.nativeElement : null;
if (this._overlayAttached &&
clickTarget !== this._element.nativeElement &&
// Normally focus moves inside `mousedown` so this condition will almost always be
// true. Its main purpose is to handle the case where the input is focused from an
// outside click which propagates up to the `body` listener within the same sequence
// and causes the panel to close immediately (see #3106).
!this._hasFocus() &&
(!formField || !formField.contains(clickTarget)) &&
(!customOrigin || !customOrigin.contains(clickTarget)) &&
!!this._overlayRef &&
!this._overlayRef.overlayElement.contains(clickTarget)) {
observer.next(event);
}
};
const cleanups = [
this._renderer.listen('document', 'click', listener),
this._renderer.listen('document', 'auxclick', listener),
this._renderer.listen('document', 'touchend', listener),
];
return () => {
cleanups.forEach(current => current());
};
});
}
// Implemented as part of ControlValueAccessor.
writeValue(value) {
Promise.resolve(null).then(() => this._assignOptionValue(value));
}
// Implemented as part of ControlValueAccessor.
registerOnChange(fn) {
this._onChange = fn;
}
// Implemented as part of ControlValueAccessor.
registerOnTouched(fn) {
this._onTouched = fn;
}
// Implemented as part of ControlValueAccessor.
setDisabledState(isDisabled) {
this._element.nativeElement.disabled = isDisabled;
}
_handleKeydown(event) {
const keyCode = event.keyCode;
const hasModifier = hasModifierKey(event);
// Prevent the default action on all escape key presses. This is here primarily to bring IE
// in line with other browsers. By default, pressing escape on IE will cause it to revert
// the input value to the one that it had on focus, however it won't dispatch any events
// which means that the model value will be out of sync with the view.
if (keyCode === ESCAPE && !hasModifier) {
event.preventDefault();
}
this._valueOnLastKeydown = this._element.nativeElement.value;
if (this.activeOption && keyCode === ENTER && this.panelOpen && !hasModifier) {
this.activeOption._selectViaInteraction();
this._resetActiveItem();
event.preventDefault();
}
else if (this.autocomplete) {
const prevActiveItem = this.autocomplete._keyManager.activeItem;
const isArrowKey = keyCode === UP_ARROW || keyCode === DOWN_ARROW;
if (keyCode === TAB || (isArrowKey && !hasModifier && this.panelOpen)) {
this.autocomplete._keyManager.onKeydown(event);
}
else if (isArrowKey && this._canOpen()) {
this._openPanelInternal(this._valueOnLastKeydown);
}
if (isArrowKey || this.autocomplete._keyManager.activeItem !== prevActiveItem) {
this._scrollToOption(this.autocomplete._keyManager.activeItemIndex || 0);
if (this.autocomplete.autoSelectActiveOption && this.activeOption) {
if (!this._pendingAutoselectedOption) {
this._valueBeforeAutoSelection = this._valueOnLastKeydown;
}
this._pendingAutoselectedOption = this.activeOption;
this._assignOptionValue(this.activeOption.value);
}
}
}
}
_handleInput(event) {
let target = event.target;
let value = target.value;
// Based on `NumberValueAccessor` from forms.
if (target.type === 'number') {
value = value == '' ? null : parseFloat(value);
}
// If the input has a placeholder, IE will fire the `input` event on page load,
// focus and blur, in addition to when the user actually changed the value. To
// filter out all of the extra events, we save the value on focus and between
// `input` events, and we check whether it changed.
// See: https://connect.microsoft.com/IE/feedback/details/885747/
if (this._previousValue !== value) {
this._previousValue = value;
this._pendingAutoselectedOption = null;
// If selection is required we don't write to the CVA while the user is typing.
// At the end of the selection either the user will have picked something
// or we'll reset the value back to null.
if (!this.autocomplete || !this.autocomplete.requireSelection) {
this._onChange(value);
}
if (!value) {
this._clearPreviousSelectedOption(null, false);
}
else if (this.panelOpen && !this.autocomplete.requireSelection) {
// Note that we don't reset this when `requireSelection` is enabled,
// because the option will be reset when the panel is closed.
const selectedOption = this.autocomplete.options?.find(option => option.selected);
if (selectedOption) {
const display = this._getDisplayValue(selectedOption.value);
if (value !== display) {
selectedOption.deselect(false);
}
}
}
if (this._canOpen() && this._hasFocus()) {
// When the `input` event fires, the input's value will have already changed. This means
// that if we take the `this._element.nativeElement.value` directly, it'll be one keystroke
// behind. This can be a problem when the user selects a value, changes a character while
// the input still has focus and then clicks away (see #28432). To work around it, we
// capture the value in `keydown` so we can use it here.
const valueOnAttach = this._valueOnLastKeydown ?? this._element.nativeElement.value;
this._valueOnLastKeydown = null;
this._openPanelInternal(valueOnAttach);
}
}
}
_handleFocus() {
if (!this._canOpenOnNextFocus) {
this._canOpenOnNextFocus = true;
}
else if (this._canOpen()) {
this._previousValue = this._element.nativeElement.value;
this._attachOverlay(this._previousValue);
this._floatLabel(true);
}
}
_handleClick() {
if (this._canOpen() && !this.panelOpen) {
this._openPanelInternal();
}
}
/** Whether the input currently has focus. */
_hasFocus() {
return _getFocusedElementPierceShadowDom() === this._element.nativeElement;
}
/**
* In "auto" mode, the label will animate down as soon as focus is lost.
* This causes the value to jump when selecting an option with the mouse.
* This method manually floats the label until the panel can be closed.
* @param shouldAnimate Whether the label should be animated when it is floated.
*/
_floatLabel(shouldAnimate = false) {
if (this._formField && this._formField.floatLabel === 'auto') {
if (shouldAnimate) {
this._formField._animateAndLockLabel();
}
else {
this._formField.floatLabel = 'always';
}
this._manuallyFloatingLabel = true;
}
}
/** If the label has been manually elevated, return it to its normal state. */
_resetLabel() {
if (this._manuallyFloatingLabel) {
if (this._formField) {
this._formField.floatLabel = 'auto';
}
this._manuallyFloatingLabel = false;
}
}
/**
* This method listens to a stream of panel closing actions and resets the
* stream every time the option list changes.
*/
_subscribeToClosingActions() {
const initialRender = new Observable(subscriber => {
afterNextRender(() => {
subscriber.next();
}, { injector: this._environmentInjector });
});
const optionChanges = this.autocomplete.options?.changes.pipe(tap(() => this._positionStrategy.reapplyLastPosition()),
// Defer emitting to the stream until the next tick, because changing
// bindings in here will cause "changed after checked" errors.
delay(0)) ?? of();
// When the options are initially rendered, and when the option list changes...
return (merge(initialRender, optionChanges)
.pipe(
// create a new stream of panelClosingActions, replacing any previous streams
// that were created, and flatten it so our stream only emits closing events...
switchMap(() => this._zone.run(() => {
// `afterNextRender` always runs outside of the Angular zone, thus we have to re-enter
// the Angular zone. This will lead to change detection being called outside of the Angular
// zone and the `autocomplete.opened` will also emit outside of the Angular.
const wasOpen = this.panelOpen;
this._resetActiveItem();
this._updatePanelState();
this._changeDetectorRef.detectChanges();
if (this.panelOpen) {
this._overlayRef.updatePosition();
}
if (wasOpen !== this.panelOpen) {
// If the `panelOpen` state changed, we need to make sure to emit the `opened` or
// `closed` event, because we may not have emitted it. This can happen
// - if the users opens the panel and there are no options, but the
// options come in slightly later or as a result of the value changing,
// - if the panel is closed after the user entered a string that did not match any
// of the available options,
// - if a valid string is entered after an invalid one.
if (this.panelOpen) {
this._emitOpened();
}
else {
this.autocomplete.closed.emit();
}
}
return this.panelClosingActions;
})),
// when the first closing event occurs...
take(1))
// set the value, close the panel, and complete.
.subscribe(event => this._setValueAndClose(event)));
}
/**
* Emits the opened event once it's known that the panel will be shown and stores
* the state of the trigger right before the opening sequence was finished.
*/
_emitOpened() {
this.autocomplete.opened.emit();
}
/** Destroys the autocomplete suggestion panel. */
_destroyPanel() {
if (this._overlayRef) {
this.closePanel();
this._overlayRef.dispose();
this._overlayRef = null;
}
}
/** Given a value, returns the string that should be shown within the input. */
_getDisplayValue(value) {
const autocomplete = this.autocomplete;
return autocomplete && autocomplete.displayWith ? autocomplete.displayWith(value) : value;
}
_assignOptionValue(value) {
const toDisplay = this._getDisplayValue(value);
if (value == null) {
this._clearPreviousSelectedOption(null, false);
}
// Simply falling back to an empty string if the display value is falsy does not work properly.
// The display value can also be the number zero and shouldn't fall back to an empty string.
this._updateNativeInputValue(toDisplay != null ? toDisplay : '');
}
_updateNativeInputValue(value) {
// If it's used within a `MatFormField`, we should set it through the property so it can go
// through change detection.
if (this._formField) {
this._formField._control.value = value;
}
else {
this._element.nativeElement.value = value;
}
this._previousValue = value;
}
/**
* This method closes the panel, and if a value is specified, also sets the associated
* control to that value. It will also mark the control as dirty if this interaction
* stemmed from the user.
*/
_setValueAndClose(event) {
const panel = this.autocomplete;
const toSelect = event ? event.source : this._pendingAutoselectedOption;
if (toSelect) {
this._clearPreviousSelectedOption(toSelect);
this._assignOptionValue(toSelect.value);
// TODO(crisbeto): this should wait until the animation is done, otherwise the value
// gets reset while the panel is still animating which looks glitchy. It'll likely break
// some tests to change it at this point.
this._onChange(toSelect.value);
panel._emitSelectEvent(toSelect);
this._element.nativeElement.focus();
}
else if (panel.requireSelection &&
this._element.nativeElement.value !== this._valueOnAttach) {
this._clearPreviousSelectedOption(null);
this._assignOptionValue(null);
this._onChange(null);
}
this.closePanel();
}
/**
* Clear any previous selected option and emit a selection change event for this option
*/
_clearPreviousSelectedOption(skip, emitEvent) {
// Null checks are necessary here, because the autocomplete
// or its options may not have been assigned yet.
this.autocomplete?.options?.forEach(option => {
if (option !== skip && option.selected) {
option.deselect(emitEvent);
}
});
}
_openPanelInternal(valueOnAttach = this._element.nativeElement.value) {
this._attachOverlay(valueOnAttach);
this._floatLabel();
// Add aria-owns attribute when the autocomplete becomes visible.
if (this._trackedModal) {
const panelId = this.autocomplete.id;
addAriaReferencedId(this._trackedModal, 'aria-owns', panelId);
}
}
_attachOverlay(valueOnAttach) {
if (!this.autocomplete && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw getMatAutocompleteMissingPanelError();
}
let overlayRef = this._overlayRef;
if (!overlayRef) {
this._portal = new TemplatePortal(this.autocomplete.template, this._viewContainerRef, {
id: this._formField?.getLabelId(),
});
overlayRef = this._overlay.create(this._getOverlayConfig());
this._overlayRef = overlayRef;
this._viewportSubscription = this._viewportRuler.change().subscribe(() => {
if (this.panelOpen && overlayRef) {
overlayRef.updateSize({ width: this._getPanelWidth() });
}
});
// Subscribe to the breakpoint events stream to detect when screen is in
// handsetLandscape.
this._handsetLandscapeSubscription = this._breakpointObserver
.observe(Breakpoints.HandsetLandscape)
.subscribe(result => {
const isHandsetLandscape = result.matches;
// Check if result.matches Breakpoints.HandsetLandscape. Apply HandsetLandscape
// settings to prevent overlay cutoff in that breakpoint. Fixes b/284148377
if (isHandsetLandscape) {
this._positionStrategy
.withFlexibleDimensions(true)
.withGrowAfterOpen(true)
.withViewportMargin(8);
}
else {
this._positionStrategy
.withFlexibleDimensions(false)
.withGrowAfterOpen(false)
.withViewportMargin(0);
}
});
}
else {
// Update the trigger, panel width and direction, in case anything has changed.
this._positionStrategy.setOrigin(this._getConnectedElement());
overlayRef.updateSize({ width: this._getPanelWidth() });
}
if (overlayRef && !overlayRef.hasAttached()) {
overlayRef.attach(this._portal);
this._valueOnAttach = valueOnAttach;
this._valueOnLastKeydown = null;
this._closingActionsSubscription = this._subscribeToClosingActions();
}
const wasOpen = this.panelOpen;
this.autocomplete._isOpen = this._overlayAttached = true;
this.autocomplete._latestOpeningTrigger = this;
this.autocomplete._setColor(this._formField?.color);
this._updatePanelState();
this._applyModalPanelOwnership();
// We need to do an extra `panelOpen` check in here, because the
// autocomplete won't be shown if there are no options.
if (this.panelOpen && wasOpen !== this.panelOpen) {
this._emitOpened();
}
}
/** Handles keyboard events coming from the overlay panel. */
_handlePanelKeydown = (event) => {
// Close when pressing ESCAPE or ALT + UP_ARROW, based on the a11y guidelines.
// See: https://www.w3.org/TR/wai-aria-practices-1.1/#textbox-keyboard-interaction
if ((event.keyCode === ESCAPE && !hasModifierKey(event)) ||
(event.keyCode === UP_ARROW && hasModifierKey(event, 'altKey'))) {
// If the user had typed something in before we autoselected an option, and they decided
// to cancel the selection, restore the input value to the one they had typed in.
if (this._pendingAutoselectedOption) {
this._updateNativeInputValue(this._valueBeforeAutoSelection ?? '');
this._pendingAutoselectedOption = null;
}
this._closeKeyEventStream.next();
this._resetActiveItem();
// We need to stop propagation, otherwise the event will eventually
// reach the input itself and cause the overlay to be reopened.
event.stopPropagation();
event.preventDefault();
}
};
/** Updates the panel's visibility state