@oslokommune/punkt-react 16.5.0 → 16.6.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.5.0",
3
+ "version": "16.6.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.5.0",
42
+ "@oslokommune/punkt-elements": "^16.6.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.4.0",
53
+ "@oslokommune/punkt-css": "^16.5.1",
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": "81940eb91344498b82fc74316684b452f9fcc82c"
112
+ "gitHead": "a5df0ac2434d55a4c73421d970a55cc9329fec0e"
113
113
  }
@@ -1,7 +1,6 @@
1
1
  import '@testing-library/jest-dom'
2
2
 
3
3
  import { render, screen } from '@testing-library/react'
4
- import exp from 'constants'
5
4
  import { axe, toHaveNoViolations } from 'jest-axe'
6
5
  import { createRef } from 'react'
7
6
 
@@ -11,11 +10,10 @@ expect.extend(toHaveNoViolations)
11
10
 
12
11
  describe('PktProgressbar', () => {
13
12
  describe('rendering', () => {
14
- test('renders with default props', async () => {
13
+ test('renders with default props', () => {
15
14
  const { container } = render(<PktProgressbar id="progress1" valueCurrent={50} />)
16
- await window.customElements.whenDefined('pkt-progressbar')
17
15
 
18
- const progressbar = container.querySelector('pkt-progressbar')
16
+ const progressbar = container.firstChild as HTMLElement
19
17
 
20
18
  expect(progressbar).toBeInTheDocument()
21
19
  expect(progressbar).toHaveAttribute('id', 'progress1')
@@ -27,119 +25,124 @@ describe('PktProgressbar', () => {
27
25
  expect(bar).toBeInTheDocument()
28
26
  })
29
27
 
30
- test('renders with custom skin', async () => {
28
+ test('renders with custom skin', () => {
31
29
  const { container } = render(<PktProgressbar id="progress2" valueCurrent={50} skin="red" />)
32
- await window.customElements.whenDefined('pkt-progressbar')
33
30
 
34
- const progressbar = container.querySelector('pkt-progressbar')
31
+ const progressbar = container.firstChild as HTMLElement
35
32
  const bar = progressbar?.querySelector('.pkt-progressbar__bar--red')
36
33
  expect(bar).toBeInTheDocument()
37
34
  })
38
35
 
39
- test('renders title correctly', async () => {
40
- const { container, getByText } = render(<PktProgressbar id="progress3" valueCurrent={50} title="Progress" />)
41
- await window.customElements.whenDefined('pkt-progressbar')
36
+ test('renders title correctly', () => {
37
+ const { container, getByText } = render(
38
+ <PktProgressbar id="progress3" valueCurrent={50} title="Progress" />,
39
+ )
42
40
 
43
- const progressbar = container.querySelector('pkt-progressbar')
41
+ const progressbar = container.firstChild as HTMLElement
44
42
 
45
- // Select by class
46
43
  const title = progressbar?.querySelector('.pkt-progressbar__title')
47
44
  expect(title).toBeInTheDocument()
48
45
 
49
- // Select by text
50
46
  const titleText = getByText('Progress')
51
47
  expect(titleText).toBeInTheDocument()
52
48
  })
53
49
 
54
- test('renders title with center position', async () => {
50
+ test('renders title with center position', () => {
55
51
  const { container } = render(
56
52
  <PktProgressbar id="progress4" valueCurrent={50} title="Progress" titlePosition="center" />,
57
53
  )
58
- await window.customElements.whenDefined('pkt-progressbar')
59
54
  const title = container.querySelector('.pkt-progressbar__title-center')
60
55
  expect(title).toBeInTheDocument()
61
56
  })
62
57
 
63
- test('renders status as percentage', async () => {
64
- const { container } = render(<PktProgressbar id="progress5" valueCurrent={50} statusType="percentage" />)
65
- await window.customElements.whenDefined('pkt-progressbar')
58
+ test('renders status as percentage', () => {
59
+ const { container } = render(
60
+ <PktProgressbar id="progress5" valueCurrent={50} statusType="percentage" />,
61
+ )
66
62
  const status = container.querySelector('.pkt-progressbar__status')
67
63
  expect(status).toBeInTheDocument()
68
64
  expect(status).toHaveTextContent('50%')
69
65
  })
70
66
 
71
- test('renders status as fraction', async () => {
67
+ test('renders status as fraction', () => {
72
68
  const { container } = render(
73
69
  <PktProgressbar id="progress6" valueCurrent={50} valueMax={200} statusType="fraction" />,
74
70
  )
75
- await window.customElements.whenDefined('pkt-progressbar')
76
71
  const status = container.querySelector('.pkt-progressbar__status')
77
72
  expect(status).toBeInTheDocument()
78
73
  expect(status).toHaveTextContent('50 av 200')
79
74
  })
80
75
 
81
- test('renders status with center placement', async () => {
76
+ test('renders status with center placement', () => {
82
77
  const { container } = render(
83
- <PktProgressbar id="progress7" valueCurrent={50} statusType="percentage" statusPlacement="center" />,
78
+ <PktProgressbar
79
+ id="progress7"
80
+ valueCurrent={50}
81
+ statusType="percentage"
82
+ statusPlacement="center"
83
+ />,
84
84
  )
85
- await window.customElements.whenDefined('pkt-progressbar')
86
85
  const status = container.querySelector('.pkt-progressbar__status--center')
87
86
  expect(status).toBeInTheDocument()
88
87
  })
89
88
 
90
- test('renders status with following placement', async () => {
89
+ test('renders status with following placement', () => {
91
90
  const { container } = render(
92
- <PktProgressbar id="progress8" valueCurrent={50} statusType="percentage" statusPlacement="following" />,
91
+ <PktProgressbar
92
+ id="progress8"
93
+ valueCurrent={50}
94
+ statusType="percentage"
95
+ statusPlacement="following"
96
+ />,
93
97
  )
94
- await window.customElements.whenDefined('pkt-progressbar')
95
98
  const status = container.querySelector('.pkt-progressbar__status-placement--following')
96
99
  expect(status).toBeInTheDocument()
97
100
  })
98
101
 
99
- test('handles aria-label correctly', async () => {
100
- const { container } = render(<PktProgressbar id="progress9" valueCurrent={50} ariaLabel="Progress bar" />)
101
- await window.customElements.whenDefined('pkt-progressbar')
102
- const progressbar = container.querySelector('pkt-progressbar')
102
+ test('handles aria-label correctly', () => {
103
+ const { container } = render(
104
+ <PktProgressbar id="progress9" valueCurrent={50} ariaLabel="Progress bar" />,
105
+ )
106
+ const progressbar = container.firstChild as HTMLElement
103
107
  expect(progressbar).toHaveAttribute('aria-label', 'Progress bar')
104
108
  expect(progressbar).not.toHaveAttribute('aria-labelledby')
105
109
  })
106
110
 
107
- test('handles role meter correctly', async () => {
111
+ test('handles role meter correctly', () => {
108
112
  const { container } = render(
109
113
  <PktProgressbar id="progress10" role="meter" valueCurrent={50} ariaLabel="Meter bar" />,
110
114
  )
111
- await window.customElements.whenDefined('pkt-progressbar')
112
- const progressbar = container.querySelector('pkt-progressbar')
115
+ const progressbar = container.firstChild as HTMLElement
113
116
  expect(progressbar).toHaveAttribute('role', 'meter')
114
117
  })
115
118
 
116
- test('handles id for title and aria-labelledby correctly', async () => {
117
- const { container } = render(<PktProgressbar valueCurrent={50} title="Progress" id="progressId" />)
118
- await window.customElements.whenDefined('pkt-progressbar')
119
+ test('handles id for title and aria-labelledby correctly', () => {
120
+ const { container } = render(
121
+ <PktProgressbar valueCurrent={50} title="Progress" id="progressId" />,
122
+ )
119
123
 
120
- const progressbar = container.querySelector('pkt-progressbar')
124
+ const progressbar = container.firstChild as HTMLElement
121
125
 
122
126
  expect(progressbar).toBeInTheDocument()
123
127
  expect(progressbar).toHaveAttribute('aria-labelledby', 'progressId-title')
124
128
  })
125
129
 
126
- test('generates id if none is provided', async () => {
130
+ test('generates id if none is provided', () => {
127
131
  const { container } = render(<PktProgressbar valueCurrent={50} />)
128
- await window.customElements.whenDefined('pkt-progressbar')
129
132
 
130
- const progressbar = container.querySelector('pkt-progressbar')
133
+ const progressbar = container.firstChild as HTMLElement
131
134
 
132
135
  const id = progressbar?.getAttribute('id')
133
136
  expect(id).toHaveLength(36)
134
137
  })
135
138
 
136
- test('sets ref correctly', async () => {
137
- const ref = createRef<HTMLElement>()
138
- const { unmount } = render(<PktProgressbar ref={ref} id="progress-ref" valueCurrent={50} title="Ref bar" />)
139
-
140
- await window.customElements.whenDefined('pkt-progressbar')
139
+ test('sets ref correctly', () => {
140
+ const ref = createRef<HTMLDivElement>()
141
+ const { unmount } = render(
142
+ <PktProgressbar ref={ref} id="progress-ref" valueCurrent={50} title="Ref bar" />,
143
+ )
141
144
 
142
- expect(ref.current).toBeInstanceOf(HTMLElement)
145
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
143
146
  unmount()
144
147
  expect(ref.current).toBeNull()
145
148
  })
@@ -152,7 +155,6 @@ describe('PktProgressbar', () => {
152
155
  const { container } = render(
153
156
  <PktProgressbar id="progress-axe" valueCurrent={50} title="Axe test" role="progressbar" />,
154
157
  )
155
- await window.customElements.whenDefined('pkt-progressbar')
156
158
  const results = await axe(container)
157
159
  expect(results).toHaveNoViolations()
158
160
  })
@@ -1,31 +1,146 @@
1
1
  'use client'
2
2
 
3
- import { createComponent } from '@lit/react'
4
- import { type IPktProgressbar as IPktElProgressbar, PktProgressbar as PktElProgressbar } from '@oslokommune/punkt-elements'
5
- // eslint-disable-next-line no-restricted-syntax -- React is required for createComponent
6
- import React, { ForwardedRef, forwardRef, type ReactElement } from 'react'
3
+ import classNames from 'classnames'
4
+ import { forwardRef, HTMLAttributes, Ref, useEffect, useMemo, useRef, useState } from 'react'
5
+ import type {
6
+ TAriaLive,
7
+ TProgressbarRole,
8
+ TProgressbarSkin,
9
+ TProgressbarStatusPlacement,
10
+ TProgressbarStatusType,
11
+ TProgressbarTitlePosition,
12
+ } from 'shared-types'
13
+ import { uuidish } from 'shared-utils/utils'
7
14
 
8
- import type { PktElConstructor, PktElType } from '@/interfaces/IPktElements'
15
+ export type { TProgressbarRole, TProgressbarSkin, TProgressbarStatusPlacement, TProgressbarStatusType, TProgressbarTitlePosition }
9
16
 
10
- type ExtendedProgressbar = IPktElProgressbar & PktElType
17
+ export interface IPktProgressbar extends Omit<HTMLAttributes<HTMLDivElement>, 'role' | 'title'> {
18
+ ariaLabel?: string | null
19
+ ariaLabelledby?: string | null
20
+ ariaLive?: TAriaLive
21
+ ariaValueText?: string | null
22
+ id?: string
23
+ role?: TProgressbarRole
24
+ skin?: TProgressbarSkin
25
+ statusPlacement?: TProgressbarStatusPlacement
26
+ statusType?: TProgressbarStatusType
27
+ title?: string | null
28
+ titlePosition?: TProgressbarTitlePosition
29
+ valueCurrent: number
30
+ valueMax?: number
31
+ valueMin?: number
32
+ ref?: Ref<HTMLDivElement>
33
+ }
11
34
 
12
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
13
- export interface IPktProgressbar extends ExtendedProgressbar {}
35
+ export const PktProgressbar = forwardRef<HTMLDivElement, IPktProgressbar>(
36
+ (
37
+ {
38
+ ariaLabel,
39
+ ariaLabelledby,
40
+ ariaLive = 'polite',
41
+ ariaValueText,
42
+ id,
43
+ role = 'progressbar',
44
+ skin = 'dark-blue',
45
+ statusPlacement = 'following',
46
+ statusType = 'none',
47
+ title,
48
+ titlePosition = 'left',
49
+ valueCurrent = 0,
50
+ valueMax = 100,
51
+ valueMin = 0,
52
+ className,
53
+ ...props
54
+ },
55
+ ref,
56
+ ) => {
57
+ const generatedId = useMemo(() => uuidish(), [])
58
+ const progressbarId = id || generatedId
14
59
 
15
- const LitComponent = createComponent({
16
- tagName: 'pkt-progressbar',
17
- elementClass: PktElProgressbar as PktElConstructor<HTMLElement>,
18
- react: React,
19
- displayName: 'PktProgressbar',
20
- events: {},
21
- })
60
+ const labelRef = useRef<HTMLSpanElement>(null)
61
+ const [labelWidth, setLabelWidth] = useState(0)
62
+
63
+ useEffect(() => {
64
+ if (labelRef.current) {
65
+ setLabelWidth(labelRef.current.getBoundingClientRect().width || 0)
66
+ }
67
+ }, [valueCurrent])
68
+
69
+ const totalSteps = valueMax - valueMin
70
+ const currentPercentage =
71
+ statusType === 'fraction'
72
+ ? Math.round((valueCurrent / totalSteps) * 100)
73
+ : Math.round(((valueCurrent - valueMin) / totalSteps) * 100)
74
+
75
+ const formattedTitle = `${valueCurrent} av ${totalSteps}`
76
+
77
+ const computedAriaValueText =
78
+ statusType === 'fraction' && !ariaValueText
79
+ ? `${valueCurrent} av ${valueMax - valueMin}`
80
+ : ariaValueText || undefined
81
+
82
+ const computedAriaLabelledby = !ariaLabel
83
+ ? ariaLabelledby || `${progressbarId}-title`
84
+ : undefined
22
85
 
23
- export const PktProgressbar = forwardRef(
24
- ({ children, ...props }: IPktProgressbar, ref: ForwardedRef<HTMLElement>): ReactElement => {
25
86
  return (
26
- <LitComponent ref={ref} {...props}>
27
- <div className="pkt-contents">{children}</div>
28
- </LitComponent>
87
+ <div
88
+ ref={ref}
89
+ id={progressbarId}
90
+ role={role}
91
+ aria-live={ariaLive}
92
+ aria-atomic="true"
93
+ aria-valuenow={valueCurrent}
94
+ aria-valuemin={valueMin}
95
+ aria-valuemax={valueMax}
96
+ aria-valuetext={computedAriaValueText}
97
+ aria-label={ariaLabel || undefined}
98
+ aria-labelledby={computedAriaLabelledby}
99
+ className={classNames('pkt-progressbar', className)}
100
+ {...props}
101
+ >
102
+ <div
103
+ className="pkt-progressbar__container"
104
+ style={{
105
+ '--pkt-progress-label-width': `${labelWidth}px`,
106
+ '--pkt-progress-width': `${currentPercentage}%`,
107
+ } as React.CSSProperties}
108
+ >
109
+ {title && (
110
+ <p
111
+ id={`${progressbarId}-title`}
112
+ className={classNames('pkt-progressbar__title', {
113
+ 'pkt-progressbar__title-center': titlePosition === 'center',
114
+ })}
115
+ >
116
+ {title}
117
+ </p>
118
+ )}
119
+
120
+ <div className="pkt-progressbar__bar-wrapper">
121
+ <div className={classNames('pkt-progressbar__bar', `pkt-progressbar__bar--${skin}`)} />
122
+ </div>
123
+
124
+ {statusType !== 'none' && (
125
+ <div
126
+ className={classNames('pkt-progressbar__status', {
127
+ 'pkt-progressbar__status--center': statusPlacement === 'center',
128
+ })}
129
+ >
130
+ <span
131
+ ref={labelRef}
132
+ className={classNames({
133
+ 'pkt-progressbar__status-placement--following': statusPlacement === 'following',
134
+ 'pkt-progressbar__status-placement--center': statusPlacement === 'center',
135
+ 'pkt-progressbar__status-placement--left': statusPlacement === 'left',
136
+ })}
137
+ >
138
+ {statusType === 'percentage' ? `${currentPercentage}%` : formattedTitle}
139
+ </span>
140
+ </div>
141
+ )}
142
+ </div>
143
+ </div>
29
144
  )
30
145
  },
31
146
  )