UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

254 lines (233 loc) 8.52 kB
import { PktElement } from '@/base-elements/element' import { PktSlotController } from '@/controllers/pkt-slot-controller' import { ref, Ref, createRef } from 'lit/directives/ref.js' import { ifDefined } from 'lit/directives/if-defined.js' import { html, nothing, PropertyValues } from 'lit' import { unsafeHTML } from 'lit/directives/unsafe-html.js' import { classMap } from 'lit/directives/class-map.js' import { customElement, property } from 'lit/decorators.js' import { ElementProps } from '@/types/typeUtils' import { uuidish } from '@/utils/stringutils' import specs from 'componentSpecs/input-wrapper.json' import '@/components/helptext' import '@/components/icon' type TCounterPosition = 'top' | 'bottom' type Props = ElementProps< PktInputWrapper, | 'forId' | 'label' | 'helptext' | 'helptextDropdown' | 'helptextDropdownButton' | 'counter' | 'counterCurrent' | 'counterMaxLength' | 'optionalTag' | 'optionalText' | 'requiredTag' | 'requiredText' | 'tagText' | 'hasError' | 'errorMessage' | 'disabled' | 'inline' | 'ariaDescribedby' | 'hasFieldset' | 'useWrapper' | 'role' > // TODO: Trekk denne logikken ut i utils, så den kan gjenbrukes andre steder type Booleanish = boolean | 'true' | 'false' const booleanishConverter = { fromAttribute(value: string | boolean | null): boolean { // Accept false, "false", undefined, null as false if (value === null || value === undefined) return false if (value === '' || value === 'true' || value === true) return true if (value === 'false' || value === false) return false return Boolean(value) }, toAttribute(value: boolean): string | null { // Always reflect as "true" or "false" return value ? 'true' : 'false' }, } @customElement('pkt-input-wrapper') export class PktInputWrapper extends PktElement<Props> { defaultSlot: Ref<HTMLElement> = createRef() helptextSlot: Ref<HTMLElement> = createRef() constructor() { super() this.slotController = new PktSlotController(this, this.defaultSlot, this.helptextSlot) } /** * Element attributes */ @property({ type: String }) forId: string = uuidish() @property({ type: String }) label: string = '' @property({ type: String }) helptext: string | null = null @property({ type: String }) helptextDropdown: string | null = null @property({ type: String }) helptextDropdownButton: string | null = null @property({ type: Boolean }) counter: boolean = specs.props.counter.default @property({ type: Number }) counterCurrent: number = 0 @property({ type: Number }) counterMaxLength: number = 0 @property({ type: String }) counterError: string | null = null @property({ type: String, reflect: false }) counterPosition: TCounterPosition = 'bottom' @property({ type: Boolean }) optionalTag: boolean = specs.props.optionalTag.default @property({ type: String }) optionalText: string = specs.props.optionalText.default @property({ type: Boolean }) requiredTag: boolean = specs.props.requiredTag.default @property({ type: String }) requiredText: string = specs.props.requiredText.default @property({ type: String }) tagText: string | null = null @property({ type: Boolean }) hasError: boolean = specs.props.hasError.default @property({ type: String }) errorMessage: string = '' @property({ type: Boolean }) disabled: boolean = specs.props.disabled.default @property({ type: Boolean }) inline: boolean = specs.props.inline.default @property({ type: String }) ariaDescribedby: string | undefined = undefined @property({ type: Boolean }) hasFieldset: boolean = specs.props.hasFieldset.default @property({ type: String, reflect: true }) role: string | null = 'group' @property({ type: Boolean, reflect: true, converter: booleanishConverter }) useWrapper: Booleanish = specs.props.useWrapper.default protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties) } render() { const classes = { 'pkt-inputwrapper': true, 'pkt-inputwrapper--error': this.hasError, 'pkt-inputwrapper--disabled': this.disabled, 'pkt-inputwrapper--inline': this.inline, } const tagClasses = 'pkt-tag pkt-tag--small pkt-tag--thin-text' const describedBy = this.ariaDescribedby ? this.ariaDescribedby : this.helptext ? `${this.forId}-helptext` : nothing const tagElement = () => { return html` ${this.tagText ? html`<span class=${tagClasses + ' pkt-tag--gray'}>${this.tagText}</span>` : nothing} ${this.optionalTag ? html`<span class=${tagClasses + ' pkt-tag--blue-light'}>${this.optionalText}</span>` : nothing} ${this.requiredTag ? html`<span class=${tagClasses + ' pkt-tag--beige'}>${this.requiredText}</span>` : nothing} ` } const labelElement = () => { if (this.useWrapper) { if (this.hasFieldset) { return html`<legend class="pkt-inputwrapper__legend" id="${this.forId}-label" @click=${this.handleLabelClick} > ${this.label} ${tagElement()} </legend>` } else { return html`<label class="pkt-inputwrapper__label" for="${this.forId}" aria-describedby="${describedBy}" id="${this.forId}-label" @click=${this.handleLabelClick} >${this.label}${tagElement()}</label >` } } else { return html`<label for="${this.forId}" class="pkt-sr-only" aria-describedby="${describedBy}" id="${this.forId}-label" > ${this.label} </label>` } } const helptextElement = () => { return html` <pkt-helptext class="${ifDefined(!this.useWrapper ? 'pkt-hide' : undefined)}" .forId=${this.forId} .helptext=${this.helptext} .helptextDropdown=${this.helptextDropdown} .helptextDropdownButton=${this.helptextDropdownButton || specs.props.helptextDropdownButton.default} @toggleHelpText=${(e: CustomEvent) => { this.toggleDropdown(e) }} ${ref(this.helptextSlot)} name="helptext" ></pkt-helptext> ` } const counterElement = () => { if (this.counter) { return html`<div class="pkt-input__counter" aria-live="polite" aria-atomic="true"> ${this.counterError ? this.counterError : nothing} ${this.counterCurrent || 0} ${this.counterMaxLength ? `/${this.counterMaxLength}` : nothing} </div>` } else { return nothing } } const errorElement = () => { if (this.hasError && this.errorMessage) { return html`<div role="alert" class="pkt-alert pkt-alert--error pkt-alert--compact" aria-live="assertive" aria-atomic="true" id="${this.forId}-error" > <pkt-icon name="alert-error" class="pkt-alert__icon"></pkt-icon> <div class="pkt-alert__text">${unsafeHTML(this.errorMessage)}</div> </div>` } else { return nothing } } const inputContent = () => { //prettier-ignore return html` ${labelElement()} ${helptextElement()} ${this.counterPosition === 'top' ? counterElement() : nothing} <div class="pkt-contents" ${ref(this.defaultSlot)}></div> ${this.counterPosition === 'bottom' ? counterElement() : nothing} ${errorElement()} ` } const wrapperElement = () => { return this.hasFieldset ? html`<fieldset class="pkt-inputwrapper__fieldset" aria-describedby="${describedBy}"> ${inputContent()} </fieldset>` : html`<div class="pkt-inputwrapper__fieldset">${inputContent()}</div>` } return html`<div class=${classMap(classes)}>${wrapperElement()}</div> ` } private toggleDropdown(e: CustomEvent) { this.dispatchEvent( new CustomEvent('toggleHelpText', { bubbles: false, detail: { isOpen: e.detail.isOpen }, }), ) } private handleLabelClick(e: MouseEvent) { if (this.disabled) { e.preventDefault() e.stopImmediatePropagation() } this.dispatchEvent( new CustomEvent('labelClick', { bubbles: true, composed: true, detail: 'label clicked', }), ) } }