@oslokommune/punkt-react 16.1.0 → 16.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-react",
3
- "version": "16.1.0",
3
+ "version": "16.2.0",
4
4
  "description": "React komponentbibliotek til Punkt, et designsystem laget av Oslo Origo",
5
5
  "homepage": "https://punkt.oslo.kommune.no",
6
6
  "author": "Team Designsystem, Oslo Origo",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@lit-labs/ssr-dom-shim": "^1.2.1",
41
41
  "@lit/react": "^1.0.7",
42
- "@oslokommune/punkt-elements": "^16.1.0",
42
+ "@oslokommune/punkt-elements": "^16.2.0",
43
43
  "classnames": "^2.5.1",
44
44
  "prettier": "^3.3.3",
45
45
  "react-hook-form": "^7.53.0"
@@ -50,7 +50,7 @@
50
50
  "@eslint/eslintrc": "^3.3.3",
51
51
  "@eslint/js": "^9.37.0",
52
52
  "@oslokommune/punkt-assets": "^16.0.0",
53
- "@oslokommune/punkt-css": "^16.0.5",
53
+ "@oslokommune/punkt-css": "^16.2.0",
54
54
  "@testing-library/jest-dom": "^6.5.0",
55
55
  "@testing-library/react": "^16.0.1",
56
56
  "@testing-library/user-event": "^14.5.2",
@@ -109,5 +109,5 @@
109
109
  "url": "https://github.com/oslokommune/punkt/issues"
110
110
  },
111
111
  "license": "MIT",
112
- "gitHead": "d6bf3b8ba1ebb83dafc1de2937609f67257d011e"
112
+ "gitHead": "554167272be133d1486fb9b041b3afe0980c5069"
113
113
  }
