@oslokommune/punkt-react 17.1.2 → 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 +35 -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
@@ -8,6 +8,8 @@ import {
8
8
  useEffect,
9
9
  useRef,
10
10
  } from 'react'
11
+ import { usePktStrings } from '../../hooks/usePktStrings'
12
+ import type { TPktStringsOverride } from 'shared-strings'
11
13
 
12
14
  export interface IPktCheckbox extends InputHTMLAttributes<HTMLInputElement> {
13
15
  id: string
@@ -24,9 +26,13 @@ export interface IPktCheckbox extends InputHTMLAttributes<HTMLInputElement> {
24
26
  hideLabel?: boolean
25
27
  labelPosition?: 'right' | 'left'
26
28
  optionalTag?: boolean
29
+ /** @deprecated Bruk `strings={{ forms: { optional: '…' } }}` eller `setPktStrings()`. */
27
30
  optionalText?: string
28
31
  requiredTag?: boolean
32
+ /** @deprecated Bruk `strings={{ forms: { required: '…' } }}` eller `setPktStrings()`. */
29
33
  requiredText?: string
34
+ /** Overstyrer tekster for denne komponenten. */
35
+ strings?: TPktStringsOverride
30
36
  tagText?: string | null
31
37
  onChange?: ChangeEventHandler<HTMLInputElement>
32
38
  }
@@ -48,14 +54,16 @@ export const PktCheckbox = forwardRef(
48
54
  checked,
49
55
  indeterminate,
50
56
  optionalTag,
51
- optionalText = 'Valgfritt',
57
+ optionalText,
52
58
  requiredTag,
53
- requiredText = 'Må fylles ut',
59
+ requiredText,
60
+ strings,
54
61
  tagText,
55
62
  ...props
56
63
  }: IPktCheckbox,
57
64
  ref: ForwardedRef<HTMLInputElement>,
58
65
  ): ReactElement => {
66
+ const s = usePktStrings(['forms'], strings)
59
67
  const internalRef = useRef<HTMLInputElement>(null)
60
68
 
61
69
  useEffect(() => {
@@ -97,8 +105,16 @@ export const PktCheckbox = forwardRef(
97
105
  return (
98
106
  <>
99
107
  {tagText && <span className={tagClasses + ` pkt-tag--gray`}>{tagText}</span>}
100
- {optionalTag && <span className={tagClasses + ` pkt-tag--blue-light`}>{optionalText}</span>}
101
- {requiredTag && <span className={tagClasses + ` pkt-tag--beige`}>{requiredText}</span>}
108
+ {optionalTag && (
109
+ <span className={tagClasses + ` pkt-tag--blue-light`}>
110
+ {optionalText ?? s.forms.optional}
111
+ </span>
112
+ )}
113
+ {requiredTag && (
114
+ <span className={tagClasses + ` pkt-tag--beige`}>
115
+ {requiredText ?? s.forms.required}
116
+ </span>
117
+ )}
102
118
  </>
103
119
  )
104
120
  }
@@ -27,6 +27,7 @@ export const PktCombobox = forwardRef<HTMLDivElement, IPktCombobox>((props, ref)
27
27
  return (
28
28
  <div className={outerClasses} ref={state.wrapperRef} id={state.id || undefined}>
29
29
  <PktInputWrapper
30
+ strings={state.strings}
30
31
  forId={wrapperForId}
31
32
  hasFieldset={props.hasFieldset ?? !hasTextInput}
32
33
  label={state.label || ''}
@@ -2,12 +2,14 @@
2
2
 
3
3
  import type { IComboboxState } from './types'
4
4
  import { PktIcon } from '../icon/Icon'
5
+ import { usePktStrings } from '../../hooks/usePktStrings'
5
6
 
6
7
  interface IListboxProps {
7
8
  state: IComboboxState
8
9
  }
9
10
 
10
11
  export const Listbox = ({ state }: IListboxProps) => {
12
+ const s = usePktStrings(['listbox'], state.strings)
11
13
  const {
12
14
  listboxId,
13
15
  label,
@@ -59,9 +61,9 @@ export const Listbox = ({ state }: IListboxProps) => {
59
61
  <input
60
62
  className="pkt-txt-16-light"
61
63
  type="text"
62
- aria-label="Søk i listen"
64
+ aria-label={s.listbox.searchLabel}
63
65
  form=""
64
- placeholder={searchPlaceholder || 'Søk...'}
66
+ placeholder={searchPlaceholder ?? s.listbox.searchPlaceholder}
65
67
  onChange={handleSearchInput}
66
68
  onKeyDown={handleSearchKeydown}
67
69
  value={searchValue}
@@ -1,5 +1,7 @@
1
1
  'use client'
2
2
 
3
+ import type { TPktStringsOverride } from 'shared-strings'
4
+
3
5
  import type {
4
6
  ChangeEvent,
5
7
  ChangeEventHandler,
@@ -52,6 +54,8 @@ export interface IPktCombobox {
52
54
  useWrapper?: boolean
53
55
  inline?: boolean
54
56
  counter?: boolean
57
+ /** Overstyrer tekster for denne komponenten. */
58
+ strings?: TPktStringsOverride
55
59
  hasFieldset?: boolean
56
60
  inputSize?: 'small' | 'medium' | 'xsmall'
57
61
 
@@ -65,6 +69,8 @@ export interface IPktCombobox {
65
69
  }
66
70
 
67
71
  export interface IComboboxState {
72
+ /** Tekstoverstyring sendt inn på komponenten. */
73
+ strings?: TPktStringsOverride
68
74
  // Identity
69
75
  id: string
70
76
  inputId: string
@@ -27,9 +27,11 @@ import {
27
27
  getInputKeyAction,
28
28
  getInputValueAction,
29
29
  getSingleValueForInput,
30
+ type TComboboxMessageKey,
30
31
  } from 'shared-utils/combobox'
31
32
 
32
33
  import type { IPktCombobox, IComboboxState } from './types'
34
+ import { usePktStrings } from '../../hooks/usePktStrings'
33
35
 
34
36
  // Options sync helper (React-specific: immutable, returns new array)
35
37
  function syncOptionsWithValues(options: IPktComboboxOption[], values: string[]): IPktComboboxOption[] {
@@ -80,6 +82,7 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
80
82
  onChange,
81
83
  onValueChange,
82
84
  children,
85
+ strings,
83
86
  } = props
84
87
 
85
88
  // Identity
@@ -177,13 +180,16 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
177
180
  const [isOpen, setIsOpen] = useState(false)
178
181
  const [searchValue, setSearchValue] = useState('')
179
182
  const [inputValue, setInputValue] = useState('')
180
- const [userInfoMessage, setUserInfoMessage] = useState('')
183
+ const [userInfoMessageKey, setUserInfoMessage] = useState<TComboboxMessageKey>('')
181
184
  const [addValueText, setAddValueText] = useState<string | null>(null)
182
185
  const [inputFocus, setInputFocus] = useState(false)
183
186
  const [editingSingleValue, setEditingSingleValue] = useState(false)
184
187
  const [focusedTagIndex, setFocusedTagIndex] = useState<number>(-1)
185
188
  const suppressNextOpenRef = useRef(false)
186
189
 
190
+ const catalog = usePktStrings(['combobox'], strings)
191
+ const userInfoMessage = userInfoMessageKey ? catalog.combobox[userInfoMessageKey] : ''
192
+
187
193
  // Derived state
188
194
  const maxIsReached = isMaxSelectionReached(values.length, maxlength ?? null)
189
195
  // Telleren vises av seg selv når det er noe å telle, men kan slås av eksplisitt
@@ -250,7 +256,7 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
250
256
  if (!optValue) return
251
257
  if (values.includes(optValue)) return
252
258
  if (multiple && isMaxSelectionReached(values.length, maxlength ?? null)) {
253
- setUserInfoMessage('Maks antall valg nådd')
259
+ setUserInfoMessage('maxSelected')
254
260
  return
255
261
  }
256
262
  const newValues = multiple ? [...values, optValue] : [optValue]
@@ -346,7 +352,7 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
346
352
 
347
353
  let shouldOptionsBeOpen = false
348
354
  let shouldResetInput = true
349
- let newUserInfoMessage = ''
355
+ let newUserInfoMessage: TComboboxMessageKey = ''
350
356
  let newSearchValue = ''
351
357
  // Track the resulting single value for resetInput (avoids stale closure reads)
352
358
  let resultingSingleValue: string | undefined
@@ -356,7 +362,7 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
356
362
  // Not in list + allowUserInput
357
363
  if (!isInOption && allowUserInput && !isEmpty) {
358
364
  addNewUserValue(val)
359
- newUserInfoMessage = 'Ny verdi lagt til'
365
+ newUserInfoMessage = 'valueAdded'
360
366
  shouldOptionsBeOpen = isMultiple
361
367
  if (isSingle) resultingSingleValue = val || ''
362
368
  }
@@ -367,7 +373,7 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
367
373
  }
368
374
  shouldResetInput = false
369
375
  shouldOptionsBeOpen = true
370
- newUserInfoMessage = 'Ingen treff i søket'
376
+ newUserInfoMessage = 'noResults'
371
377
  }
372
378
  // Already selected — deselect
373
379
  else if (isSelected) {
@@ -399,14 +405,14 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
399
405
  }
400
406
  // Multi at max
401
407
  else if (isMultiple && isMaxItemsReached) {
402
- newUserInfoMessage = 'Maks antall valg nådd'
408
+ newUserInfoMessage = 'maxSelected'
403
409
  shouldResetInput = false
404
410
  newSearchValue = val || ''
405
411
  }
406
412
  // Fallback
407
413
  else {
408
414
  if (isSingle) removeAllSelected()
409
- newUserInfoMessage = 'Ingen gyldig verdi valgt'
415
+ newUserInfoMessage = 'noValidValue'
410
416
  shouldResetInput = false
411
417
  shouldOptionsBeOpen = true
412
418
  newSearchValue = val || ''
@@ -606,7 +612,7 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
606
612
  // Just reset the input and keep the "already selected" message.
607
613
  if (input && values.includes(input)) {
608
614
  resetInput(true)
609
- setUserInfoMessage('Verdien er allerede valgt')
615
+ setUserInfoMessage('alreadySelected')
610
616
  break
611
617
  }
612
618
  toggleValue(input)
@@ -1077,6 +1083,7 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
1077
1083
  // UI state
1078
1084
  isOpen,
1079
1085
  userInfoMessage,
1086
+ strings,
1080
1087
  addValueText,
1081
1088
  maxIsReached,
1082
1089
  editingSingleValue,
@@ -1,125 +1 @@
1
- export const consentStrings = {
2
- i18n: {
3
- nb: {
4
- contentPresentation: {
5
- title: 'Oslo kommune bruker informasjonskapsler',
6
- description: [
7
- 'For at nettstedet skal fungere og være trygt, bruker Oslo kommune informasjonskapsler. Noen er teknisk nødvendige, mens andre sikrer ulik funksjonalitet.',
8
- 'Godtar du alle informasjonskapsler, tillater du også at vi samler inn data om statistikk og brukeradferd. Da hjelper du oss med å lage et bedre nettsted uten at du trenger å dele noe personlig informasjon med oss.',
9
- ],
10
- buttons: {
11
- accept: 'Godta alle',
12
- reject: 'Kun nødvendige',
13
- settings: 'Innstillinger for informasjonskapsler',
14
- },
15
- },
16
- contentSettings: {
17
- title: 'Innstillinger for informasjonskapsler',
18
- description: [
19
- 'Her kan du velge hvilke typer informasjonskapsler du vil tillate. Tillatelsen gjelder i 90 dager. Husk at nødvendige informasjonskapsler ikke kan velges bort.',
20
- 'Du kan når som helst endre innstillingene og finne mer informasjon nederst på nettstedet under «Innstillinger for informasjonskapsler» og «Personvern og informasjonskapsler».',
21
- ],
22
- buttons: {
23
- back: 'Tilbake',
24
- save: 'Lagre innstillinger',
25
- },
26
- },
27
- },
28
- en: {
29
- contentPresentation: {
30
- title: 'Before you visit Oslo kommune ...',
31
- description: [
32
- 'This website uses cookies to make improvements. In this context, we need your consent to measure the traffic on the website in relation to statistics and feedback.',
33
- 'To read more about what we use cookies for, go to our privacy declaration which you will find at the bottom of our websites.',
34
- ],
35
- buttons: {
36
- accept: 'Yes, I accept',
37
- reject: 'Only necessary',
38
- settings: 'Go to settings',
39
- },
40
- },
41
- contentSettings: {
42
- title: 'Her kan du aktivt velge mellom ulike informasjonskapsler',
43
- description: [
44
- 'For å lese mer om hva vi bruker informasjonskapsler til gå til vår personvernserklering som du finner på våre nettsider',
45
- ],
46
- buttons: {
47
- back: 'Back',
48
- save: 'Save settings',
49
- },
50
- },
51
- },
52
- },
53
- cookies: [
54
- {
55
- name: 'statistics',
56
- defaultValue: null,
57
- i18n: {
58
- nb: {
59
- title: 'Statistikk og analyse',
60
- description:
61
- 'Jeg godtar at Oslo kommune bruker informasjonskapsler til å innhente data om statistikk og brukeradferd.',
62
- buttons: {
63
- yes: 'Ja',
64
- no: 'Nei',
65
- },
66
- },
67
- en: {
68
- title: 'Statistics and analytics',
69
- description: 'Select your preference for statistics and analytics',
70
- buttons: {
71
- yes: 'Yes',
72
- no: 'No',
73
- },
74
- },
75
- },
76
- },
77
- {
78
- name: 'survey',
79
- defaultValue: null,
80
- i18n: {
81
- nb: {
82
- title: 'Skriftlige tilbakemeldinger',
83
- description:
84
- 'Jeg godtar at Oslo kommune bruker informasjonskapsler til å vise responsfelt og registrere tilbakemeldinger.',
85
- buttons: {
86
- yes: 'Ja',
87
- no: 'Nei',
88
- },
89
- },
90
- en: {
91
- title: 'Survey',
92
- description: 'Select your preference for survey',
93
- buttons: {
94
- yes: 'Yes',
95
- no: 'No',
96
- },
97
- },
98
- },
99
- },
100
- {
101
- name: 'functional',
102
- defaultValue: true,
103
- i18n: {
104
- nb: {
105
- title: 'Funksjonelle informasjonskapsler',
106
- description:
107
- 'Jeg godtar at Oslo kommune bruker informasjonskapsler for å gjøre ulike funksjoner på nettstedet mulige (for eksempel skjemaer og barnehagevelger).',
108
- buttons: {
109
- yes: 'Ja',
110
- no: 'Nei',
111
- },
112
- },
113
- en: {
114
- title: 'Functional cookies',
115
- description:
116
- 'For the website to function optimally, we recommend that you accept these cookies',
117
- buttons: {
118
- yes: 'Yes',
119
- no: 'No',
120
- },
121
- },
122
- },
123
- },
124
- ],
125
- }
1
+ export { consentStrings } from 'shared-strings/consent'
@@ -1,14 +1,15 @@
1
1
  'use client'
2
2
 
3
3
  import { sortDateStrings, fromISOtoLocal } from 'shared-utils/date-utils'
4
- import type { IDatepickerStrings } from 'shared-types/datepicker'
5
4
  import { PktTag } from '../tag/Tag'
5
+ import { usePktStrings } from '../../hooks/usePktStrings'
6
+ import type { TPktStringsOverride } from 'shared-strings'
6
7
 
7
8
  interface DateTagsProps {
8
9
  dates: string[]
9
10
  dateformat: string
10
11
  idBase: string
11
- strings?: IDatepickerStrings
12
+ strings?: TPktStringsOverride
12
13
  onDateRemoved: (date: string) => void
13
14
  }
14
15
 
@@ -20,7 +21,7 @@ export const DateTags = ({
20
21
  onDateRemoved,
21
22
  }: DateTagsProps) => {
22
23
  const sorted = sortDateStrings(dates)
23
- const deleteLabel = strings?.calendar?.buttonAltText ?? 'Slett dato'
24
+ const deleteLabel = usePktStrings(['datepicker'], strings).datepicker.deleteDate
24
25
 
25
26
  return (
26
27
  <div className="pkt-date-tags pkt-datepicker__tags" aria-live="polite">
@@ -256,7 +256,7 @@ describe('PktDatepicker', () => {
256
256
  test('uses custom strings for calendar button', () => {
257
257
  const { container } = createDatepickerTest({
258
258
  strings: {
259
- calendar: { buttonAltText: 'Open calendar' },
259
+ datepicker: { openPicker: 'Open calendar' },
260
260
  },
261
261
  })
262
262
 
@@ -270,7 +270,7 @@ describe('PktDatepicker', () => {
270
270
  showRangeLabels: true,
271
271
  strings: {
272
272
  generic: { from: 'From', to: 'To' },
273
- calendar: { buttonAltText: 'Open calendar' },
273
+ datepicker: { openPicker: 'Open calendar' },
274
274
  },
275
275
  })
276
276
 
@@ -24,6 +24,7 @@ export const PktDatepicker = forwardRef<HTMLDivElement, IPktDatepicker>((props,
24
24
  return (
25
25
  <div className={outerClasses} id={state.id || undefined}>
26
26
  <PktInputWrapper
27
+ strings={state.stringsOverride}
27
28
  ref={state.wrapperRef}
28
29
  ariaDescribedby={props.ariaDescribedby}
29
30
  forId={state.inputId}
@@ -64,10 +64,10 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
64
64
  type="button"
65
65
  onClick={toggleCalendar}
66
66
  disabled={disabled}
67
- aria-label={strings.calendar?.buttonAltText ?? 'Åpne kalender'}
67
+ aria-label={strings.datepicker.openPicker}
68
68
  >
69
69
  <PktIcon name="calendar" />
70
- <span className="pkt-btn__text">{strings.calendar?.buttonAltText ?? 'Åpne kalender'}</span>
70
+ <span className="pkt-btn__text">{strings.datepicker.openPicker}</span>
71
71
  </button>
72
72
  )
73
73
 
@@ -113,7 +113,7 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
113
113
  return (
114
114
  <div className={datepickerInputsClasses}>
115
115
  <div className="pkt-input__container">
116
- {showRangeLabels && <div className="pkt-input-prefix">{strings.generic?.from ?? 'Fra'}</div>}
116
+ {showRangeLabels && <div className="pkt-input-prefix">{strings.generic.from}</div>}
117
117
  <input
118
118
  {...restProps}
119
119
  ref={inputRef}
@@ -128,7 +128,7 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
128
128
  readOnly={readOnly || (isIOSDevice && !readOnly)}
129
129
  disabled={disabled}
130
130
  required={required}
131
- aria-label={`${label} ${strings.generic?.from ?? 'Fra'}`}
131
+ aria-label={`${label} ${strings.generic.from}`}
132
132
  aria-describedby={helptext ? `${id}-helptext` : undefined}
133
133
  aria-invalid={hasError}
134
134
  aria-errormessage={hasError ? `${id}-error` : undefined}
@@ -140,7 +140,7 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
140
140
  onChange={handleRangeFromChange}
141
141
  />
142
142
  <div className={rangeLabelClasses} id={`${id}-to-label`}>
143
- {strings.generic?.to ?? 'Til'}
143
+ {strings.generic.to}
144
144
  </div>
145
145
  {!showRangeLabels && <div className="pkt-input-separator">–</div>}
146
146
  <input
@@ -154,7 +154,7 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
154
154
  max={maxStr}
155
155
  readOnly={readOnly || (isIOSDevice && !readOnly)}
156
156
  disabled={disabled}
157
- aria-label={`${label} ${strings.generic?.to ?? 'Til'}`}
157
+ aria-label={`${label} ${strings.generic.to}`}
158
158
  aria-invalid={hasError}
159
159
  onClick={handleInputClick}
160
160
  onTouchEnd={handleInputTouchEnd}
@@ -1,5 +1,5 @@
1
+ import type { TPktStrings, TPktStringsOverride } from 'shared-strings'
1
2
  import type { ChangeEvent, InputHTMLAttributes, ReactNode } from 'react'
2
- import type { IDatepickerStrings } from 'shared-types/datepicker'
3
3
  import type { TPktInputSize } from 'shared-types/size'
4
4
 
5
5
  export interface IPktDatepicker extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
@@ -42,7 +42,8 @@ export interface IPktDatepicker extends Omit<InputHTMLAttributes<HTMLInputElemen
42
42
  useWrapper?: boolean
43
43
 
44
44
  // Localization
45
- strings?: IDatepickerStrings
45
+ /** Overstyrer tekster for denne komponenten. */
46
+ strings?: TPktStringsOverride
46
47
 
47
48
  // Events
48
49
  onChange?: (e: ChangeEvent<HTMLInputElement>) => void
@@ -103,7 +104,9 @@ export interface IDatepickerState {
103
104
  // Derived state
104
105
  inputType: string
105
106
  isIOSDevice: boolean
106
- strings: IDatepickerStrings
107
+ strings: Pick<TPktStrings, 'generic' | 'datepicker' | 'validation'>
108
+ /** Rå tekstoverstyring, til videresending til underkomponenter. */
109
+ stringsOverride?: TPktStringsOverride
107
110
  inputClasses: string
108
111
  buttonClasses: string
109
112
  rangeLabelClasses: string
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
2
- import { defaultSingleStrings, defaultRangeStrings } from 'shared-types/datepicker'
3
2
  import type { TPktInputSize } from 'shared-types/size'
4
3
  import { valueToArray } from 'shared-utils/value-utils'
5
4
  import { isIOS } from 'shared-utils/device-utils'
@@ -15,6 +14,7 @@ import {
15
14
  } from 'shared-utils/datepicker-utils'
16
15
 
17
16
  import type { IPktDatepicker, IDatepickerState } from './types'
17
+ import { usePktStrings } from '../../hooks/usePktStrings'
18
18
 
19
19
  export function useDatepickerState(props: IPktDatepicker, ref: React.ForwardedRef<HTMLDivElement>): IDatepickerState {
20
20
  const {
@@ -104,10 +104,8 @@ export function useDatepickerState(props: IPktDatepicker, ref: React.ForwardedRe
104
104
  // Derived state
105
105
  const inputType = useMemo(() => getDatepickerInputType(), [])
106
106
  const isIOSDevice = useMemo(() => isIOS(), [])
107
- const strings = useMemo(
108
- () => stringsProp ?? (range ? defaultRangeStrings : defaultSingleStrings),
109
- [stringsProp, range],
110
- )
107
+ const strings = usePktStrings(['generic', 'datepicker', 'validation'], stringsProp)
108
+ const stringsOverride = stringsProp
111
109
 
112
110
  const minStr = typeof min === 'string' ? min : min !== undefined ? String(min) : undefined
113
111
  const maxStr = typeof max === 'string' ? max : max !== undefined ? String(max) : undefined
@@ -507,6 +505,7 @@ export function useDatepickerState(props: IPktDatepicker, ref: React.ForwardedRe
507
505
  inputType,
508
506
  isIOSDevice,
509
507
  strings,
508
+ stringsOverride,
510
509
  inputClasses,
511
510
  buttonClasses,
512
511
  rangeLabelClasses,
@@ -12,10 +12,11 @@ import {
12
12
  useState,
13
13
  } from 'react'
14
14
 
15
- import { defaultFileUploadStrings } from 'shared-types'
16
15
 
17
16
  import { PktIcon } from '../icon/Icon'
18
17
  import { FileItem, TFileItemList, TUploadStrategy } from './types'
18
+ import { usePktStrings } from '../../hooks/usePktStrings'
19
+ import type { TPktStringsOverride } from 'shared-strings'
19
20
 
20
21
  /**
21
22
  * Props for the internal `DropZone` building block.
@@ -27,6 +28,8 @@ import { FileItem, TFileItemList, TUploadStrategy } from './types'
27
28
  */
28
29
  interface IDropZoneProps
29
30
  extends Omit<InputHTMLAttributes<HTMLInputElement>, 'checked' | 'size' | 'type' | 'value' | 'onChange' | 'width'> {
31
+ /** Overstyrer tekster for denne komponenten. */
32
+ strings?: TPktStringsOverride
30
33
  /** Called with `FileItem`s created from dropped/selected files. */
31
34
  onFilesAdded?: (files: TFileItemList) => void
32
35
  /** Used for the native input `id` and related label/ARIA wiring. */
@@ -63,6 +66,7 @@ export const DropZone = forwardRef<HTMLInputElement, IDropZoneProps>(
63
66
  isThumbnailView = false,
64
67
  disabled = false,
65
68
  hasVisibleLabel = false,
69
+ strings,
66
70
  ...inputProps
67
71
  }: IDropZoneProps,
68
72
  forwardedRef,
@@ -86,33 +90,34 @@ export const DropZone = forwardRef<HTMLInputElement, IDropZoneProps>(
86
90
  useImperativeHandle(forwardedRef, () => fileInputRef.current! as HTMLInputElement)
87
91
 
88
92
  const [isDragActive, setIsDragActive] = useState(false)
93
+ const s = usePktStrings(['fileupload'], strings)
89
94
 
90
95
  const uiTexts = useMemo(() => {
91
96
  if (isThumbnailView) {
92
97
  return multiple
93
98
  ? {
94
- dragInactive: defaultFileUploadStrings.dropZoneDragMultipleThumbnail,
95
- dragActive: defaultFileUploadStrings.dropZoneDragActiveMultipleThumbnail,
96
- openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogMultipleThumbnail,
99
+ dragInactive: s.fileupload.dropZoneDragMultipleThumbnail,
100
+ dragActive: s.fileupload.dropZoneDragActiveMultipleThumbnail,
101
+ openFileDialog: s.fileupload.dropZoneOpenFileDialogMultipleThumbnail,
97
102
  }
98
103
  : {
99
- dragInactive: defaultFileUploadStrings.dropZoneDragSingleThumbnail,
100
- dragActive: defaultFileUploadStrings.dropZoneDragActiveSingleThumbnail,
101
- openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogSingleThumbnail,
104
+ dragInactive: s.fileupload.dropZoneDragSingleThumbnail,
105
+ dragActive: s.fileupload.dropZoneDragActiveSingleThumbnail,
106
+ openFileDialog: s.fileupload.dropZoneOpenFileDialogSingleThumbnail,
102
107
  }
103
108
  }
104
109
  return multiple
105
110
  ? {
106
- dragInactive: defaultFileUploadStrings.dropZoneDragMultiple,
107
- dragActive: defaultFileUploadStrings.dropZoneDragActiveMultiple,
108
- openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogMultiple,
111
+ dragInactive: s.fileupload.dropZoneDragMultiple,
112
+ dragActive: s.fileupload.dropZoneDragActiveMultiple,
113
+ openFileDialog: s.fileupload.dropZoneOpenFileDialogMultiple,
109
114
  }
110
115
  : {
111
- dragInactive: defaultFileUploadStrings.dropZoneDragSingle,
112
- dragActive: defaultFileUploadStrings.dropZoneDragActiveSingle,
113
- openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogSingle,
116
+ dragInactive: s.fileupload.dropZoneDragSingle,
117
+ dragActive: s.fileupload.dropZoneDragActiveSingle,
118
+ openFileDialog: s.fileupload.dropZoneOpenFileDialogSingle,
114
119
  }
115
- }, [multiple, isThumbnailView])
120
+ }, [multiple, isThumbnailView, s])
116
121
 
117
122
  const populateNativeFileInput = useCallback(
118
123
  (fileItemList: TFileItemList) => {
@@ -240,7 +245,7 @@ export const DropZone = forwardRef<HTMLInputElement, IDropZoneProps>(
240
245
  </p>
241
246
  {acceptedFormatsReadableString ? (
242
247
  <p className={'pkt-fileupload__drop-zone__placeholder__formats'}>
243
- {defaultFileUploadStrings.supportedFormatsPrefix} {acceptedFormatsReadableString}
248
+ {s.fileupload.supportedFormatsPrefix} {acceptedFormatsReadableString}
244
249
  </p>
245
250
  ) : null}
246
251
  </div>
@@ -152,6 +152,43 @@ describe('PktFileUpload', () => {
152
152
  expectVisibleFilename('file2.docx')
153
153
  expectVisibleFilename('file3.png')
154
154
  })
155
+
156
+ it('should use drop zone text from the strings prop', () => {
157
+ render(
158
+ <PktFileUpload
159
+ multiple
160
+ name="pktFileUpload"
161
+ strings={{
162
+ fileupload: {
163
+ dropZoneDragMultiple: 'Dra dokumentene hit eller',
164
+ dropZoneOpenFileDialogMultiple: 'bla i filene',
165
+ },
166
+ }}
167
+ />,
168
+ )
169
+
170
+ expect(screen.getByText(/Dra dokumentene hit eller/)).toBeInTheDocument()
171
+ expect(screen.getByText('bla i filene')).toBeInTheDocument()
172
+ expect(screen.queryByText('velg filer')).not.toBeInTheDocument()
173
+ })
174
+
175
+ it('should use overridden drag-active drop zone text', () => {
176
+ const { container } = render(
177
+ <PktFileUpload
178
+ multiple
179
+ name="pktFileUpload"
180
+ strings={{
181
+ fileupload: {
182
+ dropZoneDragActiveMultiple: 'Slipp dokumentene her',
183
+ },
184
+ }}
185
+ />,
186
+ )
187
+
188
+ fireEvent.dragOver(container.querySelector('.pkt-fileupload__drop-zone') as HTMLElement)
189
+
190
+ expect(screen.getByText(/Slipp dokumentene her/)).toBeInTheDocument()
191
+ })
155
192
  })
156
193
 
157
194
  describe('File selection via dialog', () => {