UNPKG

@angular/material

Version:
966 lines (963 loc) 66.3 kB
import { M as MAT_OPTION_PARENT_COMPONENT, a as MatOption, b as MAT_OPTGROUP, c as MatOptionSelectionChange, _ as _countGroupLabelsBeforeOption, d as _getOptionScrollPosition } from './option-105bf9fa.mjs'; export { e as MatOptgroup, a as MatOption } from './option-105bf9fa.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-0a6b3078.mjs'; import { M as MatOptionModule } from './index-2b66fc5f.mjs'; import { M as MatCommonModule } from './common-module-5a9c16bb.mjs'; import './ripple-33861831.mjs'; import '@angular/cdk/coercion'; import '@angular/cdk/private'; import './pseudo-checkbox-18f8a087.mjs'; import './structural-styles-afbfe518.mjs'; import '@angular/common'; import '@angular/cdk/observers/private'; import './index-91512b69.mjs'; import './pseudo-checkbox-module-6293765e.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