@oslokommune/punkt-react 16.17.0 → 16.17.2

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.
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-react",
3
- "version": "16.17.0",
3
+ "version": "16.17.2",
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.0",
42
+ "@oslokommune/punkt-elements": "^16.17.2",
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.0",
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": "b9024738acca3ed6dd938238ff2a2d4219cd3c0c"
112
+ "gitHead": "3eda56e8d6942a8f8aba63b79a469ac12b375e2d"
113
113
  }
@@ -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'
@@ -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, createElement, forwardRef, HTMLProps, ReactNode } from 'react'
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
- interface SearchSuggestion {
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?: SearchSuggestion[]
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 handleSuggestionClick = (cb: (() => void) | undefined, index: number) => {
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
- <li key={`search-suggestion-${index}`}>
151
- {createElement(
152
- suggestion.href ? 'a' : suggestion.onClick ? 'button' : 'div',
153
- {
154
- href: suggestion.href,
155
- className: `pkt-searchinput__suggestion ${suggestion.onClick ? 'pkt-link-button' : ''} ${
156
- suggestion.href || suggestion.onClick ? 'pkt-searchinput__suggestion--has-hover' : ''
157
- }`,
158
- type: suggestion.onClick ? 'button' : undefined,
159
- onClick: () => handleSuggestionClick(suggestion.onClick, index),
160
- onKeyUp: (event: { key: string }) =>
161
- event.key === 'Enter' && handleSuggestionClick(suggestion.onClick, index),
162
- },
163
- <>
164
- {suggestion.title && <h3 className="pkt-searchinput__suggestion-title">{suggestion.title}</h3>}
165
- {suggestion.text && <p className="pkt-searchinput__suggestion-text">{suggestion.text}</p>}
166
- </>,
167
- )}
168
- </li>
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
  </>,