import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { afterNextRender, DestroyRef, inject, Injectable, Injector, PLATFORM_ID, signal, } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class CommandPaletteService { private readonly document = inject(DOCUMENT); private readonly destroyRef = inject(DestroyRef); private readonly injector = inject(Injector); private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); readonly open = signal(false); readonly dialogId = 'command-palette-dialog'; readonly titleId = 'command-palette-title'; readonly descriptionId = 'command-palette-description'; readonly inputId = 'command-palette-input'; readonly suggestionsId = 'command-palette-suggestions'; private opener: HTMLElement | null = null; private previousOverflow = ''; private scrollLocked = false; constructor() { afterNextRender( () => { if (!this.isBrowser) { return; } this.document.addEventListener('keydown', this.onDocumentKeydown); }, { injector: this.injector }, ); this.destroyRef.onDestroy(() => this.teardown()); } openPalette(opener?: HTMLElement | null): void { if (this.open()) { return; } const active = opener ?? this.document.activeElement; this.opener = active instanceof HTMLElement ? active : null; this.lockScroll(); this.open.set(true); } close(): void { if (!this.open()) { return; } this.open.set(false); this.unlockScroll(); this.restoreFocus(); } private restoreFocus(): void { const opener = this.opener; this.opener = null; afterNextRender( () => { if (opener instanceof HTMLElement && opener.isConnected) { opener.focus(); } }, { injector: this.injector }, ); } private lockScroll(): void { if (!this.isBrowser || this.scrollLocked) { return; } this.previousOverflow = this.document.body.style.overflow; this.document.body.style.overflow = 'hidden'; this.scrollLocked = true; } private unlockScroll(): void { if (!this.isBrowser || !this.scrollLocked) { return; } this.document.body.style.overflow = this.previousOverflow; this.previousOverflow = ''; this.scrollLocked = false; } private teardown(): void { this.document.removeEventListener('keydown', this.onDocumentKeydown); this.unlockScroll(); this.open.set(false); this.opener = null; } private readonly onDocumentKeydown = (event: KeyboardEvent): void => { if (event.key.toLowerCase() !== 'k' || !(event.ctrlKey || event.metaKey) || event.altKey) { return; } event.preventDefault(); this.openPalette(); }; }