@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
237 lines (217 loc) • 8.05 kB
text/typescript
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
*/
({ type: String }) forId: string = uuidish()
({ type: String }) label: string = ''
({ type: String }) helptext: string | null = null
({ type: String }) helptextDropdown: string | null = null
({ type: String }) helptextDropdownButton: string | null = null
({ type: Boolean }) counter: boolean = specs.props.counter.default
({ type: Number }) counterCurrent: number = 0
({ type: Number }) counterMaxLength: number = 0
({ type: String }) counterError: string | null = null
({ type: String, reflect: false }) counterPosition: TCounterPosition = 'bottom'
({ type: Boolean }) optionalTag: boolean = specs.props.optionalTag.default
({ type: String }) optionalText: string = specs.props.optionalText.default
({ type: Boolean }) requiredTag: boolean = specs.props.requiredTag.default
({ type: String }) requiredText: string = specs.props.requiredText.default
({ type: String }) tagText: string | null = null
({ type: Boolean }) hasError: boolean = specs.props.hasError.default
({ type: String }) errorMessage: string = ''
({ type: Boolean }) disabled: boolean = specs.props.disabled.default
({ type: Boolean }) inline: boolean = specs.props.inline.default
({ type: String }) ariaDescribedby: string | undefined = undefined
({ type: Boolean }) hasFieldset: boolean = specs.props.hasFieldset.default
({ type: String, reflect: true }) role: string | null = 'group'
({ 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')
}