@oslokommune/punkt-react 13.1.2 → 13.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": "13.1.2",
3
+ "version": "13.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",
@@ -38,7 +38,7 @@
38
38
  "dependencies": {
39
39
  "@lit-labs/ssr-dom-shim": "^1.2.1",
40
40
  "@lit/react": "^1.0.7",
41
- "@oslokommune/punkt-elements": "^13.1.2",
41
+ "@oslokommune/punkt-elements": "^13.2.0",
42
42
  "angular-html-parser": "^6.0.2",
43
43
  "html-format": "^1.1.7",
44
44
  "prettier": "^3.3.3",
@@ -49,7 +49,7 @@
49
49
  "devDependencies": {
50
50
  "@babel/plugin-proposal-private-property-in-object": "^7.18.6",
51
51
  "@oslokommune/punkt-assets": "^13.0.0",
52
- "@oslokommune/punkt-css": "^13.1.2",
52
+ "@oslokommune/punkt-css": "^13.2.0",
53
53
  "@testing-library/jest-dom": "^6.5.0",
54
54
  "@testing-library/react": "^16.0.1",
55
55
  "@testing-library/user-event": "^14.5.2",
@@ -112,5 +112,5 @@
112
112
  "url": "https://github.com/oslokommune/punkt/issues"
113
113
  },
114
114
  "license": "MIT",
115
- "gitHead": "cb98f3d45e891775f449eb4b78b3ae8cab40725f"
115
+ "gitHead": "36713c6c03feb884203fd9def6d137402c212eb0"
116
116
  }
@@ -2,13 +2,13 @@ import '@testing-library/jest-dom'
2
2
  import { axe, toHaveNoViolations } from 'jest-axe'
3
3
  import { PktAccordion } from './Accordion'
4
4
  import { PktAccordionItem } from './AccordionItem'
5
- import { render, screen, waitFor } from '@testing-library/react'
5
+ import { render, screen, fireEvent } from '@testing-library/react'
6
6
  import React, { createRef } from 'react'
7
7
 
8
8
  expect.extend(toHaveNoViolations)
9
9
 
