@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
268 lines (243 loc) • 8.2 kB
text/typescript
import { classMap } from 'lit/directives/class-map.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { customElement, property } from 'lit/decorators.js'
import { html, nothing } from 'lit'
import { IPktTag } from '@/components/tag'
import { PktElementWithSlot } from '@/base-elements/element-with-slot'
import { slotContent } from '@/directives/slot-content'
import { IPktHeading } from '../heading'
import specs from 'componentSpecs/card.json'
import '@/components/icon'
import '@/components/tag'
import '@/components/heading'
import type { IAriaAttributes, TCardSkin, TLayout } from 'shared-types'
export type { TCardSkin, TLayout }
export type TCardPadding = 'none' | 'default'
export type TCardImageShape = 'square' | 'round'
export type TCardTagPosition = 'top' | 'bottom'
export interface IPktCard {
ariaLabel?: IAriaAttributes['aria-label']
metaLead?: string | null
metaTrail?: string | null
layout?: TLayout
heading?: string
headingLevel?: IPktHeading['level']
image?: { src: string; alt: string }
imageShape?: TCardImageShape
clickCardLink?: string | null
openLinkInNewTab?: boolean | null
borderOnHover?: boolean | null
padding?: TCardPadding
skin?: TCardSkin
subheading?: string
tagPosition?: TCardTagPosition
tags?: (Omit<IPktTag, 'closeTag'> & { text: string })[]
}
export class PktCard extends PktElementWithSlot implements IPktCard {
// Properties
({ type: String }) ariaLabel: string = ''
({ type: String }) metaLead: string | null = null
({ type: Boolean }) borderOnHover: boolean = true
({ type: String, reflect: true }) clickCardLink: IPktCard['clickCardLink'] = null
({ type: String }) metaTrail: string | null = null
({ type: String }) layout: TLayout = specs.props.layout.default as TLayout
({ type: String }) heading: string = ''
({ type: Number }) headinglevel: IPktHeading['level'] = 3
({ type: Object }) image: { src: string; alt: string } = {
src: '',
alt: '',
}
({ type: String }) imageShape: TCardImageShape = 'square'
({ type: Boolean }) openLinkInNewTab: boolean = false
({ type: String }) padding: TCardPadding = specs.props.padding.default as TCardPadding
({
type: String,
converter: {
fromAttribute: (value: string | null): TCardSkin => {
const validSkins = specs.props.skin.type as TCardSkin[]
if (value && validSkins.includes(value as TCardSkin)) {
return value as TCardSkin
} else {
if (value && !validSkins.includes(value as TCardSkin)) {
// eslint-disable-next-line no-console -- Acceptable to log a warning for invalid skin values
console.warn(
`Invalid skin value "${value}". Using default skin "${specs.props.skin.default}".`,
)
}
return specs.props.skin.default as TCardSkin
}
},
toAttribute: (value: TCardSkin): string => value,
},
})
skin: TCardSkin = specs.props.skin.default as TCardSkin
({ type: String }) subheading: string = ''
({ type: String }) tagPosition: 'top' | 'bottom' = 'top'
({ type: Array }) tags: (Omit<IPktTag, 'closeTag'> & { text: string })[] = []
connectedCallback() {
super.connectedCallback()
}
// Render methods
// Main render method
// prettier-ignore
render() {
const classes = {
'pkt-card': true,
[`pkt-card--${this.skin}`]: this.skin,
[`pkt-card--${this.layout}`]: this.layout,
[`pkt-card--padding-${this.padding}`]: this.padding,
[`pkt-card--border-on-hover`]: this.borderOnHover,
}
//
const ariaLabelLenke =
this.ariaLabel?.trim() || (this.heading ? `${this.heading} lenkekort` : 'lenkekort')
const ariaLabelVanlig = this.ariaLabel?.trim() || (this.heading ? this.heading : 'kort')
return html`
<article
class=${classMap(classes)}
aria-label=${ifDefined(this.clickCardLink ? ariaLabelLenke : ariaLabelVanlig)}
>
${this.renderImage()}
<div class="pkt-card__wrapper">
${this.tagPosition === 'top' ? this.renderTags() : nothing}
${this.renderHeader()}
${this.renderSlot()}
${this.tagPosition === 'bottom' ? this.renderTags() : nothing}
${this.renderMetadata()}
</div>
</article>
`
}
// Render methods for different parts of the card
renderImage() {
const imageClasses = {
'pkt-card__image': true,
[`pkt-card__image-${this.imageShape}`]: this.imageShape,
}
return html`
${this.image.src &&
html`
<div class=${classMap(imageClasses)}>
<img src=${this.image.src} alt=${this.image.alt || ''} />
</div>
`}
`
}
// Do not render heading if link is present, render link heading instead
// Combine the rendering for headings into a renderHeader method
renderHeading() {
return html`
${this.heading && !this.clickCardLink
? html`
<pkt-heading
class="pkt-card__heading"
.level=${this.headinglevel || 3}
size="medium"
nospacing
weight="regular"
>
${this.heading}
</pkt-heading>
`
: nothing}
`
}
renderLinkHeading() {
return html`
${this.clickCardLink
? html`
<pkt-heading
class="pkt-card__link-heading pkt-card__heading"
.level=${this.headinglevel || 3}
size="medium"
weight="regular"
nospacing
>
<a
class="pkt-card__link"
href=${this.clickCardLink}
target=${this.openLinkInNewTab ? '_blank' : '_self'}
>${this.heading}</a
>
</pkt-heading>
`
: nothing}
`
}
renderSubheading() {
return html`
${this.subheading ? html` <p class="pkt-card__subheading ">${this.subheading}</p> ` : nothing}
`
}
// Render header
// prettier-ignore
renderHeader() {
const hasHeading = !!this.heading || !!this.subheading
return html`
${hasHeading
? html`
<header class="pkt-card__header">
${this.renderHeading()}
${this.renderLinkHeading()}
${this.renderSubheading()}
</header>
`
: nothing}
`
}
renderTags() {
const tagClasses = {
'pkt-card__tags': true,
[`pkt-card__tags-${this.tagPosition}`]: this.tagPosition,
}
return html`
${this.tags.length > 0
? html`
<div
class=${classMap(tagClasses)}
role="list"
aria-label=${this.tags.length > 1 ? 'merkelapper' : 'merkelapp'}
>
${this.tags.map(
(tag) => html`
<pkt-tag
role="listitem"
textStyle="normal-text"
size="medium"
skin=${ifDefined(tag.skin)}
iconName=${ifDefined(tag.iconName)}
>
<span>${tag.text}</span>
</pkt-tag>
`,
)}
</div>
`
: nothing}
`
}
renderSlot() {
return html`<section class="pkt-card__content">${slotContent(this)}</section>`
}
renderMetadata() {
return html`
${this.metaLead || this.metaTrail
? html`
<footer class="pkt-card__metadata">
${this.metaLead
? html`<span class="pkt-card__metadata-lead">${this.metaLead}</span>`
: nothing}
${this.metaTrail
? html`<span class="pkt-card__metadata-trail">${this.metaTrail}</span>`
: nothing}
</footer>
`
: nothing}
`
}
}
try {
customElement('pkt-card')(PktCard)
} catch (e) {
console.warn('Forsøker å definere <pkt-card>, men den er allerede definert')
}