license import { ActiveDescendantKeyManager , LiveAnnouncer } from '@angular/cdk/a11y' ; import { Directionality } from '@angular/cdk/bidi' ; import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { SelectionModel } from '@angular/cdk/collections' ; import { A , DOWN_ARROW , END , ENTER , HOME , LEFT_ARROW , RIGHT_ARROW , SPACE , UP_ARROW , hasModifierKey , } from '@angular/cdk/keycodes' ; import { CdkConnectedOverlay , Overlay , ScrollStrategy } from '@angular/cdk/overlay' ; import { ViewportRuler } from '@angular/cdk/scrolling' ; import { AfterContentInit , Attribute , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChild , ContentChildren , Directive , DoCheck , ElementRef , EventEmitter , Inject , InjectionToken , Input , isDevMode , NgZone , OnChanges , OnDestroy , OnInit , Optional , Output , QueryList , Self , SimpleChanges , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { ControlValueAccessor , FormGroupDirective , NgControl , NgForm } from '@angular/forms' ; import { _countGroupLabelsBeforeOption , _getOptionScrollPosition , CanDisable , CanDisableCtor , CanDisableRipple , CanDisableRippleCtor , CanUpdateErrorState , CanUpdateErrorStateCtor , ErrorStateMatcher , HasTabIndex , HasTabIndexCtor , MAT_OPTION_PARENT_COMPONENT , MatOptgroup , MatOption , MatOptionSelectionChange , mixinDisabled , mixinDisableRipple , mixinErrorState , mixinTabIndex , } from '@angular/material/core' ; import { MatFormField , MatFormFieldControl } from '@angular/material/form-field' ; import { defer , merge , Observable , Subject } from 'rxjs' ; import { distinctUntilChanged , filter , map , startWith , switchMap , take , takeUntil , } from 'rxjs/operators' ; import { matSelectAnimations } from './select-animations' ; import { getMatSelectDynamicMultipleError , getMatSelectNonArrayValueError , getMatSelectNonFunctionValueError , } from './select-errors' ; let nextUniqueId = 0 ; /**
 * The following style constants are necessary to save here in order
 * to properly calculate the alignment of the selected option over
 * the trigger element.
 */ /** The max height of the select's overlay panel */ export const SELECT_PANEL_MAX_HEIGHT = 256 ; /** The panel's padding on the x-axis */ export const SELECT_PANEL_PADDING_X = 16 ; /** The panel's x axis padding if it is indented (e.g. there is an option group). */ export const SELECT_PANEL_INDENT_PADDING_X = SELECT_PANEL_PADDING_X * 2 ; /** The height of the select items in `em` units. */ export const SELECT_ITEM_HEIGHT_EM = 3 ; /**
 * Distance between the panel edge and the option text in
 * multi-selection mode.
 *
 * Calculated as:
 * (SELECT_PANEL_PADDING_X * 1.5) + 20 = 44
 * The padding is multiplied by 1.5 because the checkbox's margin is half the padding.
 * The checkbox width is 16px.
 */ export let SELECT_MULTIPLE_PANEL_PADDING_X = 0 ; /**
 * The select panel will only "fit" inside the viewport if it is positioned at
 * this value or more away from the viewport boundary.
 */ export const SELECT_PANEL_VIEWPORT_PADDING = 8 ; /** Injection token that determines the scroll handling while a select is open. */ export const MAT_SELECT_SCROLL_STRATEGY = new InjectionToken < ( ) => ScrollStrategy > ( 'mat-select-scroll-strategy' ) ; docs-private export function MAT_SELECT_SCROLL_STRATEGY_PROVIDER_FACTORY ( overlay ) { return ( ) => overlay . scrollStrategies . reposition ( ) ; } docs-private export const MAT_SELECT_SCROLL_STRATEGY_PROVIDER = { provide : MAT_SELECT_SCROLL_STRATEGY , deps : [ Overlay ] , useFactory : MAT_SELECT_SCROLL_STRATEGY_PROVIDER_FACTORY , } ; /** Change event object that is emitted when the select value has changed. */ export class MatSelectChange { constructor ( /** Reference to the select that emitted the change event. */ public source , /** Current value of the select that emitted the event. */ public value ) { } } docs-private export class MatSelectBase { constructor ( public _elementRef , public _defaultErrorStateMatcher , public _parentForm , public _parentFormGroup , public ngControl ) { } } export const _MatSelectMixinBase = mixinDisableRipple ( mixinTabIndex ( mixinDisabled ( mixinErrorState ( MatSelectBase ) ) ) ) ; /**
 * Allows the user to customize the trigger that is displayed when the select has a value.
 */ @ Directive ( { selector : 'mat-select-trigger' } ) export class MatSelectTrigger { } @ Component ( { moduleId : module . id , selector : 'mat-select' , exportAs : 'matSelect' , templateUrl : 'select.html' , styleUrls : [ 'select.css' ] , inputs : [ 'disabled' , 'disableRipple' , 'tabIndex' ] , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , host : { 'role' : 'listbox' , '[attr.id]' : 'id' , '[attr.tabindex]' : 'tabIndex' , '[attr.aria-label]' : '_getAriaLabel()' , '[attr.aria-labelledby]' : '_getAriaLabelledby()' , '[attr.aria-required]' : 'required.toString()' , '[attr.aria-disabled]' : 'disabled.toString()' , '[attr.aria-invalid]' : 'errorState' , '[attr.aria-owns]' : 'panelOpen ? _optionIds : null' , '[attr.aria-multiselectable]' : 'multiple' , '[attr.aria-describedby]' : '_ariaDescribedby || null' , '[attr.aria-activedescendant]' : '_getAriaActiveDescendant()' , '[class.mat-select-disabled]' : 'disabled' , '[class.mat-select-invalid]' : 'errorState' , '[class.mat-select-required]' : 'required' , '[class.mat-select-empty]' : 'empty' , 'class' : 'mat-select' , '(keydown)' : '_handleKeydown($event)' , '(focus)' : '_onFocus()' , '(blur)' : '_onBlur()' , } , animations : [ matSelectAnimations . transformPanel ] , providers : [ { provide : MatFormFieldControl , useExisting : MatSelect } , { provide : MAT_OPTION_PARENT_COMPONENT , useExisting : MatSelect } ] , } ) export class MatSelect extends _MatSelectMixinBase implements AfterContentInit , OnChanges , OnDestroy , OnInit , DoCheck , ControlValueAccessor , CanDisable , HasTabIndex , MatFormFieldControl < any > , CanUpdateErrorState , CanDisableRipple { private _scrollStrategyFactory : ( ) => ScrollStrategy ; /** Whether or not the overlay panel is open. */ private _panelOpen = false ; /** Whether filling out the select is required in the form. */ private _required : boolean = false ; /** The scroll position of the overlay panel, calculated to center the selected option. */ private _scrollTop = 0 ; /** The placeholder displayed in the trigger of the select. */ private _placeholder : string ; /** Whether the component is in multiple selection mode. */ private _multiple : boolean = false ; /** Comparison function to specify which option is displayed. Defaults to object equality. */ private _compareWith = ( o1 , o2 ) => o1 === o2 ; /** Unique id for this input. */ private _uid = `mat-select-${ nextUniqueId ++ }` ; /** Emits whenever the component is destroyed. */ private readonly _destroy = new Subject < void > ( ) ; /** The last measured value for the trigger's client bounding rect. */ _triggerRect : ClientRect ; /** The aria-describedby attribute on the select for improved a11y. */ _ariaDescribedby : string ; /** The cached font-size of the trigger element. */ _triggerFontSize = 0 ; /** Deals with the selection logic. */ _selectionModel : SelectionModel < MatOption > ; /** Manages keyboard events for options in the panel. */ _keyManager : ActiveDescendantKeyManager < MatOption > ; /** `View -> model callback called when value changes` */ _onChange : ( value : any ) => void = ( ) => { } ; /** `View -> model callback called when select has been touched` */ _onTouched = ( ) => { } ; /** The IDs of child options to be passed to the aria-owns attribute. */ _optionIds : string = '' ; /** The value of the select panel's transform-origin property. */ _transformOrigin : string = 'top' ; /** Emits when the panel element is finished transforming in. */ _panelDoneAnimatingStream = new Subject < string > ( ) ; /** Strategy that will be used to handle scrolling while the select panel is open. */ _scrollStrategy : ScrollStrategy ; /**
     * The y-offset of the overlay panel in relation to the trigger's top start corner.
     * This must be adjusted to align the selected option text over the trigger text.
     * when the panel opens. Will change based on the y-position of the selected option.
     */ _offsetY = 0 ; /**
     * This position config ensures that the top "start" corner of the overlay
     * is aligned with with the top "start" of the origin by default (overlapping
     * the trigger completely). If the panel cannot fit below the trigger, it
     * will fall back to a position above the trigger.
     */ _positions = [ { originX : 'start' , originY : 'top' , overlayX : 'start' , overlayY : 'top' , } , { originX : 'start' , originY : 'bottom' , overlayX : 'start' , overlayY : 'bottom' , } , ] ; /** Whether the component is disabling centering of the active option over the trigger. */ private _disableOptionCentering : boolean = false ; /** Whether the select is focused. */ get focused ( ) : boolean { return this . _focused || this . _panelOpen ; } deprecated breaking-change set focused ( value ) { this . _focused = value ; } private _focused = false ; /** A name for this control that can be used by `mat-form-field`. */ controlType = 'mat-select' ; /** Trigger that opens the select. */ @ ViewChild ( 'trigger' ) trigger : ElementRef ; /** Panel containing the select options. */ @ ViewChild ( 'panel' ) panel : ElementRef ; /** Overlay pane containing the options. */ @ ViewChild ( CdkConnectedOverlay ) overlayDir : CdkConnectedOverlay ; /** All of the defined select options. */ @ ContentChildren ( MatOption , { descendants : true } ) options : QueryList < MatOption > ; /** All of the defined groups of options. */ @ ContentChildren ( MatOptgroup ) optionGroups : QueryList < MatOptgroup > ; /** Classes to be passed to the select panel. Supports the same syntax as `ngClass`. */ @ Input ( ) panelClass : string | string [ ] | Set < string > | { [ key ] : any ; } ; /** User-supplied override of the trigger element. */ @ ContentChild ( MatSelectTrigger ) customTrigger : MatSelectTrigger ; /** Placeholder to be shown if no value has been selected. */ @ Input ( ) get placeholder ( ) : string { return this . _placeholder ; } set placeholder ( value ) { this . _placeholder = value ; this . stateChanges . next ( ) ; } /** Whether the component is required. */ @ Input ( ) get required ( ) : boolean { return this . _required ; } set required ( value ) { this . _required = coerceBooleanProperty ( value ) ; this . stateChanges . next ( ) ; } /** Whether the user should be allowed to select multiple options. */ @ Input ( ) get multiple ( ) : boolean { return this . _multiple ; } set multiple ( value ) { if ( this . _selectionModel ) { throw getMatSelectDynamicMultipleError ( ) ; } this . _multiple = coerceBooleanProperty ( value ) ; } /** Whether to center the active option over the trigger. */ @ Input ( ) get disableOptionCentering ( ) : boolean { return this . _disableOptionCentering ; } set disableOptionCentering ( value ) { this . _disableOptionCentering = coerceBooleanProperty ( value ) ; } /**
     * Function to compare the option values with the selected values. The first argument
     * is a value from an option. The second is a value from the selection. A boolean
     * should be returned.
     */ @ Input ( ) get compareWith ( ) { return this . _compareWith ; } set compareWith ( fn ) { if ( typeof fn !== 'function' ) { throw getMatSelectNonFunctionValueError ( ) ; } this . _compareWith = fn ; if ( this . _selectionModel ) { this . _initializeSelection ( ) ; } } /** Value of the select control. */ @ Input ( ) get value ( ) : any { return this . _value ; } set value ( newValue ) { if ( newValue !== this . _value ) { this . writeValue ( newValue ) ; this . _value = newValue ; } } private _value : any ; /** Aria label of the select. If not specified, the placeholder will be used as label. */ @ Input ( 'aria-label' ) ariaLabel : string = '' ; /** Input that can be used to specify the `aria-labelledby` attribute. */ @ Input ( 'aria-labelledby' ) ariaLabelledby : string ; /** Object used to control when error messages are shown. */ @ Input ( ) errorStateMatcher : ErrorStateMatcher ; /**
     * Function used to sort the values in a select in multiple mode.
     * Follows the same logic as `Array.prototype.sort`.
     */ @ Input ( ) sortComparator : ( a : MatOption , b : MatOption , options : MatOption [ ] ) => number ; /** Unique id of the element. */ @ Input ( ) get id ( ) : string { return this . _id ; } set id ( value ) { this . _id = value || this . _uid ; this . stateChanges . next ( ) ; } private _id : string ; /** Combined stream of all of the child options' change events. */ readonly optionSelectionChanges : Observable < MatOptionSelectionChange > = defer ( ( ) => { if ( this . options ) { return merge ( ... this . options . map ( option => option . onSelectionChange ) ) ; } return this . _ngZone . onStable . asObservable ( ) . pipe ( take ( 1 ) , switchMap ( ( ) => this . optionSelectionChanges ) ) ; } ) ; /** Event emitted when the select panel has been toggled. */ @ Output ( ) readonly openedChange : EventEmitter < boolean > = new EventEmitter < boolean > ( ) ; /** Event emitted when the select has been opened. */ @ Output ( 'opened' ) readonly _openedStream : Observable < void > = this . openedChange . pipe ( filter ( o => o ) , map ( ( ) => { } ) ) ; /** Event emitted when the select has been closed. */ @ Output ( 'closed' ) readonly _closedStream : Observable < void > = this . openedChange . pipe ( filter ( o => ! o ) , map ( ( ) => { } ) ) ; /** Event emitted when the selected value has been changed by the user. */ @ Output ( ) readonly selectionChange : EventEmitter < MatSelectChange > = new EventEmitter < MatSelectChange > ( ) ; docs-private @ Output ( ) readonly valueChange : EventEmitter < any > = new EventEmitter < any > ( ) ; constructor ( private _viewportRuler , private _changeDetectorRef , private _ngZone , _defaultErrorStateMatcher , elementRef , @ Optional ( ) private _dir , @ Optional ( ) _parentForm , @ Optional ( ) _parentFormGroup , @ Optional ( ) private _parentFormField , @ Self ( ) @ Optional ( ) public ngControl , @ Attribute ( 'tabindex' ) tabIndex , @ Inject ( MAT_SELECT_SCROLL_STRATEGY ) scrollStrategyFactory , deprecated breaking-change private _liveAnnouncer ? ) { super ( elementRef , _defaultErrorStateMatcher , _parentForm , _parentFormGroup , ngControl ) ; if ( this . ngControl ) { this . ngControl . valueAccessor = this ; } this . _scrollStrategyFactory = scrollStrategyFactory ; this . _scrollStrategy = this . _scrollStrategyFactory ( ) ; this . tabIndex = parseInt ( tabIndex ) || 0 ; this . id = this . id ; } ngOnInit ( ) { this . _selectionModel = new SelectionModel < MatOption > ( this . multiple ) ; this . stateChanges . next ( ) ; this . _panelDoneAnimatingStream . pipe ( distinctUntilChanged ( ) , takeUntil ( this . _destroy ) ) . subscribe ( ( ) => { if ( this . panelOpen ) { this . _scrollTop = 0 ; this . openedChange . emit ( true ) ; } else { this . openedChange . emit ( false ) ; this . overlayDir . offsetX = 0 ; this . _changeDetectorRef . markForCheck ( ) ; } } ) ; } ngAfterContentInit ( ) { this . _initKeyManager ( ) ; this . _selectionModel . onChange . pipe ( takeUntil ( this . _destroy ) ) . subscribe ( event => { event . added . forEach ( option => option . select ( ) ) ; event . removed . forEach ( option => option . deselect ( ) ) ; } ) ; this . options . changes . pipe ( startWith ( null ) , takeUntil ( this . _destroy ) ) . subscribe ( ( ) => { this . _resetOptions ( ) ; this . _initializeSelection ( ) ; } ) ; } ngDoCheck ( ) { if ( this . ngControl ) { this . updateErrorState ( ) ; } } ngOnChanges ( changes ) { if ( changes . disabled ) { this . stateChanges . next ( ) ; } } ngOnDestroy ( ) { this . _destroy . next ( ) ; this . _destroy . complete ( ) ; this . stateChanges . complete ( ) ; } /** Toggles the overlay panel open or closed. */ toggle ( ) { this . panelOpen ? this . close ( ) : this . open ( ) ; } /** Opens the overlay panel. */ open ( ) { if ( this . disabled || ! this . options || ! this . options . length || this . _panelOpen ) { return ; } this . _triggerRect = this . trigger . nativeElement . getBoundingClientRect ( ) ; this . _triggerFontSize = parseInt ( getComputedStyle ( this . trigger . nativeElement ) . fontSize || '0' ) ; this . _panelOpen = true ; this . _keyManager . withHorizontalOrientation ( null ) ; this . _calculateOverlayPosition ( ) ; this . _highlightCorrectOption ( ) ; this . _changeDetectorRef . markForCheck ( ) ; this . _ngZone . onStable . asObservable ( ) . pipe ( take ( 1 ) ) . subscribe ( ( ) => { if ( this . _triggerFontSize && this . overlayDir . overlayRef && this . overlayDir . overlayRef . overlayElement ) { this . overlayDir . overlayRef . overlayElement . style . fontSize = `${ this . _triggerFontSize }px` ; } } ) ; } /** Closes the overlay panel and focuses the host element. */ close ( ) { if ( this . _panelOpen ) { this . _panelOpen = false ; this . _keyManager . withHorizontalOrientation ( this . _isRtl ( ) ? 'rtl' : 'ltr' ) ; this . _changeDetectorRef . markForCheck ( ) ; this . _onTouched ( ) ; } } param value writeValue ( value ) { if ( this . options ) { this . _setSelectionByValue ( value ) ; } } param fn registerOnChange ( fn ) { this . _onChange = fn ; } param fn registerOnTouched ( fn ) { this . _onTouched = fn ; } param isDisabled setDisabledState ( isDisabled ) { this . disabled = isDisabled ; this . _changeDetectorRef . markForCheck ( ) ; this . stateChanges . next ( ) ; } /** Whether or not the overlay panel is open. */ get panelOpen ( ) : boolean { return this . _panelOpen ; } /** The currently selected option. */ get selected ( ) : MatOption | MatOption [ ] { return this . multiple ? this . _selectionModel . selected : this . _selectionModel . selected [ 0 ] ; } /** The value displayed in the trigger. */ get triggerValue ( ) : string { if ( this . empty ) { return '' ; } if ( this . _multiple ) { const selectedOptions = this . _selectionModel . selected . map ( option => option . viewValue ) ; if ( this . _isRtl ( ) ) { selectedOptions . reverse ( ) ; } return selectedOptions . join ( ', ' ) ; } return this . _selectionModel . selected [ 0 ] . viewValue ; } /** Whether the element is in RTL mode. */ _isRtl ( ) { return this . _dir ? this . _dir . value === 'rtl' : false ; } /** Handles all keydown events on the select. */ _handleKeydown ( event ) { if ( ! this . disabled ) { this . panelOpen ? this . _handleOpenKeydown ( event ) : this . _handleClosedKeydown ( event ) ; } } /** Handles keyboard events while the select is closed. */ private _handleClosedKeydown ( event ) { const keyCode = event . keyCode ; const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW || keyCode === LEFT_ARROW || keyCode === RIGHT_ARROW ; const isOpenKey = keyCode === ENTER || keyCode === SPACE ; const manager = this . _keyManager ; if ( ( isOpenKey && ! hasModifierKey ( event ) ) || ( ( this . multiple || event . altKey ) && isArrowKey ) ) { event . preventDefault ( ) ; this . open ( ) ; } else if ( ! this . multiple ) { const selectedOption = this . selected ; if ( keyCode === HOME || keyCode === END ) { keyCode === HOME ? manager . setFirstItemActive ( ) : manager . setLastItemActive ( ) ; event . preventDefault ( ) ; } else { manager . onKeydown ( event ) ; } if ( this . _liveAnnouncer && selectedOption !== this . selected ) { this . _liveAnnouncer . announce ( ( this . selected as MatOption ) . viewValue ) ; } } } /** Handles keyboard events when the selected is open. */ private _handleOpenKeydown ( event ) { const keyCode = event . keyCode ; const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW ; const manager = this . _keyManager ; if ( keyCode === HOME || keyCode === END ) { event . preventDefault ( ) ; keyCode === HOME ? manager . setFirstItemActive ( ) : manager . setLastItemActive ( ) ; } else if ( isArrowKey && event . altKey ) { event . preventDefault ( ) ; this . close ( ) ; } else if ( ( keyCode === ENTER || keyCode === SPACE ) && manager . activeItem && ! hasModifierKey ( event ) ) { event . preventDefault ( ) ; manager . activeItem . _selectViaInteraction ( ) ; } else if ( this . _multiple && keyCode === A && event . ctrlKey ) { event . preventDefault ( ) ; const hasDeselectedOptions = this . options . some ( opt => ! opt . disabled && ! opt . selected ) ; this . options . forEach ( option => { if ( ! option . disabled ) { hasDeselectedOptions ? option . select ( ) : option . deselect ( ) ; } } ) ; } else { const previouslyFocusedIndex = manager . activeItemIndex ; manager . onKeydown ( event ) ; if ( this . _multiple && isArrowKey && event . shiftKey && manager . activeItem && manager . activeItemIndex !== previouslyFocusedIndex ) { manager . activeItem . _selectViaInteraction ( ) ; } } } _onFocus ( ) { if ( ! this . disabled ) { this . _focused = true ; this . stateChanges . next ( ) ; } } /**
     * Calls the touched callback only if the panel is closed. Otherwise, the trigger will
     * "blur" to the panel when it opens, causing a false positive.
     */ _onBlur ( ) { this . _focused = false ; if ( ! this . disabled && ! this . panelOpen ) { this . _onTouched ( ) ; this . _changeDetectorRef . markForCheck ( ) ; this . stateChanges . next ( ) ; } } /**
     * Callback that is invoked when the overlay panel has been attached.
     */ _onAttached ( ) { this . overlayDir . positionChange . pipe ( take ( 1 ) ) . subscribe ( ( ) => { this . _setPseudoCheckboxPaddingSize ( ) ; this . _changeDetectorRef . detectChanges ( ) ; this . _calculateOverlayOffsetX ( ) ; this . panel . nativeElement . scrollTop = this . _scrollTop ; } ) ; } /** Returns the theme to be used on the panel. */ _getPanelTheme ( ) { return this . _parentFormField ? `mat-${ this . _parentFormField . color }` : '' ; } /** Sets the pseudo checkbox padding size based on the width of the pseudo checkbox. */ private _setPseudoCheckboxPaddingSize ( ) { if ( ! SELECT_MULTIPLE_PANEL_PADDING_X && this . multiple ) { const pseudoCheckbox = this . panel . nativeElement . querySelector ( '.mat-pseudo-checkbox' ) ; if ( pseudoCheckbox ) { SELECT_MULTIPLE_PANEL_PADDING_X = SELECT_PANEL_PADDING_X * 1.5 + pseudoCheckbox . offsetWidth ; } } } /** Whether the select has a value. */ get empty ( ) : boolean { return ! this . _selectionModel || this . _selectionModel . isEmpty ( ) ; } private _initializeSelection ( ) { Promise . resolve ( ) . then ( ( ) => { this . _setSelectionByValue ( this . ngControl ? this . ngControl . value : this . _value ) ; } ) ; } /**
     * Sets the selected option based on a value. If no option can be
     * found with the designated value, the select trigger is cleared.
     */ private _setSelectionByValue ( value ) { if ( this . multiple && value ) { if ( ! Array . isArray ( value ) ) { throw getMatSelectNonArrayValueError ( ) ; } this . _selectionModel . clear ( ) ; value . forEach ( ( currentValue ) => this . _selectValue ( currentValue ) ) ; this . _sortValues ( ) ; } else { this . _selectionModel . clear ( ) ; const correspondingOption = this . _selectValue ( value ) ; if ( correspondingOption ) { this . _keyManager . setActiveItem ( correspondingOption ) ; } } this . _changeDetectorRef . markForCheck ( ) ; } returns private _selectValue ( value ) { const correspondingOption = this . options . find ( ( option ) => { try { return option . value != null && this . _compareWith ( option . value , value ) ; } catch ( error ) { if ( isDevMode ( ) ) { console . warn ( error ) ; } return false ; } } ) ; if ( correspondingOption ) { this . _selectionModel . select ( correspondingOption ) ; } return correspondingOption ; } /** Sets up a key manager to listen to keyboard events on the overlay panel. */ private _initKeyManager ( ) { this . _keyManager = new ActiveDescendantKeyManager < MatOption > ( this . options ) . withTypeAhead ( ) . withVerticalOrientation ( ) . withHorizontalOrientation ( this . _isRtl ( ) ? 'rtl' : 'ltr' ) . withAllowedModifierKeys ( [ 'shiftKey' ] ) ; this . _keyManager . tabOut . pipe ( takeUntil ( this . _destroy ) ) . subscribe ( ( ) => { this . focus ( ) ; this . close ( ) ; } ) ; this . _keyManager . change . pipe ( takeUntil ( this . _destroy ) ) . subscribe ( ( ) => { if ( this . _panelOpen && this . panel ) { this . _scrollActiveOptionIntoView ( ) ; } else if ( ! this . _panelOpen && ! this . multiple && this . _keyManager . activeItem ) { this . _keyManager . activeItem . _selectViaInteraction ( ) ; } } ) ; } /** Drops current option subscriptions and IDs and resets from scratch. */ private _resetOptions ( ) { const changedOrDestroyed = merge ( this . options . changes , this . _destroy ) ; this . optionSelectionChanges . pipe ( takeUntil ( changedOrDestroyed ) ) . subscribe ( event => { this . _onSelect ( event . source , event . isUserInput ) ; if ( event . isUserInput && ! this . multiple && this . _panelOpen ) { this . close ( ) ; this . focus ( ) ; } } ) ; merge ( ... this . options . map ( option => option . _stateChanges ) ) . pipe ( takeUntil ( changedOrDestroyed ) ) . subscribe ( ( ) => { this . _changeDetectorRef . markForCheck ( ) ; this . stateChanges . next ( ) ; } ) ; this . _setOptionIds ( ) ; } /** Invoked when an option is clicked. */ private _onSelect ( option , isUserInput ) { const wasSelected = this . _selectionModel . isSelected ( option ) ; if ( option . value == null && ! this . _multiple ) { option . deselect ( ) ; this . _selectionModel . clear ( ) ; this . _propagateChanges ( option . value ) ; } else { option . selected ? this . _selectionModel . select ( option ) : this . _selectionModel . deselect ( option ) ; if ( isUserInput ) { this . _keyManager . setActiveItem ( option ) ; } if ( this . multiple ) { this . _sortValues ( ) ; if ( isUserInput ) { this . focus ( ) ; } } } if ( wasSelected !== this . _selectionModel . isSelected ( option ) ) { this . _propagateChanges ( ) ; } this . stateChanges . next ( ) ; } /** Sorts the selected values in the selected based on their order in the panel. */ private _sortValues ( ) { if ( this . multiple ) { const options = this . options . toArray ( ) ; this . _selectionModel . sort ( ( a , b ) => { return this . sortComparator ? this . sortComparator ( a , b , options ) : options . indexOf ( a ) - options . indexOf ( b ) ; } ) ; this . stateChanges . next ( ) ; } } /** Emits change event to set the model value. */ private _propagateChanges ( fallbackValue ? ) { let valueToEmit = null ; if ( this . multiple ) { valueToEmit = ( this . selected as MatOption [ ] ) . map ( option => option . value ) ; } else { valueToEmit = this . selected ? ( this . selected as MatOption ) . value : fallbackValue ; } this . _value = valueToEmit ; this . valueChange . emit ( valueToEmit ) ; this . _onChange ( valueToEmit ) ; this . selectionChange . emit ( new MatSelectChange ( this , valueToEmit ) ) ; this . _changeDetectorRef . markForCheck ( ) ; } /** Records option IDs to pass to the aria-owns property. */ private _setOptionIds ( ) { this . _optionIds = this . options . map ( option => option . id ) . join ( ' ' ) ; } /**
     * Highlights the selected item. If no option is selected, it will highlight
     * the first item instead.
     */ private _highlightCorrectOption ( ) { if ( this . _keyManager ) { if ( this . empty ) { this . _keyManager . setFirstItemActive ( ) ; } else { this . _keyManager . setActiveItem ( this . _selectionModel . selected [ 0 ] ) ; } } } /** Scrolls the active option into view. */ private _scrollActiveOptionIntoView ( ) { const activeOptionIndex = this . _keyManager . activeItemIndex || 0 ; const labelCount = _countGroupLabelsBeforeOption ( activeOptionIndex , this . options , this . optionGroups ) ; this . panel . nativeElement . scrollTop = _getOptionScrollPosition ( activeOptionIndex + labelCount , this . _getItemHeight ( ) , this . panel . nativeElement . scrollTop , SELECT_PANEL_MAX_HEIGHT ) ; } /** Focuses the select element. */ focus ( ) { this . _elementRef . nativeElement . focus ( ) ; } /** Gets the index of the provided option in the option list. */ private _getOptionIndex ( option ) { return this . options . reduce ( ( result , current , index ) => { return result === undefined ? ( option === current ? index : undefined ) : result ; } , undefined ) ; } /** Calculates the scroll position and x- and y-offsets of the overlay panel. */ private _calculateOverlayPosition ( ) { const itemHeight = this . _getItemHeight ( ) ; const items = this . _getItemCount ( ) ; const panelHeight = Math . min ( items * itemHeight , SELECT_PANEL_MAX_HEIGHT ) ; const scrollContainerHeight = items * itemHeight ; const maxScroll = scrollContainerHeight - panelHeight ; let selectedOptionOffset = this . empty ? 0 : this . _getOptionIndex ( this . _selectionModel . selected [ 0 ] ) ! ; selectedOptionOffset += _countGroupLabelsBeforeOption ( selectedOptionOffset , this . options , this . optionGroups ) ; const scrollBuffer = panelHeight / 2 ; this . _scrollTop = this . _calculateOverlayScroll ( selectedOptionOffset , scrollBuffer , maxScroll ) ; this . _offsetY = this . _calculateOverlayOffsetY ( selectedOptionOffset , scrollBuffer , maxScroll ) ; this . _checkOverlayWithinViewport ( maxScroll ) ; } /**
     * Calculates the scroll position of the select's overlay panel.
     *
     * Attempts to center the selected option in the panel. If the option is
     * too high or too low in the panel to be scrolled to the center, it clamps the
     * scroll position to the min or max scroll positions respectively.
     */ _calculateOverlayScroll ( selectedIndex , scrollBuffer , maxScroll ) { const itemHeight = this . _getItemHeight ( ) ; const optionOffsetFromScrollTop = itemHeight * selectedIndex ; const halfOptionHeight = itemHeight / 2 ; const optimalScrollPosition = optionOffsetFromScrollTop - scrollBuffer + halfOptionHeight ; return Math . min ( Math . max ( 0 , optimalScrollPosition ) , maxScroll ) ; } /** Returns the aria-label of the select component. */ _getAriaLabel ( ) { return this . ariaLabelledby ? null : this . ariaLabel || this . placeholder ; } /** Returns the aria-labelledby of the select component. */ _getAriaLabelledby ( ) { if ( this . ariaLabelledby ) { return this . ariaLabelledby ; } if ( ! this . _parentFormField || ! this . _parentFormField . _hasFloatingLabel ( ) || this . _getAriaLabel ( ) ) { return null ; } return this . _parentFormField . _labelId || null ; } /** Determines the `aria-activedescendant` to be set on the host. */ _getAriaActiveDescendant ( ) { if ( this . panelOpen && this . _keyManager && this . _keyManager . activeItem ) { return this . _keyManager . activeItem . id ; } return null ; } /**
     * Sets the x-offset of the overlay panel in relation to the trigger's top start corner.
     * This must be adjusted to align the selected option text over the trigger text when
     * the panel opens. Will change based on LTR or RTL text direction. Note that the offset
     * can't be calculated until the panel has been attached, because we need to know the
     * content width in order to constrain the panel within the viewport.
     */ private _calculateOverlayOffsetX ( ) { const overlayRect = this . overlayDir . overlayRef . overlayElement . getBoundingClientRect ( ) ; const viewportSize = this . _viewportRuler . getViewportSize ( ) ; const isRtl = this . _isRtl ( ) ; const paddingWidth = this . multiple ? SELECT_MULTIPLE_PANEL_PADDING_X + SELECT_PANEL_PADDING_X : SELECT_PANEL_PADDING_X * 2 ; let offsetX ; if ( this . multiple ) { offsetX = SELECT_MULTIPLE_PANEL_PADDING_X ; } else { let selected = this . _selectionModel . selected [ 0 ] || this . options . first ; offsetX = selected && selected . group ? SELECT_PANEL_INDENT_PADDING_X : SELECT_PANEL_PADDING_X ; } if ( ! isRtl ) { offsetX *= - 1 ; } const leftOverflow = 0 - ( overlayRect . left + offsetX - ( isRtl ? paddingWidth : 0 ) ) ; const rightOverflow = overlayRect . right + offsetX - viewportSize . width + ( isRtl ? 0 : paddingWidth ) ; if ( leftOverflow > 0 ) { offsetX += leftOverflow + SELECT_PANEL_VIEWPORT_PADDING ; } else if ( rightOverflow > 0 ) { offsetX -= rightOverflow + SELECT_PANEL_VIEWPORT_PADDING ; } this . overlayDir . offsetX = Math . round ( offsetX ) ; this . overlayDir . overlayRef . updatePosition ( ) ; } /**
     * Calculates the y-offset of the select's overlay panel in relation to the
     * top start corner of the trigger. It has to be adjusted in order for the
     * selected option to be aligned over the trigger when the panel opens.
     */ private _calculateOverlayOffsetY ( selectedIndex , scrollBuffer , maxScroll ) { const itemHeight = this . _getItemHeight ( ) ; const optionHeightAdjustment = ( itemHeight - this . _triggerRect . height ) / 2 ; const maxOptionsDisplayed = Math . floor ( SELECT_PANEL_MAX_HEIGHT / itemHeight ) ; let optionOffsetFromPanelTop ; if ( this . _disableOptionCentering ) { return 0 ; } if ( this . _scrollTop === 0 ) { optionOffsetFromPanelTop = selectedIndex * itemHeight ; } else if ( this . _scrollTop === maxScroll ) { const firstDisplayedIndex = this . _getItemCount ( ) - maxOptionsDisplayed ; const selectedDisplayIndex = selectedIndex - firstDisplayedIndex ; let partialItemHeight = itemHeight - ( this . _getItemCount ( ) * itemHeight - SELECT_PANEL_MAX_HEIGHT ) % itemHeight ; optionOffsetFromPanelTop = selectedDisplayIndex * itemHeight + partialItemHeight ; } else { optionOffsetFromPanelTop = scrollBuffer - itemHeight / 2 ; } return Math . round ( optionOffsetFromPanelTop * - 1 - optionHeightAdjustment ) ; } /**
     * Checks that the attempted overlay position will fit within the viewport.
     * If it will not fit, tries to adjust the scroll position and the associated
     * y-offset so the panel can open fully on-screen. If it still won't fit,
     * sets the offset back to 0 to allow the fallback position to take over.
     */ private _checkOverlayWithinViewport ( maxScroll ) { const itemHeight = this . _getItemHeight ( ) ; const viewportSize = this . _viewportRuler . getViewportSize ( ) ; const topSpaceAvailable = this . _triggerRect . top - SELECT_PANEL_VIEWPORT_PADDING ; const bottomSpaceAvailable = viewportSize . height - this . _triggerRect . bottom - SELECT_PANEL_VIEWPORT_PADDING ; const panelHeightTop = Math . abs ( this . _offsetY ) ; const totalPanelHeight = Math . min ( this . _getItemCount ( ) * itemHeight , SELECT_PANEL_MAX_HEIGHT ) ; const panelHeightBottom = totalPanelHeight - panelHeightTop - this . _triggerRect . height ; if ( panelHeightBottom > bottomSpaceAvailable ) { this . _adjustPanelUp ( panelHeightBottom , bottomSpaceAvailable ) ; } else if ( panelHeightTop > topSpaceAvailable ) { this . _adjustPanelDown ( panelHeightTop , topSpaceAvailable , maxScroll ) ; } else { this . _transformOrigin = this . _getOriginBasedOnOption ( ) ; } } /** Adjusts the overlay panel up to fit in the viewport. */ private _adjustPanelUp ( panelHeightBottom , bottomSpaceAvailable ) { const distanceBelowViewport = Math . round ( panelHeightBottom - bottomSpaceAvailable ) ; this . _scrollTop -= distanceBelowViewport ; this . _offsetY -= distanceBelowViewport ; this . _transformOrigin = this . _getOriginBasedOnOption ( ) ; if ( this . _scrollTop <= 0 ) { this . _scrollTop = 0 ; this . _offsetY = 0 ; this . _transformOrigin = `50% bottom 0px` ; } } /** Adjusts the overlay panel down to fit in the viewport. */ private _adjustPanelDown ( panelHeightTop , topSpaceAvailable , maxScroll ) { const distanceAboveViewport = Math . round ( panelHeightTop - topSpaceAvailable ) ; this . _scrollTop += distanceAboveViewport ; this . _offsetY += distanceAboveViewport ; this . _transformOrigin = this . _getOriginBasedOnOption ( ) ; if ( this . _scrollTop >= maxScroll ) { this . _scrollTop = maxScroll ; this . _offsetY = 0 ; this . _transformOrigin = `50% top 0px` ; return ; } } /** Sets the transform origin point based on the selected option. */ private _getOriginBasedOnOption ( ) { const itemHeight = this . _getItemHeight ( ) ; const optionHeightAdjustment = ( itemHeight - this . _triggerRect . height ) / 2 ; const originY = Math . abs ( this . _offsetY ) - optionHeightAdjustment + itemHeight / 2 ; return `50% ${ originY }px 0px` ; } /** Calculates the amount of items in the select. This includes options and group labels. */ private _getItemCount ( ) { return this . options . length + this . optionGroups . length ; } /** Calculates the height of the select's options. */ private _getItemHeight ( ) { return this . _triggerFontSize * SELECT_ITEM_HEIGHT_EM ; } docs-private setDescribedByIds ( ids ) { this . _ariaDescribedby = ids . join ( ' ' ) ; } docs-private onContainerClick ( ) { this . focus ( ) ; this . open ( ) ; } docs-private get shouldLabelFloat ( ) : boolean { return this . _panelOpen || ! this . empty ; } }