import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
cssManager,
css,
unsafeCSS,
state,
} from '@design.estate/dees-element';
// Import design tokens
import { colors, bdTheme } from './00colors.js';
import { spacing, radius, shadows, transitions } from './00tokens.js';
import { fontFamilies } from './00fonts.js';
export interface IDropdownMenuItem {
id: string;
label: string;
icon?: string;
divider?: boolean;
destructive?: boolean;
disabled?: boolean;
}
declare global {
interface HTMLElementTagNameMap {
'sio-dropdown-menu': SioDropdownMenu;
}
}
@customElement('sio-dropdown-menu')
export class SioDropdownMenu extends DeesElement {
public static demo = () => html`
`;
@property({ type: Array })
public items: IDropdownMenuItem[] = [];
@property({ type: String })
public align: 'left' | 'right' = 'right';
@state()
private isOpen: boolean = false;
private documentClickHandler: (e: MouseEvent) => void;
private scrollHandler: () => void;
private resizeHandler: () => void;
public static styles = [
cssManager.defaultStyles,
css`
:host {
position: relative;
display: inline-block;
font-family: ${unsafeCSS(fontFamilies.sans)};
}
.trigger {
cursor: pointer;
}
.dropdown {
position: absolute;
top: calc(100% + 10px);
right: 0;
min-width: 200px;
background: ${bdTheme('background')};
border: 1px solid ${bdTheme('border')};
border-radius: ${unsafeCSS(radius.lg)};
box-shadow: ${unsafeCSS(shadows.lg)};
overflow: hidden;
z-index: 100000;
opacity: 0;
transform: translateY(-10px) scale(0.95);
pointer-events: none;
transition: all 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
transform-origin: top right;
}
.dropdown.align-left {
right: auto;
left: 0;
transform-origin: top left;
}
.dropdown.open {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: all;
}
.menu-item {
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing["3"])};
padding: ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing["3"])};
font-size: 0.875rem;
line-height: 1.5;
color: ${bdTheme('foreground')};
cursor: pointer;
transition: ${unsafeCSS(transitions.all)};
user-select: none;
border: none;
background: none;
width: 100%;
text-align: left;
}
.menu-item:hover:not(.disabled) {
background: ${bdTheme('accent')};
}
.menu-item:active:not(.disabled) {
transform: scale(0.98);
}
.menu-item.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.menu-item.destructive {
color: ${bdTheme('destructive')};
}
.menu-item.destructive:hover:not(.disabled) {
background: ${bdTheme('destructive')}10;
}
.menu-icon {
flex-shrink: 0;
color: ${bdTheme('mutedForeground')};
}
.menu-item.destructive .menu-icon {
color: ${bdTheme('destructive')};
}
.menu-label {
flex: 1;
}
.divider {
height: 1px;
background: ${bdTheme('border')};
margin: ${unsafeCSS(spacing["1"])} 0;
}
/* Mobile adjustments */
@media (max-width: 600px) {
.dropdown {
position: fixed;
top: auto !important;
left: ${unsafeCSS(spacing["4"])} !important;
right: ${unsafeCSS(spacing["4"])};
bottom: ${unsafeCSS(spacing["4"])};
width: auto;
transform-origin: bottom center;
}
.dropdown.open {
transform: translateY(0) scale(1);
}
.dropdown:not(.open) {
transform: translateY(10px) scale(0.95);
}
}
`,
];
public render(): TemplateResult {
return html`
${this.items.map(item =>
item.divider ? html`
` : html`
`
)}
`;
}
private toggleDropdown = (e: Event) => {
console.log('[Dropdown] Toggle called, current state:', this.isOpen);
e.preventDefault();
e.stopPropagation();
this.isOpen = !this.isOpen;
console.log('[Dropdown] New state:', this.isOpen);
if (this.isOpen) {
this.addDocumentListener();
} else {
this.removeDocumentListener();
}
}
private updateDropdownPosition() {
// For absolute positioning, we don't need to calculate position dynamically
// The CSS handles it with top: calc(100% + 10px) and right: 0
console.log('[Dropdown] Position is handled by CSS (absolute positioning)');
}
private handleItemClick(item: IDropdownMenuItem) {
if (item.disabled || item.divider) return;
this.isOpen = false;
this.removeDocumentListener();
// Dispatch custom event with the selected item
this.dispatchEvent(new CustomEvent('item-selected', {
detail: { item },
bubbles: true,
composed: true
}));
}
private addDocumentListener() {
// Close dropdown when clicking outside
this.documentClickHandler = (e: MouseEvent) => {
const path = e.composedPath();
if (!path.includes(this)) {
this.isOpen = false;
this.removeDocumentListener();
}
};
// Update position on scroll/resize
this.scrollHandler = () => this.updateDropdownPosition();
this.resizeHandler = () => {
this.updateDropdownPosition();
if (window.innerWidth <= 600) {
// Close on mobile resize to prevent positioning issues
this.isOpen = false;
this.removeDocumentListener();
}
};
// Delay to avoid immediate closing
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler);
window.addEventListener('scroll', this.scrollHandler, true);
window.addEventListener('resize', this.resizeHandler);
}, 0);
}
private removeDocumentListener() {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
}
if (this.scrollHandler) {
window.removeEventListener('scroll', this.scrollHandler, true);
}
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler);
}
}
public async disconnectedCallback() {
await super.disconnectedCallback();
this.removeDocumentListener();
}
public close() {
this.isOpen = false;
this.removeDocumentListener();
}
}