import { FocusKeyManager , FocusOrigin } from '@angular/cdk/a11y' ; import { DOWN_ARROW , END , HOME , LEFT_ARROW , RIGHT_ARROW , UP_ARROW } from '@angular/cdk/keycodes' ; import { Injectable , OnDestroy , QueryList } from '@angular/core' ; import { takeUntil } from 'rxjs/operators' ; import { Subject } from 'rxjs' ; import { TabbableListItemDirective } from './tabbable-list-item.directive' ; @ Injectable ( ) export class TabbableListService implements OnDestroy { /** Indicate is this is being using on a hierarchichal set of items */ hierarchy : boolean = false ; /** Determine if we all the alt key */ allowAltModifier : boolean = true ; /** Determine if we all the ctrl key */ allowCtrlModifier : boolean = true ; /** Determine if we allow the Home/End keys */ allowBoundaryKeys : boolean = false ; /** Determine if we should scroll the item into view on focus */ shouldScrollInView : boolean = true ; /** Store the instance of the focus key manager */ focusKeyManager : FocusKeyManager < TabbableListItemDirective > ; /** Indicate if we should refocus an item on QueryList change - for use within virtual lists */ shouldFocusOnChange : boolean = true ; /** Store the container element */ containerRef : HTMLElement ; /** Emit whenever focus does not change but tabindexes have */ onTabIndexChange = new Subject < void > ( ) ; /** Determine if focus is currently within the tabbable list */ isFocused : boolean = false ; /** Store the list of tabbable items */ private _items : QueryList < TabbableListItemDirective > ; /** Store the direction of the list */ private _direction : 'horizontal' | 'vertical' ; /** Unsubscribe from all observables on destroy */ private _onDestroy = new Subject < void > ( ) ; ngOnDestroy ( ) { this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; this . onTabIndexChange . complete ( ) ; } initialize ( items , direction , wrap ) { this . _items = items ; this . focusKeyManager = new FocusKeyManager ( items ) ; direction === 'vertical' ? this . focusKeyManager . withVerticalOrientation ( ) : this . focusKeyManager . withHorizontalOrientation ( 'ltr' ) ; this . _direction = direction ; if ( wrap ) { this . focusKeyManager . withWrap ( ) ; } this . setFirstItemTabbable ( ) ; this . _items . forEach ( item => item . onInit ( ) ) ; this . _items . changes . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => { this . _items . filter ( item => ! item . initialized ) . forEach ( item => item . onInit ( ) ) ; this . onTabIndexChange . next ( ) ; this . ensureTabbableItem ( ) ; } ) ; } /** Give and item focus or just make it the current tabbable item */ activate ( item , updateIndexOnly = false ) { if ( ! item ) { return ; } const index = this . _items . toArray ( ) . indexOf ( item ) ; this . activateItemAtIndex ( index , updateIndexOnly ) ; } /** Give and item focus or just make it the current tabbable item */ activateItemAtIndex ( index , updateIndexOnly = false ) { if ( updateIndexOnly ) { return this . updateActiveItemIndex ( index ) ; } if ( this . focusKeyManager . activeItemIndex !== index ) { this . focusKeyManager . setActiveItem ( index ) ; } } isItemActive ( item ) { if ( ! this . _items ) { return false ; } const index = this . _items . toArray ( ) . findIndex ( _item => _item . id === item . id ) ; return this . focusKeyManager && this . focusKeyManager . activeItemIndex === index ; } setFirstItemTabbable ( ) { const first = this . _items . toArray ( ) . findIndex ( item => ! item . disabled ) ; if ( first !== - 1 ) { this . updateActiveItemIndex ( first ) ; } } ensureTabbableItem ( ) { const active = this . _items . find ( item => this . isItemActive ( item ) ) ; if ( ! active ) { this . setFirstItemTabbable ( ) ; } } focusTabbableItem ( ) { if ( ! this . _items ) { return ; } const index = this . _items . toArray ( ) . findIndex ( item => this . isItemActive ( item ) ) ; if ( index !== - 1 ) { this . focusKeyManager . setActiveItem ( index ) ; } } onKeydown ( source , event ) { if ( ! this . allowAltModifier && event . altKey || ! this . allowCtrlModifier && event . ctrlKey ) { return ; } this . focusKeyManager . onKeydown ( event ) ; if ( this . allowBoundaryKeys ) { switch ( event . which ) { case HOME : this . focusKeyManager . setFirstItemActive ( ) ; event . preventDefault ( ) ; break ; case END : this . focusKeyManager . setLastItemActive ( ) ; event . preventDefault ( ) ; break ; } } if ( this . hierarchy ) { if ( ( this . _direction === 'horizontal' && event . keyCode === DOWN_ARROW ) || ( this . _direction === 'vertical' && event . keyCode === RIGHT_ARROW ) ) { source . keyboardExpanded$ . next ( true ) ; } else if ( ( this . _direction === 'horizontal' && event . keyCode === UP_ARROW ) || ( this . _direction === 'vertical' && event . keyCode === LEFT_ARROW ) ) { if ( source . children . length > 0 && source . expanded ) { source . keyboardExpanded$ . next ( false ) ; } else if ( source . parent ) { source . parent . keyboardExpanded$ . next ( false ) ; } } } } sortItemsByHierarchy ( list ) { const topLevel = [ ] ; list . forEach ( item => item . children = [ ] ) ; list . forEach ( item => { if ( item . parent ) { item . parent . children . push ( item ) ; } else { topLevel . push ( item ) ; } } ) ; return this . flattenHierarchy ( topLevel ) ; } /**
     * In a uxVirtualFor list cells can be resused. This means that when we scroll
     * the data associated with a given element may change and not the actual elements. If only the data changes
     * then the QueryList will not emit a change so we may show focus indicatator on the element that previously displayed
     * the correct data but no longer does.
     *
     * We need to handle this correctly here. We already have keys implements to handle virtual elements so we can check
     * if a key changes and use it to update the focused item even if the QueryList doesn't inform us that we have changed.
     */ itemReferenceChange ( previousKey , origin ) { const item = this . getItemByKey ( previousKey ) ; if ( ! item ) { return this . ensureTabbableItem ( ) ; } const index = this . _items . toArray ( ) . indexOf ( item ) ; this . updateActiveItemIndex ( index ) ; item . focusWithOrigin ( origin ) ; } /** Update the active item without causing focus */ updateActiveItemIndex ( index ) { this . focusKeyManager . updateActiveItem ( index ) ; this . onTabIndexChange . next ( ) ; } /** Determine if there is an item with a tabindex of 0 */ hasTabbableItem ( ) { return this . focusKeyManager && this . focusKeyManager . activeItemIndex >= 0 ; } private getItemByKey ( key ) { return this . _items . find ( item => item . key === key ) ; } private flattenHierarchy ( items ) { const flatList = [ ] ; items . forEach ( item => { item . children . sort ( ( a , b ) => a . rank - b . rank ) ; flatList . push ( item , ... this . flattenHierarchy ( item . children ) ) ; } ) ; return flatList ; } }