UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

257 lines (233 loc) 8.72 kB
import { html, PropertyValues } from 'lit' import { customElement, property, state } from 'lit/decorators.js' import { Ref, createRef, ref } from 'lit/directives/ref.js' import { ifDefined } from 'lit/directives/if-defined.js' import { PktInputElement } from '@/base-elements/input-element' import { PktOptionsSlotController } from '@/controllers/pkt-options-controller' import { PktSlotController } from '@/controllers/pkt-slot-controller' import '@/components/input-wrapper' export type TSelectOption = { value: string label: string selected?: boolean disabled?: boolean hidden?: boolean } export interface IPktSelect { options: TSelectOption[] value: string } /** * Pkt Select is a wrapper for the native select element using the pkt-input-wrapper component. * * The component will prioritize options passed as a prop over options passed as children if both are provided. * This is to allow for dynamic options that might change in the case of both children/slot and props are provided. * * @slot (default) - Options to be rendered as children * @prop {TSelectOption[]} options - Options to be rendered as children * * */ declare global { interface HTMLElementTagNameMap { 'pkt-select': PktSelect & HTMLSelectElement } } @customElement('pkt-select') export class PktSelect extends PktInputElement implements IPktSelect { private inputRef: Ref<HTMLSelectElement> = createRef() private helptextSlot: Ref<HTMLElement> = createRef() @property({ type: Array }) options: TSelectOption[] = [] @property({ type: String }) value: string = '' @state() private _options: TSelectOption[] = [] public selectedIndex: number | null = -1 public selectedOptions: HTMLCollectionOf<HTMLOptionElement> | undefined = undefined constructor() { super() this.optionsController = new PktOptionsSlotController(this) this.slotController = new PktSlotController(this, this.helptextSlot) this.slotController.skipOptions = true } // Used for initilization connectedCallback(): void { super.connectedCallback() const optionsReceivedFromProps = this.options.length > 0 const checkIfOptionNodesInSlot = this.optionsController.nodes.length && this.optionsController.nodes.length > 0 if (!optionsReceivedFromProps && checkIfOptionNodesInSlot) { this.optionsController.nodes.forEach((node: Element) => { const option: TSelectOption = { value: node.hasAttribute('value') ? (node.getAttribute('value') ?? '') : (node.textContent ?? ''), label: node.textContent || node.getAttribute('value') || '', disabled: node.hasAttribute('disabled'), hidden: node.hasAttribute('data-hidden'), } if (node.getAttribute('selected') && !this.value) { this.value = option.value } this._options.push(option) }) } else { this._options = this.options this._options.forEach((option) => { if (option.selected && !this.value) { this.value = option.value } }) } } // Support native Select method `add` public add(item: HTMLOptionElement, before?: HTMLOptionElement | number) { this.inputRef.value?.add(item, before) this._options.push({ value: item.value || item.text, label: item.text || item.value, selected: item.selected, disabled: item.disabled, }) if (item.selected) { this.value = item.value || item.text this.selectedIndex = this.returnNumberOrNull(this.inputRef.value?.selectedIndex) this.selectedOptions = this.inputRef.value?.selectedOptions } this.requestUpdate() } // Support native Select method `remove` public remove(item?: number) { if (typeof item === 'number') { if (this.selectedIndex === item) { this.value = this._options[0]?.value || '' } this.inputRef.value?.remove(item) } } // Support native Select method `item` public item(index: number) { return this.inputRef.value?.item(index) } // Support native Select method `namedItem` public namedItem(name: string) { return this.inputRef.value?.namedItem(name) } // Support native Select method `showPicker` public showPicker() { this.inputRef.value?.showPicker() } public attributeChangedCallback(name: string, _old: string, value: string): void { if (name === 'options') { this._options = value ? JSON.parse(value) : [] } if (name === 'value' && this.value !== _old) { this.selectedIndex = this.touched ? this.returnNumberOrNull(this.inputRef.value?.selectedIndex) : this.options.findIndex((option) => option.value === value) this.selectedOptions = this.inputRef.value?.selectedOptions this.valueChanged(value, _old) } super.attributeChangedCallback(name, _old, value) } update(changedProperties: PropertyValues) { super.update(changedProperties) if (changedProperties.has('value') && this.value !== changedProperties.get('value')) { this.selectedIndex = this.touched ? this.returnNumberOrNull(this.inputRef.value?.selectedIndex) : this.options.findIndex((option) => option.value === this.value) this.selectedOptions = this.inputRef.value?.selectedOptions this.valueChanged(this.value, changedProperties.get('value')) } if (changedProperties.has('id')) { !this.name && this.id && (this.name = this.id) } } protected firstUpdated(_changedProperties: PropertyValues): void { super.firstUpdated(_changedProperties) if (this.options.length) { this._options = this.options } // If no options are selected, set value and selectedIndex to the first option if (!this.value && this._options.length > 0) { this.value = this._options[0].value this.selectedIndex = 0 } else { this.selectedIndex = this._options.findIndex((option) => option.value === this.value) } this.selectedOptions = this.inputRef.value?.selectedOptions } render() { const inputClass = `pkt-input ${this.fullwidth ? 'pkt-input--fullwidth' : ''}` return html` <pkt-input-wrapper ?counter=${this.counter} ?disabled=${this.disabled} ?hasError=${this.hasError} ?hasFieldset=${this.hasFieldset} ?inline=${this.inline} ?optionalTag=${this.optionalTag} ?requiredTag=${this.requiredTag} ?useWrapper=${this.useWrapper} ariaDescribedBy=${ifDefined(this.ariaDescribedBy)} class="pkt-select" errorMessage=${ifDefined(this.errorMessage)} forId=${this.id + '-input'} helptext=${ifDefined(this.helptext)} helptextDropdown=${ifDefined(this.helptextDropdown)} helptextDropdownButton=${ifDefined(this.helptextDropdownButton)} label=${ifDefined(this.label)} optionalText=${ifDefined(this.optionalText)} requiredText=${ifDefined(this.requiredText)} tagText=${ifDefined(this.tagText)} > <select class=${inputClass} aria-invalid=${this.hasError} aria-errormessage=${`${this.id}-error`} aria-labelledby=${ifDefined(this.ariaLabelledby)} ?disabled=${this.disabled} id=${this.id + '-input'} name=${(this.name || this.id) + '-input'} value=${this.value} @change=${(e: Event) => { this.touched = true this.value = (e.target as HTMLSelectElement).value e.stopImmediatePropagation() }} @input=${(e: Event) => { this.onInput() e.stopImmediatePropagation() }} @focus=${(e: FocusEvent) => { this.onFocus() e.stopImmediatePropagation() }} @blur=${(e: FocusEvent) => { this.onBlur() e.stopImmediatePropagation() }} ${ref(this.inputRef)} > ${this._options.length > 0 && this._options.map( (option) => html`<option value=${option.value} ?selected=${this.value == option.value || option.selected} ?disabled=${option.disabled} ?hidden=${option.hidden} > ${option.label} </option>`, )} </select> <div class="pkt-contents" ${ref(this.helptextSlot)} name="helptext" slot="helptext"></div> </pkt-input-wrapper> ` } private returnNumberOrNull(value: number | null | undefined): number | null { if (value === undefined || value === null || isNaN(value)) { return null } return value } }