import { FocusableOption , FocusOrigin } from '@angular/cdk/a11y' ; import { Platform } from '@angular/cdk/platform' ; import { ChangeDetectorRef , Directive , ElementRef , EventEmitter , HostListener , Input , OnDestroy , Output , Renderer2 } from '@angular/core' ; import { Subject } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { tick } from '../../../common/index' ; import { FocusIndicator } from '../focus-indicator/focus-indicator' ; import { FocusIndicatorOriginService } from '../focus-indicator/focus-indicator-origin/focus-indicator-origin.service' ; import { FocusIndicatorService } from '../focus-indicator/focus-indicator.service' ; import { ManagedFocusContainerService } from '../managed-focus-container/managed-focus-container.service' ; import { TabbableListService } from './tabbable-list.service' ; let nextId = 0 ; let uniqueKey = 0 ; @ Directive ( { selector : '[uxTabbableListItem]' , exportAs : 'ux-tabbable-list-item' } ) export class TabbableListItemDirective implements FocusableOption , OnDestroy { /** Indicate the parent tabbable list item if one is present. */ @ Input ( ) parent : TabbableListItemDirective ; @ Input ( ) rank : number = 0 ; /** Indicate if this item is disabled */ @ Input ( ) disabled : boolean = false ; /** Indicate if the item is expanded if used as a hierarchical item. */ @ Input ( ) expanded : boolean = false ; /** Provide a unique key to help identify items when used in a virtual list */ @ Input ( ) set key ( key ) { const previousKey = this . _key ; const didChangeRef = previousKey && key !== previousKey ; this . _key = key ; if ( didChangeRef && this . isTabbable ( ) ) { this . _changeDetector . detectChanges ( ) ; this . _tabbableList . focusKeyManager . updateActiveItem ( - 1 ) ; const origin = this . _focusOrigin ; this . _elementRef . nativeElement . blur ( ) ; this . _tabbableList . itemReferenceChange ( previousKey , origin ) ; } } get key ( ) : any { return this . _key || this . _defaultKey ; } /** Emit when the expanded state changes. */ @ Output ( ) expandedChange = new EventEmitter < boolean > ( ) ; /** Emit when the element receives focus via the tabbable list. */ @ Output ( ) activated = new EventEmitter < FocusOrigin > ( ) ; get tabindex ( ) : number { return this . _tabbableList . isItemActive ( this ) ? 0 : - 1 ; } /** Give each tabbable item a unique id */ id : number = nextId ++ ; /** Each item in the list needs to be initialised by the service. When the item QueryList changes this is used to identify which items previously existed and which are new */ initialized : boolean = false ; /** Store a list of all child tabbable items */ children : TabbableListItemDirective [ ] = [ ] ; /** Emit whenever the expanded state changes */ keyboardExpanded$ = new Subject < boolean > ( ) ; /** Automatically unsubscribe from all observables */ private _onDestroy = new Subject < void > ( ) ; /** Store a reference to the focus indicator instance */ private _focusIndicator : FocusIndicator ; /** Store the current key - it may change in a ngFor/uxVirtualFor if the cell is reused. */ private _key : any ; /** Store a default key to use if one is not provided */ private _defaultKey : string = `tabbable-list-key-${ uniqueKey ++ }` ; /** Determine if this element has a focus indicator visible */ private _focusOrigin : FocusOrigin = null ; constructor ( /** Access the tabbable list service */ private _tabbableList , /** Access the tabbable item element */ private _elementRef , /** Access the service to programmatically control focus indicators */ focusIndicatorService , /** Access the service responsible for handling focus in child elements */ private _managedFocusContainerService , /** Access the service which can provide us with browser identification */ private _platform , /** Access the change detector to ensure tabindex gets updated as expects */ private _changeDetector , /** Access the focus origin if one is provided */ private _focusOriginService , /** Access the renderer to make manual dom manipulations */ private _renderer ) { this . _focusIndicator = focusIndicatorService . monitor ( _elementRef . nativeElement ) ; this . _focusIndicator . origin$ . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( origin => this . _focusOrigin = origin ) ; this . _tabbableList . onTabIndexChange . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => this . setTabIndex ( ) ) ; this . keyboardExpanded$ . pipe ( tick ( ) , takeUntil ( this . _onDestroy ) ) . subscribe ( expanded => { this . expandedChange . emit ( expanded ) ; if ( expanded ) { if ( this . children . length > 0 ) { this . _tabbableList . activate ( this . children [ 0 ] ) ; } } else { this . _tabbableList . activate ( this ) ; } } ) ; } onInit ( ) { this . initialized = true ; this . _managedFocusContainerService . register ( this . _elementRef . nativeElement , this ) ; this . setTabIndex ( ) ; } ngOnDestroy ( ) { if ( this . tabindex === 0 ) { this . _tabbableList . setFirstItemTabbable ( ) ; } this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; this . _focusIndicator . destroy ( ) ; this . _managedFocusContainerService . unregister ( this . _elementRef . nativeElement , this ) ; } focus ( ) { const hasTabbableItem = this . _tabbableList . hasTabbableItem ( ) ; const origin = hasTabbableItem ? this . _focusOriginService . getOrigin ( ) || 'keyboard' : 'keyboard' ; this . focusWithOrigin ( origin , ! this . _tabbableList . shouldScrollInView ) ; this . _tabbableList . activate ( this , hasTabbableItem ) ; this . activated . emit ( origin ) ; } @ HostListener ( 'focus' ) @ HostListener ( 'click' ) onFocus ( ) { if ( ! this . _tabbableList . isItemActive ( this ) ) { this . _tabbableList . activate ( this , true ) ; } this . _tabbableList . isFocused = true ; } @ HostListener ( 'blur' ) onBlur ( ) { if ( this . _tabbableList . isItemActive ( this ) ) { this . _tabbableList . isFocused = false ; } } @ HostListener ( 'keydown' , [ '$event' ] ) onKeydown ( event ) { this . _tabbableList . onKeydown ( this , event ) ; } getFocused ( ) { return this . _elementRef . nativeElement === document . activeElement ; } /** We can programmatically focus an element but may want a different origin than 'programmatic' */ focusWithOrigin ( origin , preventScroll = true ) { if ( origin ) { const scrollTop = this . _tabbableList . containerRef . scrollTop ; this . _focusIndicator . focus ( origin , { preventScroll } ) ; if ( preventScroll && ! this . _platform . WEBKIT ) { this . _tabbableList . containerRef . scrollTop = scrollTop ; } } } private isTabbable ( ) { return this . tabindex === 0 ; } private setTabIndex ( ) { this . _renderer . setAttribute ( this . _elementRef . nativeElement , 'tabindex' , this . tabindex . toString ( ) ) ; } }