@@ -0,0 +1,373 @@
1
+ import '@testing-library/jest-dom'
2
+
3
+ import { cleanup, render } from '@testing-library/react'
4
+ import { axe, toHaveNoViolations } from 'jest-axe'
5
+ import { createRef } from 'react'
6
+
7
+ import { PktCard } from './Card'
8
+
9
+ expect.extend(toHaveNoViolations)
10
+
11
+ afterEach(cleanup)
12
+
13
+ describe('PktCard', () => {
14
+ describe('Rendering and basic functionality', () => {
15
+ test('renders without errors', () => {
16
+ const { container } = render(<PktCard>innhold</PktCard>)
17
+ const article = container.querySelector('article.pkt-card')
18
+ expect(article).toBeInTheDocument()
19
+ })
20
+
21
+ test('renders content in children', () => {
22
+ const { container } = render(<PktCard><p>Test content here</p></PktCard>)
23
+ const content = container.querySelector('.pkt-card__content')
24
+ expect(content).toBeInTheDocument()
25
+ expect(content?.textContent).toContain('Test content here')
26
+ })
27
+
28
+ test('renders basic structure correctly', () => {
29
+ const { container } = render(<PktCard heading="Test Heading">Test content</PktCard>)
30
+ const article = container.querySelector('article')
31
+ const wrapper = article?.querySelector('.pkt-card__wrapper')
32
+ const header = wrapper?.querySelector('.pkt-card__header')
33
+ const content = wrapper?.querySelector('.pkt-card__content')
34
+
35
+ expect(wrapper).toBeInTheDocument()
36
+ expect(header).toBeInTheDocument()
37
+ expect(content).toBeInTheDocument()
38
+ })
39
+
40
+ test('forwards ref correctly', () => {
41
+ const ref = createRef<HTMLDivElement>()
42
+ const { unmount } = render(<PktCard ref={ref} heading="Test">innhold</PktCard>)
43
+ expect(ref.current).toBeInstanceOf(HTMLElement)
44
+ unmount()
45
+ expect(ref.current).toBeNull()
46
+ })
47
+ })
48
+
49
+ describe('Properties and attributes', () => {
50
+ test('applies default properties correctly', () => {
51
+ const { container } = render(<PktCard>innhold</PktCard>)
52
+ const article = container.querySelector('article')
53
+ expect(article).toHaveClass('pkt-card--outlined')
54
+ expect(article).toHaveClass('pkt-card--vertical')
55
+ expect(article).toHaveClass('pkt-card--padding-default')
56
+ expect(article).toHaveClass('pkt-card--border-on-hover')
57
+ })
58
+
59
+ test('applies different skin properties correctly', () => {
60
+ const skins = ['outlined', 'outlined-beige', 'gray', 'beige', 'green', 'blue'] as const
61
+ for (const skin of skins) {
62
+ const { container, unmount } = render(<PktCard skin={skin}>innhold</PktCard>)
63
+ const article = container.querySelector('article')
64
+ expect(article).toHaveClass(`pkt-card--${skin}`)
65
+ unmount()
66
+ }
67
+ })
68
+
69
+ test('applies different layout properties correctly', () => {
70
+ const layouts = ['vertical', 'horizontal'] as const
71
+ for (const layout of layouts) {
72
+ const { container, unmount } = render(<PktCard layout={layout}>innhold</PktCard>)
73
+ const article = container.querySelector('article')
74
+ expect(article).toHaveClass(`pkt-card--${layout}`)
75
+ unmount()
76
+ }
77
+ })
78
+
79
+ test('applies different padding properties correctly', () => {
80
+ const paddingOptions = ['none', 'default'] as const
81
+ for (const padding of paddingOptions) {
82
+ const { container, unmount } = render(<PktCard padding={padding}>innhold</PktCard>)
83
+ const article = container.querySelector('article')
84
+ expect(article).toHaveClass(`pkt-card--padding-${padding}`)
85
+ unmount()
86
+ }
87
+ })
88
+
89
+ test('handles borderOnHover property correctly', () => {
90
+ const { container } = render(<PktCard borderOnHover={false}>innhold</PktCard>)
91
+ const article = container.querySelector('article')
92
+ expect(article).not.toHaveClass('pkt-card--border-on-hover')
93
+ })
94
+
95
+ test('supports custom className', () => {
96
+ const { container } = render(<PktCard className="custom-class">innhold</PktCard>)
97
+ const wrapper = container.querySelector('.pkt-card-root')
98
+ expect(wrapper).toHaveClass('custom-class')
99
+ })
100
+ })
101
+
102
+ describe('Heading functionality', () => {
103
+ test('renders heading when provided', () => {
104
+ const { container } = render(<PktCard heading="Test Card Title">innhold</PktCard>)
105
+ const heading = container.querySelector('.pkt-card__heading')
106
+ expect(heading).toBeInTheDocument()
107
+ expect(heading?.textContent?.trim()).toBe('Test Card Title')
108
+ })
109
+
110
+ test('renders subheading when provided', () => {
111
+ const { container } = render(<PktCard subheading="Test Subheading">innhold</PktCard>)
112
+ const subheading = container.querySelector('.pkt-card__subheading')
113
+ expect(subheading).toBeInTheDocument()
114
+ expect(subheading?.textContent).toBe('Test Subheading')
115
+ })
116
+
117
+ test('applies correct heading level', () => {
118
+ const { container } = render(
119
+ <PktCard heading="Test" headingLevel={2}>innhold</PktCard>,
120
+ )
121
+ const heading = container.querySelector('h2.pkt-card__heading')
122
+ expect(heading).toBeInTheDocument()
123
+ })
124
+
125
+ test('defaults to h3 heading level', () => {
126
+ const { container } = render(<PktCard heading="Test">innhold</PktCard>)
127
+ const heading = container.querySelector('h3.pkt-card__heading')
128
+ expect(heading).toBeInTheDocument()
129
+ })
130
+
131
+ test('does not render header when no heading or subheading', () => {
132
+ const { container } = render(<PktCard>innhold</PktCard>)
133
+ const header = container.querySelector('.pkt-card__header')
134
+ expect(header).not.toBeInTheDocument()
135
+ })
136
+ })
137
+
138
+ describe('Link functionality', () => {
139
+ test('renders as regular card when no clickCardLink', () => {
140
+ const { container } = render(<PktCard heading="Test Title">innhold</PktCard>)
141
+ const link = container.querySelector('.pkt-card__link')
142
+ expect(link).not.toBeInTheDocument()
143
+
144
+ const article = container.querySelector('article')
145
+ expect(article?.getAttribute('aria-label')).toBe('Test Title')
146
+ })
147
+
148
+ test('renders as link card when clickCardLink provided', () => {
149
+ const { container } = render(
150
+ <PktCard heading="Test Title" clickCardLink="/test-url">innhold</PktCard>,
151
+ )
152
+ const linkHeading = container.querySelector('.pkt-card__link-heading')
153
+ const link = container.querySelector('.pkt-card__link')
154
+ expect(linkHeading).toBeInTheDocument()
155
+ expect(link).toBeInTheDocument()
156
+ expect(link?.getAttribute('href')).toBe('/test-url')
157
+ expect(link?.textContent).toBe('Test Title')
158
+
159
+ const article = container.querySelector('article')
160
+ expect(article?.getAttribute('aria-label')).toBe('Test Title lenkekort')
161
+ })
162
+
163
+ test('does not render plain heading when clickCardLink is set', () => {
164
+ const { container } = render(
165
+ <PktCard heading="Test" clickCardLink="/test">innhold</PktCard>,
166
+ )
167
+ const headings = container.querySelectorAll('.pkt-card__heading')
168
+ // Should only have the link heading, not both
169
+ expect(headings).toHaveLength(1)
170
+ expect(headings[0]).toHaveClass('pkt-card__link-heading')
171
+ })
172
+
173
+ test('handles openLinkInNewTab correctly', () => {
174
+ const { container } = render(
175
+ <PktCard heading="Test" clickCardLink="/test" openLinkInNewTab>innhold</PktCard>,
176
+ )
177
+ const link = container.querySelector('.pkt-card__link')
178
+ expect(link?.getAttribute('target')).toBe('_blank')
179
+ expect(link?.getAttribute('rel')).toBe('noopener noreferrer')
180
+ })
181
+
182
+ test('applies correct aria-label for link cards with custom ariaLabel', () => {
183
+ const { container } = render(
184
+ <PktCard clickCardLink="/test" ariaLabel="Custom aria label">innhold</PktCard>,
185
+ )
186
+ const article = container.querySelector('article')
187
+ expect(article?.getAttribute('aria-label')).toBe('Custom aria label')
188
+ })
189
+ })
190
+
191
+ describe('Image functionality', () => {
192
+ test('renders image when provided', () => {
193
+ const { container } = render(
194
+ <PktCard image={{ src: '/test-image.jpg', alt: 'Test image' }}>innhold</PktCard>,
195
+ )
196
+ const imageDiv = container.querySelector('.pkt-card__image')
197
+ const img = imageDiv?.querySelector('img')
198
+ expect(imageDiv).toBeInTheDocument()
199
+ expect(img).toBeInTheDocument()
200
+ expect(img?.getAttribute('src')).toBe('/test-image.jpg')
201
+ expect(img?.getAttribute('alt')).toBe('Test image')
202
+ })
203
+
204
+ test('does not render image when not provided', () => {
205
+ const { container } = render(<PktCard>innhold</PktCard>)
206
+ const imageDiv = container.querySelector('.pkt-card__image')
207
+ expect(imageDiv).not.toBeInTheDocument()
208
+ })
209
+
210
+ test('applies correct image shape classes', () => {
211
+ const shapes = ['square', 'round'] as const
212
+ for (const shape of shapes) {
213
+ const { container, unmount } = render(
214
+ <PktCard image={{ src: '/test.jpg', alt: 'Test' }} imageShape={shape}>innhold</PktCard>,
215
+ )
216
+ const imageDiv = container.querySelector('.pkt-card__image')
217
+ expect(imageDiv).toHaveClass(`pkt-card__image-${shape}`)
218
+ unmount()
219
+ }
220
+ })
221
+ })
222
+
223
+ describe('Tags functionality', () => {
224
+ test('renders tags when provided', () => {
225
+ const tags = [
226
+ { text: 'Tag 1', skin: 'blue' as const },
227
+ { text: 'Tag 2', skin: 'green' as const },
228
+ ]
229
+ const { container } = render(<PktCard tags={tags}>innhold</PktCard>)
230
+ const tagsContainer = container.querySelector('.pkt-card__tags')
231
+ expect(tagsContainer).toBeInTheDocument()
232
+ expect(tagsContainer?.getAttribute('aria-label')).toBe('merkelapper')
233
+ })
234
+
235
+ test('renders single tag with correct aria-label', () => {
236
+ const tags = [{ text: 'Single Tag' }]
237
+ const { container } = render(<PktCard tags={tags}>innhold</PktCard>)
238
+ const tagsContainer = container.querySelector('.pkt-card__tags')
239
+ expect(tagsContainer?.getAttribute('aria-label')).toBe('merkelapp')
240
+ })
241
+
242
+ test('applies correct tag position classes', () => {
243
+ const positions = ['top', 'bottom'] as const
244
+ for (const position of positions) {
245
+ const tags = [{ text: 'Test Tag' }]
246
+ const { container, unmount } = render(
247
+ <PktCard tags={tags} tagPosition={position}>innhold</PktCard>,
248
+ )
249
+ const tagsContainer = container.querySelector('.pkt-card__tags')
250
+ expect(tagsContainer).toHaveClass(`pkt-card__tags-${position}`)
251
+ unmount()
252
+ }
253
+ })
254
+
255
+ test('does not render tags when array is empty', () => {
256
+ const { container } = render(<PktCard>innhold</PktCard>)
257
+ const tagsContainer = container.querySelector('.pkt-card__tags')
258
+ expect(tagsContainer).not.toBeInTheDocument()
259
+ })
260
+ })
261
+
262
+ describe('Metadata functionality', () => {
263
+ test('renders metadata when provided', () => {
264
+ const { container } = render(
265
+ <PktCard metaLead="Author Name" metaTrail="2023-12-01">innhold</PktCard>,
266
+ )
267
+ const metadata = container.querySelector('.pkt-card__metadata')
268
+ const metaLead = metadata?.querySelector('.pkt-card__metadata-lead')
269
+ const metaTrail = metadata?.querySelector('.pkt-card__metadata-trail')
270
+
271
+ expect(metadata).toBeInTheDocument()
272
+ expect(metaLead?.textContent).toBe('Author Name')
273
+ expect(metaTrail?.textContent).toBe('2023-12-01')
274
+ })
275
+
276
+ test('renders only metaLead when metaTrail not provided', () => {
277
+ const { container } = render(<PktCard metaLead="Author Only">innhold</PktCard>)
278
+ const metaLead = container.querySelector('.pkt-card__metadata-lead')
279
+ const metaTrail = container.querySelector('.pkt-card__metadata-trail')
280
+ expect(metaLead).toBeInTheDocument()
281
+ expect(metaTrail).not.toBeInTheDocument()
282
+ })
283
+
284
+ test('renders only metaTrail when metaLead not provided', () => {
285
+ const { container } = render(<PktCard metaTrail="Date Only">innhold</PktCard>)
286
+ const metaLead = container.querySelector('.pkt-card__metadata-lead')
287
+ const metaTrail = container.querySelector('.pkt-card__metadata-trail')
288
+ expect(metaLead).not.toBeInTheDocument()
289
+ expect(metaTrail).toBeInTheDocument()
290
+ })
291
+
292
+ test('does not render metadata when neither provided', () => {
293
+ const { container } = render(<PktCard>innhold</PktCard>)
294
+ const metadata = container.querySelector('.pkt-card__metadata')
295
+ expect(metadata).not.toBeInTheDocument()
296
+ })
297
+ })
298
+
299
+ describe('Content placement and structure', () => {
300
+ test('renders content elements in correct order', () => {
301
+ const tags = [{ text: 'Test Tag' }]
302
+ const { container } = render(
303
+ <PktCard
304
+ heading="Test Title"
305
+ subheading="Test Sub"
306
+ tags={tags}
307
+ image={{ src: '/test.jpg', alt: 'Test' }}
308
+ metaLead="Author"
309
+ metaTrail="Date"
310
+ >
311
+ innhold
312
+ </PktCard>,
313
+ )
314
+ const article = container.querySelector('article')
315
+ const children = Array.from(article?.children || [])
316
+
317
+ // Image first, then wrapper
318
+ expect(children[0]).toHaveClass('pkt-card__image')
319
+ expect(children[1]).toHaveClass('pkt-card__wrapper')
320
+
321
+ const wrapperChildren = Array.from(children[1]?.children || [])
322
+
323
+ // Order within wrapper: tags (top), header, content, metadata
324
+ expect(wrapperChildren[0]).toHaveClass('pkt-card__tags-top')
325
+ expect(wrapperChildren[1]).toHaveClass('pkt-card__header')
326
+ expect(wrapperChildren[2]).toHaveClass('pkt-card__content')
327
+ expect(wrapperChildren[3]).toHaveClass('pkt-card__metadata')
328
+ })
329
+
330
+ test('places tags at bottom when tagPosition is bottom', () => {
331
+ const tags = [{ text: 'Test Tag' }]
332
+ const { container } = render(
333
+ <PktCard heading="Test Title" tags={tags} tagPosition="bottom">innhold</PktCard>,
334
+ )
335
+ const wrapperChildren = Array.from(
336
+ container.querySelector('.pkt-card__wrapper')?.children || [],
337
+ )
338
+
339
+ // Order: header, content, tags (bottom)
340
+ expect(wrapperChildren[0]).toHaveClass('pkt-card__header')
341
+ expect(wrapperChildren[1]).toHaveClass('pkt-card__content')
342
+ expect(wrapperChildren[2]).toHaveClass('pkt-card__tags-bottom')
343
+ })
344
+ })
345
+
346
+ describe('Accessibility', () => {
347
+ test('has no accessibility violations', async () => {
348
+ const { container } = render(<PktCard heading="Accessible Card">innhold</PktCard>)
349
+ const results = await axe(container)
350
+ expect(results).toHaveNoViolations()
351
+ })
352
+
353
+ test('applies custom aria-label', () => {
354
+ const { container } = render(
355
+ <PktCard heading="Test" ariaLabel="Custom accessible label">innhold</PktCard>,
356
+ )
357
+ const article = container.querySelector('article')
358
+ expect(article?.getAttribute('aria-label')).toBe('Custom accessible label')
359
+ })
360
+
361
+ test('falls back to heading for aria-label', () => {
362
+ const { container } = render(<PktCard heading="Default Aria Label">innhold</PktCard>)
363
+ const article = container.querySelector('article')
364
+ expect(article?.getAttribute('aria-label')).toBe('Default Aria Label')
365
+ })
366
+
367
+ test('falls back to "kort" when no heading or aria-label', () => {
368
+ const { container } = render(<PktCard>innhold</PktCard>)
369
+ const article = container.querySelector('article')
370
+ expect(article?.getAttribute('aria-label')).toBe('kort')
371
+ })
372
+ })
373
+ })
@@ -1,53 +1,188 @@
1
1
  'use client'
