@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
194 lines (172 loc) • 7.19 kB
text/typescript
import { classMap } from 'lit/directives/class-map.js'
import { customElement, property, state } from 'lit/decorators.js'
import { html, nothing } from 'lit'
import { PktElement } from '@/base-elements/element'
import { Ref, createRef, ref } from 'lit/directives/ref.js'
import { styleMap } from 'lit/directives/style-map.js'
import { uuidish } from '@/utils/stringutils'
import '@/components/icon'
import { TAriaLive } from '@/types/aria'
export type TProgressbarRole = 'progressbar' | 'meter'
export type TProgressbarSkin = 'dark-blue' | 'light-blue' | 'green' | 'red'
export type TProgressbarStatusPlacement = 'center' | 'left' | 'following'
export type TProgressbarStatusType = 'none' | 'percentage' | 'fraction'
export type TProgressbarTitlePosition = 'left' | 'center'
export interface IPktProgressbar {
ariaLabel?: string | null
ariaLabelledby?: string | null
ariaLive?: TAriaLive | null
ariaValueText?: string | null
id?: string | null
role?: TProgressbarRole
skin?: TProgressbarSkin
statusPlacement?: TProgressbarStatusPlacement
statusType?: TProgressbarStatusType
title?: string | null
titlePosition?: TProgressbarTitlePosition
valueCurrent: number
valueMax?: number
valueMin?: number
}
export class PktProgressbar extends PktElement implements IPktProgressbar {
constructor() {
super()
}
// Public properties
ariaLabel: string | null = null
ariaLabelledby: string | null = null
ariaValueText: string | null = null
ariaLive: TAriaLive = 'polite'
id: string = uuidish()
role: TProgressbarRole = 'progressbar'
skin: TProgressbarSkin = 'dark-blue'
statusPlacement: TProgressbarStatusPlacement = 'following'
statusType: TProgressbarStatusType = 'none'
title = ''
titlePosition: TProgressbarTitlePosition = 'left'
valueCurrent = 0
valueMax = 100
valueMin = 0
// State
private labelWidth = 0
private progressbarId: string = this.id
private computedAriaLabelledby: string | null = null
private computedAriaValueText = ''
// Private refs
private labelRef: Ref<HTMLSpanElement> = createRef()
private progressBarRef: Ref<HTMLDivElement> = createRef()
firstUpdated(changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(changedProperties)
this.setComputedValues()
this.syncAttributes()
}
updated(changedProperties: Map<string | number | symbol, unknown>) {
super.updated(changedProperties)
if (changedProperties.has('valueCurrent') && this.labelRef.value) {
this.labelWidth = this.labelRef.value.getBoundingClientRect().width || 0
}
if (changedProperties.has('id') && this.id) {
this.progressBarId = this.id
}
if (
changedProperties.has('statusType') ||
changedProperties.has('id') ||
changedProperties.has('ariaLabelledby')
) {
this.progressbarId = this.id || uuidish()
this.computedAriaLabelledby = this.ariaLabelledby || `${this.progressbarId}-title`
}
if (
changedProperties.has('ariaValueText') ||
changedProperties.has('valueCurrent') ||
changedProperties.has('valueMax')
) {
this.computedAriaValueText =
this.statusType === 'fraction' && !this.ariaValueText
? `${this.valueCurrent} av ${this.valueMax - this.valueMin}`
: this.ariaValueText || ''
}
this.syncAttributes()
}
render() {
const hasStatus = this.statusType !== 'none'
const totalSteps = this.valueMax - this.valueMin
const percentageOfSteps = (this.valueCurrent / totalSteps) * 100
const currentPercentage =
this.statusType === 'fraction'
? Math.round(percentageOfSteps)
: Math.round(((this.valueCurrent - this.valueMin) / (this.valueMax - this.valueMin)) * 100)
const formattedTitle = `${this.valueCurrent} av ${totalSteps}`
const barClasses = classMap({
'pkt-progressbar__bar': true,
[`pkt-progressbar__bar--${this.skin}`]: !!this.skin,
})
const titleClasses = classMap({
'pkt-progressbar__title': true,
[`pkt-progressbar__title-center`]: this.titlePosition === 'center',
})
const statusClasses = classMap({
'pkt-progressbar__status': true,
[`pkt-progressbar__status--center`]: this.statusPlacement === 'center',
})
const placementClasses = classMap({
[`pkt-progressbar__status-placement--following`]: this.statusPlacement === 'following',
[`pkt-progressbar__status-placement--center`]: this.statusPlacement === 'center',
[`pkt-progressbar__status-placement--left`]: this.statusPlacement === 'left',
})
return html` <div
class="pkt-progressbar__container"
.ref=${this.progressBarRef}
style=${styleMap({
'--pkt-progress-label-width': `${this.labelWidth}px`,
'--pkt-progress-width': `${currentPercentage}%`,
})}
>
${this.title
? html`<p id=${`${this.progressBarId}-title`} class=${titleClasses}>${this.title}</p>`
: nothing}
<div class="pkt-progressbar__bar-wrapper">
<div class=${barClasses}></div>
</div>
${hasStatus
? html`<div class=${statusClasses}>
<span class=${placementClasses} ${ref(this.labelRef)}>
${this.statusType === 'percentage' ? `${currentPercentage}%` : formattedTitle}
</span>
</div>`
: nothing}
</div>`
}
// methods
private setComputedValues() {
this.progressbarId = this.id || uuidish()
this.computedAriaLabelledby = this.ariaLabelledby || `${this.progressbarId}-title`
this.computedAriaValueText =
this.statusType === 'fraction' && !this.ariaValueText
? `${this.valueCurrent} av ${this.valueMax - this.valueMin}`
: this.ariaValueText || ''
}
private syncAttributes() {
this._handleAttribute('aria-live', this.ariaLive)
this._handleAttribute('aria-valuenow', this.valueCurrent)
this._handleAttribute('aria-valuemin', this.valueMin)
this._handleAttribute('aria-valuemax', this.valueMax)
this._handleAttribute('aria-valuetext', this.computedAriaValueText)
this._handleAttribute('aria-label', this.ariaLabel)
this._handleAttribute('role', this.role)
this._handleAttribute('aria-atomic', 'true')
this._handleAttribute('id', this.progressbarId)
if (!this.ariaLabel) {
this._handleAttribute('aria-labelledby', this.computedAriaLabelledby)
}
}
private _handleAttribute(name: string, value: string | number | null) {
if (value == null || value === '') {
this.removeAttribute(name)
} else {
this.setAttribute(name, String(value))
}
}
}
export default PktProgressbar