10
10
  describe('PktAccordion', () => {
11
- test('renders without errors', async () => {
11
+ test('renders without errors', () => {
12
12
  render(
13
13
  <PktAccordion>
14
14
  <PktAccordionItem id="render-id" title="test">
@@ -16,11 +16,10 @@ describe('PktAccordion', () => {
16
16
  </PktAccordionItem>
17
17
  </PktAccordion>,
18
18
  )
19
- await window.customElements.whenDefined('pkt-icon')
20
19
  // Assert that the Accordion component renders without throwing any errors
21
20
  })
22
21
 
23
- test('renders children', async () => {
22
+ test('renders children', () => {
24
23
  const mockToggleOpen = jest.fn()
25
24
  render(
26
25
  <PktAccordion>
@@ -32,7 +31,6 @@ describe('PktAccordion', () => {
32
31
  </PktAccordionItem>
33
32
  </PktAccordion>,
34
33
  )
35
- await window.customElements.whenDefined('pkt-icon')
36
34
 
37
35
  // Assert that the Accordion component renders its children correctly
38
36
  expect(screen.getByText('Title 1')).toBeInTheDocument()
@@ -41,12 +39,11 @@ describe('PktAccordion', () => {
41
39
  expect(screen.getByText('Content 2')).toBeInTheDocument()
42
40
  })
43
41
 
44
- test('applies compact and skin', async () => {
45
- const ref = createRef<any>()
42
+ test('applies compact and skin classes', () => {
46
43
  const { container } = render(
47
44
  <>
48
45
  <h3 id="accordion-heading">Accordion Heading</h3>
49
- <PktAccordion ref={ref}>
46
+ <PktAccordion compact skin="blue" ariaLabelledBy="accordion-heading" data-testid="accordion">
50
47
  <PktAccordionItem title="Title" id="item1">
51
48
  Content
52
49
  </PktAccordionItem>
@@ -54,48 +51,139 @@ describe('PktAccordion', () => {
54
51
  </>,
55
52
  )
56
53
 
57
- // Wait for the element to be defined
58
- await window.customElements.whenDefined('pkt-accordion')
59
- await window.customElements.whenDefined('pkt-accordion-item')
54
+ const accordion = container.querySelector('[data-testid="pkt-accordion"]')!
55
+ expect(accordion).toBeInTheDocument()
56
+ expect(accordion).toHaveClass('pkt-accordion')
57
+ expect(accordion).toHaveClass('pkt-accordion--compact')
58
+ expect(accordion).toHaveClass('pkt-accordion--blue')
59
+ expect(accordion).toHaveAttribute('aria-labelledby', 'accordion-heading')
60
+ })
60
61
 
61
- // Now manually set properties on the element
62
- if (ref.current) {
63
- ref.current.skin = 'blue'
64
- ref.current.compact = true
65
- await ref.current.updateComplete // Wait for Lit to update
66
- }
62
+ test('forwards ref correctly', () => {
63
+ const ref = createRef<HTMLDivElement>()
64
+ render(
65
+ <PktAccordion ref={ref}>
66
+ <PktAccordionItem title="Title" id="item1">
67
+ Content
68
+ </PktAccordionItem>
69
+ </PktAccordion>,
70
+ )
67
71
 
68
- const accordion = container.querySelector('pkt-accordion')!
69
- expect(accordion).toBeInTheDocument()
70
- expect(accordion).toHaveAttribute('compact') // compact reflected as boolean attribute
71
- expect(accordion).toHaveAttribute('skin', 'blue')
72
+ expect(ref.current).toBeDefined()
73
+ expect(ref.current?.tagName).toBe('DIV')
74
+ expect(ref.current).toHaveClass('pkt-accordion')
72
75
  })
73
76
  })
74
77
 
75
78
  describe('PktAccordionItem', () => {
76
- test('applies compact and skin', async () => {
77
- const ref = createRef<any>()
79
+ test('applies compact and skin classes', () => {
78
80
  const { container } = render(
79
- <>
80
- <PktAccordionItem ref={ref} title="Title" id="item1" skin="blue" compact>
81
- Content
82
- </PktAccordionItem>
83
- </>,
81
+ <PktAccordionItem title="Title" id="item1" skin="blue" compact>
82
+ Content
83
+ </PktAccordionItem>,
84
+ )
85
+
86
+ const details = container.querySelector('details')!
87
+ expect(details).toBeInTheDocument()
88
+ expect(details).toHaveClass('pkt-accordion-item')
89
+ expect(details).toHaveClass('pkt-accordion-item--blue')
90
+ expect(details).toHaveClass('pkt-accordion-item--compact')
91
+ })
92
+
93
+ test('renders with correct structure', () => {
94
+ const { container } = render(
95
+ <PktAccordionItem title="Test Title" id="test-item">
96
+ Test Content
97
+ </PktAccordionItem>,
98
+ )
99
+
100
+ const details = container.querySelector('details')!
101
+ const summary = details.querySelector('summary')!
102
+ const content = details.querySelector('.pkt-accordion-item__content')!
103
+ const contentInner = content.querySelector('.pkt-accordion-item__content-inner')!
104
+
105
+ expect(details).toHaveAttribute('id', 'test-item')
106
+ expect(summary).toHaveClass('pkt-accordion-item__title')
107
+ expect(summary).toHaveAttribute('id', 'pkt-accordion-item-summary-test-item')
108
+ expect(summary).toHaveTextContent('Test Title')
109
+ expect(content).toHaveAttribute('id', 'pkt-accordion-item__content-test-item')
110
+ expect(content).toHaveAttribute('role', 'region')
111
+ expect(contentInner).toHaveTextContent('Test Content')
112
+ })
113
+
114
+ test('handles defaultOpen prop', () => {
115
+ const { container } = render(
116
+ <PktAccordionItem title="Title" id="item1" defaultOpen>
117
+ Content
118
+ </PktAccordionItem>,
84
119
  )
85
120
 
86
- // Wait for the element to be defined
87
- await window.customElements.whenDefined('pkt-accordion-item')
121
+ const details = container.querySelector('details')!
122
+ expect(details).toHaveAttribute('open')
123
+ })
124
+
125
+ test('handles controlled isOpen prop', () => {
126
+ const { container, rerender } = render(
127
+ <PktAccordionItem title="Title" id="item1" isOpen={false}>
128
+ Content
129
+ </PktAccordionItem>,
130
+ )
131
+
132
+ const details = container.querySelector('details')!
133
+ expect(details).not.toHaveAttribute('open')
88
134
 
89
- // Now manually set properties on the element
90
- if (ref.current) {
91
- ref.current.skin = 'blue'
92
- ref.current.compact = true
93
- ref.current.requestUpdate()
94
- await ref.current.updateComplete
95
- }
135
+ rerender(
136
+ <PktAccordionItem title="Title" id="item1" isOpen={true}>
137
+ Content
138
+ </PktAccordionItem>,
139
+ )
140
+
141
+ expect(details).toHaveAttribute('open')
142
+ })
143
+
144
+ test('calls onClick handler', () => {
145
+ const mockOnClick = jest.fn()
146
+ const { container } = render(
147
+ <PktAccordionItem title="Title" id="item1" onClick={mockOnClick}>
148
+ Content
149
+ </PktAccordionItem>,
150
+ )
96
151
 
97
152
  const details = container.querySelector('details')!
98
- expect(details.classList.contains('pkt-accordion-item--blue')).toBe(true)
153
+ fireEvent.click(details)
154
+
155
+ // onClick is called after a setTimeout, so we need to wait
156
+ setTimeout(() => {
157
+ expect(mockOnClick).toHaveBeenCalledTimes(1)
158
+ }, 10)
159
+ })
160
+
161
+ test('forwards ref correctly', () => {
162
+ const ref = createRef<HTMLDetailsElement>()
163
+ render(
164
+ <PktAccordionItem ref={ref} title="Title" id="item1">
165
+ Content
166
+ </PktAccordionItem>,
167
+ )
168
+
169
+ expect(ref.current).toBeDefined()
170
+ expect(ref.current?.tagName).toBe('DETAILS')
171
+ expect(ref.current).toHaveClass('pkt-accordion-item')
172
+ })
173
+
174
+ test('renders PktIcon', async () => {
175
+ const { container } = render(
176
+ <PktAccordionItem title="Title" id="item1">
177
+ Content
178
+ </PktAccordionItem>,
179
+ )
180
+
181
+ await window.customElements.whenDefined('pkt-icon')
182
+ const icon = container.querySelector('pkt-icon')
183
+ expect(icon).toBeInTheDocument()
184
+ expect(icon).toHaveAttribute('name', 'chevron-thin-down')
185
+ expect(icon).toHaveClass('pkt-accordion-item__icon')
186
+ expect(icon).toHaveAttribute('aria-hidden', 'true')
99
187
  })
100
188
  })
101
189
 
@@ -111,7 +199,6 @@ describe('accessibility', () => {
111
199
  </PktAccordionItem>
112
200
  </PktAccordion>,
113
201
  )
114
- await window.customElements.whenDefined('pkt-icon')
115
202
  const results = await axe(container)
116
203
  expect(results).toHaveNoViolations()
117
204
  })
@@ -1,13 +1,10 @@
1
1
  'use client'
2
2
 
3
- import { PktElConstructor, PktElType } from '@/interfaces/IPktElements'
4
- import { createComponent } from '@lit/react'
5
- import React, { ForwardRefExoticComponent, LegacyRef } from 'react'
6
- import { FC, forwardRef } from 'react'
7
- import { PktAccordion as PktElAccordion } from '@oslokommune/punkt-elements'
8
- import { TPktAccordionSkin } from '@oslokommune/punkt-elements'
9
-
10
- export interface IPktAccordion extends PktElType {
3
+ import React, { ReactNode, forwardRef, createContext, useContext } from 'react'
4
+
5
+ export type TPktAccordionSkin = 'borderless' | 'outlined' | 'beige' | 'blue'
6
+
7
+ export interface IPktAccordion {
11
8
  compact?: boolean
12
9
  /**
13
10
  * @default skin: "borderless"
@@ -17,25 +14,40 @@ export interface IPktAccordion extends PktElType {
17
14
  * @description A unique identifier to connect the accordion with a heading
18
15
  */
19
16
  ariaLabelledBy?: string
20
- /**
21
- * @description A unique identifier to connect the accordion with a heading
22
- */
23
- ref?: LegacyRef<HTMLElement>
17
+ children?: ReactNode
18
+ name?: string
19
+ className?: string
24
20
  }
25
21
 
26
- const LitComponent: FC<IPktAccordion> = createComponent({
27
- tagName: 'pkt-accordion',
28
- elementClass: PktElAccordion as PktElConstructor<HTMLElement>,
29
- react: React,
30
- displayName: 'PktAccordion',
31
- }) as ForwardRefExoticComponent<IPktAccordion>
32
-
33
- export const PktAccordion: FC<IPktAccordion> = forwardRef(({ children, ...props }: IPktAccordion, ref) => {
34
- return (
35
- <LitComponent data-testid="pkt-accordion" {...props} ref={ref}>
36
- <div className="pkt-contents">{children}</div>
37
- </LitComponent>
38
- )
39
- })
22
+ // Send name som context til AccordionItem-children
23
+ const AccordionContext = createContext<{ name?: string }>({})
24
+
25
+ export const useAccordionContext = () => useContext(AccordionContext)
26
+
27
+ export const PktAccordion = forwardRef<HTMLDivElement, IPktAccordion>(
28
+ ({ compact = false, skin = 'borderless', ariaLabelledBy, children, name, className }, ref) => {
29
+ const accordionClasses = [
30
+ 'pkt-accordion',
31
+ compact && 'pkt-accordion--compact',
32
+ skin && `pkt-accordion--${skin}`,
33
+ className,
34
+ ]
35
+ .filter(Boolean)
36
+ .join(' ')
37
+
38
+ return (
39
+ <AccordionContext.Provider value={{ name }}>
40
+ <div
41
+ ref={ref}
42
+ className={accordionClasses}
43
+ data-testid="pkt-accordion"
44
+ aria-labelledby={ariaLabelledBy || undefined}
45
+ >
46
+ {children}
47
+ </div>
48
+ </AccordionContext.Provider>
49
+ )
50
+ },
51
+ )
40
52
 
41
53
  PktAccordion.displayName = 'PktAccordion'
@@ -1,38 +1,102 @@
1
1
  'use client'
2
2
 
3
- import React, { FC, ForwardRefExoticComponent, LegacyRef, ReactElement, forwardRef } from 'react'
4
- import { PktElConstructor, PktElType, PktEventWithTarget } from '@/interfaces/IPktElements'
5
- import { createComponent, EventName } from '@lit/react'
6
- import { PktAccordionItem as PktElAccordionItem } from '@oslokommune/punkt-elements'
7
- import { TPktAccordionSkin } from '@oslokommune/punkt-elements'
3
+ import React, { ReactNode, useState, useEffect, forwardRef } from 'react'
4
+ import { PktIcon } from '../icon/Icon'
5
+ import { useAccordionContext } from './Accordion'
8
6
 
9
- export interface IPktAccordionItem extends Omit<PktElType, 'onClick'> {
10
- compact?: boolean
11
- skin?: TPktAccordionSkin
12
- title?: string
7
+ export type TPktAccordionSkin = 'borderless' | 'outlined' | 'beige' | 'blue'
8
+
9
+ export interface IPktAccordionItem {
13
10
  defaultOpen?: boolean
11
+ id: string
12
+ title: string | ReactNode
13
+ skin?: TPktAccordionSkin
14
+ compact?: boolean
14
15
  isOpen?: boolean
15
- id?: string
16
- onClick?: (e: PktEventWithTarget) => void
17
- ref?: LegacyRef<HTMLElement>
16
+ children?: ReactNode
17
+ name?: string
18
+ className?: string
19
+ onClick?: (e: React.MouseEvent<HTMLDetailsElement>) => void
20
+ onToggle?: (e: React.SyntheticEvent<HTMLDetailsElement>) => void
18
21
  }
19
22
 
20
- const LitComponent: FC<IPktAccordionItem> = createComponent({
21
- tagName: 'pkt-accordion-item',
22
- elementClass: PktElAccordionItem as PktElConstructor<HTMLElement>,
23
- react: React,
24
- displayName: 'PktAccordionItem',
25
- events: {
26
- onClick: 'click' as EventName<PktEventWithTarget>,
27
- },
28
- }) as ForwardRefExoticComponent<IPktAccordionItem>
23
+ export const PktAccordionItem = forwardRef<HTMLDetailsElement, IPktAccordionItem>(
24
+ (
25
+ {
26
+ defaultOpen = false,
27
+ id,
28
+ title,
29
+ skin,
30
+ compact = false,
31
+ isOpen: controlledIsOpen,
32
+ children,
33
+ name,
34
+ className,
35
+ onClick,
36
+ onToggle,
37
+ },
38
+ ref,
39
+ ) => {
40
+ const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen)
41
+ const { name: contextName } = useAccordionContext()
42
+
43
+ // Bruk name fra props eller fra context
44
+ const actualName = name || contextName
45
+
46
+ // Bruk controlled isOpen om prop, ellers bruk intern state
47
+ const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen
48
+
49
+ useEffect(() => {
50
+ if (controlledIsOpen === undefined) {
51
+ setInternalIsOpen(defaultOpen)
52
+ }
53
+ }, [defaultOpen, controlledIsOpen])
54
+
55
+ const handleToggle = (e: React.SyntheticEvent<HTMLDetailsElement>) => {
56
+ const detailsElement = e.currentTarget
57
+ const newOpenState = detailsElement.open
58
+
59
+ if (controlledIsOpen === undefined) {
60
+ setInternalIsOpen(newOpenState)
61
+ }
62
+
63
+ onToggle?.(e)
64
+ }
65
+
66
+ const handleClick = (e: React.MouseEvent<HTMLDetailsElement>) => {
67
+ // La native toggle skje først, så trigge onClick
68
+ setTimeout(() => {
69
+ onClick?.(e)
70
+ }, 0)
71
+ }
72
+
73
+ const accordionItemClass = [
74
+ 'pkt-accordion-item',
75
+ compact && 'pkt-accordion-item--compact',
76
+ skin && `pkt-accordion-item--${skin}`,
77
+ className,
78
+ ]
79
+ .filter(Boolean)
80
+ .join(' ')
29
81
 
30
- export const PktAccordionItem: FC<IPktAccordionItem> = forwardRef(
31
- ({ children, ...props }: Omit<IPktAccordionItem, 'ref'>, ref): ReactElement => {
32
82
  return (
33
- <LitComponent ref={ref} {...props}>
34
- <div className="pkt-contents">{children}</div>
35
- </LitComponent>
83
+ <details
84
+ ref={ref}
85
+ className={accordionItemClass}
86
+ id={id}
87
+ open={isOpen}
88
+ onClick={handleClick}
89
+ name={actualName}
90
+ onToggle={handleToggle}
91
+ >
92
+ <summary className="pkt-accordion-item__title" id={`pkt-accordion-item-summary-${id}`}>
93
+ {title}
94
+ <PktIcon name="chevron-thin-down" className="pkt-accordion-item__icon" aria-hidden="true" />
95
+ </summary>
96
+ <div className="pkt-accordion-item__content" id={`pkt-accordion-item__content-${id}`} role="region">
97
+ <div className="pkt-accordion-item__content-inner">{children}</div>
98
+ </div>
99
+ </details>
36
100
  )
37
101
  },
38
102
  )
@@ -1,11 +1,11 @@
1
- import React, { ChangeEventHandler, ForwardedRef, forwardRef } from 'react'
1
+ import React, { ChangeEventHandler, ForwardedRef, forwardRef, ReactNode } from 'react'
2
2
 
3
3
  export interface IPktCheckbox extends React.InputHTMLAttributes<HTMLInputElement> {
4
4
  id: string
5
5
  hasTile?: boolean
6
6
  disabled?: boolean
7
7
  label?: string
8
- checkHelptext?: string
8
+ checkHelptext?: ReactNode | ReactNode[] | string
9
9
  hasError?: boolean
10
10
  defaultChecked?: boolean
11
11
  checked?: boolean
@@ -1,4 +1,4 @@
1
- import React, { ChangeEventHandler, ForwardedRef, forwardRef } from 'react'
1
+ import React, { ChangeEventHandler, ForwardedRef, forwardRef, ReactNode } from 'react'
2
2
 
3
3
  export interface IPktRadioButton extends React.InputHTMLAttributes<HTMLInputElement> {
4
4
  id: string
@@ -6,7 +6,7 @@ export interface IPktRadioButton extends React.InputHTMLAttributes<HTMLInputElem
6
6
  label: string
7
7
  hasTile?: boolean
8
8
  disabled?: boolean
9
- checkHelptext?: string | React.ReactNode | React.ReactNode[]
9
+ checkHelptext?: ReactNode | ReactNode[] | string
10
10
  defaultChecked?: boolean
11
11
  checked?: boolean
12
12
  hasError?: boolean