license import { MDCFoundation } from '@material/base/foundation' ; import { KEY , normalizeKey } from '@material/dom/keyboard' ; import { Corner } from '@material/menu-surface/constants' ; import { MDCSelectAdapter } from './adapter' ; import { cssClasses , numbers , strings } from './constants' ; import { MDCSelectHelperTextFoundation } from './helper-text/foundation' ; import { MDCSelectIconFoundation } from './icon/foundation' ; import { MDCSelectFoundationMap } from './types' ; export class MDCSelectFoundation extends MDCFoundation < MDCSelectAdapter > { static override get cssClasses ( ) { return cssClasses ; } static override get numbers ( ) { return numbers ; } static override get strings ( ) { return strings ; } /**
     * See  MDCSelectAdapter for typing information on parameters and return types.
      static override get defaultAdapter ( ) : MDCSelectAdapter { return { addClass : ( ) => undefined , removeClass : ( ) => undefined , hasClass : ( ) => false , activateBottomLine : ( ) => undefined , deactivateBottomLine : ( ) => undefined , getSelectedIndex : ( ) => - 1 , setSelectedIndex : ( ) => undefined , hasLabel : ( ) => false , floatLabel : ( ) => undefined , getLabelWidth : ( ) => 0 , setLabelRequired : ( ) => undefined , hasOutline : ( ) => false , notchOutline : ( ) => undefined , closeOutline : ( ) => undefined , setRippleCenter : ( ) => undefined , notifyChange : ( ) => undefined , setSelectedText : ( ) => undefined , isSelectAnchorFocused : ( ) => false , getSelectAnchorAttr : ( ) => '' , setSelectAnchorAttr : ( ) => undefined , removeSelectAnchorAttr : ( ) => undefined , addMenuClass : ( ) => undefined , removeMenuClass : ( ) => undefined , openMenu : ( ) => undefined , closeMenu : ( ) => undefined , getAnchorElement : ( ) => null , setMenuAnchorElement : ( ) => undefined , setMenuAnchorCorner : ( ) => undefined , setMenuWrapFocus : ( ) => undefined , focusMenuItemAtIndex : ( ) => undefined , getMenuItemCount : ( ) => 0 , getMenuItemValues : ( ) => [ ] , getMenuItemTextAtIndex : ( ) => '' , isTypeaheadInProgress : ( ) => false , typeaheadMatchItem : ( ) => - 1 , } ; } private readonly leadingIcon : MDCSelectIconFoundation | undefined ; private readonly helperText : MDCSelectHelperTextFoundation | undefined ; private disabled = false ; private isMenuOpen = false ; private useDefaultValidation = true ; private customValidity = true ; private lastSelectedIndex = numbers . UNSET_INDEX ; private clickDebounceTimeout = 0 ; private recentlyClicked = false ; param adapter param foundationMap constructor ( adapter ? , foundationMap = { } ) { super ( { ... MDCSelectFoundation . defaultAdapter , ... adapter } ) ; this . leadingIcon = foundationMap . leadingIcon ; this . helperText = foundationMap . helperText ; } /** Returns the index of the currently selected menu item, or -1 if none. */ getSelectedIndex ( ) { return this . adapter . getSelectedIndex ( ) ; } setSelectedIndex ( index , closeMenu = false , skipNotify = false ) { if ( index >= this . adapter . getMenuItemCount ( ) ) { return ; } if ( index === numbers . UNSET_INDEX ) { this . adapter . setSelectedText ( '' ) ; } else { this . adapter . setSelectedText ( this . adapter . getMenuItemTextAtIndex ( index ) . trim ( ) ) ; } this . adapter . setSelectedIndex ( index ) ; if ( closeMenu ) { this . adapter . closeMenu ( ) ; } if ( ! skipNotify && this . lastSelectedIndex !== index ) { this . handleChange ( ) ; } this . lastSelectedIndex = index ; } setValue ( value , skipNotify = false ) { const index = this . adapter . getMenuItemValues ( ) . indexOf ( value ) ; this . setSelectedIndex ( index , false , skipNotify ) ; } getValue ( ) { const index = this . adapter . getSelectedIndex ( ) ; const menuItemValues = this . adapter . getMenuItemValues ( ) ; return index !== numbers . UNSET_INDEX ? menuItemValues [ index ] : '' ; } getDisabled ( ) { return this . disabled ; } setDisabled ( isDisabled ) { this . disabled = isDisabled ; if ( this . disabled ) { this . adapter . addClass ( cssClasses . DISABLED ) ; this . adapter . closeMenu ( ) ; } else { this . adapter . removeClass ( cssClasses . DISABLED ) ; } if ( this . leadingIcon ) { this . leadingIcon . setDisabled ( this . disabled ) ; } if ( this . disabled ) { this . adapter . removeSelectAnchorAttr ( 'tabindex' ) ; } else { this . adapter . setSelectAnchorAttr ( 'tabindex' , '0' ) ; } this . adapter . setSelectAnchorAttr ( 'aria-disabled' , this . disabled . toString ( ) ) ; } /** Opens the menu. */ openMenu ( ) { this . adapter . addClass ( cssClasses . ACTIVATED ) ; this . adapter . openMenu ( ) ; this . isMenuOpen = true ; this . adapter . setSelectAnchorAttr ( 'aria-expanded' , 'true' ) ; } param content setHelperTextContent ( content ) { if ( this . helperText ) { this . helperText . setContent ( content ) ; } } /**
     * Re-calculates if the notched outline should be notched and if the label
     * should float.
     */ layout ( ) { if ( this . adapter . hasLabel ( ) ) { const optionHasValue = this . getValue ( ) . length > 0 ; const isFocused = this . adapter . hasClass ( cssClasses . FOCUSED ) ; const shouldFloatAndNotch = optionHasValue || isFocused ; const isRequired = this . adapter . hasClass ( cssClasses . REQUIRED ) ; this . notchOutline ( shouldFloatAndNotch ) ; this . adapter . floatLabel ( shouldFloatAndNotch ) ; this . adapter . setLabelRequired ( isRequired ) ; } } /**
     * Synchronizes the list of options with the state of the foundation. Call
     * this whenever menu options are dynamically updated.
     */ layoutOptions ( ) { const menuItemValues = this . adapter . getMenuItemValues ( ) ; const selectedIndex = menuItemValues . indexOf ( this . getValue ( ) ) ; this . setSelectedIndex ( selectedIndex , false , true ) ; } handleMenuOpened ( ) { if ( this . adapter . getMenuItemValues ( ) . length === 0 ) { return ; } const selectedIndex = this . getSelectedIndex ( ) ; const focusItemIndex = selectedIndex >= 0 ? selectedIndex : 0 ; this . adapter . focusMenuItemAtIndex ( focusItemIndex ) ; } handleMenuClosing ( ) { this . adapter . setSelectAnchorAttr ( 'aria-expanded' , 'false' ) ; } handleMenuClosed ( ) { this . adapter . removeClass ( cssClasses . ACTIVATED ) ; this . isMenuOpen = false ; if ( ! this . adapter . isSelectAnchorFocused ( ) ) { this . blur ( ) ; } } /**
     * Handles value changes, via change event or programmatic updates.
     */ handleChange ( ) { this . layout ( ) ; this . adapter . notifyChange ( this . getValue ( ) ) ; const isRequired = this . adapter . hasClass ( cssClasses . REQUIRED ) ; if ( isRequired && this . useDefaultValidation ) { this . setValid ( this . isValid ( ) ) ; } } handleMenuItemAction ( index ) { this . setSelectedIndex ( index , true ) ; } /**
     * Handles focus events from select element.
     */ handleFocus ( ) { this . adapter . addClass ( cssClasses . FOCUSED ) ; this . layout ( ) ; this . adapter . activateBottomLine ( ) ; } /**
     * Handles blur events from select element.
     */ handleBlur ( ) { if ( this . isMenuOpen ) { return ; } this . blur ( ) ; } handleClick ( normalizedX ) { if ( this . disabled || this . recentlyClicked ) { return ; } this . setClickDebounceTimeout ( ) ; if ( this . isMenuOpen ) { this . adapter . closeMenu ( ) ; return ; } this . adapter . setRippleCenter ( normalizedX ) ; this . openMenu ( ) ; } /**
     * Handles keydown events on select element. Depending on the type of
     * character typed, does typeahead matching or opens menu.
     */ handleKeydown ( event ) { if ( this . isMenuOpen || ! this . adapter . hasClass ( cssClasses . FOCUSED ) ) { return ; } const isEnter = normalizeKey ( event ) === KEY . ENTER ; const isSpace = normalizeKey ( event ) === KEY . SPACEBAR ; const arrowUp = normalizeKey ( event ) === KEY . ARROW_UP ; const arrowDown = normalizeKey ( event ) === KEY . ARROW_DOWN ; const isModifier = event . ctrlKey || event . metaKey ; if ( ! isModifier && ( ! isSpace && event . key && event . key . length === 1 || isSpace && this . adapter . isTypeaheadInProgress ( ) ) ) { const key = isSpace ? ' ' : event . key ; const typeaheadNextIndex = this . adapter . typeaheadMatchItem ( key , this . getSelectedIndex ( ) ) ; if ( typeaheadNextIndex >= 0 ) { this . setSelectedIndex ( typeaheadNextIndex ) ; } event . preventDefault ( ) ; return ; } if ( ! isEnter && ! isSpace && ! arrowUp && ! arrowDown ) { return ; } this . openMenu ( ) ; event . preventDefault ( ) ; } /**
     * Opens/closes the notched outline.
     */ notchOutline ( openNotch ) { if ( ! this . adapter . hasOutline ( ) ) { return ; } const isFocused = this . adapter . hasClass ( cssClasses . FOCUSED ) ; if ( openNotch ) { const labelScale = numbers . LABEL_SCALE ; const labelWidth = this . adapter . getLabelWidth ( ) * labelScale ; this . adapter . notchOutline ( labelWidth ) ; } else if ( ! isFocused ) { this . adapter . closeOutline ( ) ; } } /**
     * Sets the aria label of the leading icon.
     */ setLeadingIconAriaLabel ( label ) { if ( this . leadingIcon ) { this . leadingIcon . setAriaLabel ( label ) ; } } /**
     * Sets the text content of the leading icon.
     */ setLeadingIconContent ( content ) { if ( this . leadingIcon ) { this . leadingIcon . setContent ( content ) ; } } getUseDefaultValidation ( ) { return this . useDefaultValidation ; } setUseDefaultValidation ( useDefaultValidation ) { this . useDefaultValidation = useDefaultValidation ; } setValid ( isValid ) { if ( ! this . useDefaultValidation ) { this . customValidity = isValid ; } this . adapter . setSelectAnchorAttr ( 'aria-invalid' , ( ! isValid ) . toString ( ) ) ; if ( isValid ) { this . adapter . removeClass ( cssClasses . INVALID ) ; this . adapter . removeMenuClass ( cssClasses . MENU_INVALID ) ; } else { this . adapter . addClass ( cssClasses . INVALID ) ; this . adapter . addMenuClass ( cssClasses . MENU_INVALID ) ; } this . syncHelperTextValidity ( isValid ) ; } isValid ( ) { if ( this . useDefaultValidation && this . adapter . hasClass ( cssClasses . REQUIRED ) && ! this . adapter . hasClass ( cssClasses . DISABLED ) ) { return this . getSelectedIndex ( ) !== numbers . UNSET_INDEX && ( this . getSelectedIndex ( ) !== 0 || Boolean ( this . getValue ( ) ) ) ; } return this . customValidity ; } setRequired ( isRequired ) { if ( isRequired ) { this . adapter . addClass ( cssClasses . REQUIRED ) ; } else { this . adapter . removeClass ( cssClasses . REQUIRED ) ; } this . adapter . setSelectAnchorAttr ( 'aria-required' , isRequired . toString ( ) ) ; this . adapter . setLabelRequired ( isRequired ) ; } getRequired ( ) { return this . adapter . getSelectAnchorAttr ( 'aria-required' ) === 'true' ; } override init ( ) { const anchorEl = this . adapter . getAnchorElement ( ) ; if ( anchorEl ) { this . adapter . setMenuAnchorElement ( anchorEl ) ; this . adapter . setMenuAnchorCorner ( Corner . BOTTOM_START ) ; } this . adapter . setMenuWrapFocus ( false ) ; this . setDisabled ( this . adapter . hasClass ( cssClasses . DISABLED ) ) ; this . syncHelperTextValidity ( ! this . adapter . hasClass ( cssClasses . INVALID ) ) ; this . layout ( ) ; this . layoutOptions ( ) ; } /**
     * Unfocuses the select component.
     */ private blur ( ) { this . adapter . removeClass ( cssClasses . FOCUSED ) ; this . layout ( ) ; this . adapter . deactivateBottomLine ( ) ; const isRequired = this . adapter . hasClass ( cssClasses . REQUIRED ) ; if ( isRequired && this . useDefaultValidation ) { this . setValid ( this . isValid ( ) ) ; } } private syncHelperTextValidity ( isValid ) { if ( ! this . helperText ) { return ; } this . helperText . setValidity ( isValid ) ; const helperTextVisible = this . helperText . isVisible ( ) ; const helperTextId = this . helperText . getId ( ) ; if ( helperTextVisible && helperTextId ) { this . adapter . setSelectAnchorAttr ( strings . ARIA_DESCRIBEDBY , helperTextId ) ; } else { this . adapter . removeSelectAnchorAttr ( strings . ARIA_DESCRIBEDBY ) ; } } private setClickDebounceTimeout ( ) { clearTimeout ( this . clickDebounceTimeout ) ; this . clickDebounceTimeout = setTimeout ( ( ) => { this . recentlyClicked = false ; } , numbers . CLICK_DEBOUNCE_TIMEOUT_MS ) ; this . recentlyClicked = true ; } } export default MDCSelectFoundation ;