@oslokommune/punkt-react 17.1.3 → 18.0.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/components/backlink/BackLink.d.ts +3 -0
  3. package/dist/components/breadcrumbs/Breadcrumbs.d.ts +3 -0
  4. package/dist/components/calendar/types.d.ts +14 -12
  5. package/dist/components/checkbox/Checkbox.d.ts +5 -0
  6. package/dist/components/combobox/types.d.ts +5 -0
  7. package/dist/components/consent/strings.d.ts +1 -85
  8. package/dist/components/datepicker/DateTags.d.ts +2 -2
  9. package/dist/components/datepicker/types.d.ts +6 -3
  10. package/dist/components/fileupload/DropZone.d.ts +3 -0
  11. package/dist/components/fileupload/FileUpload.d.ts +3 -0
  12. package/dist/components/header/types.d.ts +5 -0
  13. package/dist/components/index.d.ts +4 -0
  14. package/dist/components/inputwrapper/InputWrapper.d.ts +6 -0
  15. package/dist/components/radio/RadioButton.d.ts +5 -0
  16. package/dist/components/searchinput/SearchInput.d.ts +4 -0
  17. package/dist/components/select/Select.d.ts +3 -0
  18. package/dist/components/strings/PktStringsProvider.d.ts +21 -0
  19. package/dist/components/strings/strings-override.test.d.ts +1 -0
  20. package/dist/components/tag/Tag.d.ts +3 -0
  21. package/dist/components/textarea/Textarea.d.ts +3 -0
  22. package/dist/components/textinput/Textinput.d.ts +3 -0
  23. package/dist/components/timepicker/types.d.ts +3 -2
  24. package/dist/components/timepicker/useTimepickerState.d.ts +9 -1
  25. package/dist/punkt-react.cjs +1 -1
  26. package/dist/punkt-react.js +2969 -2749
  27. package/dist/shared-strings/consent.d.ts +71 -0
  28. package/dist/shared-strings/format.d.ts +21 -0
  29. package/dist/shared-strings/index.d.ts +14 -0
  30. package/dist/shared-strings/nb.d.ts +118 -0
  31. package/dist/shared-strings/resolve.d.ts +12 -0
  32. package/dist/shared-strings/store.d.ts +27 -0
  33. package/dist/shared-strings/types.d.ts +29 -0
  34. package/dist/shared-types/fileupload.d.ts +0 -27
  35. package/dist/shared-types/index.d.ts +1 -2
  36. package/dist/shared-utils/combobox/index.d.ts +1 -0
  37. package/dist/shared-utils/combobox/input-utils.d.ts +2 -1
  38. package/dist/shared-utils/combobox/messages.d.ts +7 -0
  39. package/dist/shared-utils/combobox/option-utils.d.ts +2 -1
  40. package/dist/shared-utils/combobox/selection-manager.d.ts +3 -2
  41. package/package.json +4 -4
  42. package/src/components/backlink/BackLink.tsx +11 -2
  43. package/src/components/breadcrumbs/Breadcrumbs.tsx +8 -2
  44. package/src/components/calendar/Calendar.core.test.tsx +7 -5
  45. package/src/components/calendar/types.ts +14 -34
  46. package/src/components/calendar/useCalendarState.ts +16 -2
  47. package/src/components/checkbox/Checkbox.tsx +20 -4
  48. package/src/components/combobox/Combobox.tsx +1 -0
  49. package/src/components/combobox/Listbox.tsx +4 -2
  50. package/src/components/combobox/types.ts +6 -0
  51. package/src/components/combobox/useComboboxState.ts +15 -8
  52. package/src/components/consent/strings.ts +1 -125
  53. package/src/components/datepicker/DateTags.tsx +4 -3
  54. package/src/components/datepicker/Datepicker.accessibility.test.tsx +2 -2
  55. package/src/components/datepicker/Datepicker.tsx +1 -0
  56. package/src/components/datepicker/DatepickerInputs.tsx +6 -6
  57. package/src/components/datepicker/types.ts +6 -3
  58. package/src/components/datepicker/useDatepickerState.ts +4 -5
  59. package/src/components/fileupload/DropZone.tsx +20 -15
  60. package/src/components/fileupload/FileUpload.test.tsx +37 -0
  61. package/src/components/fileupload/FileUpload.tsx +41 -8
  62. package/src/components/header/HeaderGlobal.tsx +5 -2
  63. package/src/components/header/HeaderService.tsx +12 -7
  64. package/src/components/header/types.ts +6 -0
  65. package/src/components/index.ts +10 -0
  66. package/src/components/inputwrapper/InputWrapper.tsx +28 -6
  67. package/src/components/radio/RadioButton.tsx +18 -2
  68. package/src/components/searchinput/SearchInput.tsx +13 -3
  69. package/src/components/select/Select.tsx +5 -0
  70. package/src/components/strings/PktStringsProvider.tsx +38 -0
  71. package/src/components/strings/strings-override.test.tsx +130 -0
  72. package/src/components/tag/Tag.tsx +13 -2
  73. package/src/components/textarea/Textarea.tsx +5 -0
  74. package/src/components/textinput/Textinput.tsx +5 -0
  75. package/src/components/timepicker/Timepicker.tsx +1 -0
  76. package/src/components/timepicker/types.ts +3 -2
  77. package/src/components/timepicker/useTimepickerState.ts +20 -22
