import { DeesElement, type TemplateResult, customElement, property, state, html, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; 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'; // Import required components import '../dees-icon.js'; import '../dees-windowcontrols.js'; import '../dees-appui-profiledropdown.js'; declare global { interface HTMLElementTagNameMap { 'dees-appui-appbar': DeesAppuiBar; } } @customElement('dees-appui-appbar') export class DeesAppuiBar extends DeesElement { 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; email?: string; avatar?: string; status?: 'online' | 'offline' | 'busy' | 'away'; }; @property({ type: Array }) public profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = []; @property({ type: Boolean }) public showSearch: boolean = false; // STATE @state() private activeMenu: string | null = null; @state() private openDropdowns: Set = new Set(); @state() private focusedItem: string | null = null; @state() private focusedDropdownItem: number = -1; @state() private isProfileDropdownOpen: boolean = false; public static styles = appuiAppbarStyles; // INSTANCE public render(): TemplateResult { return renderAppuiAppbar(this); } public renderMenuItems(): TemplateResult { 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``; } const menuItem = item as interfaces.IAppBarMenuItemRegular; const isActive = this.activeMenu === itemId; const hasSubmenu = menuItem.submenu && menuItem.submenu.length > 0; return html` `; } private renderDropdown(items: interfaces.IAppBarMenuItem[], parentId: string, isOpen: boolean): TemplateResult { return html` `; } private renderDropdownItem(item: interfaces.IAppBarMenuItem, itemId: string): TemplateResult { if ('divider' in item && item.divider) { return html``; } const menuItem = item as interfaces.IAppBarMenuItemRegular; const itemIndex = parseInt(itemId.split('-').pop() || '0'); const isFocused = this.focusedDropdownItem === itemIndex; return html` `; } public renderBreadcrumbs(): TemplateResult { if (!this.breadcrumbs) { return html``; } const parts = this.breadcrumbs.split(this.breadcrumbSeparator); return html` ${parts.map((part, index) => html` ${index > 0 ? html`${this.breadcrumbSeparator}` : ''} this.handleBreadcrumbClick(part, index)} > ${part} `)} `; } public renderAccountSection(): TemplateResult { return html` ${this.showSearch ? html` ` : ''} ${this.user ? html`
this.handleProfileMenuSelect(e)} >
` : ''} `; } // 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() { this.isProfileDropdownOpen = !this.isProfileDropdownOpen; // Also emit the event for backward compatibility this.dispatchEvent(new CustomEvent('user-menu-open', { bubbles: true, composed: true })); } 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 })); } // 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; // Note: Profile dropdown handles its own outside clicks } 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++; } } }