import { html, css, TemplateResult, CSSResultArray } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import NileElement from '../../internal/nile-element'; export interface SuggestItem { key: string; label: string; /** Secondary text shown right-aligned (e.g. slash-command hint). */ hint?: string; } /** * Keyboard-navigable suggestion popup shared by mentions and slash commands. * The host positions it, feeds `items`, and drives navigation through * next()/prev()/selectCurrent(); picking an item emits * `wysiwyg-suggest-select` with the item. */ @customElement('nile-wysiwyg-suggest-list') export class NileWysiwygSuggestList extends NileElement { @property({ type: Boolean, reflect: true }) open = false; @property({ attribute: false }) items: SuggestItem[] = []; @state() private activeIndex = 0; public static get styles(): CSSResultArray { return [ css` :host { font-family: var( --nile-font-family-sans-serif, var(--ng-font-family-body, system-ui, -apple-system, sans-serif) ); position: absolute; z-index: 25; display: none; } :host([open]) { display: block; } .list { min-width: 200px; max-width: 320px; max-height: 240px; overflow-y: auto; padding: 4px; background: var(--nile-wysiwyg-popover-bg, #ffffff); border: 1px solid var(--nile-colors-neutral-400, #d0d5dd); border-radius: 8px; box-shadow: var(--nile-wysiwyg-popover-shadow, 0 4px 12px rgba(16, 24, 40, 0.12)); font-size: 13px; } .item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 8px; border-radius: 5px; cursor: pointer; color: var(--nile-colors-neutral-700, #344054); } .item.active { background: var(--nile-colors-primary-50, var(--ng-componentcolors-utility-blue-50, #eff4ff)); color: var(--nile-colors-primary-700, var(--ng-componentcolors-utility-blue-700, #1d4ed8)); } .hint { font-size: 11px; color: var(--nile-colors-neutral-500, #98a2b3); } .empty { padding: 6px 8px; color: var(--nile-colors-neutral-500, #98a2b3); } `, ]; } protected willUpdate(): void { if (this.activeIndex >= this.items.length) this.activeIndex = 0; } protected updated(changed: Map): void { // Keep the highlighted item visible as the user arrows through a list // that overflows the popup's max-height. `nearest` is a no-op when the // item is already visible (e.g. after a hover), so it never fights the mouse. if (changed.has('activeIndex') || changed.has('items')) { const active = this.renderRoot.querySelector('.item.active'); (active as HTMLElement | null)?.scrollIntoView({ block: 'nearest' }); } } public next(): void { if (this.items.length) this.activeIndex = (this.activeIndex + 1) % this.items.length; } public prev(): void { if (this.items.length) { this.activeIndex = (this.activeIndex - 1 + this.items.length) % this.items.length; } } public resetActive(): void { this.activeIndex = 0; } public selectCurrent(): boolean { const item = this.items[this.activeIndex]; if (!item) return false; this.emit('wysiwyg-suggest-select', { item }); return true; } public render(): TemplateResult { return html`
e.preventDefault()}> ${this.items.length ? this.items.map( (item, index) => html`
{ this.activeIndex = index; }} @click=${() => this.selectCurrent()} > ${item.label} ${item.hint ? html`${item.hint}` : ''}
` ) : html`
No results
`}
`; } } declare global { interface HTMLElementTagNameMap { 'nile-wysiwyg-suggest-list': NileWysiwygSuggestList; } }