@@ -27,7 +27,6 @@ import {
27
27
  useState,
28
28
  } from 'react'
29
29
 
30
- import { defaultFileUploadStrings } from 'shared-types'
31
30
  import {
32
31
  countFileTransferStates,
33
32
  resolveAcceptAttribute,
@@ -54,6 +53,8 @@ import {
54
53
  TQueueItemOperation,
55
54
  TUploadStrategy,
56
55
  } from './types'
56
+ import { usePktStrings } from '../../hooks/usePktStrings'
57
+ import { formatString, plural, type TPktStringsOverride } from 'shared-strings'
57
58
 
58
59
  // Re-export for external use
59
60
  export { formatFileSize, parseFileSize } from 'shared-utils/fileupload'
@@ -135,6 +136,8 @@ interface IBaseFileUploadProps extends Omit<
135
136
  tagText?: string | null
136
137
  /** Marks the upload as required. Native validation is used in `form`; submit validation in `custom`. */
137
138
  required?: boolean
139
+ /** Overstyrer tekster for denne komponenten. */
140
+ strings?: TPktStringsOverride
138
141
  /** Enable image preview modal (only applies to thumbnail renderer). */
139
142
  enableImagePreview?: boolean
140
143
  }
@@ -193,6 +196,7 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
193
196
  enableImagePreview = false,
194
197
  ['aria-describedby']: ariaDescribedByProp,
195
198
  ['aria-invalid']: ariaInvalidProp,
199
+ strings,
196
200
  ...props
197
201
  }: IPktFileUpload,
198
202
  forwardedRef,
@@ -203,6 +207,7 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
203
207
  useImperativeHandle(forwardedRef, () => fileInputRef.current as HTMLInputElement, [])
204
208
 
205
209
  const transfers = 'transfers' in props ? props.transfers : undefined
210
+ const fileuploadStrings = usePktStrings(['fileupload'], strings).fileupload
206
211
  const [validationError, setValidationError] = useState<string | null>(null)
207
212
  const [addedAnnouncement, setAddedAnnouncement] = useState('')
208
213
 
@@ -262,7 +267,10 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
262
267
  uploadStrategy,
263
268
  required,
264
269
  currentFileCount: value.length,
265
- onMissingFiles: useCallback(() => setValidationError(defaultFileUploadStrings.requiredMissing), []),
270
+ onMissingFiles: useCallback(
271
+ () => setValidationError(fileuploadStrings.requiredMissing),
272
+ [fileuploadStrings],
273
+ ),
266
274
  })
267
275
 
268
276
  const itemRenderer = typeof itemRendererProp === 'string' ? ItemRenderers[itemRendererProp] : itemRendererProp
@@ -293,8 +301,10 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
293
301
  const acceptedItems: TFileItemList = multiple ? addedFileItems : [addedFileItems[0]]