2
2
 
3
- import { createComponent } from '@lit/react'
4
- import { IPktHeading, PktCard as PktElCard } from '@oslokommune/punkt-elements'
5
- // eslint-disable-next-line no-restricted-syntax -- React is required for createComponent
6
- import React, { FC, ForwardedRef, forwardRef, type ReactElement } from 'react'
3
+ import classNames from 'classnames'
4
+ import { forwardRef, HTMLAttributes, ReactNode, Ref } from 'react'
7
5
  import type { TCardSkin, TLayout } from 'shared-types'
8
6
 
9
- import type { PktElConstructor, PktElType } from '@/interfaces/IPktElements'
10
-
11
- import { IPktTag } from '../tag/Tag'
7
+ import { PktIcon } from '../icon/Icon'
8
+ import { PktTag } from '../tag/Tag'
12
9
 
13
10
  export type { TCardSkin, TLayout }
14
11
  type TCardPadding = 'none' | 'default'
15
12
  type TCardImageShape = 'square' | 'round'
16
13
  type TCardTagPosition = 'top' | 'bottom'
14
+ type THeadingLevel = 1 | 2 | 3 | 4 | 5 | 6
15
+
16
+ type TTagSkin = 'blue' | 'blue-light' | 'blue-dark' | 'green' | 'red' | 'beige' | 'yellow' | 'grey' | 'gray'
17
17
 
