import { CSSResultArray , html , SpectrumElement , TemplateResult , } from '@spectrum-web-components/base' ; import { queryAssignedNodes } from '@spectrum-web-components/base/src/decorators.js' ; import { FocusVisiblePolyfillMixin } from '@spectrum-web-components/shared/src/focus-visible.js' ; import { Tag } from './Tag.js' ; import styles from './tags.css.js' ; element slot export class Tags extends FocusVisiblePolyfillMixin ( SpectrumElement ) { public static get styles ( ) : CSSResultArray { return [ styles ] ; } @ queryAssignedNodes ( '' ) public defaultNodes ! : Node [ ] ; public get tags ( ) : Tag [ ] { return this . defaultNodes . filter ( ( node ) => ( node as HTMLElement ) instanceof Tag ) as Tag [ ] ; } constructor ( ) { super ( ) ; this . addEventListener ( 'focusin' , this . handleFocusin ) ; } public focus ( ) { if ( ! this . tags . length ) { return ; } const firstFocusableTag = this . tags . find ( ( tag ) => ! tag . disabled && tag . deletable ) ; if ( firstFocusableTag ) { firstFocusableTag . focus ( ) ; } } private handleFocusin = ( ) : void => { this . addEventListener ( 'focusout' , this . handleFocusout ) ; this . addEventListener ( 'keydown' , this . handleKeydown ) ; requestAnimationFrame ( ( ) => { const firstTagWithTabIndex = this . tags . find ( ( tag ) => tag . tabIndex === 0 ) ; if ( firstTagWithTabIndex ) { firstTagWithTabIndex . tabIndex = - 1 ; } } ) ; } ; private handleKeydown = ( event ) : void => { const { code } = event ; const activeElement = ( this . getRootNode ( ) as Document ) . activeElement as Tag ; if ( ! activeElement ) { return ; } let nextIndex = this . tags . indexOf ( activeElement ) ; if ( nextIndex === - 1 ) { return ; } const circularIndexedElement = < T extends HTMLElement > ( list , index ) : T => list [ ( list . length + index ) % list . length ] ; const tagFromDelta = ( delta ) : void => { nextIndex += delta ; let nextTag = circularIndexedElement ( this . tags , nextIndex ) ; while ( nextTag . disabled || ! nextTag . deletable ) { nextIndex += delta ; nextTag = circularIndexedElement ( this . tags , nextIndex ) ; } } ; switch ( code ) { case 'ArrowUp' : tagFromDelta ( - 1 ) ; break ; case 'ArrowLeft' : tagFromDelta ( this . isLTR ? - 1 : 1 ) ; break ; case 'ArrowRight' : tagFromDelta ( this . isLTR ? 1 : - 1 ) ; break ; case 'ArrowDown' : tagFromDelta ( 1 ) ; break ; case 'End' : nextIndex = this . tags . length ; tagFromDelta ( - 1 ) ; break ; case 'Home' : nextIndex = - 1 ; tagFromDelta ( 1 ) ; break ; case 'PageUp' : case 'PageDown' : const tagsSiblings = [ ... ( this . getRootNode ( ) as Document ) . querySelectorAll < Tags > ( 'sp-tags' ) , ] ; if ( tagsSiblings . length < 2 ) { return ; } event . preventDefault ( ) ; const currentIndex = tagsSiblings . indexOf ( this ) ; const offset = code === 'PageUp' ? - 1 : 1 ; let nextTagsIndex = currentIndex + offset ; let nextTags = circularIndexedElement ( tagsSiblings , nextTagsIndex ) ; while ( ! nextTags . tags . length ) { nextTagsIndex += offset ; nextTags = circularIndexedElement ( tagsSiblings , nextTagsIndex ) ; } nextTags . focus ( ) ; return ; default : return ; } event . preventDefault ( ) ; circularIndexedElement ( this . tags , nextIndex ) . focus ( ) ; } ; private handleFocusout = ( ) : void => { const firstFocusableTag = this . tags . find ( ( tag ) => ! tag . disabled && tag . deletable ) ; if ( firstFocusableTag ) { firstFocusableTag . tabIndex = 0 ; } this . removeEventListener ( 'keydown' , this . handleKeydown ) ; this . removeEventListener ( 'focusout' , this . handleFocusout ) ; } ; protected render ( ) { return html `
            <slot></slot>
        ` ; } protected firstUpdated ( ) { if ( ! this . hasAttribute ( 'role' ) ) { this . setAttribute ( 'role' , 'list' ) ; } if ( ! this . hasAttribute ( 'aria-label' ) ) { this . setAttribute ( 'aria-label' , 'Tags' ) ; } } }