@oslokommune/punkt-react 16.17.1 → 16.17.3
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/{dialog-polyfill.esm-DrODM08x-C8rdFsL8.js → dialog-polyfill.esm-B7W3teD0-CHMBcS-s.js} +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/punkt-react.es.js +3912 -3750
- package/dist/punkt-react.umd.js +121 -62
- package/dist/shared-types/index.d.ts +1 -0
- package/dist/shared-types/searchinput.d.ts +41 -0
- package/dist/shared-utils/modal-scroll-lock.d.ts +10 -0
- package/package.json +4 -4
- package/src/components/header/HeaderService.tsx +2 -2
- package/src/components/index.ts +1 -0
- package/src/components/modal/Modal.tsx +19 -16
- package/src/components/searchinput/SearchInput.test.tsx +14 -0
- package/src/components/searchinput/SearchInput.tsx +47 -26
|
@@ -17,5 +17,6 @@ export type { ITimepickerStrings } from './timepicker';
|
|
|
17
17
|
export { defaultTimepickerStrings } from './timepicker';
|
|
18
18
|
export type { TProgressbarRole, TProgressbarSkin, TProgressbarStatusPlacement, TProgressbarStatusType, TProgressbarTitlePosition, } from './progressbar';
|
|
19
19
|
export type { IPktComboboxOption, TPktComboboxTagSkin, TPktComboboxDisplayValue, TPktComboboxTagPlacement, } from './combobox';
|
|
20
|
+
export type { TPktSearchInputAppearance, TPktSearchInputMethod, IPktSearchInputSuggestion, IPktSearchInput, } from './searchinput';
|
|
20
21
|
export type { TUploadStrategy as TFileUploadStrategy, TFileUploadItemRenderer, TFilesChangedReason, TFileValidator, TTransferProgress, IFileItem, IFileTransfer, IFileValidateDetail, ITransferCancelledDetail, IFilesChangedDetail, IFileUploadStrings, } from './fileupload';
|
|
21
22
|
export { defaultFileUploadStrings } from './fileupload';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for Search input (`pkt-searchinput` / `PktSearchInput`).
|
|
3
|
+
*
|
|
4
|
+
* Shared between Elements and React so API contracts and suggestion payloads
|
|
5
|
+
* stay aligned. React extends suggestions with `ReactNode` and optional
|
|
6
|
+
* `onClick` in the component layer — see `@oslokommune/punkt-react`.
|
|
7
|
+
*/
|
|
8
|
+
export type TPktSearchInputAppearance = 'local' | 'local-with-button' | 'global';
|
|
9
|
+
export type TPktSearchInputMethod = 'get' | 'post' | 'dialog';
|
|
10
|
+
/**
|
|
11
|
+
* Plain suggestion item (JSON-serializable). Used by Elements and as the
|
|
12
|
+
* portable shape for API data and design-token documentation.
|
|
13
|
+
*/
|
|
14
|
+
export interface IPktSearchInputSuggestion {
|
|
15
|
+
title?: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
href?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Presentational row only (e.g. «Ingen resultater»). Renders as a non-focusable
|
|
20
|
+
* container — not a link or button — and does not emit selection events.
|
|
21
|
+
* Ignored when `href` is set (link always wins).
|
|
22
|
+
*/
|
|
23
|
+
nonInteractive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Public props for the web component (Elements). React mirrors these and adds
|
|
27
|
+
* callback props in its wrapper.
|
|
28
|
+
*/
|
|
29
|
+
export interface IPktSearchInput {
|
|
30
|
+
appearance?: TPktSearchInputAppearance;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
fullwidth?: boolean;
|
|
33
|
+
id: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
value?: string;
|
|
38
|
+
suggestions?: IPktSearchInputSuggestion[];
|
|
39
|
+
action?: string;
|
|
40
|
+
method?: TPktSearchInputMethod;
|
|
41
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const acquireModalScrollLock: (token: {
|
|
2
|
+
locked: boolean;
|
|
3
|
+
}) => void;
|
|
4
|
+
export declare const releaseModalScrollLock: (token: {
|
|
5
|
+
locked: boolean;
|
|
6
|
+
}) => void;
|
|
7
|
+
export type ModalScrollLockToken = {
|
|
8
|
+
locked: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare const createModalScrollLockToken: () => ModalScrollLockToken;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oslokommune/punkt-react",
|
|
3
|
-
"version": "16.17.
|
|
3
|
+
"version": "16.17.3",
|
|
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.17.
|
|
42
|
+
"@oslokommune/punkt-elements": "^16.17.3",
|
|
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.17.
|
|
53
|
+
"@oslokommune/punkt-css": "^16.17.2",
|
|
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": "0e923719992be1851617872d5e529a4588126aed"
|
|
113
113
|
}
|
|
@@ -347,7 +347,7 @@ export const PktHeaderService = forwardRef(
|
|
|
347
347
|
</span>
|
|
348
348
|
)}
|
|
349
349
|
{serviceName && (
|
|
350
|
-
<span className="pkt-header-service__service-name">
|
|
350
|
+
<span className="pkt-header-service__service-name" translate="no">
|
|
351
351
|
{serviceLink ? (
|
|
352
352
|
<PktLink href={serviceLink} className="pkt-header-service__service-link ">
|
|
353
353
|
{serviceName}
|
|
@@ -505,7 +505,7 @@ export const PktHeaderService = forwardRef(
|
|
|
505
505
|
onClick={() => handleMenuToggle('user')}
|
|
506
506
|
>
|
|
507
507
|
<span className="pkt-sr-only">Brukermeny: </span>
|
|
508
|
-
{representing?.name || user.name}
|
|
508
|
+
<span translate="no">{representing?.name || user.name}</span>
|
|
509
509
|
</PktButton>
|
|
510
510
|
{openMenu === 'user' && user && (
|
|
511
511
|
<div
|
package/src/components/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export { PktModal } from './modal/Modal'
|
|
|
30
30
|
export { PktProgressbar } from './progressbar/Progressbar'
|
|
31
31
|
export { PktRadioButton } from './radio/RadioButton'
|
|
32
32
|
export { PktSearchInput } from './searchinput/SearchInput'
|
|
33
|
+
export type { PktSearchInputSuggestion } from './searchinput/SearchInput'
|
|
33
34
|
export { PktSelect } from './select/Select'
|
|
34
35
|
export { PktStep } from './stepper/Step'
|
|
35
36
|
export { PktStepper } from './stepper/Stepper'
|
|
@@ -13,6 +13,12 @@ import {
|
|
|
13
13
|
useRef,
|
|
14
14
|
} from 'react'
|
|
15
15
|
|
|
16
|
+
import {
|
|
17
|
+
acquireModalScrollLock,
|
|
18
|
+
createModalScrollLockToken,
|
|
19
|
+
releaseModalScrollLock,
|
|
20
|
+
} from 'shared-utils/modal-scroll-lock'
|
|
21
|
+
|
|
16
22
|
import { PktButton } from '../button/Button'
|
|
17
23
|
|
|
18
24
|
export interface IPktModal
|
|
@@ -53,6 +59,7 @@ export const PktModal = forwardRef(
|
|
|
53
59
|
ref: ForwardedRef<HTMLDialogElement>,
|
|
54
60
|
) => {
|
|
55
61
|
const internalRef = useRef<HTMLDialogElement | null>(null)
|
|
62
|
+
const scrollLockToken = useRef(createModalScrollLockToken())
|
|
56
63
|
|
|
57
64
|
// Merge consumer ref + internal ref so we always have a working DOM handle
|
|
58
65
|
// for our own logic, regardless of whether the consumer passes a callback ref
|
|
@@ -69,38 +76,33 @@ export const PktModal = forwardRef(
|
|
|
69
76
|
const close = useCallback(() => {
|
|
70
77
|
if (!internalRef.current?.open) return
|
|
71
78
|
internalRef.current.close()
|
|
79
|
+
releaseModalScrollLock(scrollLockToken.current)
|
|
72
80
|
onClose?.()
|
|
73
81
|
}, [onClose])
|
|
74
82
|
|
|
75
|
-
// Synkroniser scroll-lock med dialogens faktiske åpen/lukket-tilstand
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
const dialog = internalRef.current
|
|
78
|
-
if (!dialog) return
|
|
79
|
-
|
|
80
|
-
const observer = new MutationObserver(() => {
|
|
81
|
-
if (dialog.open) {
|
|
82
|
-
document.body.classList.add('pkt-modal--open')
|
|
83
|
-
} else {
|
|
84
|
-
document.body.classList.remove('pkt-modal--open')
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
observer.observe(dialog, { attributes: true, attributeFilter: ['open'] })
|
|
88
|
-
return () => observer.disconnect()
|
|
89
|
-
}, [])
|
|
90
|
-
|
|
91
83
|
// Deklarativ åpne/lukke via open-prop
|
|
92
84
|
useEffect(() => {
|
|
93
85
|
if (open) {
|
|
94
86
|
if (!internalRef.current?.open) {
|
|
95
87
|
internalRef.current?.showModal()
|
|
96
88
|
}
|
|
89
|
+
acquireModalScrollLock(scrollLockToken.current)
|
|
97
90
|
} else {
|
|
98
91
|
if (internalRef.current?.open) {
|
|
99
92
|
internalRef.current.close()
|
|
100
93
|
}
|
|
94
|
+
releaseModalScrollLock(scrollLockToken.current)
|
|
101
95
|
}
|
|
102
96
|
}, [open])
|
|
103
97
|
|
|
98
|
+
// Rydd opp scroll-lock hvis modalen avmonteres mens den er åpen
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const token = scrollLockToken.current
|
|
101
|
+
return () => {
|
|
102
|
+
releaseModalScrollLock(token)
|
|
103
|
+
}
|
|
104
|
+
}, [])
|
|
105
|
+
|
|
104
106
|
const handleBackdropClick = useCallback(
|
|
105
107
|
(event: React.MouseEvent<HTMLDialogElement>) => {
|
|
106
108
|
if (closeOnBackdropClick && event.target === internalRef.current) {
|
|
@@ -111,6 +113,7 @@ export const PktModal = forwardRef(
|
|
|
111
113
|
)
|
|
112
114
|
|
|
113
115
|
const handleNativeClose = useCallback(() => {
|
|
116
|
+
releaseModalScrollLock(scrollLockToken.current)
|
|
114
117
|
onClose?.()
|
|
115
118
|
}, [onClose])
|
|
116
119
|
|
|
@@ -69,6 +69,20 @@ describe('PktSearchInput', () => {
|
|
|
69
69
|
)
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
+
it('renders nonInteractive suggestion as a div, not a button', async () => {
|
|
73
|
+
render(
|
|
74
|
+
<PktSearchInput
|
|
75
|
+
id="test-search-input"
|
|
76
|
+
suggestions={[{ text: 'Ingen resultater', nonInteractive: true }]}
|
|
77
|
+
/>,
|
|
78
|
+
)
|
|
79
|
+
await window.customElements.whenDefined('pkt-icon')
|
|
80
|
+
|
|
81
|
+
const row = screen.getByText('Ingen resultater').closest('.pkt-searchinput__suggestion')
|
|
82
|
+
expect(row?.tagName.toLowerCase()).toBe('div')
|
|
83
|
+
expect(screen.queryAllByRole('button', { name: /ingen resultater/i })).toHaveLength(0)
|
|
84
|
+
})
|
|
85
|
+
|
|
72
86
|
it('renders search suggestions when suggestions prop exists', async () => {
|
|
73
87
|
const suggestions = [
|
|
74
88
|
{ title: 'Suggestion 1', onClick: vi.fn() },
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { ChangeEvent,
|
|
3
|
+
import { ChangeEvent, forwardRef, HTMLProps, ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { IPktSearchInputSuggestion } from 'shared-types/searchinput'
|
|
4
6
|
|
|
5
7
|
import { useFocusModality } from '../../hooks/useFocusModality'
|
|
6
8
|
import { PktButton } from '../button/Button'
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* React suggestion item: portable fields from {@link IPktSearchInputSuggestion}
|
|
12
|
+
* plus optional rich content and per-row handler.
|
|
13
|
+
*/
|
|
14
|
+
export type PktSearchInputSuggestion = Omit<IPktSearchInputSuggestion, 'title' | 'text'> & {
|
|
9
15
|
title?: string | ReactNode
|
|
10
16
|
text?: string | ReactNode
|
|
11
|
-
href?: string
|
|
12
17
|
onClick?: () => void
|
|
13
18
|
}
|
|
14
19
|
|
|
@@ -20,7 +25,7 @@ interface ISearch extends HTMLProps<HTMLInputElement> {
|
|
|
20
25
|
label?: string
|
|
21
26
|
name?: string
|
|
22
27
|
placeholder?: string
|
|
23
|
-
suggestions?:
|
|
28
|
+
suggestions?: PktSearchInputSuggestion[]
|
|
24
29
|
value?: string | undefined
|
|
25
30
|
onSearch?: (value: string) => void
|
|
26
31
|
onSuggestionClick?: (index: number) => void
|
|
@@ -59,7 +64,7 @@ export const PktSearchInput = forwardRef<HTMLInputElement, ISearchInput | ISearc
|
|
|
59
64
|
},
|
|
60
65
|
ref,
|
|
61
66
|
) => {
|
|
62
|
-
const
|
|
67
|
+
const handleSuggestionActivate = (cb: (() => void) | undefined, index: number) => {
|
|
63
68
|
if (cb) {
|
|
64
69
|
cb()
|
|
65
70
|
} else if (onSuggestionClick) {
|
|
@@ -92,6 +97,16 @@ export const PktSearchInput = forwardRef<HTMLInputElement, ISearchInput | ISearc
|
|
|
92
97
|
<div {...wrapperProps}>{children}</div>
|
|
93
98
|
)
|
|
94
99
|
|
|
100
|
+
const suggestionContent = (suggestion: PktSearchInputSuggestion) => (
|
|
101
|
+
<>
|
|
102
|
+
{suggestion.title && <h3 className="pkt-searchinput__suggestion-title">{suggestion.title}</h3>}
|
|
103
|
+
{suggestion.text && <p className="pkt-searchinput__suggestion-text">{suggestion.text}</p>}
|
|
104
|
+
</>
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const suggestionClassName = (suggestion: PktSearchInputSuggestion) =>
|
|
108
|
+
['pkt-searchinput__suggestion', suggestion.onClick ? 'pkt-link-button' : ''].filter(Boolean).join(' ')
|
|
109
|
+
|
|
95
110
|
return WrapperElement(
|
|
96
111
|
<>
|
|
97
112
|
{label && (
|
|
@@ -146,27 +161,33 @@ export const PktSearchInput = forwardRef<HTMLInputElement, ISearchInput | ISearc
|
|
|
146
161
|
|
|
147
162
|
{suggestions && (
|
|
148
163
|
<ul id={`${id}-suggestions`} className="pkt-searchinput__suggestions" aria-live="assertive">
|
|
149
|
-
{suggestions.map((suggestion, index) =>
|
|
150
|
-
|
|
151
|
-
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
{suggestions.map((suggestion, index) =>
|
|
165
|
+
suggestion.href ? (
|
|
166
|
+
<li key={`search-suggestion-${index}`}>
|
|
167
|
+
<a
|
|
168
|
+
href={suggestion.href}
|
|
169
|
+
className={suggestionClassName(suggestion)}
|
|
170
|
+
onClick={() => handleSuggestionActivate(suggestion.onClick, index)}
|
|
171
|
+
>
|
|
172
|
+
{suggestionContent(suggestion)}
|
|
173
|
+
</a>
|
|
174
|
+
</li>
|
|
175
|
+
) : suggestion.nonInteractive ? (
|
|
176
|
+
<li key={`search-suggestion-${index}`}>
|
|
177
|
+
<div className="pkt-searchinput__suggestion">{suggestionContent(suggestion)}</div>
|
|
178
|
+
</li>
|
|
179
|
+
) : (
|
|
180
|
+
<li key={`search-suggestion-${index}`}>
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
className={suggestionClassName(suggestion)}
|
|
184
|
+
onClick={() => handleSuggestionActivate(suggestion.onClick, index)}
|
|
185
|
+
>
|
|
186
|
+
{suggestionContent(suggestion)}
|
|
187
|
+
</button>
|
|
188
|
+
</li>
|
|
189
|
+
),
|
|
190
|
+
)}
|
|
170
191
|
</ul>
|
|
171
192
|
)}
|
|
172
193
|
</>,
|