import { classMap } from "lit/directives/class-map.js"; import { type CSSResultGroup, html, unsafeCSS } from 'lit'; import { LocalizeController } from "../../utilities/localize"; import { property, query, state } from 'lit/decorators.js'; import { watch } from '../../internal/watch'; import ZincElement from '../../internal/zinc-element'; import ZnIcon from "../icon"; import styles from './option.scss'; /** * @summary Short summary of the component's intended use. * @documentation https://zinc.style/components/option * @status experimental * @since 1.0 * * @dependency zn-icon * * @slot - The option's label. * @slot prefix - Used to prepend an icon or similar element to the menu item. * @slot suffix - Used to append an icon or similar element to the menu item. * * @csspart checked-option-icon - The checked option icon, an `` element. * @csspart base - The component's base wrapper. * @csspart label - The option's label. * @csspart prefix - The container that wraps the prefix. * @csspart suffix - The container that wraps the suffix. */ export default class ZnOption extends ZincElement { static styles: CSSResultGroup = unsafeCSS(styles); static dependencies = { 'zn-icon': ZnIcon }; private cachedTextLabel: string; // @ts-expect-error - Controller is currently unused private readonly localize = new LocalizeController(this); public multiple = false; @query('.option__label') defaultSlot: HTMLSlotElement; @state() current = false; // the user has keyed into the option, but hasn't selected it yet (shows a highlight) @state() hasHover = false; // we need this because Safari doesn't honor :hover styles while dragging /** * The option's value. When selected, the containing form control will receive this value. The value must be unique * from other options in the same group. Values may contain spaces when using JSON array syntax on the parent select. */ @property({ reflect: true }) value = ''; /** Draws the option in a disabled state, preventing selection. */ @property({ type: Boolean, reflect: true }) disabled = false; @property({ type: Boolean, reflect: true }) selected = false; connectedCallback() { super.connectedCallback(); this.setAttribute('role', 'option'); this.setAttribute('aria-selected', 'false'); } private handleDefaultSlotChange() { const textLabel = this.getTextLabel(); // Ignore the first time the label is set if (typeof this.cachedTextLabel === 'undefined') { this.cachedTextLabel = textLabel; return; } // When the label changes, emit a slotchange event so parent controls see it if (textLabel !== this.cachedTextLabel) { this.cachedTextLabel = textLabel; this.emit('slotchange', { bubbles: true, composed: false, cancelable: false }); } } private handleMouseEnter() { this.hasHover = true; } private handleMouseLeave() { this.hasHover = false; } @watch('disabled') handleDisabledChange() { this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } @watch('selected') handleSelectedChange() { this.setAttribute('aria-selected', this.selected ? 'true' : 'false'); } /** Returns a plain text label based on the option's content. */ getTextLabel() { const nodes = this.childNodes; let label = ''; [...nodes].forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (!(node as HTMLElement).hasAttribute('slot')) { label += (node as HTMLElement).textContent; } } if (node.nodeType === Node.TEXT_NODE) { label += node.textContent; } }); return label.trim(); } render() { return html`
`; } }