import { ZuiFormAssociatedElement } from '@zywave/zui-base'; import { property, query } from 'lit/decorators.js'; import { html } from 'lit'; import { style } from './zui-select-css.js'; import { repeat } from 'lit/directives/repeat.js'; import { ZuiOptionElement } from './zui-option'; import '@zywave/zui-icons'; import type { PropertyValues } from 'lit'; /** * A Basic select, best for shorter declaratively created lists, by passing in ``'s. * @element zui-select * @attr {string | undefined} [placeholder=null] - Input placeholder (ghost) text * @attr {string | null} [name=null] - The name of this element that is associated with form submission * @attr {boolean} [disabled=false] - Represents whether a user can make changes to this element; if true, the value of this element will be excluded from the form submission * @attr {boolean} [readonly=false] - Represents whether a user can make changes to this element; the value of this element will still be included in the form submission * @attr {boolean} [autofocus=false] - If true, this element will be focused when connected to the document * * @prop {string | undefined} [placeholder=null] - Input placeholder (ghost) text * @prop {string | null} [name=null] - The name of this element that is associated with form submission * @prop {boolean} [disabled=false] - Represents whether a user can make changes to this element; if true, the value of this element will be excluded from the form submission * @prop {boolean} [readOnly=false] - Represents whether a user can make changes to this element; the value of this element will still be included in the form submission * @prop {boolean} [autofocus=false] - If true, this element will be focused when connected to the document * * @csspart control - For custom styling of the `select` element; this is exposed as a CSS shadow part and can be accessed with `::part(control)` * * @event {CustomEvent} change - Event fires when a change to select occurs, contains details about `value` chosen */ export class ZuiSelectElement extends ZuiFormAssociatedElement { get _focusControlSelector() { return 'select'; } get _formValue() { return this.value; } protected formResetCallback() { this.value = this.#resetValue; } /** * (deprecated): use zui-select-dropdown or other similar components */ @property({ type: Boolean }) multiple = false; /** * Placeholder value */ @property({ type: String }) placeholder: string | undefined = undefined; /** * (deprecated): use `value` instead */ @property({ type: String, reflect: true }) get selected(): string { return this.value; } set selected(val: string) { this.value = val; } /** * The currently selected item */ @property({ type: String }) value = ''; /** * Whether a selection is required */ @property({ type: Boolean }) required = false; @query('select') _select: HTMLSelectElement; #resetValue = ''; #zuiOptions: ZuiOptionElement[] = []; #nodeObserver: MutationObserver; static get styles() { return [super.styles, style]; } /** * Index, numerical value for selected option */ get selectedIndex(): number { return this._select?.selectedIndex ?? 0; } connectedCallback() { super.connectedCallback(); this.#setupMutationObserver(); let initValue: string | null = null; if (this.hasAttribute('value')) { initValue = this.getAttribute('value'); } else if (this.hasAttribute('placeholder')) { initValue = null; } else if (this.querySelector('zui-option[selected]')) { const selectedOption = this.querySelector('zui-option[selected]'); initValue = selectedOption!.getAttribute('value') ?? selectedOption!.textContent; } else if (this.querySelector('zui-option')) { const firstOption = this.querySelector('zui-option'); initValue = firstOption!.getAttribute('value') ?? firstOption!.textContent; } this.#resetValue = initValue ?? ''; this._setFormValue(initValue); } firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); this.#defaultValue(); } updated(changedProps: PropertyValues) { super.updated(changedProps); if (changedProps.has('value')) { this._setFormValue(this.value); // TODO(pat): how does multiple select function? if (!this.multiple) { this._select.value = this.value; } } } render() { this.#zuiOptions = Array.from(this.querySelectorAll('zui-option')); const placeholderOption = this.placeholder ? this.#getPlaceholderOption() : null; return html`
`; } /** * Return a `ZuiOptionElement` based upon number index value passed in * @param {number} index * @returns {ZuiOptionElement | null} */ item(index: number): ZuiOptionElement | null { return this.#zuiOptions?.[index] ?? null; } #getPlaceholderOption() { return html` `; } #setupMutationObserver() { this.#nodeObserver = new MutationObserver((mutationsList) => { for (const m of mutationsList) { if (m.type === 'childList') { this.requestUpdate(); } } }); this.#nodeObserver.observe(this, { childList: true }); } #defaultValue() { const nativeSelect = this.shadowRoot.querySelector('select'); this.value = nativeSelect.value; } #selectedUpdate(e: Event) { if (!this.multiple) { this.value = (e.target as HTMLSelectElement).value; } else { const options = Array.from((e.srcElement as HTMLSelectElement).options); const selectedValues = options.filter((option) => option.selected).map((option) => option.value); this.value = selectedValues.join(','); } e.preventDefault(); e.stopPropagation(); this._setFormValue(this.value); this.dispatchEvent(new CustomEvent('change', { detail: this.value, bubbles: true })); } #determineSelected(value: string) { if (this.multiple) { return this.value && this.value.split(',').includes(value); } else { return this.value === value; } } } window.customElements.define('zui-select', ZuiSelectElement); declare global { interface HTMLElementTagNameMap { 'zui-select': ZuiSelectElement; } }