/** * 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 { LitElement, CSSResultArray, TemplateResult } from 'lit'; import { styles } from './nile-option.css'; import '../nile-icon'; import { classMap } from 'lit/directives/class-map.js'; import { customElement, property, query, state } from 'lit/decorators.js'; import { html } from 'lit'; import { watch } from '../internal/watch'; import type { CSSResultGroup, PropertyValues } from 'lit'; import NileElement from '../internal/nile-element'; import '../nile-checkbox'; /** * Nile icon component. * * @tag nile-option * */ /** * @summary Options define the selectable items within various form controls such as [select](/components/select). * @status stable * * @dependency nile-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('nile-option') export class NileOption extends NileElement { static styles: CSSResultGroup = styles; private cachedTextLabel: string; @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 @property({ type: Boolean, reflect: true, attribute: true }) hidden = false; // the option is hidden @state() isMultipleSelect = false; /** * 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 = ''; @property({ type: Boolean, reflect: true, attribute: true }) showCheckbox: boolean = false; /** Draws the option in a disabled state, preventing selection. */ @property({ type: Boolean, reflect: true }) disabled = false; /** Indicates whether the option is selected. */ @property({ type: Boolean, reflect: true }) selected = false; /* used to pass group name to the option, so that grouped options can be shown */ @property({ type: String, reflect: true, attribute: true }) groupName: string = ''; /* used to pass description to the option, so that description can be shown */ @property({ type: String, reflect: true, attribute: true }) description: string = ''; @query('slot[name="prefix"]') prefixSlot!: HTMLSlotElement; @query('slot[name="suffix"]') suffixSlot!: HTMLSlotElement; @query('.option__label-container') labelContainer!: HTMLElement; @state() isParentVirtualSelect = false; @property({ type: Boolean, reflect: true, attribute: true }) isDescriptionEnabled = false; connectedCallback() { super.connectedCallback(); this.setAttribute('role', 'option'); this.setAttribute('aria-selected', 'false'); this.checkIfMultipleSelect(); } protected firstUpdated() { if (this.parentElement?.tagName === 'NILE-SELECT') { return; } this.isParentVirtualSelect = true; this.applyWidthToLabelContainer(); } applyWidthToLabelContainer() { const hasPrefix = (this.prefixSlot?.assignedNodes({ flatten: true }) ?? []).length > 0; const hasSuffix = (this.suffixSlot?.assignedNodes({ flatten: true }) ?? []).length > 0; let totalWidth = 0; if (hasPrefix) totalWidth += 30; if (hasSuffix) totalWidth += 30; if (this.showCheckbox) totalWidth += 50; if (this.labelContainer) { this.labelContainer.style.width = `calc(100% - ${totalWidth}px)`; } } checkIfMultipleSelect() { // Find the closest parent 'nile-select' element const parentSelect = this.closest('nile-select'); // Check if the parent has the 'multiple' attribute if (parentSelect && parentSelect.hasAttribute('multiple')) { if(parentSelect.hasAttribute('multiple') && parentSelect.getAttribute('multiple') === '') { this.isMultipleSelect = true; } else if (parentSelect.getAttribute('multiple') === 'true') { this.isMultipleSelect = true; } else { this.isMultipleSelect = false; } } else { this.isMultipleSelect = this.showCheckbox; } } protected updated(_changedProperties: PropertyValues): void { this.checkIfMultipleSelect(); } 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'); } } 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() { // Ensure the value is a string. This ensures the next line doesn't error and allows framework users to pass numbers // instead of requiring them to cast the value to a string. if (typeof this.value !== 'string') { this.value = String(this.value); } // 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() { // Search for a label element inside the component const labelElement = this.querySelector('label'); // If a label element is found, return its text content if (labelElement) { return labelElement.textContent?.trim() ?? ''; } // If no label element is found, return the existing behavior return (this.textContent ?? '').trim(); } render() { return html` ${!this.hidden ? html`
${this.isMultipleSelect ? html`` : ''} ${this.isDescriptionEnabled ? html`
${this.description ? html` ${this.description} ` : null}
` : html` `}
` : ''} `; } } export default NileOption; declare global { interface HTMLElementTagNameMap { 'nile-option': NileOption; } }