294
302
  setAddedAnnouncement(
295
303
  acceptedItems.length === 1
296
- ? defaultFileUploadStrings.srFileAdded(acceptedItems[0].attributes.targetFilename)
297
- : defaultFileUploadStrings.srFilesAdded(acceptedItems.length),
304
+ ? formatString(fileuploadStrings.srFileAdded, {
305
+ filename: acceptedItems[0].attributes.targetFilename,
306
+ })
307
+ : formatString(fileuploadStrings.srFilesAdded, { count: acceptedItems.length }),
298
308
  )
299
309
 
300
310
  if (!isControlled) setInternalValue(newValue)
@@ -306,7 +316,16 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
306
316
  })
307
317
  }
308
318
  },
309
- [onFilesChanged, value, multiple, uploadStrategy, onFileUploadRequested, validateFile, isControlled],
319
+ [
320
+ onFilesChanged,
321
+ value,
322
+ multiple,
323
+ uploadStrategy,
324
+ onFileUploadRequested,
325
+ validateFile,
326
+ isControlled,
327
+ fileuploadStrings,
328
+ ],
310
329
  )
311
330
 
312
331
  const onFileUpdated = useCallback(
@@ -346,7 +365,11 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
346
365
  [value, transfers],
347
366
  )
348
367
 
349
- const totalLabel = defaultFileUploadStrings.fileLabel(totalCount)
368
+ const totalLabel = plural(
369
+ totalCount,
370
+ fileuploadStrings.fileLabelOne,
371
+ fileuploadStrings.fileLabelOther,
372
+ )
350
373
 
351
374
  const fileUploadContent = (
352
375
  <div
@@ -362,13 +385,21 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
362
385
  <div className="pkt-sr-only" aria-live="polite" aria-atomic="true">
363
386
  {addedAnnouncement ||
364
387
  (totalCount > 0 && uploadedCount > 0
365
- ? defaultFileUploadStrings.srFilesUploadedOfTotal(uploadedCount, totalCount, totalLabel)
388
+ ? formatString(fileuploadStrings.srFilesUploadedOfTotal, {
389
+ uploaded: uploadedCount,
390
+ total: totalCount,
391
+ label: totalLabel,
392
+ })
366
393
  : '')}
367
394
  </div>
368
395
  <div className="pkt-sr-only" aria-live="assertive" aria-atomic="true">
369
396
  {totalCount > 0 &&
370
397
  failedCount > 0 &&
371
- defaultFileUploadStrings.srFilesFailedOfTotal(failedCount, totalCount, totalLabel)}
398
+ formatString(fileuploadStrings.srFilesFailedOfTotal, {
399
+ failed: failedCount,
400
+ total: totalCount,
401
+ label: totalLabel,
402
+ })}
372
403
  </div>
373
404
 
374
405
  <DropZone
@@ -387,6 +418,7 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
387
418
  aria-invalid={isAriaInvalid ? 'true' : undefined}
388
419
  aria-describedby={describedBy}
389
420
  hasVisibleLabel={!!label}
421
+ strings={strings}
390
422
  />