18
- export interface IPktCard extends PktElType {
18
+ interface ICardTag {
19
+ skin?: TTagSkin
20
+ iconName?: string
21
+ ariaLabel?: string
22
+ text: string
23
+ }
24
+
25
+ export interface IPktCard extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
19
26
  ariaLabel?: string
20
27
  metaLead?: string | null
21
- borderOnHover?: boolean | null
28
+ borderOnHover?: boolean
22
29
  metaTrail?: string | null
23
30
  layout?: TLayout
24
31
  heading?: string
25
- headingLevel?: IPktHeading['level']
32
+ headingLevel?: THeadingLevel
26
33
  image?: { src: string; alt: string }
27
34
  imageShape?: TCardImageShape
28
35
  clickCardLink?: string | null
29
36
  padding?: TCardPadding
30
- openLinkInNewTab?: boolean | null
37
+ openLinkInNewTab?: boolean
31
38
  skin?: TCardSkin
32
39
  subheading?: string
33
40
  tagPosition?: TCardTagPosition
34
- tags?: (Omit<IPktTag, 'closeTag'> & { text: string })[]
41
+ tags?: ICardTag[]
42
+ children?: ReactNode
43
+ ref?: Ref<HTMLDivElement>
35
44
  }
36
45
 
37
- const LitComponent: FC<IPktCard> = createComponent({
38
- tagName: 'pkt-card',
39
- elementClass: PktElCard as PktElConstructor<HTMLElement>,
40
- react: React,
41
- displayName: 'PktCard',
42
- events: {},
43
- })
46
+ export const PktCard = forwardRef<HTMLDivElement, IPktCard>(
47
+ (
48
+ {
49
+ children,
50
+ className,
51
+ ariaLabel,
52
+ metaLead,
53
+ borderOnHover = true,
54
+ metaTrail,
55
+ layout = 'vertical',
56
+ heading = '',
57
+ headingLevel = 3,
58
+ image,
59
+ imageShape = 'square',
60
+ clickCardLink,
61
+ padding = 'default',
62
+ openLinkInNewTab = false,
63
+ skin = 'outlined',
64
+ subheading = '',
65
+ tagPosition = 'top',
66
+ tags = [],
67
+ ...props
68
+ },
69
+ ref,
70
+ ) => {
71
+ const classes = classNames({
72
+ 'pkt-card': true,
73
+ [`pkt-card--${skin}`]: skin,
74
+ [`pkt-card--${layout}`]: layout,
75
+ [`pkt-card--padding-${padding}`]: padding,
76
+ 'pkt-card--border-on-hover': borderOnHover,
77
+ })
78
+
79
+ const ariaLabelLenke =
80
+ ariaLabel?.trim() || (heading ? `${heading} lenkekort` : 'lenkekort')
81
+ const ariaLabelVanlig = ariaLabel?.trim() || (heading ? heading : 'kort')
82
+
83
+ const HeadingTag = `h${headingLevel}` as keyof JSX.IntrinsicElements
84
+
85
+ const renderImage = () => {
86
+ if (!image?.src) return null
87
+ return (
88
+ <div className={classNames('pkt-card__image', `pkt-card__image-${imageShape}`)}>
89
+ <img src={image.src} alt={image.alt || ''} />
90
+ </div>
91
+ )
92
+ }
93
+
94
+ const renderTags = () => {
95
+ if (tags.length === 0) return null
96
+ return (
97
+ <div
98
+ className={classNames('pkt-card__tags', `pkt-card__tags-${tagPosition}`)}
99
+ role="list"
100
+ aria-label={tags.length > 1 ? 'merkelapper' : 'merkelapp'}
101
+ >
102
+ {tags.map((tag, index) => (
103
+ <PktTag
104
+ key={index}
105
+ role="listitem"
106
+ textStyle="normal-text"
107
+ size="medium"
108
+ skin={tag.skin}
109
+ iconName={tag.iconName}
110
+ >
111
+ <span>{tag.text}</span>
112
+ </PktTag>
113
+ ))}
114
+ </div>
115
+ )
116
+ }
117
+
118
+ const renderHeading = () => {
119
+ if (!heading || clickCardLink) return null
120
+ return (
121
+ <HeadingTag className="pkt-heading pkt-heading--medium pkt-heading--fw-regular pkt-card__heading">
122
+ {heading}
123
+ </HeadingTag>
124
+ )
125
+ }
126
+
127
+ const renderLinkHeading = () => {
128
+ if (!clickCardLink) return null
129
+ return (
130
+ <HeadingTag className="pkt-heading pkt-heading--medium pkt-heading--fw-regular pkt-card__link-heading pkt-card__heading">
131
+ <a
132
+ className="pkt-card__link"
133
+ href={clickCardLink}
134
+ target={openLinkInNewTab ? '_blank' : '_self'}
135
+ rel={openLinkInNewTab ? 'noopener noreferrer' : undefined}
136
+ >
137
+ {heading}
138
+ </a>
139
+ </HeadingTag>
140
+ )
141
+ }
142
+
143
+ const renderSubheading = () => {
144
+ if (!subheading) return null
145
+ return <p className="pkt-card__subheading">{subheading}</p>
146
+ }
147
+
148
+ const renderHeader = () => {
149
+ if (!heading && !subheading) return null
150
+ return (
151
+ <header className="pkt-card__header">
152
+ {renderHeading()}
153
+ {renderLinkHeading()}
154
+ {renderSubheading()}
155
+ </header>
156
+ )
157
+ }
158
+
159
+ const renderMetadata = () => {
160
+ if (!metaLead && !metaTrail) return null
161
+ return (
162
+ <footer className="pkt-card__metadata">
163
+ {metaLead && <span className="pkt-card__metadata-lead">{metaLead}</span>}
164
+ {metaTrail && <span className="pkt-card__metadata-trail">{metaTrail}</span>}
165
+ </footer>
166
+ )
167
+ }
44
168
 
45
- export const PktCard: FC<IPktCard> = forwardRef(
46
- ({ children, ...props }: IPktCard, ref: ForwardedRef<HTMLElement>): ReactElement => {
47
169
  return (
48
- <LitComponent {...props} ref={ref}>
49
- <div className="pkt-contents">{children}</div>
50
- </LitComponent>
170
+ <div ref={ref} className={classNames('pkt-card-root', className)} style={{ display: 'block', width: '100%' }}>
171
+ <article
172
+ {...props}
173
+ className={classes}
174
+ aria-label={clickCardLink ? ariaLabelLenke : ariaLabelVanlig}
175
+ >
176
+ {renderImage()}
177
+ <div className="pkt-card__wrapper">
178
+ {tagPosition === 'top' && renderTags()}
179
+ {renderHeader()}
180
+ <section className="pkt-card__content">{children}</section>
181
+ {tagPosition === 'bottom' && renderTags()}
182
+ {renderMetadata()}
183
+ </div>
184
+ </article>
185
+ </div>
51
186
  )
52
187
  },
53
188
  )