@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
253 lines (228 loc) • 7.61 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 { PktElement } from '@/base-elements/element'
import { PktSlotController } from '@/controllers/pkt-slot-controller'
import { ref, createRef, Ref } from 'lit/directives/ref.js'
import { IPktHeading } from '../heading'
import specs from 'componentSpecs/card.json'
import '@/components/icon'
import '@/components/tag'
import { IAriaAttributes } from '@/types/aria'
export type TCardSkin = 'outlined' | 'outlined-beige' | 'gray' | 'beige' | 'green' | 'blue'
export type TLayout = 'vertical' | 'horizontal'
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 })[]
}
('pkt-card')
export class PktCard extends PktElement implements IPktCard {
// Refs
defaultSlot: Ref<HTMLElement> = createRef()
//Constructor
constructor() {
super()
this.slotController = new PktSlotController(this, this.defaultSlot)
}
// 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 }) 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
>
${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"
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)}
>
${tag.text}
</pkt-tag>
`,
)}
</div>
`
: nothing}
`
}
renderSlot() {
return html`
${this.defaultSlot &&
html`<section class="pkt-card__content" ${ref(this.defaultSlot)}></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}
`
}
}