import { CSSResultArray , html , PropertyValues , TemplateResult , } from '@spectrum-web-components/base' ; import { property , query , } from '@spectrum-web-components/base/src/decorators.js' ; import '@spectrum-web-components/icons-ui/icons/sp-icon-checkmark100.js' ; import { LikeAnchor } from '@spectrum-web-components/shared/src/like-anchor.js' ; import { Focusable } from '@spectrum-web-components/shared/src/focusable.js' ; import menuItemStyles from './menu-item.css.js' ; import checkmarkStyles from '@spectrum-web-components/icon/src/spectrum-icon-checkmark.css.js' ; import { Menu } from './Menu.js' ; export class MenuItemRemovedEvent extends Event { constructor ( ) { super ( 'sp-menu-item-removed' , { bubbles : true , composed : true , } ) ; } get item ( ) : MenuItem { return this . _item ; } _item ! : MenuItem ; focused = false ; reset ( item ) { this . _item = item ; } } export class MenuItemAddedOrUpdatedEvent extends Event { constructor ( ) { super ( 'sp-menu-item-added-or-updated' , { bubbles : true , composed : true , } ) ; } set focusRoot ( root ) { this . item . menuData . focusRoot = this . item . menuData . focusRoot || root ; } set selectionRoot ( root ) { this . item . menuData . selectionRoot = this . item . menuData . selectionRoot || root ; } get item ( ) : MenuItem { return this . _item ; } _item ! : MenuItem ; set currentAncestorWithSelects ( ancestor ) { this . _currentAncestorWithSelects = ancestor ; } get currentAncestorWithSelects ( ) : Menu | undefined { return this . _currentAncestorWithSelects ; } _currentAncestorWithSelects ? : Menu ; reset ( item ) { this . _item = item ; this . _currentAncestorWithSelects = undefined ; item . menuData = { focusRoot : undefined , selectionRoot : undefined , } ; } } export type MenuItemChildren = { icon : Element [ ] ; content : Node [ ] ; } ; const addOrUpdateEvent = new MenuItemAddedOrUpdatedEvent ( ) ; const removeEvent = new MenuItemRemovedEvent ( ) ; element slot slot slot fires fires export class MenuItem extends LikeAnchor ( Focusable ) { public static get styles ( ) : CSSResultArray { return [ menuItemStyles , checkmarkStyles ] ; } static instanceCount = 0 ; @ property ( { type : Boolean , reflect : true } ) public active = false ; @ property ( { type : Boolean , reflect : true } ) public focused = false ; @ property ( { type : Boolean , reflect : true } ) public selected = false ; @ property ( { type : String } ) public get value ( ) : string { return this . _value || this . itemText ; } public set value ( value ) { if ( value === this . _value ) { return ; } this . _value = value || '' ; if ( this . _value ) { this . setAttribute ( 'value' , this . _value ) ; } else { this . removeAttribute ( 'value' ) ; } } private _value = '' ; private public get itemText ( ) : string { return ( this . textContent || '' ) . trim ( ) ; } @ property ( { type : Boolean , reflect : true , attribute : 'no-wrap' , hasChanged ( ) { return false ; } , } ) public noWrap = false ; @ query ( '.anchor' ) private anchorElement ! : HTMLAnchorElement ; public get focusElement ( ) : HTMLElement { return this ; } public get itemChildren ( ) : MenuItemChildren { if ( this . _itemChildren ) { return this . _itemChildren ; } const iconSlot = this . shadowRoot . querySelector ( 'slot[name="icon"]' ) as HTMLSlotElement ; const icon = ! iconSlot ? [ ] : iconSlot . assignedElements ( ) . map ( ( element ) => { const newElement = element . cloneNode ( true ) as HTMLElement ; newElement . removeAttribute ( 'slot' ) ; newElement . classList . toggle ( 'icon' ) ; return newElement ; } ) ; const contentSlot = this . shadowRoot . querySelector ( 'slot:not([name])' ) as HTMLSlotElement ; const content = ! contentSlot ? [ ] : contentSlot . assignedNodes ( ) . map ( ( node ) => node . cloneNode ( true ) ) ; this . _itemChildren = { icon , content } ; return this . _itemChildren ; } private _itemChildren ? : MenuItemChildren ; constructor ( ) { super ( ) ; this . proxyFocus = this . proxyFocus . bind ( this ) ; this . addEventListener ( 'click' , this . handleClickCapture , { capture : true , } ) ; } public click ( ) { if ( this . disabled ) { return ; } if ( this . shouldProxyClick ( ) ) { return ; } super . click ( ) ; } private handleClickCapture ( event ) { if ( this . disabled ) { event . preventDefault ( ) ; event . stopImmediatePropagation ( ) ; event . stopPropagation ( ) ; return false ; } } private proxyFocus ( ) { this . focus ( ) ; } private shouldProxyClick ( ) { let handled = false ; if ( this . anchorElement ) { this . anchorElement . click ( ) ; handled = true ; } return handled ; } protected breakItemChildrenCache ( ) { this . _itemChildren = undefined ; this . triggerUpdate ( ) ; } protected render ( ) { return html `
            <slot name="icon" @slotchange=${ this . breakItemChildrenCache }></slot>
            <div id="label">
                <slot
                    id="slot"
                    @slotchange=${ this . breakItemChildrenCache }
                ></slot>
            </div>
            <slot name="value"></slot>
            ${ this . selected ? html `
                      <sp-icon-checkmark100
                          id="selected"
                          class="spectrum-UIIcon-Checkmark100 icon checkmark"
                      ></sp-icon-checkmark100>
                  ` : html `` }
            ${ this . href && this . href . length > 0 ? super . renderAnchor ( { id : 'button' , ariaHidden : true , className : 'button anchor hidden' , } ) : html `` }
        ` ; } private handleRemoveActive ( ) { this . active = false ; } private handlePointerdown ( ) { this . active = true ; } protected firstUpdated ( changes ) { super . firstUpdated ( changes ) ; this . setAttribute ( 'tabindex' , '-1' ) ; this . addEventListener ( 'pointerdown' , this . handlePointerdown ) ; if ( ! this . hasAttribute ( 'id' ) ) { this . id = `sp-menu-item-${ MenuItem . instanceCount ++ }` ; } } updateAriaSelected ( ) { const role = this . getAttribute ( 'role' ) ; if ( role === 'option' ) { this . setAttribute ( 'aria-selected' , this . selected ? 'true' : 'false' ) ; } else if ( role === 'menuitemcheckbox' || role === 'menuitemradio' ) { this . setAttribute ( 'aria-checked' , this . selected ? 'true' : 'false' ) ; } } public setRole ( role ) { this . setAttribute ( 'role' , role ) ; this . updateAriaSelected ( ) ; } protected updated ( changes ) { super . updated ( changes ) ; if ( changes . has ( 'label' ) ) { this . setAttribute ( 'aria-label' , this . label || '' ) ; } if ( changes . has ( 'active' ) ) { if ( this . active ) { this . addEventListener ( 'pointerup' , this . handleRemoveActive ) ; this . addEventListener ( 'pointerleave' , this . handleRemoveActive ) ; } else { this . removeEventListener ( 'pointerup' , this . handleRemoveActive ) ; this . removeEventListener ( 'pointerleave' , this . handleRemoveActive ) ; } } if ( this . anchorElement ) { this . anchorElement . addEventListener ( 'focus' , this . proxyFocus ) ; this . anchorElement . tabIndex = - 1 ; } if ( changes . has ( 'selected' ) ) { this . updateAriaSelected ( ) ; } } public connectedCallback ( ) { super . connectedCallback ( ) ; addOrUpdateEvent . reset ( this ) ; this . dispatchEvent ( addOrUpdateEvent ) ; this . _parentElement = this . parentElement as HTMLElement ; } _parentElement ! : HTMLElement ; public disconnectedCallback ( ) { removeEvent . reset ( this ) ; this . _parentElement ?. dispatchEvent ( removeEvent ) ; super . disconnectedCallback ( ) ; } public async triggerUpdate ( ) { await new Promise ( ( ready ) => requestAnimationFrame ( ready ) ) ; addOrUpdateEvent . reset ( this ) ; this . dispatchEvent ( addOrUpdateEvent ) ; } public menuData : { focusRoot ? : Menu ; selectionRoot ? : Menu ; } = { focusRoot : undefined , selectionRoot : undefined , } ; } declare global { interface GlobalEventHandlersEventMap { 'sp-menu-item-added-or-updated' : MenuItemAddedOrUpdatedEvent ; 'sp-menu-item-removed' : MenuItemRemovedEvent ; } }