import { ActiveDescendantKeyManager , LiveAnnouncer } from '@angular/cdk/a11y' ; import { BooleanInput , coerceBooleanProperty , coerceNumberProperty , NumberInput , } from '@angular/cdk/coercion' ; import { SelectionModel } from '@angular/cdk/collections' ; import { A , DOWN_ARROW , ENTER , hasModifierKey , LEFT_ARROW , RIGHT_ARROW , SPACE , UP_ARROW , } from '@angular/cdk/keycodes' ; import { CdkConnectedOverlay , CdkOverlayOrigin , ConnectedPosition , Overlay , RepositionScrollStrategy , ScrollStrategy , ViewportRuler , } from '@angular/cdk/overlay' ; import { AfterContentInit , Attribute , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChildren , DoCheck , ElementRef , EventEmitter , HostListener , Inject , InjectionToken , Input , NgZone , OnChanges , OnDestroy , OnInit , Optional , Output , QueryList , Self , SimpleChanges , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { ControlValueAccessor , FormGroupDirective , NgControl , NgForm , Validators , } from '@angular/forms' ; import { CanDisable , CanUpdateErrorState , getOptionScrollPosition , HasTabIndex , mixinDisabled , mixinErrorState , mixinTabIndex , mixinVariant , SbbErrorStateMatcher , SbbOptgroup , SbbOption , SbbOptionSelectionChange , SBB_OPTION_PARENT_COMPONENT , TypeRef , } from '@sbb-esta/angular/core' ; import { SbbFormField , SbbFormFieldControl , SBB_FORM_FIELD } from '@sbb-esta/angular/form-field' ; import { defer , merge , Observable , Subject } from 'rxjs' ; import { distinctUntilChanged , filter , map , startWith , switchMap , take , takeUntil , } from 'rxjs/operators' ; import { sbbSelectAnimations } from '../select-animations' ; import { getSbbSelectDynamicMultipleError , getSbbSelectNonArrayValueError , getSbbSelectNonFunctionValueError , } 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.
 */ /** Injection token that determines the scroll handling while a select is open. */ export const SBB_SELECT_SCROLL_STRATEGY = new InjectionToken < ( ) => ScrollStrategy > ( 'sbb-select-scroll-strategy' ) ; docs-private export function SBB_SELECT_SCROLL_STRATEGY_PROVIDER_FACTORY ( overlay ) { return ( ) => overlay . scrollStrategies . reposition ( ) ; } /** Object that can be used to configure the default options for the select module. */ export interface SbbSelectConfig { /** Time to wait in milliseconds after the last keystroke before moving focus to an item. */ typeaheadDebounceInterval ? : number ; /** Class or list of classes to be applied to the menu's overlay panel. */ overlayPanelClass ? : string | string [ ] ; } /** Injection token that can be used to provide the default options the select module. */ export const SBB_SELECT_CONFIG = new InjectionToken < SbbSelectConfig > ( 'SBB_SELECT_CONFIG' ) ; docs-private export const SBB_SELECT_SCROLL_STRATEGY_PROVIDER = { provide : SBB_SELECT_SCROLL_STRATEGY , deps : [ Overlay ] , useFactory : SBB_SELECT_SCROLL_STRATEGY_PROVIDER_FACTORY , } ; /** Change event object that is emitted when the select value has changed. */ export class SbbSelectChange { constructor ( /** Reference to the select that emitted the change event. */ public source , /** Current value of the select that emitted the event. */ public value ) { } } const _SbbSelectMixinBase = mixinTabIndex ( mixinDisabled ( mixinErrorState ( mixinVariant ( class { constructor ( public _defaultErrorStateMatcher , public _parentForm , public _parentFormGroup , public ngControl ) { } } ) ) ) ) ; @ Component ( { selector : 'sbb-select' , exportAs : 'sbbSelect' , templateUrl : './select.html' , styleUrls : [ './select.css' ] , inputs : [ 'disabled' , 'tabIndex' ] , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , host : { role : 'combobox' , 'aria-autocomplete' : 'none' , 'aria-haspopup' : 'true' , class : 'sbb-select sbb-input-element' , '[attr.id]' : 'id' , '[attr.tabindex]' : 'readonly ? -1 : tabIndex' , '[attr.aria-controls]' : 'panelOpen ? id + "-panel" : null' , '[attr.aria-owns]' : "panelOpen ? id + '-panel' : null" , '[attr.aria-expanded]' : 'panelOpen' , '[attr.aria-label]' : 'ariaLabel || null' , '[attr.aria-required]' : 'required.toString()' , '[attr.aria-disabled]' : 'disabled.toString()' , '[attr.aria-invalid]' : 'errorState' , '[attr.aria-describedby]' : '_ariaDescribedby || null' , '[attr.aria-activedescendant]' : '_getAriaActiveDescendant()' , '[class.sbb-disabled]' : 'disabled' , '[class.sbb-select-invalid]' : 'errorState' , '[class.sbb-select-required]' : 'required' , '[class.sbb-select-empty]' : 'empty' , '[class.sbb-select-multiple]' : 'multiple' , '[class.sbb-readonly]' : 'readonly' , '[class.sbb-focused]' : 'focused' , '[class.sbb-input-with-open-panel]' : 'panelOpen' , } , animations : [ sbbSelectAnimations . transformPanel ] , providers : [ { provide : SbbFormFieldControl , useExisting : SbbSelect } , { provide : SBB_OPTION_PARENT_COMPONENT , useExisting : SbbSelect } , ] , } ) export class SbbSelect extends _SbbSelectMixinBase implements AfterContentInit , OnChanges , OnDestroy , OnInit , DoCheck , ControlValueAccessor , CanDisable , HasTabIndex , SbbFormFieldControl < any > , CanUpdateErrorState { /** The scroll position of the overlay panel, calculated to center the selected option. */ private _scrollTop = 0 ; /** The last measured value for the trigger's client bounding rect. */ _triggerRect : ClientRect ; /** The cached font-size of the trigger element. */ _triggerFontSize : number = 0 ; /** The value of the select panel's transform-origin property. */ _transformOrigin : string = 'top' ; /**
     * 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 : number = 0 ; /** All of the defined select options. */ @ ContentChildren ( SbbOption , { descendants : true } ) options : QueryList < SbbOption > ; /** All of the defined groups of options. */ @ ContentChildren ( SbbOptgroup , { descendants : true } ) optionGroups : QueryList < SbbOptgroup > ; /**
     * 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 : ConnectedPosition [ ] = [ { originX : 'start' , originY : 'bottom' , overlayX : 'start' , overlayY : 'top' , } , { originX : 'start' , originY : 'top' , overlayX : 'start' , overlayY : 'bottom' , } , ] ; _overlayOrigin : CdkOverlayOrigin ; /** Factory function used to create a scroll strategy for this select. */ private _scrollStrategyFactory : ( ) => ScrollStrategy ; /** Whether or not the overlay panel is open. */ private _panelOpen = 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 = `sbb-select-${ nextUniqueId ++ }` ; /** Current `aria-labelledby` value for the select trigger. */ private _triggerAriaLabelledBy : string | null = null ; /** Emits whenever the component is destroyed. */ private readonly _destroy = new Subject < void > ( ) ; /** The aria-describedby attribute on the select for improved a11y. */ _ariaDescribedby : string ; /** Deals with the selection logic. */ _selectionModel : SelectionModel < SbbOption > ; /** Manages keyboard events for options in the panel. */ _keyManager : ActiveDescendantKeyManager < SbbOption > ; /** `View -> model callback called when value changes` */ _onChange : ( value : any ) => void = ( ) => { } ; /** `View -> model callback called when select has been touched` */ _onTouched : ( ) => void = ( ) => { } ; /** ID for the DOM node containing the select's value. */ _valueId : string = `sbb-select-value-${ nextUniqueId ++ }` ; /** Emits when the panel element is finished transforming in. */ readonly _panelDoneAnimatingStream : Subject < string > = new Subject < string > ( ) ; /** Strategy that will be used to handle scrolling while the select panel is open. */ _scrollStrategy : ScrollStrategy ; _overlayPanelClass : string | string [ ] = this . _defaultOptions ?. overlayPanelClass || 'sbb-overlay-panel' ; /** Whether the select is focused. */ get focused ( ) : boolean { return this . _focused || this . _panelOpen ; } private _focused = false ; /** A name for this control that can be used by `sbb-form-field`. */ controlType : string = 'sbb-select' ; /** Panel containing the select options. */ @ ViewChild ( 'panel' ) panel : ElementRef < HTMLElement > ; /** Overlay pane containing the options. */ @ ViewChild ( CdkConnectedOverlay ) overlayDir : CdkConnectedOverlay ; /** Classes to be passed to the select panel. Supports the same syntax as `ngClass`. */ @ Input ( ) panelClass : string | string [ ] | Set < string > | { [ key ] : any ; } ; /** 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 ( ) ; } private _placeholder : string ; /** Whether the component is required. */ @ Input ( ) get required ( ) : boolean { return this . _required ?? this . ngControl ?. control ?. hasValidator ( Validators . required ) ?? false ; } set required ( value ) { this . _required = coerceBooleanProperty ( value ) ; this . stateChanges . next ( ) ; } private _required : boolean | undefined ; /** Whether the user should be allowed to select multiple options. */ @ Input ( ) get multiple ( ) : boolean { return this . _multiple ; } set multiple ( value ) { if ( this . _selectionModel && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) { throw getSbbSelectDynamicMultipleError ( ) ; } this . _multiple = coerceBooleanProperty ( value ) ; } private _multiple : boolean = false ; /** Whether the element is readonly. */ @ Input ( ) get readonly ( ) : boolean { return this . _readonly ; } set readonly ( value ) { this . _readonly = coerceBooleanProperty ( value ) ; this . stateChanges . next ( ) ; } private _readonly : boolean = false ; /**
     * 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' && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) { throw getSbbSelectNonFunctionValueError ( ) ; } 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 . _multiple && Array . isArray ( newValue ) ) ) { if ( this . options ) { this . _setSelectionByValue ( 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 ( ) override errorStateMatcher : SbbErrorStateMatcher ; /** Time to wait in milliseconds after the last keystroke before moving focus to an item. */ @ Input ( ) get typeaheadDebounceInterval ( ) : number { return this . _typeaheadDebounceInterval ; } set typeaheadDebounceInterval ( value ) { this . _typeaheadDebounceInterval = coerceNumberProperty ( value ) ; } private _typeaheadDebounceInterval : number ; /**
     * Function used to sort the values in a select in multiple mode.
     * Follows the same logic as `Array.prototype.sort`.
     */ @ Input ( ) sortComparator : ( a : SbbOption , b : SbbOption , options : SbbOption [ ] ) => 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 < SbbOptionSelectionChange > = defer ( ( ) => { const options = this . options ; if ( options ) { return options . changes . pipe ( startWith ( options ) , switchMap ( ( ) => merge ( ... options . map ( ( option ) => option . onSelectionChange ) ) ) ) ; } return this . _ngZone . onStable . pipe ( take ( 1 ) , switchMap ( ( ) => this . optionSelectionChanges ) ) ; } ) as Observable < SbbOptionSelectionChange > ; /** 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 < SbbSelectChange > = new EventEmitter < SbbSelectChange > ( ) ; docs-private @ Output ( ) readonly valueChange : EventEmitter < any > = new EventEmitter < any > ( ) ; constructor ( private _viewportRuler , private _changeDetectorRef , private _ngZone , defaultErrorStateMatcher , private _elementRef , @ Optional ( ) parentForm , @ Optional ( ) parentFormGroup , @ Optional ( ) @ Inject ( SBB_FORM_FIELD ) private _parentFormField , @ Self ( ) @ Optional ( ) public override ngControl , @ Attribute ( 'tabindex' ) tabIndex , @ Inject ( SBB_SELECT_SCROLL_STRATEGY ) scrollStrategyFactory , private _liveAnnouncer , @ Optional ( ) @ Inject ( SBB_SELECT_CONFIG ) private _defaultOptions ? ) { super ( defaultErrorStateMatcher , parentForm , parentFormGroup , ngControl ) ; if ( this . ngControl ) { this . ngControl . valueAccessor = this ; } if ( _defaultOptions ?. typeaheadDebounceInterval != null ) { this . _typeaheadDebounceInterval = _defaultOptions . typeaheadDebounceInterval ; } this . _scrollStrategyFactory = scrollStrategyFactory ; this . _scrollStrategy = this . _scrollStrategyFactory ( ) ; this . tabIndex = parseInt ( tabIndex , 10 ) || 0 ; this . id = this . id ; this . _overlayOrigin = new CdkOverlayOrigin ( _elementRef ) ; } ngOnInit ( ) { this . _selectionModel = new SelectionModel < SbbOption > ( this . multiple ) ; this . stateChanges . next ( ) ; this . _panelDoneAnimatingStream . pipe ( distinctUntilChanged ( ) , takeUntil ( this . _destroy ) ) . subscribe ( ( ) => this . _panelDoneAnimating ( this . panelOpen ) ) ; this . _viewportRuler . change ( ) . pipe ( takeUntil ( this . _destroy ) ) . subscribe ( ( ) => { if ( this . panelOpen ) { this . _triggerRect = this . _elementRef . nativeElement . getBoundingClientRect ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } } ) ; } ngAfterContentInit ( ) { this . _initKeyManager ( ) ; this . _selectionModel . changed . 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 ( ) { const newAriaLabelledby = this . _getTriggerAriaLabelledby ( ) ; if ( newAriaLabelledby !== this . _triggerAriaLabelledBy ) { const element = this . _elementRef . nativeElement ; this . _triggerAriaLabelledBy = newAriaLabelledby ; if ( newAriaLabelledby ) { element . setAttribute ( 'aria-labelledby' , newAriaLabelledby ) ; } else { element . removeAttribute ( 'aria-labelledby' ) ; } } if ( this . ngControl ) { this . updateErrorState ( ) ; } } ngOnChanges ( changes ) { if ( changes [ 'disabled' ] ) { this . stateChanges . next ( ) ; } if ( changes [ 'typeaheadDebounceInterval' ] && this . _keyManager ) { this . _keyManager . withTypeAhead ( this . _typeaheadDebounceInterval ) ; } } ngOnDestroy ( ) { this . _destroy . next ( ) ; this . _destroy . complete ( ) ; this . stateChanges . complete ( ) ; } /** Toggles the overlay panel open or closed. */ @ HostListener ( 'click' ) toggle ( ) { this . panelOpen ? this . close ( ) : this . open ( ) ; } /** Opens the overlay panel. */ open ( ) { if ( this . _canOpen ( ) ) { this . _panelOpen = true ; this . _keyManager . withHorizontalOrientation ( null ) ; this . _highlightCorrectOption ( ) ; this . _changeDetectorRef . markForCheck ( ) ; this . _triggerRect = this . _elementRef . nativeElement . getBoundingClientRect ( ) ; this . _triggerFontSize = parseInt ( getComputedStyle ( this . _elementRef . nativeElement ) . fontSize || '0' , 10 ) ; this . _ngZone . onStable . pipe ( take ( 1 ) ) . subscribe ( ( ) => { if ( this . _triggerFontSize && this . overlayDir . overlayRef && this . overlayDir . overlayRef . overlayElement ) { this . overlayDir . overlayRef . overlayElement . style . fontSize = `${ this . _triggerFontSize }px` ; } this . _calculateOverlayPosition ( ) ; } ) ; } } /** Closes the overlay panel and focuses the host element. */ close ( ) { if ( this . _panelOpen ) { this . _panelOpen = false ; this . _keyManager . withHorizontalOrientation ( 'ltr' ) ; this . _changeDetectorRef . markForCheck ( ) ; this . _onTouched ( ) ; } } param value writeValue ( value ) { this . value = 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 ( ) : SbbOption | SbbOption [ ] { 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 ) ; return selectedOptions . join ( ', ' ) ; } return this . _selectionModel . selected [ 0 ] . viewValue ; } /** Handles all keydown events on the select. */ @ HostListener ( 'keydown' , [ '$event' ] ) _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 ( ( ! manager . isTyping ( ) && isOpenKey && ! hasModifierKey ( event ) ) || ( ( this . multiple || event . altKey ) && isArrowKey ) ) { event . preventDefault ( ) ; this . open ( ) ; } else if ( ! this . multiple ) { const previouslySelectedOption = this . selected ; manager . onKeydown ( event ) ; const selectedOption = this . selected ; if ( selectedOption && previouslySelectedOption !== selectedOption ) { this . _liveAnnouncer . announce ( ( selectedOption as SbbOption ) . viewValue , 10000 ) ; } } } /** Handles keyboard events when the selected is open. */ private _handleOpenKeydown ( event ) { const manager = this . _keyManager ; const keyCode = event . keyCode ; const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW ; const isTyping = manager . isTyping ( ) ; if ( isArrowKey && event . altKey ) { event . preventDefault ( ) ; this . close ( ) ; } else if ( ! isTyping && ( keyCode === ENTER || keyCode === SPACE ) && manager . activeItem && ! hasModifierKey ( event ) ) { event . preventDefault ( ) ; manager . activeItem . _selectViaInteraction ( ) ; } else if ( ! isTyping && 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 ( ) ; } } } @ HostListener ( 'focus' ) _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.
     */ @ HostListener ( 'blur' ) _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 ( ( positions ) => { if ( positions . connectionPair . originY === 'top' ) { this . panel . nativeElement . classList . add ( 'sbb-panel-above' ) ; this . _elementRef . nativeElement . classList . add ( 'sbb-input-with-open-panel-above' ) ; } else { this . panel . nativeElement . classList . remove ( 'sbb-panel-above' ) ; this . _elementRef . nativeElement . classList . remove ( 'sbb-input-with-open-panel-above' ) ; } this . _changeDetectorRef . detectChanges ( ) ; this . _positioningSettled ( ) ; } ) ; } /** 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 ) ; this . stateChanges . next ( ) ; } ) ; } /**
     * 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 ) { this . _selectionModel . selected . forEach ( ( option ) => option . setInactiveStyles ( ) ) ; this . _selectionModel . clear ( ) ; if ( this . multiple && value ) { if ( ! Array . isArray ( value ) && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) { throw getSbbSelectNonArrayValueError ( ) ; } value . forEach ( ( currentValue ) => this . _selectValue ( currentValue ) ) ; this . _sortValues ( ) ; } else { const correspondingOption = this . _selectValue ( value ) ; if ( correspondingOption ) { this . _keyManager . updateActiveItem ( correspondingOption ) ; } else if ( ! this . panelOpen ) { this . _keyManager . updateActiveItem ( - 1 ) ; } } this . _changeDetectorRef . markForCheck ( ) ; } returns private _selectValue ( value ) { const correspondingOption = this . options . find ( ( option ) => { if ( this . _selectionModel . isSelected ( option ) ) { return false ; } try { return option . value != null && this . _compareWith ( option . value , value ) ; } catch ( error ) { if ( typeof ngDevMode === 'undefined' || ngDevMode ) { 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 < SbbOption > ( this . options ) . withTypeAhead ( this . _typeaheadDebounceInterval ) . withVerticalOrientation ( ) . withHorizontalOrientation ( 'ltr' ) . withHomeAndEnd ( ) . withAllowedModifierKeys ( [ 'shiftKey' ] ) ; this . _keyManager . tabOut . pipe ( takeUntil ( this . _destroy ) ) . subscribe ( ( ) => { if ( this . panelOpen ) { if ( ! this . multiple && this . _keyManager . activeItem ) { this . _keyManager . activeItem . _selectViaInteraction ( ) ; } this . focus ( ) ; this . close ( ) ; } } ) ; this . _keyManager . change . pipe ( takeUntil ( this . _destroy ) ) . subscribe ( ( ) => { if ( this . _panelOpen && this . panel ) { this . _scrollOptionIntoView ( this . _keyManager . activeItemIndex || 0 ) ; } 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 ( ) ; } ) ; } /** 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 ( ) ; if ( this . value != null ) { this . _propagateChanges ( option . value ) ; } } else { if ( wasSelected !== option . selected ) { 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 SbbOption [ ] ) . map ( ( option ) => option . value ) ; } else { valueToEmit = this . selected ? ( this . selected as SbbOption ) . value : fallbackValue ; } this . _value = valueToEmit ; this . valueChange . emit ( valueToEmit ) ; this . _onChange ( valueToEmit ) ; this . selectionChange . emit ( new SbbSelectChange ( this , valueToEmit ) ) ; this . _changeDetectorRef . markForCheck ( ) ; } /**
     * 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 ] ) ; } } } /** Whether the panel is allowed to open. */ private _canOpen ( ) { return ! this . _panelOpen && ! this . disabled && this . options ?. length > 0 && ! this . readonly ; } /** Focuses the select element. */ focus ( options ? ) { this . _elementRef . nativeElement . focus ( options ) ; } /** Gets the aria-labelledby for the select panel. */ _getPanelAriaLabelledby ( ) { if ( this . ariaLabel ) { return null ; } const labelId = this . _parentFormField ?. getLabelId ( ) ; const labelExpression = labelId ? labelId + ' ' : '' ; return this . ariaLabelledby ? labelExpression + this . ariaLabelledby : labelId ; } /** 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 ; } /** Gets the aria-labelledby of the select component trigger. */ private _getTriggerAriaLabelledby ( ) { if ( this . ariaLabel ) { return null ; } const labelId = this . _parentFormField ?. getLabelId ( ) ; let value = ( labelId ? labelId + ' ' : '' ) + this . _valueId ; if ( this . ariaLabelledby ) { value += ' ' + this . ariaLabelledby ; } return value ; } /** Called when the overlay panel is done animating. */ _panelDoneAnimating ( isOpen ) { if ( this . panelOpen ) { this . _scrollTop = 0 ; } else { this . overlayDir . offsetX = 0 ; this . _changeDetectorRef . markForCheck ( ) ; } if ( ! isOpen ) { this . _elementRef . nativeElement . classList . remove ( 'sbb-input-with-open-panel-above' ) ; } this . openedChange . emit ( isOpen ) ; } docs-private setDescribedByIds ( ids ) { this . _ariaDescribedby = ids . join ( ' ' ) ; } docs-private onContainerClick ( ) { this . focus ( ) ; this . open ( ) ; } /**
     * 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 ( selectedOption , scrollBuffer , maxScroll ) { const itemHeight = selectedOption . _getHostElement ( ) . offsetHeight ; const optionOffsetFromScrollTop = selectedOption . _getHostElement ( ) . offsetTop ; const halfOptionHeight = itemHeight / 2 ; const optimalScrollPosition = optionOffsetFromScrollTop - scrollBuffer + halfOptionHeight ; return Math . min ( Math . max ( 0 , optimalScrollPosition ) , maxScroll ) ; } /** Scrolls the active option into view. */ private _scrollOptionIntoView ( index ) { if ( index === 0 ) { this . panel . nativeElement . scrollTop = 0 ; } else { const option = this . options . toArray ( ) [ index ] ; if ( option ) { const element = option . _getHostElement ( ) ; this . panel . nativeElement . scrollTop = getOptionScrollPosition ( element . offsetTop , element . offsetHeight , this . panel . nativeElement . scrollTop , this . panel . nativeElement . offsetHeight ) ; } } } private _positioningSettled ( ) { this . panel . nativeElement . scrollTop = this . _scrollTop ; } /** Calculates the scroll position */ private _calculateOverlayPosition ( ) { if ( ! this . panel ) { return ; } const panelHeight = this . panel . nativeElement . clientHeight ; const scrollContainerHeight = this . panel . nativeElement . scrollHeight ; const maxScroll = scrollContainerHeight - panelHeight ; const firstSelectedOption = this . _selectionModel . selected [ 0 ] ; if ( this . empty || this . options . toArray ( ) . indexOf ( firstSelectedOption ) === - 1 ) { this . _scrollTop = 0 ; return ; } const scrollBuffer = panelHeight / 2 ; this . _scrollTop = this . _calculateOverlayScroll ( firstSelectedOption , scrollBuffer , maxScroll ) ; } }