UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

237 lines (217 loc) 8.05 kB
import { PktElementWithSlot } from '@/base-elements/element-with-slot' import { slotContent } from '@/directives/slot-content' 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 'shared-utils/utils' import { booleanishConverter, type Booleanish } from 'shared-types' import { FocusModalityController } from '@/controllers/focus-modality-controller' import specs from 'componentSpecs/input-wrapper.json' import '@/components/helptext' import '@/components/icon' import '@/components/alert' 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' > export class PktInputWrapper extends PktElementWithSlot<Props> { /** * 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) } // Speiler navigeringsmodus til den interne pkt-inputwrapper-div-en som // data-navtype="pointer" (eller fjerner attributtet) så CSS kan skille // dempet fokus (klikk) fra kraftig fokus (tab). Controlleren registrerer // seg selv på hosten i konstruktøren, så vi trenger ikke beholde referansen. focusModality = new FocusModalityController(this, '.pkt-inputwrapper') 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) }} >${slotContent(this, '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`<pkt-alert skin="error" compact id=${`${this.forId}-error`} aria-live="assertive" aria-atomic="true" > ${unsafeHTML(this.errorMessage)} </pkt-alert>` } else { return nothing } } const inputContent = () => { //prettier-ignore return html` ${labelElement()} ${helptextElement()} ${this.counterPosition === 'top' ? counterElement() : nothing} <div class="pkt-contents">${slotContent(this)}</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() { this.dispatchEvent( new CustomEvent('labelClick', { bubbles: true, composed: true, detail: 'label clicked', }), ) } } try { customElement('pkt-input-wrapper')(PktInputWrapper) } catch (e) { console.warn('Forsøker å definere <pkt-input-wrapper>, men den er allerede definert') }