391
423
  {hasError && errorMessage && (
392
424
  <PktAlert
@@ -423,6 +455,7 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
423
455
  return (
424
456
  <div className="pkt-fileupload-component">
425
457
  <PktInputWrapper
458
+ strings={strings}
426
459
  forId={id}
427
460
  label={label}
428
461
  helptext={helptext}
@@ -14,6 +14,7 @@ import { PktHeaderUserMenu } from '../headerUserMenu/UserMenu'
14
14
  import { PktIcon } from '../icon/Icon'
15
15
  import { PktSearchInput } from '../searchinput/SearchInput'
16
16
  import { IPktHeaderGlobal } from './types'
17
+ import { usePktStrings } from '../../hooks/usePktStrings'
17
18
 
18
19
  const defaultLogoPath = 'https://punkt-cdn.oslo.kommune.no/latest/logos/'
19
20
 
@@ -56,6 +57,7 @@ export const PktHeaderGlobal = forwardRef(
56
57
  tabletBreakpoint = 1024,
57
58
  onDataLoaded,
58
59
  onDataError,
60
+ strings,
59
61
  }: IPktHeaderGlobal,
60
62
  ref: ForwardedRef<HTMLElement>,
61
63
  ) => {
@@ -251,10 +253,11 @@ export const PktHeaderGlobal = forwardRef(
251
253
 
252
254
  const showLogoutInUserMenu = logOutButtonPlacement === 'userMenu' || logOutButtonPlacement === 'both'
253
255
 
256
+ const headerStrings = usePktStrings(['header'], strings).header
254
257
  const search = localeData?.search
255
258
  const contact = localeData?.header?.contact
256
- const menuText = localeData?.i18n?.menu || 'Meny'
257
- const searchText = localeData?.i18n?.search || 'Søk'
259
+ const menuText = localeData?.i18n?.menu || headerStrings.menu
260
+ const searchText = localeData?.i18n?.search || headerStrings.search
258
261
  const isMenuOpen = openMenu === 'menu'
259
262
  const isUserOpen = openMenu === 'user'
260
263
  const isSearchOpen = openMenu === 'search'
@@ -26,6 +26,7 @@ import { PktTextinput } from '../textinput/Textinput'
26
26
  import { DEFAULT_HEADER_LOGO_LINK } from 'shared-types'
27
27
 
28
28
  import { IPktHeader, Representing, THeaderMenu, User, UserMenuFooterItem, UserMenuItem } from './types'
29
+ import { usePktStrings } from '../../hooks/usePktStrings'
29
30
 
30
31
  export type { Representing, THeaderMenu, User, UserMenuFooterItem, UserMenuItem }
31
32
  export type IPktHeaderService = IPktHeader
@@ -58,7 +59,7 @@ export const PktHeaderService = forwardRef(
58
59
  onSearch,
59
60
  onSearchChange,
60
61
  searchValue,
61
- searchPlaceholder = 'Søk',
62
+ searchPlaceholder,
62
63
  representing,
63
64
  serviceLink,
64
65
  serviceClick,
@@ -77,6 +78,7 @@ export const PktHeaderService = forwardRef(
77
78
  scrollBehavior = 'hide',
78
79
  slotMenuVariant = 'icon-only',
79
80
  slotMenuText = 'Meny',
81
+ strings,
80
82
  }: IPktHeaderService,
81
83
  ref: ForwardedRef<HTMLDivElement>,
82
84
  ) => {
@@ -92,6 +94,9 @@ export const PktHeaderService = forwardRef(
92
94
  }
93
95
  }, [userMenuFooter])
94
96
 
97
+ const headerStrings = usePktStrings(['header'], strings).header
98
+ const resolvedSearchPlaceholder = searchPlaceholder ?? headerStrings.search
99
+
95
100
  // Warning for userOptions - no longer available
96
101
  useEffect(() => {
97
102
  if (userOptions) {
@@ -341,7 +346,7 @@ export const PktHeaderService = forwardRef(
341
346
  onClick={() => handleMenuToggle('slot')}
342
347
  aria-expanded={openMenu === 'slot'}
343
348
  aria-controls="mobile-slot-menu"
344
- aria-label="Åpne meny"
349
+ aria-label={headerStrings.openMenu}
345
350
  >
346
351
  {slotMenuText}
347
352
  </PktButton>
@@ -375,7 +380,7 @@ export const PktHeaderService = forwardRef(
375
380
  state={openMenu === 'search' ? 'active' : 'normal'}
376
381
  aria-expanded={openMenu === 'search'}
377
382
  aria-controls="mobile-search-menu"
378
- aria-label="Åpne søkefelt"
383
+ aria-label={headerStrings.openSearch}
379
384
  >
380
385
  Søk
381
386
  </PktButton>
@@ -392,9 +397,9 @@ export const PktHeaderService = forwardRef(
392
397
  id="mobile-search-menu"
393
398
  className="pkt-header-service__search-input"
394
399
  type="search"
395
- label="Søk"
400
+ label={headerStrings.search}
396
401
  useWrapper={false}
397
- placeholder={searchPlaceholder}
402
+ placeholder={resolvedSearchPlaceholder}
398
403
  value={searchValue}
399
404
  autoFocus
400
405
  fullwidth={true}
@@ -413,9 +418,9 @@ export const PktHeaderService = forwardRef(
413
418
  id="header-service-search"
414
419
  className="pkt-header-service__search-input"
415
420
  type="search"
416
- label="Søk"
421
+ label={headerStrings.search}
417
422
  useWrapper={false}
418
- placeholder={searchPlaceholder}
423
+ placeholder={resolvedSearchPlaceholder}
419
424
  value={searchValue}
420
425
  onChange={(e: ChangeEvent<HTMLInputElement>) => onSearchChange?.(e.target.value)}
421
426
  onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
@@ -9,6 +9,7 @@ import type {
9
9
  THeaderMenuLocale,
10
10
  IPktHeaderGlobal as ISharedPktHeaderGlobal,
11
11
  } from 'shared-types'
12
+ import type { TPktStringsOverride } from 'shared-strings'
12
13
 
13
14
  export type {
14
15
  THeaderMenu,
@@ -53,6 +54,9 @@ export interface UserMenuFooterItem {
53
54
  }
54
55
 
55
56
  export interface IPktHeader {
57
+ /** Overstyrer tekster for denne komponenten. */
58
+ strings?: TPktStringsOverride
59
+
56
60
  children?: ReactNode
57
61
  /** Additional CSS class name(s) to apply to the header */
58
62
  className?: string
@@ -127,6 +131,8 @@ export interface IPktHeader {
127
131
  * (in shared-types) with React-specific callbacks and DOM props.
128
132
  */
129
133
  export interface IPktHeaderGlobal extends ISharedPktHeaderGlobal {
134
+ /** Overstyrer tekster for denne komponenten. */
135
+ strings?: TPktStringsOverride
130
136
  /** Additional CSS class name(s) to apply to the header */
131
137
  className?: string
132
138
  /** Set dark mode on the header */
@@ -52,3 +52,13 @@ export { PktTextinput } from './textinput/Textinput'
52
52
  export { PktTimepicker } from './timepicker/Timepicker'
53
53
  export * from './interfaces'
54
54
  export * from './types'
55
+ export { PktStringsProvider } from './strings/PktStringsProvider'
56
+ export type { IPktStringsProvider } from './strings/PktStringsProvider'
57
+ export {
58
+ getPktStrings,
59
+ nb as pktStringsNb,
60
+ resetPktStrings,
61
+ setPktStrings,
62
+ subscribePktStrings,
63
+ } from 'shared-strings'
64
+ export type { TPktStrings, TPktStringsNamespace, TPktStringsOverride } from 'shared-strings'
@@ -3,6 +3,8 @@
3
3
  import { ForwardedRef, forwardRef, type ReactNode, RefAttributes } from 'react'
4
4
 
5
5
  import { useFocusModality } from '../../hooks/useFocusModality'
6
+ import { usePktStrings } from '../../hooks/usePktStrings'
7
+ import { formatString, type TPktStringsOverride } from 'shared-strings'
6
8
  import { counterStatusId, isCounterExceeded } from 'shared-utils/forms/described-by'
7
9
  import { PktAlert } from '../alert/Alert'
8
10
  import { PktHelptext } from '../helptext/Helptext'
@@ -12,13 +14,16 @@ export interface IPktInputWrapper extends RefAttributes<HTMLElement> {
12
14
  label: string
13
15
  helptext?: string | ReactNode
14
16
  helptextDropdown?: string | ReactNode
17
+ /** @deprecated Bruk `strings={{ forms: { readMore: '…' } }}` eller `setPktStrings()`. */
15
18
  helptextDropdownButton?: string
16
19
  counter?: boolean
17
20
  counterCurrent?: number
18
21
  counterMaxLength?: number
19
22
  optionalTag?: boolean
23
+ /** @deprecated Bruk `strings={{ forms: { optional: '…' } }}` eller `setPktStrings()`. */
20
24
  optionalText?: string
21
25
  requiredTag?: boolean
26
+ /** @deprecated Bruk `strings={{ forms: { required: '…' } }}` eller `setPktStrings()`. */
22
27
  requiredText?: string
23
28
  tagText?: string | null
24
29
  hasError?: boolean
@@ -33,6 +38,8 @@ export interface IPktInputWrapper extends RefAttributes<HTMLElement> {
33
38
  role?: string
34
39
  counterPosition?: 'top' | 'bottom'
35
40
  size?: 'small' | 'medium' | 'xsmall'
41
+ /** Overstyrer tekster for denne komponenten. */
42
+ strings?: TPktStringsOverride
36
43
  }
37
44
 
38
45
  export const PktInputWrapper = forwardRef(
@@ -47,9 +54,9 @@ export const PktInputWrapper = forwardRef(
47
54
  counterCurrent = 0,
48
55
  counterMaxLength,
49
56
  optionalTag = false,
50
- optionalText = 'Valgfritt',
57
+ optionalText,
51
58
  requiredTag = false,
52
- requiredText = 'Må fylles ut',
59
+ requiredText,
53
60
  tagText = null,
54
61
  hasError = false,
55
62
  errorMessage,
@@ -63,10 +70,12 @@ export const PktInputWrapper = forwardRef(
63
70
  role,
64
71
  counterPosition = 'bottom',
65
72
  size = 'medium',
73
+ strings,
66
74
  }: IPktInputWrapper,
67
75
  ref: ForwardedRef<HTMLDivElement>,
68
76
  ) => {
69
77
  const focusModality = useFocusModality<HTMLDivElement>(ref)
78
+ const s = usePktStrings(['forms', 'validation'], strings)
70
79
 
71
80
  const describedBy = ariaDescribedby || (helptext ? `${forId}-helptext` : undefined)
72
81
  const showCounter = !!counter
@@ -95,7 +104,12 @@ export const PktInputWrapper = forwardRef(
95
104
  {counterMaxLength ? `/${counterMaxLength}` : ''}
96
105
  </div>
97
106
  <div className="pkt-sr-only" id={counterStatusId(forId)} aria-live="polite">
98
- {counterExceeded ? `Du har brukt ${counterCurrent} av ${counterMaxLength} tegn` : ''}
107
+ {counterExceeded
108
+ ? formatString(s.validation.counterExceeded, {
109
+ current: counterCurrent,
110
+ max: counterMaxLength ?? 0,
111
+ })
112
+ : ''}
99
113
  </div>
100
114
  </>
101
115
  ) : null
@@ -104,8 +118,16 @@ export const PktInputWrapper = forwardRef(
104
118
  return (
105
119
  <>
106
120
  {tagText && <span className={tagClasses + ` pkt-tag--gray`}>{tagText}</span>}
107
- {optionalTag && <span className={tagClasses + ` pkt-tag--blue-light`}>{optionalText}</span>}
108
- {requiredTag && <span className={tagClasses + ` pkt-tag--beige`}>{requiredText}</span>}
121
+ {optionalTag && (
122
+ <span className={tagClasses + ` pkt-tag--blue-light`}>
123
+ {optionalText ?? s.forms.optional}
124
+ </span>
125
+ )}
126
+ {requiredTag && (
127
+ <span className={tagClasses + ` pkt-tag--beige`}>
128
+ {requiredText ?? s.forms.required}
129
+ </span>
130
+ )}
109
131
  </>
110
132
  )
111
133
  }
@@ -148,7 +170,7 @@ export const PktInputWrapper = forwardRef(
148
170
  forId={forId}
149
171
  helptext={visibleHelptext}
150
172
  helptextDropdown={helptextDropdown}
151
- helptextDropdownButton={helptextDropdownButton}
173
+ helptextDropdownButton={helptextDropdownButton ?? s.forms.readMore}
152
174
  />
153
175
  )
154
176
  }
@@ -6,6 +6,8 @@ import {
6
6
  ReactElement,
7
7
  ReactNode,
8
8
  } from 'react'
9
+ import { usePktStrings } from '../../hooks/usePktStrings'
10
+ import type { TPktStringsOverride } from 'shared-strings'
9
11
 
10
12
  export interface IPktRadioButton extends InputHTMLAttributes<HTMLInputElement> {
11
13
  id: string
@@ -19,9 +21,13 @@ export interface IPktRadioButton extends InputHTMLAttributes<HTMLInputElement> {
19
21
  hasError?: boolean
20
22
  value?: string
21
23
  optionalTag?: boolean
24
+ /** @deprecated Bruk `strings={{ forms: { optional: '…' } }}` eller `setPktStrings()`. */
22
25
  optionalText?: string
23
26
  requiredTag?: boolean
27
+ /** @deprecated Bruk `strings={{ forms: { required: '…' } }}` eller `setPktStrings()`. */
24
28
  requiredText?: string
29
+ /** Overstyrer tekster for denne komponenten. */
30
+ strings?: TPktStringsOverride
25
31
  tagText?: string | null
26
32
  onChange?: ChangeEventHandler<HTMLInputElement>
27
33
  }
@@ -43,11 +49,13 @@ export const PktRadioButton = forwardRef(
43
49
  optionalText,
44
50
  requiredTag,
45
51
  requiredText,
52
+ strings,
46
53
  tagText,
47
54
  ...props
48
55
  }: IPktRadioButton,
49
56
  ref: ForwardedRef<HTMLInputElement>,
50
57
  ): ReactElement => {
58
+ const s = usePktStrings(['forms'], strings)
51
59
  const classes = [className, 'pkt-input-check'].filter(Boolean).join(' ')
52
60
 
53
61
  const inputTileClasses = [
@@ -75,8 +83,16 @@ export const PktRadioButton = forwardRef(
75
83
  return (
76
84
  <>
77
85
  {tagText && <span className={tagClasses + ` pkt-tag--gray`}>{tagText}</span>}
78
- {optionalTag && <span className={tagClasses + ` pkt-tag--blue-light`}>{optionalText}</span>}
79
- {requiredTag && <span className={tagClasses + ` pkt-tag--beige`}>{requiredText}</span>}
86
+ {optionalTag && (
87
+ <span className={tagClasses + ` pkt-tag--blue-light`}>
88
+ {optionalText ?? s.forms.optional}
89
+ </span>
90
+ )}
91
+ {requiredTag && (
92
+ <span className={tagClasses + ` pkt-tag--beige`}>
93
+ {requiredText ?? s.forms.required}
94
+ </span>
95
+ )}
80
96
  </>
81
97
  )
82
98
  }
@@ -6,6 +6,8 @@ import type { IPktSearchInputSuggestion } from 'shared-types/searchinput'
6
6
 
7
7
  import { useFocusModality } from '../../hooks/useFocusModality'
8
8
  import { PktButton } from '../button/Button'
9
+ import { usePktStrings } from '../../hooks/usePktStrings'
10
+ import type { TPktStringsOverride } from 'shared-strings'
9
11
 
10
12
  /**
11
13
  * React suggestion item: portable fields from {@link IPktSearchInputSuggestion}
@@ -18,6 +20,9 @@ export type PktSearchInputSuggestion = Omit<IPktSearchInputSuggestion, 'title' |
18
20
  }
19
21
 
20
22
  interface ISearch extends HTMLProps<HTMLInputElement> {
23
+ /** Overstyrer tekster for denne komponenten. */
24
+ strings?: TPktStringsOverride
25
+
21
26
  appearance?: 'local' | 'local-with-button' | 'global'
22
27
  inputSize?: 'small' | 'medium' | 'xsmall'
23
28
  disabled?: boolean
@@ -25,6 +30,7 @@ interface ISearch extends HTMLProps<HTMLInputElement> {
25
30
  id: string
26
31
  label?: string
27
32
  name?: string
33
+ /** @deprecated Bruk `strings={{ searchinput: { placeholder: '…' } }}` eller `setPktStrings()`. */
28
34
  placeholder?: string
29
35
  suggestions?: PktSearchInputSuggestion[]
30
36
  value?: string | undefined
@@ -54,7 +60,7 @@ export const PktSearchInput = forwardRef<HTMLInputElement, ISearchInput | ISearc
54
60
  id,
55
61
  label,
56
62
  name,
57
- placeholder = 'Søk…',
63
+ placeholder,
58
64
  suggestions,
59
65
  value = '',
60
66
  action,
@@ -62,10 +68,14 @@ export const PktSearchInput = forwardRef<HTMLInputElement, ISearchInput | ISearc
62
68
  onChange,
63
69
  onSearch,
64
70
  onSuggestionClick,
71
+ strings,
65
72
  ...props
66
73
  },
67
74
  ref,
68
75
  ) => {
76
+ const s = usePktStrings(['searchinput'], strings)
77
+ const resolvedPlaceholder = placeholder ?? s.searchinput.placeholder
78
+
69
79
  const handleSuggestionActivate = (cb: (() => void) | undefined, index: number) => {
70
80
  if (cb) {
71
81
  cb()
@@ -130,7 +140,7 @@ export const PktSearchInput = forwardRef<HTMLInputElement, ISearchInput | ISearc
130
140
  type="search"
131
141
  name={name || id}
132
142
  id={id}
133
- placeholder={placeholder}
143
+ placeholder={resolvedPlaceholder}
134
144
  defaultValue={value}
135
145
  disabled={disabled}
136
146
  autoComplete="off"
@@ -167,7 +177,7 @@ export const PktSearchInput = forwardRef<HTMLInputElement, ISearchInput | ISearc
167
177
  })
168
178
  }
169
179
  >
170
- {label || placeholder}
180
+ {label || resolvedPlaceholder}
171
181
  </PktButton>
172
182
  </div>
173
183
 
@@ -4,10 +4,13 @@ import { ForwardedRef, forwardRef, ReactNode, SelectHTMLAttributes } from 'react
4
4
 
5
5
  import { PktInputWrapper } from '../inputwrapper/InputWrapper'
6
6
  import { describedByIds } from 'shared-utils/forms/described-by'
7
+ import type { TPktStringsOverride } from 'shared-strings'
7
8
 
8
9
  export type { IPktSelectOption, TSelectOption } from 'shared-types/select'
9
10
 
10
11
  export interface IPktSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
12
+ /** Overstyrer tekster for denne komponenten. */
13
+ strings?: TPktStringsOverride
11
14
  ariaDescribedby?: string
12
15
  ariaLabelledby?: string
13
16
  children?: ReactNode | ReactNode[]
@@ -60,6 +63,7 @@ export const PktSelect = forwardRef(
60
63
  useWrapper = true,
61
64
  hasFieldset,
62
65
  inputSize = 'medium',
66
+ strings,
63
67
  ...props
64
68
  }: IPktSelectProps,
65
69
  ref: ForwardedRef<HTMLSelectElement>,
@@ -69,6 +73,7 @@ export const PktSelect = forwardRef(
69
73
  return (
70
74
  <div className={outerClasses}>
71
75
  <PktInputWrapper
76
+ strings={strings}
72
77
  forId={`${id}-input`}
73
78
  label={label}
74
79
  helptext={helptext}
@@ -0,0 +1,38 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from 'react'
2
+ import type { TPktStringsOverride } from 'shared-strings'
3
+
4
+ export const PktStringsContext = createContext<TPktStringsOverride | undefined>(undefined)
5
+
6
+ export interface IPktStringsProvider {
7
+ /** Tekstene som skal gjelde for dette undertreet. */
8
+ strings: TPktStringsOverride
9
+ children?: ReactNode
10
+ }
11
+
12
+ /**
13
+ * Overstyrer Punkt-tekster for et undertre.
14
+ *
15
+ * Providere kan nestes — en indre provider merges oppå den ytre, og en `strings`-prop
16
+ * på en enkelt komponent vinner uansett over begge.
17
+ *
18
+ * ```tsx
19
+ * <PktStringsProvider strings={{ forms: { optional: 'Kan stå tomt' } }}>
20
+ * <PktTextinput id="navn" name="navn" label="Navn" />
21
+ * </PktStringsProvider>
22
+ * ```
23
+ */
24
+ export const PktStringsProvider = ({ strings, children }: IPktStringsProvider) => {
25
+ const parent = useContext(PktStringsContext)
26
+
27
+ const value = useMemo(() => {
28
+ if (!parent) return strings
29
+
30
+ const merged: TPktStringsOverride = { ...parent }
31
+ for (const key of Object.keys(strings) as (keyof TPktStringsOverride)[]) {
32
+ merged[key] = { ...merged[key], ...strings[key] } as TPktStringsOverride[typeof key]
33
+ }
34
+ return merged
35
+ }, [parent, strings])
36
+
37
+ return <PktStringsContext.Provider value={value}>{children}</PktStringsContext.Provider>
38
+ }