| 
									
										
										
										
											2024-01-24 12:18:37 +01:00
										 |  |  | import { | 
					
						
							|  |  |  |   DeesElement, | 
					
						
							|  |  |  |   type TemplateResult, | 
					
						
							|  |  |  |   customElement, | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |   property, | 
					
						
							|  |  |  |   state, | 
					
						
							| 
									
										
										
										
											2024-01-24 12:18:37 +01:00
										 |  |  |   html, | 
					
						
							|  |  |  | } from '@design.estate/dees-element'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  | import * as domtools from '@design.estate/dees-domtools'; | 
					
						
							| 
									
										
										
										
											2025-09-19 15:26:21 +00:00
										 |  |  | import * as interfaces from '../interfaces/index.js'; | 
					
						
							|  |  |  | import * as plugins from '../00plugins.js'; | 
					
						
							|  |  |  | import { demoFunc } from './demo.js'; | 
					
						
							|  |  |  | import { appuiAppbarStyles } from './styles.js'; | 
					
						
							|  |  |  | import { renderAppuiAppbar } from './template.js'; | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | // Import required components
 | 
					
						
							| 
									
										
										
										
											2025-09-19 15:26:21 +00:00
										 |  |  | import '../dees-icon.js'; | 
					
						
							|  |  |  | import '../dees-windowcontrols.js'; | 
					
						
							|  |  |  | import '../dees-appui-profiledropdown.js'; | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | declare global { | 
					
						
							|  |  |  |   interface HTMLElementTagNameMap { | 
					
						
							|  |  |  |     'dees-appui-appbar': DeesAppuiBar; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-24 12:18:37 +01:00
										 |  |  | @customElement('dees-appui-appbar') | 
					
						
							|  |  |  | export class DeesAppuiBar extends DeesElement { | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |   public static demo = demoFunc; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // INSTANCE PROPERTIES
 | 
					
						
							|  |  |  |   @property({ type: Array }) | 
					
						
							|  |  |  |   public menuItems: interfaces.IAppBarMenuItem[] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property({ type: String }) | 
					
						
							|  |  |  |   public breadcrumbs: string = ''; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property({ type: String }) | 
					
						
							|  |  |  |   public breadcrumbSeparator: string = ' > '; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property({ type: Boolean }) | 
					
						
							|  |  |  |   public showWindowControls: boolean = true; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property({ type: Object }) | 
					
						
							|  |  |  |   public user?: { | 
					
						
							|  |  |  |     name: string; | 
					
						
							| 
									
										
										
										
											2025-06-17 09:55:28 +00:00
										 |  |  |     email?: string; | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |     avatar?: string; | 
					
						
							|  |  |  |     status?: 'online' | 'offline' | 'busy' | 'away'; | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-17 09:55:28 +00:00
										 |  |  |   @property({ type: Array }) | 
					
						
							|  |  |  |   public profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |   @property({ type: Boolean }) | 
					
						
							|  |  |  |   public showSearch: boolean = false; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // STATE
 | 
					
						
							|  |  |  |   @state() | 
					
						
							|  |  |  |   private activeMenu: string | null = null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @state() | 
					
						
							|  |  |  |   private openDropdowns: Set<string> = new Set(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @state() | 
					
						
							|  |  |  |   private focusedItem: string | null = null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @state() | 
					
						
							|  |  |  |   private focusedDropdownItem: number = -1; | 
					
						
							| 
									
										
										
										
											2024-01-24 12:18:37 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-17 09:55:28 +00:00
										 |  |  |   @state() | 
					
						
							|  |  |  |   private isProfileDropdownOpen: boolean = false; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-19 15:26:21 +00:00
										 |  |  |   public static styles = appuiAppbarStyles; | 
					
						
							| 
									
										
										
										
											2024-01-24 12:18:37 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // INSTANCE
 | 
					
						
							|  |  |  |   public render(): TemplateResult { | 
					
						
							| 
									
										
										
										
											2025-09-19 15:26:21 +00:00
										 |  |  |     return renderAppuiAppbar(this); | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-19 15:26:21 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public renderMenuItems(): TemplateResult { | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |     return html`
 | 
					
						
							|  |  |  |       ${this.menuItems.map((item, index) => this.renderMenuItem(item, `menu-${index}`))} | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private renderMenuItem(item: interfaces.IAppBarMenuItem, itemId: string): TemplateResult { | 
					
						
							|  |  |  |     if ('divider' in item && item.divider) { | 
					
						
							|  |  |  |       return html`<div class="dropdown-divider"></div>`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const menuItem = item as interfaces.IAppBarMenuItemRegular; | 
					
						
							|  |  |  |     const isActive = this.activeMenu === itemId; | 
					
						
							|  |  |  |     const hasSubmenu = menuItem.submenu && menuItem.submenu.length > 0; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return html`
 | 
					
						
							|  |  |  |       <div | 
					
						
							|  |  |  |         class="menuItem ${isActive ? 'active' : ''}" | 
					
						
							|  |  |  |         ?disabled=${menuItem.disabled} | 
					
						
							|  |  |  |         tabindex="${menuItem.disabled ? -1 : 0}" | 
					
						
							|  |  |  |         data-item-id="${itemId}" | 
					
						
							|  |  |  |         @click=${() => this.handleMenuClick(menuItem, itemId)} | 
					
						
							|  |  |  |         @keydown=${(e: KeyboardEvent) => this.handleMenuKeydown(e, menuItem, itemId)} | 
					
						
							|  |  |  |         role="menuitem" | 
					
						
							|  |  |  |         aria-haspopup="${hasSubmenu}" | 
					
						
							|  |  |  |         aria-expanded="${isActive}" | 
					
						
							|  |  |  |       > | 
					
						
							| 
									
										
										
										
											2025-06-17 08:41:36 +00:00
										 |  |  |         ${menuItem.iconName ? html`<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>` : ''} | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |         ${menuItem.name} | 
					
						
							|  |  |  |         ${hasSubmenu ? this.renderDropdown(menuItem.submenu, itemId, isActive) : ''} | 
					
						
							|  |  |  |       </div> | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private renderDropdown(items: interfaces.IAppBarMenuItem[], parentId: string, isOpen: boolean): TemplateResult { | 
					
						
							|  |  |  |     return html`
 | 
					
						
							|  |  |  |       <div  | 
					
						
							|  |  |  |         class="dropdown ${isOpen ? 'open' : ''}"  | 
					
						
							|  |  |  |         @click=${(e: Event) => e.stopPropagation()} | 
					
						
							|  |  |  |         @keydown=${(e: KeyboardEvent) => this.handleDropdownKeydown(e, items, parentId)} | 
					
						
							|  |  |  |         tabindex="${isOpen ? 0 : -1}" | 
					
						
							|  |  |  |         role="menu" | 
					
						
							|  |  |  |       > | 
					
						
							|  |  |  |         ${items.map((item, index) => this.renderDropdownItem(item, `${parentId}-${index}`))} | 
					
						
							| 
									
										
										
										
											2024-01-24 12:18:37 +01:00
										 |  |  |       </div> | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   private renderDropdownItem(item: interfaces.IAppBarMenuItem, itemId: string): TemplateResult { | 
					
						
							|  |  |  |     if ('divider' in item && item.divider) { | 
					
						
							|  |  |  |       return html`<div class="dropdown-divider"></div>`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const menuItem = item as interfaces.IAppBarMenuItemRegular; | 
					
						
							|  |  |  |     const itemIndex = parseInt(itemId.split('-').pop() || '0'); | 
					
						
							|  |  |  |     const isFocused = this.focusedDropdownItem === itemIndex; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     return html`
 | 
					
						
							|  |  |  |       <div | 
					
						
							|  |  |  |         class="dropdown-item ${isFocused ? 'focused' : ''}" | 
					
						
							|  |  |  |         ?disabled=${menuItem.disabled} | 
					
						
							|  |  |  |         @click=${() => this.handleDropdownItemClick(menuItem)} | 
					
						
							|  |  |  |         @mouseenter=${() => this.focusedDropdownItem = itemIndex} | 
					
						
							|  |  |  |         role="menuitem" | 
					
						
							|  |  |  |         tabindex="${menuItem.disabled ? -1 : 0}" | 
					
						
							|  |  |  |       > | 
					
						
							| 
									
										
										
										
											2025-06-17 08:41:36 +00:00
										 |  |  |         ${menuItem.iconName ? html`<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>` : ''} | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |         <span>${menuItem.name}</span> | 
					
						
							|  |  |  |         ${menuItem.shortcut ? html`<span class="shortcut">${menuItem.shortcut}</span>` : ''} | 
					
						
							|  |  |  |       </div> | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-19 15:26:21 +00:00
										 |  |  |   public renderBreadcrumbs(): TemplateResult { | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |     if (!this.breadcrumbs) { | 
					
						
							|  |  |  |       return html``; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const parts = this.breadcrumbs.split(this.breadcrumbSeparator); | 
					
						
							|  |  |  |     return html`
 | 
					
						
							|  |  |  |       ${parts.map((part, index) => html`
 | 
					
						
							|  |  |  |         ${index > 0 ? html`<span class="breadcrumb-separator">${this.breadcrumbSeparator}</span>` : ''} | 
					
						
							|  |  |  |         <span  | 
					
						
							|  |  |  |           class="breadcrumb-item"  | 
					
						
							|  |  |  |           @click=${() => this.handleBreadcrumbClick(part, index)} | 
					
						
							|  |  |  |         > | 
					
						
							|  |  |  |           ${part} | 
					
						
							|  |  |  |         </span> | 
					
						
							|  |  |  |       `)}
 | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-19 15:26:21 +00:00
										 |  |  |   public renderAccountSection(): TemplateResult { | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |     return html`
 | 
					
						
							|  |  |  |       ${this.showSearch ? html`
 | 
					
						
							|  |  |  |         <dees-icon  | 
					
						
							|  |  |  |           class="search-icon"  | 
					
						
							| 
									
										
										
										
											2025-06-17 09:55:28 +00:00
										 |  |  |           .icon=${'lucide:search'} | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |           @click=${this.handleSearchClick} | 
					
						
							|  |  |  |         ></dees-icon> | 
					
						
							|  |  |  |       ` : ''}
 | 
					
						
							|  |  |  |       ${this.user ? html`
 | 
					
						
							| 
									
										
										
										
											2025-06-17 09:55:28 +00:00
										 |  |  |         <div style="position: relative;"> | 
					
						
							|  |  |  |           <div class="user-info" @click=${this.handleUserClick}> | 
					
						
							|  |  |  |             <div class="user-avatar"> | 
					
						
							|  |  |  |               ${this.user.avatar ?  | 
					
						
							|  |  |  |                 html`<img src="${this.user.avatar}" alt="${this.user.name}">` :  | 
					
						
							|  |  |  |                 html`${this.user.name.charAt(0).toUpperCase()}` | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  |               ${this.user.status ? html`
 | 
					
						
							|  |  |  |                 <div class="user-status ${this.user.status}"></div> | 
					
						
							|  |  |  |               ` : ''}
 | 
					
						
							|  |  |  |             </div> | 
					
						
							|  |  |  |             <span>${this.user.name}</span> | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |           </div> | 
					
						
							| 
									
										
										
										
											2025-06-17 09:55:28 +00:00
										 |  |  |           <dees-appui-profiledropdown | 
					
						
							|  |  |  |             .user=${this.user} | 
					
						
							|  |  |  |             .menuItems=${this.profileMenuItems} | 
					
						
							|  |  |  |             .isOpen=${this.isProfileDropdownOpen} | 
					
						
							|  |  |  |             .position=${'top-right'} | 
					
						
							|  |  |  |             @menu-select=${(e: CustomEvent) => this.handleProfileMenuSelect(e)} | 
					
						
							|  |  |  |           ></dees-appui-profiledropdown> | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |         </div> | 
					
						
							|  |  |  |       ` : ''}
 | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Event handlers
 | 
					
						
							|  |  |  |   private handleMenuClick(item: interfaces.IAppBarMenuItemRegular, itemId: string) { | 
					
						
							|  |  |  |     if (item.disabled) return; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (item.submenu && item.submenu.length > 0) { | 
					
						
							|  |  |  |       // Toggle dropdown
 | 
					
						
							|  |  |  |       if (this.activeMenu === itemId) { | 
					
						
							|  |  |  |         this.activeMenu = null; | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         this.activeMenu = itemId; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       // Execute action
 | 
					
						
							|  |  |  |       this.activeMenu = null; | 
					
						
							|  |  |  |       if (item.action) { | 
					
						
							|  |  |  |         item.action(); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       this.dispatchEvent(new CustomEvent('menu-select', {  | 
					
						
							|  |  |  |         detail: { item }, | 
					
						
							|  |  |  |         bubbles: true, | 
					
						
							|  |  |  |         composed: true  | 
					
						
							|  |  |  |       })); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private handleDropdownItemClick(item: interfaces.IAppBarMenuItemRegular) { | 
					
						
							|  |  |  |     if (item.disabled) return; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     this.activeMenu = null; | 
					
						
							|  |  |  |     if (item.action) { | 
					
						
							|  |  |  |       item.action(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     this.dispatchEvent(new CustomEvent('menu-select', {  | 
					
						
							|  |  |  |       detail: { item }, | 
					
						
							|  |  |  |       bubbles: true, | 
					
						
							|  |  |  |       composed: true  | 
					
						
							|  |  |  |     })); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private handleMenuKeydown(e: KeyboardEvent, item: interfaces.IAppBarMenuItemRegular, itemId: string) { | 
					
						
							|  |  |  |     switch (e.key) { | 
					
						
							|  |  |  |       case 'Enter': | 
					
						
							|  |  |  |       case ' ': | 
					
						
							|  |  |  |         e.preventDefault(); | 
					
						
							|  |  |  |         this.handleMenuClick(item, itemId); | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'ArrowDown': | 
					
						
							|  |  |  |         if (item.submenu && this.activeMenu === itemId) { | 
					
						
							|  |  |  |           e.preventDefault(); | 
					
						
							|  |  |  |           // Focus first non-disabled item in dropdown
 | 
					
						
							|  |  |  |           this.focusedDropdownItem = 0; | 
					
						
							|  |  |  |           const firstValidItem = this.findNextValidItem(item.submenu, -1, 1); | 
					
						
							|  |  |  |           if (firstValidItem !== -1) { | 
					
						
							|  |  |  |             this.focusedDropdownItem = firstValidItem; | 
					
						
							|  |  |  |             // Focus the dropdown element
 | 
					
						
							|  |  |  |             setTimeout(() => { | 
					
						
							|  |  |  |               const dropdown = this.renderRoot.querySelector('.dropdown.open'); | 
					
						
							|  |  |  |               if (dropdown) { | 
					
						
							|  |  |  |                 (dropdown as HTMLElement).focus(); | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  |             }, 0); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'Escape': | 
					
						
							|  |  |  |         this.activeMenu = null; | 
					
						
							|  |  |  |         this.focusedDropdownItem = -1; | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'Tab': | 
					
						
							|  |  |  |         // Let default tab navigation work but close dropdown
 | 
					
						
							|  |  |  |         if (this.activeMenu === itemId) { | 
					
						
							|  |  |  |           this.activeMenu = null; | 
					
						
							|  |  |  |           this.focusedDropdownItem = -1; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'ArrowRight': | 
					
						
							|  |  |  |         e.preventDefault(); | 
					
						
							|  |  |  |         this.focusNextMenuItem(itemId, 1); | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'ArrowLeft': | 
					
						
							|  |  |  |         e.preventDefault(); | 
					
						
							|  |  |  |         this.focusNextMenuItem(itemId, -1); | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private handleBreadcrumbClick(breadcrumb: string, index: number) { | 
					
						
							|  |  |  |     this.dispatchEvent(new CustomEvent('breadcrumb-navigate', {  | 
					
						
							|  |  |  |       detail: { breadcrumb, index }, | 
					
						
							|  |  |  |       bubbles: true, | 
					
						
							|  |  |  |       composed: true  | 
					
						
							|  |  |  |     })); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private handleSearchClick() { | 
					
						
							|  |  |  |     this.dispatchEvent(new CustomEvent('search-click', {  | 
					
						
							|  |  |  |       bubbles: true, | 
					
						
							|  |  |  |       composed: true  | 
					
						
							|  |  |  |     })); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private handleUserClick() { | 
					
						
							| 
									
										
										
										
											2025-06-17 09:55:28 +00:00
										 |  |  |     this.isProfileDropdownOpen = !this.isProfileDropdownOpen; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Also emit the event for backward compatibility
 | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |     this.dispatchEvent(new CustomEvent('user-menu-open', {  | 
					
						
							|  |  |  |       bubbles: true, | 
					
						
							|  |  |  |       composed: true  | 
					
						
							|  |  |  |     })); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-17 09:55:28 +00:00
										 |  |  |   private handleProfileMenuSelect(e: CustomEvent) { | 
					
						
							|  |  |  |     this.isProfileDropdownOpen = false; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Re-emit the event
 | 
					
						
							|  |  |  |     this.dispatchEvent(new CustomEvent('profile-menu-select', {  | 
					
						
							|  |  |  |       detail: e.detail, | 
					
						
							|  |  |  |       bubbles: true, | 
					
						
							|  |  |  |       composed: true  | 
					
						
							|  |  |  |     })); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |   // Lifecycle
 | 
					
						
							|  |  |  |   async connectedCallback() { | 
					
						
							|  |  |  |     await super.connectedCallback(); | 
					
						
							|  |  |  |     // Add global click listener to close dropdowns
 | 
					
						
							|  |  |  |     this.addEventListener('click', this.handleGlobalClick); | 
					
						
							|  |  |  |     document.addEventListener('click', this.handleDocumentClick); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async disconnectedCallback() { | 
					
						
							|  |  |  |     await super.disconnectedCallback(); | 
					
						
							|  |  |  |     document.removeEventListener('click', this.handleDocumentClick); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private handleGlobalClick = (e: Event) => { | 
					
						
							|  |  |  |     // Prevent closing when clicking inside
 | 
					
						
							|  |  |  |     e.stopPropagation(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private handleDocumentClick = () => { | 
					
						
							|  |  |  |     // Close all dropdowns when clicking outside
 | 
					
						
							|  |  |  |     this.activeMenu = null; | 
					
						
							|  |  |  |     this.focusedDropdownItem = -1; | 
					
						
							| 
									
										
										
										
											2025-06-17 09:55:28 +00:00
										 |  |  |     // Note: Profile dropdown handles its own outside clicks
 | 
					
						
							| 
									
										
										
										
											2025-06-16 23:16:25 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private handleDropdownKeydown(e: KeyboardEvent, items: interfaces.IAppBarMenuItem[], _parentId: string) { | 
					
						
							|  |  |  |     const validItems = items.filter(item => !('divider' in item && item.divider)); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     switch (e.key) { | 
					
						
							|  |  |  |       case 'ArrowDown': | 
					
						
							|  |  |  |         e.preventDefault(); | 
					
						
							|  |  |  |         const nextIndex = this.findNextValidItem(items, this.focusedDropdownItem, 1); | 
					
						
							|  |  |  |         if (nextIndex !== -1) { | 
					
						
							|  |  |  |           this.focusedDropdownItem = nextIndex; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'ArrowUp': | 
					
						
							|  |  |  |         e.preventDefault(); | 
					
						
							|  |  |  |         const prevIndex = this.findNextValidItem(items, this.focusedDropdownItem, -1); | 
					
						
							|  |  |  |         if (prevIndex !== -1) { | 
					
						
							|  |  |  |           this.focusedDropdownItem = prevIndex; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'Enter': | 
					
						
							|  |  |  |         e.preventDefault(); | 
					
						
							|  |  |  |         if (this.focusedDropdownItem !== -1) { | 
					
						
							|  |  |  |           const focusedItem = validItems[this.focusedDropdownItem]; | 
					
						
							|  |  |  |           if (focusedItem && 'action' in focusedItem && !focusedItem.disabled) { | 
					
						
							|  |  |  |             this.handleDropdownItemClick(focusedItem as interfaces.IAppBarMenuItemRegular); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'Home': | 
					
						
							|  |  |  |         e.preventDefault(); | 
					
						
							|  |  |  |         const firstIndex = this.findNextValidItem(items, -1, 1); | 
					
						
							|  |  |  |         if (firstIndex !== -1) { | 
					
						
							|  |  |  |           this.focusedDropdownItem = firstIndex; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'End': | 
					
						
							|  |  |  |         e.preventDefault(); | 
					
						
							|  |  |  |         const lastIndex = this.findNextValidItem(items, items.length, -1); | 
					
						
							|  |  |  |         if (lastIndex !== -1) { | 
					
						
							|  |  |  |           this.focusedDropdownItem = lastIndex; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'Escape': | 
					
						
							|  |  |  |         e.preventDefault(); | 
					
						
							|  |  |  |         this.activeMenu = null; | 
					
						
							|  |  |  |         this.focusedDropdownItem = -1; | 
					
						
							|  |  |  |         // Return focus to menu item
 | 
					
						
							|  |  |  |         const menuItem = this.renderRoot.querySelector(`.menuItem.active`); | 
					
						
							|  |  |  |         if (menuItem) { | 
					
						
							|  |  |  |           (menuItem as HTMLElement).focus(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private findNextValidItem(items: interfaces.IAppBarMenuItem[], currentIndex: number, direction: number): number { | 
					
						
							|  |  |  |     let index = currentIndex + direction; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     while (index >= 0 && index < items.length) { | 
					
						
							|  |  |  |       const item = items[index]; | 
					
						
							|  |  |  |       // Skip dividers and disabled items
 | 
					
						
							|  |  |  |       if (!('divider' in item && item.divider) && !('disabled' in item && item.disabled)) { | 
					
						
							|  |  |  |         return index; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       index += direction; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     return -1; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private focusNextMenuItem(currentItemId: string, direction: number) { | 
					
						
							|  |  |  |     const menuItems = Array.from(this.renderRoot.querySelectorAll('.menuItem')); | 
					
						
							|  |  |  |     const currentIndex = menuItems.findIndex(item => item.getAttribute('data-item-id') === currentItemId); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     if (currentIndex === -1) return; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     let nextIndex = currentIndex + direction; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Wrap around
 | 
					
						
							|  |  |  |     if (nextIndex < 0) { | 
					
						
							|  |  |  |       nextIndex = menuItems.length - 1; | 
					
						
							|  |  |  |     } else if (nextIndex >= menuItems.length) { | 
					
						
							|  |  |  |       nextIndex = 0; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Find next non-disabled item
 | 
					
						
							|  |  |  |     let attempts = 0; | 
					
						
							|  |  |  |     while (attempts < menuItems.length) { | 
					
						
							|  |  |  |       const nextItem = menuItems[nextIndex] as HTMLElement; | 
					
						
							|  |  |  |       if (!nextItem.hasAttribute('disabled')) { | 
					
						
							|  |  |  |         nextItem.focus(); | 
					
						
							|  |  |  |         // Close current dropdown if open
 | 
					
						
							|  |  |  |         if (this.activeMenu) { | 
					
						
							|  |  |  |           this.activeMenu = null; | 
					
						
							|  |  |  |           this.focusedDropdownItem = -1; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       nextIndex = (nextIndex + direction + menuItems.length) % menuItems.length; | 
					
						
							|  |  |  |       attempts++; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-01-24 12:18:37 +01:00
										 |  |  | } |