@oslokommune/punkt-react 16.17.3 → 16.18.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/CHANGELOG.md +36 -0
- package/dist/index.d.ts +9 -13
- package/dist/punkt-react.es.js +16417 -16340
- package/dist/punkt-react.umd.js +569 -569
- package/dist/shared-types/index.d.ts +1 -1
- package/dist/shared-types/skin.d.ts +4 -0
- package/package.json +4 -4
- package/src/components/accordion/Accordion.test.tsx +5 -4
- package/src/components/alert/Alert.tsx +2 -1
- package/src/components/button/Button.test.tsx +5 -5
- package/src/components/card/Card.tsx +1 -3
- package/src/components/fileupload/DropZone.tsx +1 -1
- package/src/components/fileupload/QueueItemContent.tsx +1 -1
- package/src/components/fileupload/Subcomponents.tsx +3 -1
- package/src/components/header-menu/HeaderMenu.test.tsx +4 -6
- package/src/components/icon/Icon.test.tsx +68 -3
- package/src/components/icon/Icon.tsx +172 -13
- package/src/components/link/Link.tsx +1 -1
- package/src/components/linkcard/LinkCard.tsx +1 -1
- package/src/components/messagebox/Messagebox.tsx +1 -1
- package/src/components/tabs/TabItem.tsx +4 -4
- package/src/components/tabs/Tabs.tsx +3 -3
- package/src/components/tag/Tag.tsx +4 -2
|
@@ -9,7 +9,7 @@ export { DEFAULT_HEADER_FOOTER_URL } from './header-menu';
|
|
|
9
9
|
export type { TLayout, TVerticalPosition, THorizontalPosition, TPosition, TPositionWithCenter, } from './layout';
|
|
10
10
|
export type { THTMLButtonType, TSelectionMode } from './form';
|
|
11
11
|
export type { TSize3, TSize5, TSize7 } from './size';
|
|
12
|
-
export type { TAlertSkin, TMessageboxSkin, TAccordionSkin, TCardSkin } from './skin';
|
|
12
|
+
export type { TAlertSkin, TMessageboxSkin, TAccordionSkin, TCardSkin, TTagSkin } from './skin';
|
|
13
13
|
export type { IDateConstraints, ICalendarGridDimensions, IGridCellPosition, TDateRangeMap, ISelectionState, ISelectionOptions, } from './calendar';
|
|
14
14
|
export type { IDatepickerStrings } from './datepicker';
|
|
15
15
|
export { defaultSingleStrings, defaultRangeStrings } from './datepicker';
|
|
@@ -22,3 +22,7 @@ export type TAccordionSkin = 'borderless' | 'outlined' | 'beige' | 'blue' | 'plu
|
|
|
22
22
|
* Card component skin variants
|
|
23
23
|
*/
|
|
24
24
|
export type TCardSkin = 'outlined' | 'outlined-beige' | 'gray' | 'beige' | 'green' | 'blue';
|
|
25
|
+
/**
|
|
26
|
+
* Tag component skin variants
|
|
27
|
+
*/
|
|
28
|
+
export type TTagSkin = 'blue' | 'blue-light' | 'blue-dark' | 'green' | 'red' | 'beige' | 'yellow' | 'grey' | 'gray';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oslokommune/punkt-react",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.18.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.
|
|
42
|
+
"@oslokommune/punkt-elements": "^16.18.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.16.1",
|
|
53
|
-
"@oslokommune/punkt-css": "^16.
|
|
53
|
+
"@oslokommune/punkt-css": "^16.18.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": "
|
|
112
|
+
"gitHead": "693c10d8d7a30c6b5c8acb6a7cb072b0afe50554"
|
|
113
113
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import '@testing-library/jest-dom'
|
|
2
2
|
|
|
3
|
-
import { fireEvent,render, screen } from '@testing-library/react'
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
4
4
|
import { axe, toHaveNoViolations } from 'jest-axe'
|
|
5
5
|
import { vi } from 'vitest'
|
|
6
6
|
import { createRef } from 'react'
|
|
@@ -181,12 +181,13 @@ describe('PktAccordionItem', () => {
|
|
|
181
181
|
</PktAccordionItem>,
|
|
182
182
|
)
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
const icon = container.querySelector('pkt-icon')
|
|
184
|
+
const icon = container.querySelector('.pkt-icon')
|
|
186
185
|
expect(icon).toBeInTheDocument()
|
|
187
186
|
expect(icon).toHaveAttribute('name', 'chevron-thin-down')
|
|
188
187
|
expect(icon).toHaveClass('pkt-accordion-item__icon')
|
|
189
|
-
|
|
188
|
+
await waitFor(() =>
|
|
189
|
+
expect(icon?.querySelector('svg')).toHaveAttribute('aria-hidden', 'true'),
|
|
190
|
+
)
|
|
190
191
|
})
|
|
191
192
|
})
|
|
192
193
|
|
|
@@ -4,7 +4,8 @@ import classNames from 'classnames'
|
|
|
4
4
|
import { FC, HTMLAttributes, ReactElement, ReactNode, useCallback, useState } from 'react'
|
|
5
5
|
import type { TAlertRole, TAlertSkin,TAriaLive } from 'shared-types'
|
|
6
6
|
|
|
7
|
-
import { PktButton
|
|
7
|
+
import { PktButton } from '../button/Button'
|
|
8
|
+
import { PktIcon } from '../icon/Icon'
|
|
8
9
|
|
|
9
10
|
export interface IPktAlert extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
10
11
|
skin?: TAlertSkin
|
|
@@ -141,7 +141,7 @@ describe('PktButton icon paths', () => {
|
|
|
141
141
|
)
|
|
142
142
|
|
|
143
143
|
const icon = document.querySelector('.pkt-btn__icon:not(.pkt-btn__spinner)') as HTMLElement & { path?: string }
|
|
144
|
-
expect(icon?.path).toBe(customPath)
|
|
144
|
+
expect(icon?.dataset.path).toBe(customPath)
|
|
145
145
|
})
|
|
146
146
|
|
|
147
147
|
test('does not set path property when iconPath is not provided', () => {
|
|
@@ -153,7 +153,7 @@ describe('PktButton icon paths', () => {
|
|
|
153
153
|
|
|
154
154
|
const icon = document.querySelector('.pkt-btn__icon:not(.pkt-btn__spinner)') as HTMLElement & { path?: string }
|
|
155
155
|
// When not provided, path uses PktIcon's default
|
|
156
|
-
expect(icon?.path).toBe('https://punkt-cdn.oslo.kommune.no/latest/icons/')
|
|
156
|
+
expect(icon?.dataset.path).toBe('https://punkt-cdn.oslo.kommune.no/latest/icons/')
|
|
157
157
|
})
|
|
158
158
|
|
|
159
159
|
test('passes secondIconPath to second PktIcon when provided', () => {
|
|
@@ -168,7 +168,7 @@ describe('PktButton icon paths', () => {
|
|
|
168
168
|
HTMLElement & { path?: string }
|
|
169
169
|
>
|
|
170
170
|
expect(icons).toHaveLength(2)
|
|
171
|
-
expect(icons[1]?.path).toBe(customPath)
|
|
171
|
+
expect(icons[1]?.dataset.path).toBe(customPath)
|
|
172
172
|
})
|
|
173
173
|
|
|
174
174
|
test('handles both iconPath and secondIconPath independently', () => {
|
|
@@ -190,7 +190,7 @@ describe('PktButton icon paths', () => {
|
|
|
190
190
|
HTMLElement & { path?: string }
|
|
191
191
|
>
|
|
192
192
|
expect(icons).toHaveLength(2)
|
|
193
|
-
expect(icons[0]?.path).toBe(iconPath)
|
|
194
|
-
expect(icons[1]?.path).toBe(secondIconPath)
|
|
193
|
+
expect(icons[0]?.dataset.path).toBe(iconPath)
|
|
194
|
+
expect(icons[1]?.dataset.path).toBe(secondIconPath)
|
|
195
195
|
})
|
|
196
196
|
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import classNames from 'classnames'
|
|
4
4
|
import { forwardRef, HTMLAttributes, ReactNode, Ref } from 'react'
|
|
5
|
-
import type { TCardSkin, TLayout } from 'shared-types'
|
|
5
|
+
import type { TCardSkin, TLayout, TTagSkin } from 'shared-types'
|
|
6
6
|
|
|
7
7
|
import { PktTag } from '../tag/Tag'
|
|
8
8
|
|
|
@@ -12,8 +12,6 @@ type TCardImageShape = 'square' | 'round'
|
|
|
12
12
|
type TCardTagPosition = 'top' | 'bottom'
|
|
13
13
|
type THeadingLevel = 1 | 2 | 3 | 4 | 5 | 6
|
|
14
14
|
|
|
15
|
-
type TTagSkin = 'blue' | 'blue-light' | 'blue-dark' | 'green' | 'red' | 'beige' | 'yellow' | 'grey' | 'gray'
|
|
16
|
-
|
|
17
15
|
interface ICardTag {
|
|
18
16
|
skin?: TTagSkin
|
|
19
17
|
iconName?: string
|
|
@@ -2,7 +2,9 @@ import { RefObject, useEffect, useState } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { getDisplayFilename, trapTabInside } from 'shared-utils/fileupload'
|
|
4
4
|
|
|
5
|
-
import { PktButton
|
|
5
|
+
import { PktButton } from '../button/Button'
|
|
6
|
+
import { PktIcon } from '../icon/Icon'
|
|
7
|
+
import { PktProgressbar } from '../progressbar/Progressbar'
|
|
6
8
|
import { PktModal } from '../modal/Modal'
|
|
7
9
|
import { Truncate } from './Truncate'
|
|
8
10
|
import { TFileAndTransfer, TQueueItemOperation, TQueueOperationContext, TTransferItemInProgress } from './types'
|
|
@@ -81,8 +81,7 @@ describe('PktHeaderMenu rendering', () => {
|
|
|
81
81
|
|
|
82
82
|
test('maps ODS icon names on service links', async () => {
|
|
83
83
|
const { container } = render(<PktHeaderMenu data={LOCAL_DATA} open />)
|
|
84
|
-
|
|
85
|
-
const icons = container.querySelectorAll<HTMLElement>('.pkt-header-menu__service-icon')
|
|
84
|
+
const icons = container.querySelectorAll<HTMLElement>('.pkt-header-menu__service-icon')
|
|
86
85
|
await waitFor(() => expect(icons[0]).toHaveAttribute('name', '24h'))
|
|
87
86
|
expect(icons[1]).toHaveAttribute('name', 'shield-fire')
|
|
88
87
|
// Unmapped passes through
|
|
@@ -123,15 +122,14 @@ describe('PktHeaderMenu rendering', () => {
|
|
|
123
122
|
|
|
124
123
|
test('renders social links with derived aria-labels and icons', async () => {
|
|
125
124
|
const { container } = render(<PktHeaderMenu data={LOCAL_DATA} open />)
|
|
126
|
-
|
|
127
|
-
const social = container.querySelectorAll<HTMLElement>('.pkt-header-menu__social-link')
|
|
125
|
+
const social = container.querySelectorAll<HTMLElement>('.pkt-header-menu__social-link')
|
|
128
126
|
expect(social).toHaveLength(2)
|
|
129
127
|
expect(social[0]).toHaveAttribute('aria-label', 'Facebook')
|
|
130
128
|
expect(social[1]).toHaveAttribute('aria-label', 'LinkedIn')
|
|
131
129
|
await waitFor(() =>
|
|
132
|
-
expect(social[0].querySelector('pkt-icon')).toHaveAttribute('name', 'facebook'),
|
|
130
|
+
expect(social[0].querySelector('.pkt-icon')).toHaveAttribute('name', 'facebook'),
|
|
133
131
|
)
|
|
134
|
-
expect(social[1].querySelector('pkt-icon')).toHaveAttribute('name', 'linkedin')
|
|
132
|
+
expect(social[1].querySelector('.pkt-icon')).toHaveAttribute('name', 'linkedin')
|
|
135
133
|
})
|
|
136
134
|
|
|
137
135
|
test('calls onDataLoaded with the supplied payload', async () => {
|
|
@@ -21,11 +21,11 @@ describe('PktIcon', () => {
|
|
|
21
21
|
describe('with default fetcher', () => {
|
|
22
22
|
test('fetches SVG with default fetcher', async () => {
|
|
23
23
|
await render(<PktIcon name="arrow" />)
|
|
24
|
-
await window.customElements.whenDefined('pkt-icon')
|
|
25
24
|
|
|
26
|
-
const icon = await screen.
|
|
25
|
+
const icon = await screen.findByTestId('icon')
|
|
27
26
|
expect(icon).toMatchInlineSnapshot(`
|
|
28
27
|
<svg
|
|
28
|
+
aria-hidden="true"
|
|
29
29
|
data-filename="stiarrow.svg"
|
|
30
30
|
data-testid="icon"
|
|
31
31
|
role="img"
|
|
@@ -43,7 +43,6 @@ describe('PktIcon', () => {
|
|
|
43
43
|
<PktIcon name={'some-icon'} />
|
|
44
44
|
</div>,
|
|
45
45
|
)
|
|
46
|
-
await window.customElements.whenDefined('pkt-icon')
|
|
47
46
|
|
|
48
47
|
await waitFor(async () => {
|
|
49
48
|
const icons = await screen.findAllByTestId('icon')
|
|
@@ -51,4 +50,70 @@ describe('PktIcon', () => {
|
|
|
51
50
|
})
|
|
52
51
|
})
|
|
53
52
|
})
|
|
53
|
+
|
|
54
|
+
describe('aria-hidden', () => {
|
|
55
|
+
test('defaults to true on the SVG', async () => {
|
|
56
|
+
await render(<PktIcon name="arrow" />)
|
|
57
|
+
const svg = await screen.findByTestId('icon')
|
|
58
|
+
|
|
59
|
+
expect(svg).toHaveAttribute('aria-hidden', 'true')
|
|
60
|
+
expect(svg).not.toHaveAttribute('aria-label')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('ariaHidden={false} sets aria-hidden="false" and aria-label fallback to name', async () => {
|
|
64
|
+
await render(<PktIcon name="arrow" ariaHidden={false} />)
|
|
65
|
+
const svg = await screen.findByTestId('icon')
|
|
66
|
+
|
|
67
|
+
expect(svg).toHaveAttribute('aria-hidden', 'false')
|
|
68
|
+
expect(svg).toHaveAttribute('aria-label', 'arrow')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('ariaHidden="false" string is treated as boolean false', async () => {
|
|
72
|
+
await render(<PktIcon name="arrow" ariaHidden="false" />)
|
|
73
|
+
const svg = await screen.findByTestId('icon')
|
|
74
|
+
|
|
75
|
+
expect(svg).toHaveAttribute('aria-hidden', 'false')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('ariaHidden="true" string is treated as boolean true', async () => {
|
|
79
|
+
await render(<PktIcon name="arrow" ariaHidden="true" />)
|
|
80
|
+
const svg = await screen.findByTestId('icon')
|
|
81
|
+
|
|
82
|
+
expect(svg).toHaveAttribute('aria-hidden', 'true')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('back-compat: aria-hidden kebab attribute still routes to the SVG', async () => {
|
|
86
|
+
const { container } = await render(<PktIcon name="arrow" aria-hidden={false} />)
|
|
87
|
+
const svg = await screen.findByTestId('icon')
|
|
88
|
+
|
|
89
|
+
expect(svg).toHaveAttribute('aria-hidden', 'false')
|
|
90
|
+
// Wrapper span itself should not carry aria-hidden
|
|
91
|
+
expect(container.querySelector('.pkt-icon')).not.toHaveAttribute('aria-hidden')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('aria-label', () => {
|
|
96
|
+
test('ariaLabel alone makes aria-hidden default to false', async () => {
|
|
97
|
+
await render(<PktIcon name="arrow" ariaLabel="Gå til forsiden" />)
|
|
98
|
+
const svg = await screen.findByTestId('icon')
|
|
99
|
+
|
|
100
|
+
expect(svg).toHaveAttribute('aria-hidden', 'false')
|
|
101
|
+
expect(svg).toHaveAttribute('aria-label', 'Gå til forsiden')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('ariaLabel + ariaHidden={true} keeps aria-hidden="true" and omits aria-label', async () => {
|
|
105
|
+
await render(<PktIcon name="arrow" ariaLabel="Lukk" ariaHidden={true} />)
|
|
106
|
+
const svg = await screen.findByTestId('icon')
|
|
107
|
+
|
|
108
|
+
expect(svg).toHaveAttribute('aria-hidden', 'true')
|
|
109
|
+
expect(svg).not.toHaveAttribute('aria-label')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('ariaHidden={false} without ariaLabel falls back to the icon name', async () => {
|
|
113
|
+
await render(<PktIcon name="chevron-right" ariaHidden={false} />)
|
|
114
|
+
const svg = await screen.findByTestId('icon')
|
|
115
|
+
|
|
116
|
+
expect(svg).toHaveAttribute('aria-label', 'chevron-right')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
54
119
|
})
|
|
@@ -1,23 +1,182 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import classNames from 'classnames'
|
|
4
|
+
import {
|
|
5
|
+
forwardRef,
|
|
6
|
+
HTMLAttributes,
|
|
7
|
+
MutableRefObject,
|
|
8
|
+
useEffect,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
useRef,
|
|
11
|
+
} from 'react'
|
|
7
12
|
import type { Booleanish } from 'shared-types'
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
const defaultPath = 'https://punkt-cdn.oslo.kommune.no/latest/icons/'
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
if (typeof window !== 'undefined') {
|
|
17
|
+
window.pktFetch =
|
|
18
|
+
window.pktFetch === undefined ? (fetch as unknown as typeof window.pktFetch) : window.pktFetch
|
|
19
|
+
window.pktIconPath = window.pktIconPath || defaultPath
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const errorSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"></svg>'
|
|
23
|
+
const dlCache: { [key: string]: Promise<string> } = {}
|
|
24
|
+
const MAX_RETRIES = 2
|
|
25
|
+
const RETRY_DELAY = 1500
|
|
26
|
+
|
|
27
|
+
const isSessionStorageAvailable =
|
|
28
|
+
typeof Storage !== 'undefined' && typeof sessionStorage !== 'undefined'
|
|
29
|
+
|
|
30
|
+
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
|
31
|
+
|
|
32
|
+
const getCachedIconSync = (name: string, path: string | undefined): string => {
|
|
33
|
+
if (!name || !isSessionStorageAvailable) return ''
|
|
34
|
+
return sessionStorage.getItem(path + name + '.svg') ?? ''
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fetchIcon = (url: string): Promise<string> =>
|
|
38
|
+
(window.pktFetch as unknown as typeof fetch)(url).then((response) => {
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error('Missing icon: ' + url)
|
|
41
|
+
}
|
|
42
|
+
return response.text()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const fetchIconWithRetry = async (url: string, retries = MAX_RETRIES): Promise<string> => {
|
|
46
|
+
try {
|
|
47
|
+
return await fetchIcon(url)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (retries > 0) {
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY))
|
|
51
|
+
return fetchIconWithRetry(url, retries - 1)
|
|
52
|
+
}
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.error('Failed to fetch icon: ' + url)
|
|
55
|
+
return errorSvg
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const downloadIconOrGetFromCache = async (
|
|
60
|
+
name: string,
|
|
61
|
+
path: string | undefined,
|
|
62
|
+
): Promise<string | null> => {
|
|
63
|
+
const key = path + name + '.svg'
|
|
64
|
+
|
|
65
|
+
if (isSessionStorageAvailable && sessionStorage.getItem(key)) {
|
|
66
|
+
return sessionStorage.getItem(key)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (key in dlCache) {
|
|
70
|
+
return dlCache[key]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof window !== 'undefined' && typeof window.pktFetch === 'function') {
|
|
74
|
+
dlCache[key] = fetchIconWithRetry(key).then((text) => {
|
|
75
|
+
if (text !== errorSvg && isSessionStorageAvailable) {
|
|
76
|
+
sessionStorage.setItem(key, text)
|
|
77
|
+
}
|
|
78
|
+
delete dlCache[key]
|
|
79
|
+
return text
|
|
80
|
+
})
|
|
81
|
+
return dlCache[key]
|
|
82
|
+
}
|
|
83
|
+
return errorSvg
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface IPktIcon extends HTMLAttributes<HTMLElement> {
|
|
12
87
|
name?: string
|
|
13
88
|
path?: string
|
|
14
|
-
|
|
89
|
+
ariaHidden?: Booleanish
|
|
90
|
+
ariaLabel?: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const toBoolean = (v: Booleanish | undefined): boolean | undefined => {
|
|
94
|
+
if (v === undefined) return undefined
|
|
95
|
+
return v === true || v === 'true'
|
|
15
96
|
}
|
|
16
97
|
|
|
17
|
-
export const PktIcon
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
98
|
+
export const PktIcon = forwardRef<HTMLSpanElement, IPktIcon>(function PktIcon(
|
|
99
|
+
{
|
|
100
|
+
name = '',
|
|
101
|
+
path,
|
|
102
|
+
className,
|
|
103
|
+
ariaHidden,
|
|
104
|
+
ariaLabel,
|
|
105
|
+
'aria-hidden': ariaHiddenAttr,
|
|
106
|
+
'aria-label': ariaLabelAttr,
|
|
107
|
+
...rest
|
|
108
|
+
},
|
|
109
|
+
forwardedRef,
|
|
110
|
+
) {
|
|
111
|
+
const resolvedPath =
|
|
112
|
+
path ?? (typeof window !== 'undefined' ? window.pktIconPath : defaultPath)
|
|
113
|
+
const elementRef = useRef<HTMLSpanElement | null>(null)
|
|
114
|
+
|
|
115
|
+
const explicitAriaHidden = toBoolean(ariaHidden) ?? toBoolean(ariaHiddenAttr)
|
|
116
|
+
const explicitAriaLabel = ariaLabel ?? ariaLabelAttr
|
|
117
|
+
const effectiveAriaHidden =
|
|
118
|
+
explicitAriaHidden !== undefined ? explicitAriaHidden : !explicitAriaLabel
|
|
119
|
+
const effectiveAriaLabel = effectiveAriaHidden ? undefined : explicitAriaLabel || name
|
|
120
|
+
|
|
121
|
+
useIsomorphicLayoutEffect(() => {
|
|
122
|
+
const el = elementRef.current
|
|
123
|
+
if (!el) return
|
|
124
|
+
|
|
125
|
+
const applyAria = (root: HTMLElement) => {
|
|
126
|
+
const svg = root.querySelector('svg')
|
|
127
|
+
if (!svg) return
|
|
128
|
+
svg.setAttribute('aria-hidden', String(effectiveAriaHidden))
|
|
129
|
+
if (effectiveAriaLabel) {
|
|
130
|
+
svg.setAttribute('aria-label', effectiveAriaLabel)
|
|
131
|
+
} else {
|
|
132
|
+
svg.removeAttribute('aria-label')
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!name) {
|
|
137
|
+
el.innerHTML = ''
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const cached = getCachedIconSync(name, resolvedPath)
|
|
142
|
+
if (cached) {
|
|
143
|
+
el.innerHTML = cached
|
|
144
|
+
applyAria(el)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let cancelled = false
|
|
149
|
+
downloadIconOrGetFromCache(name, resolvedPath).then((text) => {
|
|
150
|
+
if (cancelled) return
|
|
151
|
+
const current = elementRef.current
|
|
152
|
+
if (current) {
|
|
153
|
+
current.innerHTML = text || errorSvg
|
|
154
|
+
applyAria(current)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
return () => {
|
|
158
|
+
cancelled = true
|
|
159
|
+
}
|
|
160
|
+
}, [name, resolvedPath, effectiveAriaHidden, effectiveAriaLabel])
|
|
161
|
+
|
|
162
|
+
const setRef = (el: HTMLSpanElement | null) => {
|
|
163
|
+
elementRef.current = el
|
|
164
|
+
if (typeof forwardedRef === 'function') {
|
|
165
|
+
forwardedRef(el)
|
|
166
|
+
} else if (forwardedRef) {
|
|
167
|
+
;(forwardedRef as MutableRefObject<HTMLSpanElement | null>).current = el
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<span
|
|
173
|
+
{...rest}
|
|
174
|
+
ref={setRef}
|
|
175
|
+
className={classNames('pkt-icon', className)}
|
|
176
|
+
{...(name ? { name } : {})}
|
|
177
|
+
data-path={resolvedPath}
|
|
178
|
+
/>
|
|
179
|
+
)
|
|
23
180
|
})
|
|
181
|
+
|
|
182
|
+
PktIcon.displayName = 'PktIcon'
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import classNames from 'classnames'
|
|
4
4
|
import { HTMLAttributes, ReactNode } from 'react'
|
|
5
5
|
|
|
6
|
-
import { PktIcon } from '
|
|
6
|
+
import { PktIcon } from '../icon/Icon'
|
|
7
7
|
|
|
8
8
|
export interface IPktLinkCard extends Omit<HTMLAttributes<HTMLAnchorElement>, 'title'> {
|
|
9
9
|
skin?: 'normal' | 'no-padding' | 'blue' | 'beige' | 'green' | 'gray' | 'beige-outline' | 'gray-outline'
|
|
@@ -4,7 +4,7 @@ import classNames from 'classnames'
|
|
|
4
4
|
import { createRef, FC, HTMLAttributes, ReactNode, useCallback, useState } from 'react'
|
|
5
5
|
import type { TMessageboxSkin } from 'shared-types'
|
|
6
6
|
|
|
7
|
-
import { PktIcon } from '
|
|
7
|
+
import { PktIcon } from '../icon/Icon'
|
|
8
8
|
|
|
9
9
|
export type { TMessageboxSkin }
|
|
10
10
|
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import { forwardRef, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react'
|
|
4
4
|
|
|
5
5
|
import { PktIcon } from '../icon/Icon'
|
|
6
|
-
import
|
|
7
|
-
import { useTabsContext } from './Tabs'
|
|
6
|
+
import type { TTagSkin } from 'shared-types'
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
import { PktTag } from '../tag/Tag'
|
|
9
|
+
import { useTabsContext } from './Tabs'
|
|
10
10
|
|
|
11
11
|
export interface IPktTabItem {
|
|
12
12
|
children: ReactNode
|
|
@@ -17,7 +17,7 @@ export interface IPktTabItem {
|
|
|
17
17
|
icon?: string
|
|
18
18
|
controls?: string
|
|
19
19
|
tag?: string
|
|
20
|
-
tagSkin?:
|
|
20
|
+
tagSkin?: TTagSkin
|
|
21
21
|
index?: number
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -12,11 +12,11 @@ import {
|
|
|
12
12
|
useRef,
|
|
13
13
|
} from 'react'
|
|
14
14
|
|
|
15
|
+
import type { TTagSkin } from 'shared-types'
|
|
16
|
+
|
|
15
17
|
import { PktIcon } from '../icon/Icon'
|
|
16
18
|
import { PktTabItem } from './TabItem'
|
|
17
19
|
|
|
18
|
-
export type TSkin = 'blue' | 'green' | 'red' | 'beige' | 'yellow' | 'grey' | 'gray' | 'blue-light'
|
|
19
|
-
|
|
20
20
|
// Context for passing tab navigation logic to children
|
|
21
21
|
interface ITabsContext {
|
|
22
22
|
arrowNav: boolean
|
|
@@ -48,7 +48,7 @@ export interface IPktTab {
|
|
|
48
48
|
controls?: string
|
|
49
49
|
tag?: {
|
|
50
50
|
text: string
|
|
51
|
-
skin:
|
|
51
|
+
skin: TTagSkin | undefined
|
|
52
52
|
}
|
|
53
53
|
active?: boolean
|
|
54
54
|
}
|
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
useState,
|
|
15
15
|
} from 'react'
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import type { TTagSkin } from 'shared-types'
|
|
18
|
+
|
|
19
|
+
import { PktIcon } from '../icon/Icon'
|
|
18
20
|
|
|
19
21
|
interface BasePktTagProps {
|
|
20
|
-
skin?:
|
|
22
|
+
skin?: TTagSkin
|
|
21
23
|
textStyle?: 'normal-text' | 'thin-text'
|
|
22
24
|
size?: 'small' | 'medium' | 'large'
|
|
23
25
|
closeTag?: boolean
|