license import { MDCFoundation } from '@material/base/foundation' ; import { MDCChipTrailingActionNavigationEvent } from '../trailingaction/types' ; import { MDCChipAdapter } from './adapter' ; import { cssClasses , Direction , EventSource , jumpChipKeys , navigationKeys , strings } from './constants' ; const emptyClientRect = { bottom : 0 , height : 0 , left : 0 , right : 0 , top : 0 , width : 0 , } as any ; enum FocusBehavior { SHOULD_FOCUS , SHOULD_NOT_FOCUS } export class MDCChipFoundation extends MDCFoundation < MDCChipAdapter > { static override get strings ( ) { return strings ; } static override get cssClasses ( ) { return cssClasses ; } static override get defaultAdapter ( ) : MDCChipAdapter { return { addClass : ( ) => undefined , addClassToLeadingIcon : ( ) => undefined , eventTargetHasClass : ( ) => false , focusPrimaryAction : ( ) => undefined , focusTrailingAction : ( ) => undefined , getAttribute : ( ) => null , getCheckmarkBoundingClientRect : ( ) => emptyClientRect , getComputedStyleValue : ( ) => '' , getRootBoundingClientRect : ( ) => emptyClientRect , hasClass : ( ) => false , hasLeadingIcon : ( ) => false , isRTL : ( ) => false , isTrailingActionNavigable : ( ) => false , notifyEditFinish : ( ) => undefined , notifyEditStart : ( ) => undefined , notifyInteraction : ( ) => undefined , notifyNavigation : ( ) => undefined , notifyRemoval : ( ) => undefined , notifySelection : ( ) => undefined , notifyTrailingIconInteraction : ( ) => undefined , removeClass : ( ) => undefined , removeClassFromLeadingIcon : ( ) => undefined , removeTrailingActionFocus : ( ) => undefined , setPrimaryActionAttr : ( ) => undefined , setStyleProperty : ( ) => undefined , } ; } /** Whether a trailing icon click should immediately trigger exit/removal of the chip. */ private shouldRemoveOnTrailingIconClick = true ; /**
     * Whether the primary action should receive focus on click. Should only be
     * set to true for clients who programmatically give focus to a different
     * element on the page when a chip is clicked (like a menu).
     */ private shouldFocusPrimaryActionOnClick = true ; constructor ( adapter ? ) { super ( { ... MDCChipFoundation . defaultAdapter , ... adapter } ) ; } isSelected ( ) { return this . adapter . hasClass ( cssClasses . SELECTED ) ; } isEditable ( ) { return this . adapter . hasClass ( cssClasses . EDITABLE ) ; } isEditing ( ) { return this . adapter . hasClass ( cssClasses . EDITING ) ; } setSelected ( selected ) { this . setSelectedImpl ( selected ) ; this . notifySelection ( selected ) ; } setSelectedFromChipSet ( selected , shouldNotifyClients ) { this . setSelectedImpl ( selected ) ; if ( shouldNotifyClients ) { this . notifyIgnoredSelection ( selected ) ; } } getShouldRemoveOnTrailingIconClick ( ) { return this . shouldRemoveOnTrailingIconClick ; } setShouldRemoveOnTrailingIconClick ( shouldRemove ) { this . shouldRemoveOnTrailingIconClick = shouldRemove ; } setShouldFocusPrimaryActionOnClick ( shouldFocus ) { this . shouldFocusPrimaryActionOnClick = shouldFocus ; } getDimensions ( ) { const getRootRect = ( ) => this . adapter . getRootBoundingClientRect ( ) ; const getCheckmarkRect = ( ) => this . adapter . getCheckmarkBoundingClientRect ( ) ; if ( ! this . adapter . hasLeadingIcon ( ) ) { const checkmarkRect = getCheckmarkRect ( ) ; if ( checkmarkRect ) { const rootRect = getRootRect ( ) ; return { bottom : rootRect . bottom , height : rootRect . height , left : rootRect . left , right : rootRect . right , top : rootRect . top , width : rootRect . width + checkmarkRect . height , } as any ; } } return getRootRect ( ) ; } /**
     * Begins the exit animation which leads to removal of the chip.
     */ beginExit ( ) { this . adapter . addClass ( cssClasses . CHIP_EXIT ) ; } handleClick ( ) { this . adapter . notifyInteraction ( ) ; this . setPrimaryActionFocusable ( this . getFocusBehavior ( ) ) ; } handleDoubleClick ( ) { if ( this . isEditable ( ) ) { this . startEditing ( ) ; } } /**
     * Handles a transition end event on the root element.
     */ handleTransitionEnd ( evt ) { const shouldHandle = this . adapter . eventTargetHasClass ( evt . target , cssClasses . CHIP_EXIT ) ; const widthIsAnimating = evt . propertyName === 'width' ; const opacityIsAnimating = evt . propertyName === 'opacity' ; if ( shouldHandle && opacityIsAnimating ) { const chipWidth = this . adapter . getComputedStyleValue ( 'width' ) ; requestAnimationFrame ( ( ) => { this . adapter . setStyleProperty ( 'width' , chipWidth ) ; this . adapter . setStyleProperty ( 'padding' , '0' ) ; this . adapter . setStyleProperty ( 'margin' , '0' ) ; requestAnimationFrame ( ( ) => { this . adapter . setStyleProperty ( 'width' , '0' ) ; } ) ; } ) ; return ; } if ( shouldHandle && widthIsAnimating ) { this . removeFocus ( ) ; const removedAnnouncement = this . adapter . getAttribute ( strings . REMOVED_ANNOUNCEMENT_ATTRIBUTE ) ; this . adapter . notifyRemoval ( removedAnnouncement ) ; } if ( ! opacityIsAnimating ) { return ; } const shouldHideLeadingIcon = this . adapter . eventTargetHasClass ( evt . target , cssClasses . LEADING_ICON ) && this . adapter . hasClass ( cssClasses . SELECTED ) ; const shouldShowLeadingIcon = this . adapter . eventTargetHasClass ( evt . target , cssClasses . CHECKMARK ) && ! this . adapter . hasClass ( cssClasses . SELECTED ) ; if ( shouldHideLeadingIcon ) { this . adapter . addClassToLeadingIcon ( cssClasses . HIDDEN_LEADING_ICON ) ; return ; } if ( shouldShowLeadingIcon ) { this . adapter . removeClassFromLeadingIcon ( cssClasses . HIDDEN_LEADING_ICON ) ; return ; } } handleFocusIn ( evt ) { if ( ! this . eventFromPrimaryAction ( evt ) ) { return ; } this . adapter . addClass ( cssClasses . PRIMARY_ACTION_FOCUSED ) ; } handleFocusOut ( evt ) { if ( ! this . eventFromPrimaryAction ( evt ) ) { return ; } if ( this . isEditing ( ) ) { this . finishEditing ( ) ; } this . adapter . removeClass ( cssClasses . PRIMARY_ACTION_FOCUSED ) ; } /**
     * Handles an interaction event on the trailing icon element. This is used to
     * prevent the ripple from activating on interaction with the trailing icon.
     */ handleTrailingActionInteraction ( ) { this . adapter . notifyTrailingIconInteraction ( ) ; this . removeChip ( ) ; } /**
     * Handles a keydown event from the root element.
     */ handleKeydown ( evt ) { if ( this . isEditing ( ) ) { if ( this . shouldFinishEditing ( evt ) ) { evt . preventDefault ( ) ; this . finishEditing ( ) ; } return ; } if ( this . isEditable ( ) ) { if ( this . shouldStartEditing ( evt ) ) { evt . preventDefault ( ) ; this . startEditing ( ) ; } } if ( this . shouldNotifyInteraction ( evt ) ) { this . adapter . notifyInteraction ( ) ; this . setPrimaryActionFocusable ( this . getFocusBehavior ( ) ) ; return ; } if ( this . isDeleteAction ( evt ) ) { evt . preventDefault ( ) ; this . removeChip ( ) ; return ; } if ( ! navigationKeys . has ( evt . key ) ) { return ; } evt . preventDefault ( ) ; this . focusNextAction ( evt . key , EventSource . PRIMARY ) ; } handleTrailingActionNavigation ( evt ) { this . focusNextAction ( evt . detail . key , EventSource . TRAILING ) ; } /**
     * Called by the chip set to remove focus from the chip actions.
     */ removeFocus ( ) { this . adapter . setPrimaryActionAttr ( strings . TAB_INDEX , '-1' ) ; this . adapter . removeTrailingActionFocus ( ) ; } /**
     * Called by the chip set to focus the primary action.
     *
     */ focusPrimaryAction ( ) { this . setPrimaryActionFocusable ( FocusBehavior . SHOULD_FOCUS ) ; } /**
     * Called by the chip set to focus the trailing action (if present), otherwise
     * gives focus to the trailing action.
     */ focusTrailingAction ( ) { const trailingActionIsNavigable = this . adapter . isTrailingActionNavigable ( ) ; if ( trailingActionIsNavigable ) { this . adapter . setPrimaryActionAttr ( strings . TAB_INDEX , '-1' ) ; this . adapter . focusTrailingAction ( ) ; return ; } this . focusPrimaryAction ( ) ; } private setPrimaryActionFocusable ( focusBehavior ) { this . adapter . setPrimaryActionAttr ( strings . TAB_INDEX , '0' ) ; if ( focusBehavior === FocusBehavior . SHOULD_FOCUS ) { this . adapter . focusPrimaryAction ( ) ; } this . adapter . removeTrailingActionFocus ( ) ; } private getFocusBehavior ( ) { if ( this . shouldFocusPrimaryActionOnClick ) { return FocusBehavior . SHOULD_FOCUS ; } return FocusBehavior . SHOULD_NOT_FOCUS ; } private focusNextAction ( key , source ) { const isTrailingActionNavigable = this . adapter . isTrailingActionNavigable ( ) ; const dir = this . getDirection ( key ) ; if ( jumpChipKeys . has ( key ) || ! isTrailingActionNavigable ) { this . adapter . notifyNavigation ( key , source ) ; return ; } if ( source === EventSource . PRIMARY && dir === Direction . RIGHT ) { this . focusTrailingAction ( ) ; return ; } if ( source === EventSource . TRAILING && dir === Direction . LEFT ) { this . focusPrimaryAction ( ) ; return ; } this . adapter . notifyNavigation ( key , EventSource . NONE ) ; } private getDirection ( key ) { const isRTL = this . adapter . isRTL ( ) ; const isLeftKey = key === strings . ARROW_LEFT_KEY || key === strings . IE_ARROW_LEFT_KEY ; const isRightKey = key === strings . ARROW_RIGHT_KEY || key === strings . IE_ARROW_RIGHT_KEY ; if ( ! isRTL && isLeftKey || isRTL && isRightKey ) { return Direction . LEFT ; } return Direction . RIGHT ; } private removeChip ( ) { if ( this . shouldRemoveOnTrailingIconClick ) { this . beginExit ( ) ; } } private shouldStartEditing ( evt ) { return this . eventFromPrimaryAction ( evt ) && evt . key === strings . ENTER_KEY ; } private shouldFinishEditing ( evt ) { return evt . key === strings . ENTER_KEY ; } private shouldNotifyInteraction ( evt ) { return evt . key === strings . ENTER_KEY || evt . key === strings . SPACEBAR_KEY ; } private isDeleteAction ( evt ) { const isDeletable = this . adapter . hasClass ( cssClasses . DELETABLE ) ; return isDeletable && ( evt . key === strings . BACKSPACE_KEY || evt . key === strings . DELETE_KEY || evt . key === strings . IE_DELETE_KEY ) ; } private setSelectedImpl ( selected ) { if ( selected ) { this . adapter . addClass ( cssClasses . SELECTED ) ; this . adapter . setPrimaryActionAttr ( strings . ARIA_CHECKED , 'true' ) ; } else { this . adapter . removeClass ( cssClasses . SELECTED ) ; this . adapter . setPrimaryActionAttr ( strings . ARIA_CHECKED , 'false' ) ; } } private notifySelection ( selected ) { this . adapter . notifySelection ( selected , false ) ; } private notifyIgnoredSelection ( selected ) { this . adapter . notifySelection ( selected , true ) ; } private eventFromPrimaryAction ( evt ) { return this . adapter . eventTargetHasClass ( evt . target , cssClasses . PRIMARY_ACTION ) ; } private startEditing ( ) { this . adapter . addClass ( cssClasses . EDITING ) ; this . adapter . notifyEditStart ( ) ; } private finishEditing ( ) { this . adapter . removeClass ( cssClasses . EDITING ) ; this . adapter . notifyEditFinish ( ) ; } } export default MDCChipFoundation ;