import '../icon/icon'; import { classMap } from 'lit/directives/class-map.js'; import { customElement, property, query, state } from 'lit/decorators.js'; import { html } from 'lit'; import { LocalizeController } from '../../utilities/localize'; import { watch } from '../../internal/watch'; import ShoelaceElement from '../../internal/shoelace-element'; import styles from './option.styles'; import type { CSSResultGroup } from 'lit'; /** * @summary Options define the selectable items within various form controls such as [select](/components/select). * @documentation https://shoelace.style/components/option * @status stable * @since 2.0 * * @dependency sl-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-icon - The checked 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. */ @customElement('sl-option') export default class SlOption extends ShoelaceElement { static styles: CSSResultGroup = styles; private cachedTextLabel: string; // @ts-expect-error - Controller is currently unused private readonly localize = new LocalizeController(this); @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() selected = false; // the option is selected and has aria-selected="true" @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 not contain spaces, as spaces are used as delimiters when listing * multiple values. */ @property({ reflect: true }) value = ''; /** Draws the option in a disabled state, preventing selection. */ @property({ type: Boolean, reflect: true }) disabled = 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'); } @watch('value') handleValueChange() { if (this.value.includes(' ')) { console.error(`Option values cannot include a space. All spaces have been replaced with underscores.`, this); this.value = this.value.replace(/ /g, '_'); } } /** Returns a plain text label based on the option's content. */ getTextLabel() { return (this.textContent ?? '').trim(); } render() { return html`
`; } } declare global { interface HTMLElementTagNameMap { 'sl-option': SlOption; } }