/** * Copyright Aquera Inc 2023 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ import { html, nothing } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { styles } from './nile-command-menu-item.css'; import { getTextContent } from '../internal/slot'; import { watch } from '../internal/watch'; import NileElement from '../internal/nile-element'; import type { CSSResultGroup } from 'lit'; /** * An individual selectable command inside a `nile-command-menu`. * * @tag nile-command-menu-item * * @summary Represents a single command/action entry within a command menu. * @status stable * @since 2.0 * * @slot - The item's label. * @slot icon - Used to prepend an icon (or similar element) before the label. * @slot suffix - Used to append content (e.g. a hint) after the label, before the shortcut. * * @csspart base - The component's base wrapper. * @csspart icon - The leading icon container. * @csspart label - The item label. * @csspart suffix - The trailing suffix container. * @csspart shortcut - The keyboard-shortcut container. */ @customElement('nile-command-menu-item') export class NileCommandMenuItem extends NileElement { static styles: CSSResultGroup = styles; private cachedTextLabel: string; @query('slot:not([name])') defaultSlot: HTMLSlotElement; /** Controls vertical padding of the item. */ @property({ reflect: true }) size: 'small' | 'medium' = 'small'; /** A unique value used to identify the item when it is selected. */ @property() value = ''; /** Draws the item in a disabled state, preventing selection. */ @property({ type: Boolean, reflect: true }) disabled = false; /** Draws the item in an active/highlighted state. Managed by the parent menu for keyboard navigation. */ @property({ type: Boolean, reflect: true }) active = false; /** * Extra keywords used by the parent menu when filtering, in addition to the visible label. * Useful for matching synonyms or abbreviations. */ @property() keywords = ''; /** * The search scope this item belongs to. When the menu has active scopes (the "Searching for" row), * an item is shown only if it has no scope or its scope is among the active ones. */ @property() scope = ''; /** * A keyboard shortcut hint shown on the trailing edge of the item. Separate keys with a * space to render each one in its own key box (e.g. `"⌘ K"`). */ @property() shortcut = ''; connectedCallback() { super.connectedCallback(); this.setAttribute('role', 'option'); this.handleHostClick = this.handleHostClick.bind(this); this.addEventListener('click', this.handleHostClick); } disconnectedCallback() { super.disconnectedCallback(); this.removeEventListener('click', this.handleHostClick); } private handleHostClick(event: MouseEvent) { // Swallow clicks while disabled so the menu never selects a disabled item. if (this.disabled) { event.preventDefault(); event.stopImmediatePropagation(); } } private handleDefaultSlotChange() { const textLabel = this.getTextLabel(); if (typeof this.cachedTextLabel === 'undefined') { this.cachedTextLabel = textLabel; return; } if (textLabel !== this.cachedTextLabel) { this.cachedTextLabel = textLabel; this.emit('slotchange'); } } @watch('disabled') handleDisabledChange() { this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } @watch('active') handleActiveChange() { this.setAttribute('aria-selected', this.active ? 'true' : 'false'); } /** Returns the plain-text label based on the default slot's content. */ getTextLabel() { return getTextContent(this.defaultSlot); } /** Returns the string the parent menu uses to match this item against a search query. */ getFilterText() { return `${this.getTextLabel()} ${this.keywords} ${this.value}`.toLowerCase(); } private renderShortcut() { if (!this.shortcut) { return nothing; } const keys = this.shortcut.trim().split(/\s+/); return html` ${keys.map(key => html`${key}`)} `; } render() { return html`