@oslokommune/punkt-react 16.26.3 → 17.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 (100) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/components/accordion/Accordion.d.ts +1 -0
  3. package/dist/components/alert/Alert.d.ts +9 -0
  4. package/dist/components/button/Button.d.ts +1 -1
  5. package/dist/components/combobox/types.d.ts +2 -0
  6. package/dist/components/datepicker/types.d.ts +3 -0
  7. package/dist/components/fileupload/FileUpload.d.ts +12 -2
  8. package/dist/components/footer/Footer.accessibility.test.d.ts +0 -0
  9. package/dist/components/footer/Footer.d.ts +12 -34
  10. package/dist/components/footer/Footer.test.d.ts +1 -0
  11. package/dist/components/footer/FooterGlobal.d.ts +23 -0
  12. package/dist/components/footerSimple/FooterSimple.d.ts +19 -16
  13. package/dist/components/footerSimple/FooterSimple.test.d.ts +1 -0
  14. package/dist/components/header/Header.d.ts +15 -3
  15. package/dist/components/header/HeaderGlobal.accessibility.test.d.ts +0 -0
  16. package/dist/components/header/HeaderGlobal.d.ts +2 -0
  17. package/dist/components/header/HeaderGlobal.test.d.ts +0 -0
  18. package/dist/components/header/HeaderService.accessibility.test.d.ts +0 -0
  19. package/dist/components/header/types.d.ts +30 -2
  20. package/dist/components/header-menu/HeaderMenu.d.ts +2 -4
  21. package/dist/components/index.d.ts +4 -0
  22. package/dist/components/inputwrapper/InputWrapper.d.ts +1 -0
  23. package/dist/components/searchinput/SearchInput.d.ts +1 -0
  24. package/dist/components/select/Select.d.ts +4 -7
  25. package/dist/components/textarea/Textarea.d.ts +2 -0
  26. package/dist/components/textinput/Textinput.d.ts +1 -0
  27. package/dist/components/timepicker/types.d.ts +3 -0
  28. package/dist/components/timepicker/useTimepickerState.d.ts +2 -0
  29. package/dist/punkt-react.cjs +1 -1
  30. package/dist/punkt-react.js +3191 -2727
  31. package/dist/shared-types/footer.d.ts +95 -0
  32. package/dist/shared-types/{header-menu.d.ts → header-footer.d.ts} +51 -4
  33. package/dist/shared-types/header.d.ts +1 -1
  34. package/dist/shared-types/index.d.ts +6 -4
  35. package/dist/shared-types/searchinput.d.ts +2 -0
  36. package/dist/shared-types/select.d.ts +3 -2
  37. package/dist/shared-types/size.d.ts +4 -0
  38. package/dist/shared-types/timepicker.d.ts +0 -6
  39. package/dist/shared-utils/datepicker-utils.d.ts +5 -4
  40. package/dist/shared-utils/fileupload/focus-trap.d.ts +4 -10
  41. package/dist/shared-utils/focus-trap/index.d.ts +22 -0
  42. package/dist/shared-utils/{header-menu → header-footer}/example-data.d.ts +5 -4
  43. package/dist/shared-utils/header-footer/external-domain.d.ts +26 -0
  44. package/dist/shared-utils/{header-menu → header-footer}/fetch-header-footer-data.d.ts +6 -2
  45. package/dist/shared-utils/{header-menu → header-footer}/index.d.ts +2 -1
  46. package/dist/shared-utils/{header-menu → header-footer}/select-megamenu.d.ts +1 -1
  47. package/dist/shared-utils/scroll-lock/index.d.ts +24 -0
  48. package/package.json +4 -4
  49. package/src/components/accordion/Accordion.test.tsx +51 -0
  50. package/src/components/accordion/Accordion.tsx +3 -3
  51. package/src/components/accordion/AccordionItem.tsx +7 -14
  52. package/src/components/alert/Alert.test.tsx +45 -9
  53. package/src/components/alert/Alert.tsx +13 -3
  54. package/src/components/button/Button.tsx +2 -2
  55. package/src/components/combobox/Combobox.core.test.tsx +14 -0
  56. package/src/components/combobox/Combobox.tsx +1 -0
  57. package/src/components/combobox/ComboboxInput.tsx +2 -0
  58. package/src/components/combobox/types.ts +2 -0
  59. package/src/components/combobox/useComboboxState.ts +2 -0
  60. package/src/components/consent/Consent.test.tsx +8 -14
  61. package/src/components/consent/Consent.tsx +9 -19
  62. package/src/components/datepicker/Datepicker.core.test.tsx +11 -0
  63. package/src/components/datepicker/Datepicker.tsx +4 -1
  64. package/src/components/datepicker/DatepickerInputs.tsx +16 -1
  65. package/src/components/datepicker/types.ts +3 -0
  66. package/src/components/datepicker/useDatepickerState.ts +17 -9
  67. package/src/components/fileupload/FileUpload.tsx +27 -9
  68. package/src/components/footer/Footer.accessibility.test.tsx +46 -0
  69. package/src/components/footer/Footer.test.tsx +192 -86
  70. package/src/components/footer/Footer.tsx +169 -159
  71. package/src/components/footer/FooterGlobal.tsx +181 -0
  72. package/src/components/footerSimple/FooterSimple.test.tsx +49 -46
  73. package/src/components/footerSimple/FooterSimple.tsx +117 -78
  74. package/src/components/header/Header.tsx +17 -4
  75. package/src/components/header/HeaderGlobal.accessibility.test.tsx +86 -0
  76. package/src/components/header/HeaderGlobal.test.tsx +254 -0
  77. package/src/components/header/HeaderGlobal.tsx +467 -0
  78. package/src/components/header/HeaderService.accessibility.test.tsx +70 -0
  79. package/src/components/header/HeaderService.test.tsx +29 -16
  80. package/src/components/header/HeaderService.tsx +24 -84
  81. package/src/components/header/types.ts +35 -0
  82. package/src/components/header-menu/HeaderMenu.test.tsx +16 -39
  83. package/src/components/header-menu/HeaderMenu.tsx +62 -70
  84. package/src/components/index.ts +4 -0
  85. package/src/components/inputwrapper/InputWrapper.tsx +6 -1
  86. package/src/components/modal/Modal.tsx +7 -11
  87. package/src/components/searchinput/SearchInput.test.tsx +10 -0
  88. package/src/components/searchinput/SearchInput.tsx +19 -7
  89. package/src/components/select/Select.test.tsx +18 -0
  90. package/src/components/select/Select.tsx +8 -4
  91. package/src/components/textarea/Textarea.test.tsx +10 -0
  92. package/src/components/textarea/Textarea.tsx +5 -1
  93. package/src/components/textinput/Textinput.test.tsx +28 -0
  94. package/src/components/textinput/Textinput.tsx +5 -2
  95. package/src/components/timepicker/Timepicker.test.tsx +10 -0
  96. package/src/components/timepicker/Timepicker.tsx +8 -7
  97. package/src/components/timepicker/types.ts +8 -5
  98. package/src/components/timepicker/useTimepickerState.ts +4 -0
  99. package/dist/shared-utils/modal-scroll-lock.d.ts +0 -10
  100. /package/dist/shared-utils/{header-menu → header-footer}/icon-map.d.ts +0 -0
@@ -64,20 +64,56 @@ describe('PktAlert', () => {
64
64
  ${''} | ${undefined}
65
65
  ${null} | ${undefined}
66
66
  ${(
67
- <span>
67
+ <span>
68
68
  Complex <strong>title</strong>
69
69
  </span>
70
- )} | ${'<span>Complex <strong>title</strong></span>'}
71
- `('support complex title %$', async ({title, expected}) => {
72
- const { container } = render(
73
- <PktAlert title={title}>
74
- Alert text
75
- </PktAlert>,
76
- )
77
- const titleElement = container.querySelector('.pkt-alert__title');
70
+ )} | ${'<span>Complex <strong>title</strong></span>'}
71
+ `('support complex title %$', async ({ title, expected }) => {
72
+ const { container } = render(<PktAlert title={title}>Alert text</PktAlert>)
73
+ const titleElement = container.querySelector('.pkt-alert__title')
78
74
  expect(titleElement?.innerHTML).toEqual(expected)
79
75
  })
80
76
 
77
+ describe('size and compact', () => {
78
+ test('renders with pkt-alert--medium by default', async () => {
79
+ const { container } = render(<PktAlert>Alert text</PktAlert>)
80
+ const alert = container.querySelector('.pkt-alert')
81
+ expect(alert).toHaveClass('pkt-alert--medium')
82
+ expect(alert).not.toHaveClass('pkt-alert--compact')
83
+ })
84
+
85
+ test.each`
86
+ size | expectedClass
87
+ ${'small'} | ${'pkt-alert--small'}
88
+ ${'xsmall'} | ${'pkt-alert--xsmall'}
89
+ `('renders with $expectedClass when size is $size', async ({ size, expectedClass }) => {
90
+ const { container } = render(<PktAlert size={size}>Alert text</PktAlert>)
91
+ const alert = container.querySelector('.pkt-alert')
92
+ expect(alert).toHaveClass(expectedClass)
93
+ expect(alert).not.toHaveClass('pkt-alert--medium')
94
+ })
95
+
96
+ test('renders with pkt-alert--small when compact is set without size', async () => {
97
+ const { container } = render(<PktAlert compact>Alert text</PktAlert>)
98
+ const alert = container.querySelector('.pkt-alert')
99
+ expect(alert).toHaveClass('pkt-alert--small')
100
+ expect(alert).not.toHaveClass('pkt-alert--compact')
101
+ expect(alert).not.toHaveClass('pkt-alert--medium')
102
+ })
103
+
104
+ test('explicit size wins over compact', async () => {
105
+ const { container } = render(
106
+ <PktAlert compact size="xsmall">
107
+ Alert text
108
+ </PktAlert>,
109
+ )
110
+ const alert = container.querySelector('.pkt-alert')
111
+ expect(alert).toHaveClass('pkt-alert--xsmall')
112
+ expect(alert).not.toHaveClass('pkt-alert--compact')
113
+ expect(alert).not.toHaveClass('pkt-alert--small')
114
+ })
115
+ })
116
+
81
117
  describe('accessibility', () => {
82
118
  test('renders with no wcag errors with axe', async () => {
83
119
  const { container } = render(<PktAlert title="Error" skin="error"></PktAlert>)
@@ -2,7 +2,7 @@
2
2
 
3
3
  import classNames from 'classnames'
4
4
  import { FC, HTMLAttributes, ReactElement, ReactNode, useCallback, useState } from 'react'
5
- import type { TAlertRole, TAlertSkin,TAriaLive } from 'shared-types'
5
+ import type { TAlertRole, TAlertSkin, TAriaLive } from 'shared-types'
6
6
 
7
7
  import { PktButton } from '../button/Button'
8
8
  import { PktIcon } from '../icon/Icon'
@@ -13,8 +13,17 @@ export interface IPktAlert extends Omit<HTMLAttributes<HTMLDivElement>, 'title'>
13
13
  title?: ReactNode
14
14
  date?: string
15
15
  ariaLive?: TAriaLive
16
+ /**
17
+ * @deprecated Bruk size i stedet. Uten eksplisitt size gir compact størrelsen 'small'.
18
+ */
16
19
  compact?: boolean
17
20
  role?: TAlertRole
21
+ /**
22
+ * Størrelse på varslingen.
23
+ *
24
+ * @default medium
25
+ */
26
+ size?: 'medium' | 'small' | 'xsmall'
18
27
  onClose?: (e: CustomEvent) => void
19
28
  }
20
29
 
@@ -24,6 +33,7 @@ export const PktAlert: FC<IPktAlert> = ({
24
33
  children,
25
34
  closeAlert,
26
35
  compact,
36
+ size = compact ? 'small' : 'medium',
27
37
  title,
28
38
  date,
29
39
  ariaLive: ariaLiveCamel,
@@ -37,7 +47,7 @@ export const PktAlert: FC<IPktAlert> = ({
37
47
 
38
48
  const classes = {
39
49
  'pkt-alert': true,
40
- 'pkt-alert--compact': compact,
50
+ [`pkt-alert--${size}`]: size,
41
51
  [`pkt-alert--${skin}`]: skin,
42
52
  'pkt-hide': isClosed,
43
53
  }
@@ -70,7 +80,7 @@ export const PktAlert: FC<IPktAlert> = ({
70
80
  <PktButton
71
81
  tabIndex={0}
72
82
  aria-label="close"
73
- size={compact ? 'small' : 'medium'}
83
+ size={size}
74
84
  type="button"
75
85
  skin="tertiary"
76
86
  iconName="close"
@@ -1,5 +1,5 @@
1
1
  'use client'
2
- import { type ButtonHTMLAttributes,ForwardedRef, forwardRef } from 'react'
2
+ import { type ButtonHTMLAttributes, ForwardedRef, forwardRef } from 'react'
3
3
 
4
4
  import { PktIcon } from '../icon/Icon'
5
5
  declare global {
@@ -19,7 +19,7 @@ export interface IPktButton extends ButtonHTMLAttributes<HTMLButtonElement> {
19
19
  iconPath?: string
20
20
  secondIconPath?: string
21
21
  mode?: 'light' | 'dark'
22
- size?: 'small' | 'medium' | 'large'
22
+ size?: 'xsmall' | 'small' | 'medium' | 'large'
23
23
  fullWidth?: boolean
24
24
  fullWidthOnMobile?: boolean
25
25
  color?:
@@ -107,6 +107,20 @@ describe('PktCombobox', () => {
107
107
  expect(inputDiv).toHaveClass('pkt-combobox__input--fullwidth')
108
108
  })
109
109
 
110
+ test('applies input size classes for small and xsmall', () => {
111
+ const { container: smallContainer } = createComboboxTest({ inputSize: 'small' })
112
+
113
+ const smallInput = smallContainer.querySelector('.pkt-combobox__input')
114
+ expect(smallInput).toHaveClass('pkt-combobox__input--small')
115
+ expect(smallInput).not.toHaveClass('pkt-combobox__input--xsmall')
116
+
117
+ const { container: xsmallContainer } = createComboboxTest({ inputSize: 'xsmall' })
118
+
119
+ const xsmallInput = xsmallContainer.querySelector('.pkt-combobox__input')
120
+ expect(xsmallInput).toHaveClass('pkt-combobox__input--xsmall')
121
+ expect(xsmallInput).not.toHaveClass('pkt-combobox__input--small')
122
+ })
123
+
110
124
  test('renders with custom className', () => {
111
125
  const { container } = createComboboxTest({ className: 'my-custom-class' })
112
126
 
@@ -42,6 +42,7 @@ export const PktCombobox = forwardRef<HTMLDivElement, IPktCombobox>((props, ref)
42
42
  requiredText={state.requiredText}
43
43
  tagText={state.tagText}
44
44
  useWrapper={state.useWrapper}
45
+ size={props.inputSize ?? 'medium'}
45
46
  counter={state.hasCounter}
46
47
  counterCurrent={state.values.length}
47
48
  counterMaxLength={state.maxlength}
@@ -37,10 +37,12 @@ export const ComboboxInput = ({ state }: IComboboxInputProps) => {
37
37
  handlePlaceholderClick,
38
38
  handleSelectOnlyKeydown,
39
39
  name,
40
+ inputSize,
40
41
  } = state
41
42
 
42
43
  const inputClasses = [
43
44
  'pkt-combobox__input',
45
+ `pkt-combobox__input--${inputSize}`,
44
46
  fullwidth && 'pkt-combobox__input--fullwidth',
45
47
  isOpen && 'pkt-combobox__input--open',
46
48
  hasError && 'pkt-combobox__input--error',
@@ -50,6 +50,7 @@ export interface IPktCombobox {
50
50
  optionalText?: string
51
51
  tagText?: string
52
52
  useWrapper?: boolean
53
+ inputSize?: 'small' | 'medium' | 'xsmall'
53
54
 
54
55
  // Events
55
56
  onChange?: ChangeEventHandler<HTMLInputElement>
@@ -109,6 +110,7 @@ export interface IComboboxState {
109
110
  requiredText?: string
110
111
  tagText?: string
111
112
  useWrapper: boolean
113
+ inputSize: 'small' | 'medium' | 'xsmall'
112
114
  name?: string
113
115
  className?: string
114
116
 
@@ -72,6 +72,7 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
72
72
  requiredText,
73
73
  tagText,
74
74
  useWrapper = true,
75
+ inputSize = 'medium',
75
76
  name,
76
77
  className,
77
78
  onChange,
@@ -1104,6 +1105,7 @@ export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HT
1104
1105
  requiredText,
1105
1106
  tagText,
1106
1107
  useWrapper,
1108
+ inputSize,
1107
1109
  name,
1108
1110
  className,
1109
1111
 
@@ -65,9 +65,7 @@ describe('PktConsent', () => {
65
65
  test('renders the default button trigger with the bokmål settings label', () => {
66
66
  render(<PktConsent />)
67
67
 
68
- expect(
69
- screen.getByRole('button', { name: 'Innstillinger for informasjonskapsler' }),
70
- ).toBeInTheDocument()
68
+ expect(screen.getByRole('button', { name: 'Innstillinger for informasjonskapsler' })).toBeInTheDocument()
71
69
  })
72
70
 
73
71
  test('uses the English settings label when i18nLanguage is "en"', () => {
@@ -91,14 +89,14 @@ describe('PktConsent', () => {
91
89
  expect(link).toHaveTextContent('Innstillinger')
92
90
  })
93
91
 
94
- test('renders a footer link with chevron icon when triggerType="footerlink"', () => {
95
- const { container } = render(
96
- <PktConsent triggerType="footerlink" triggerText="Innstillinger" />,
97
- )
92
+ test('renders a plain footer link when triggerType="footerlink"', () => {
93
+ const { container } = render(<PktConsent triggerType="footerlink" triggerText="Innstillinger" />)
98
94
 
99
95
  const link = container.querySelector('a.pkt-footer__link')
100
96
  expect(link).toBeInTheDocument()
101
- expect(link?.querySelector('.pkt-icon')).toHaveAttribute('name', 'chevron-right')
97
+ expect(link).toHaveAttribute('aria-haspopup', 'dialog')
98
+ // Ny footer-stil: ren understreket lenke uten ikon.
99
+ expect(link?.querySelector('.pkt-icon')).toBeNull()
102
100
  })
103
101
 
104
102
  test('renders an icon-only button when triggerType="icon"', () => {
@@ -199,17 +197,13 @@ describe('PktConsent', () => {
199
197
  })
200
198
 
201
199
  test('link trigger has no axe violations', async () => {
202
- const { container } = render(
203
- <PktConsent triggerType="link" triggerText="Cookie settings" />,
204
- )
200
+ const { container } = render(<PktConsent triggerType="link" triggerText="Cookie settings" />)
205
201
  const results = await axe(container)
206
202
  expect(results).toHaveNoViolations()
207
203
  })
208
204
 
209
205
  test('footer link trigger has no axe violations', async () => {
210
- const { container } = render(
211
- <PktConsent triggerType="footerlink" triggerText="Cookie settings" />,
212
- )
206
+ const { container } = render(<PktConsent triggerType="footerlink" triggerText="Cookie settings" />)
213
207
  const results = await axe(container)
214
208
  expect(results).toHaveNoViolations()
215
209
  })
@@ -1,16 +1,8 @@
1
1
  'use client'
2
2
 
3
- import {
4
- forwardRef,
5
- HTMLAttributes,
6
- MouseEvent as ReactMouseEvent,
7
- Ref,
8
- useEffect,
9
- useRef,
10
- } from 'react'
3
+ import { forwardRef, HTMLAttributes, MouseEvent as ReactMouseEvent, Ref, useEffect, useRef } from 'react'
11
4
 
12
5
  import { PktButton } from '../button/Button'
13
- import { PktIcon } from '../icon/Icon'
14
6
  import { consentStrings } from './strings'
15
7
 
16
8
  interface ConsentItem {
@@ -154,11 +146,8 @@ export const PktConsent = forwardRef<HTMLElement, IPktConsent>(function PktConse
154
146
 
155
147
  const triggerInitRef = useRef<() => void>(() => {})
156
148
 
157
- const language = (i18nLanguage in consentStrings.i18n
158
- ? i18nLanguage
159
- : 'nb') as keyof typeof consentStrings.i18n
160
- const resolvedTriggerText =
161
- triggerText ?? consentStrings.i18n[language].contentPresentation.buttons.settings
149
+ const language = (i18nLanguage in consentStrings.i18n ? i18nLanguage : 'nb') as keyof typeof consentStrings.i18n
150
+ const resolvedTriggerText = triggerText ?? consentStrings.i18n[language].contentPresentation.buttons.settings
162
151
 
163
152
  useEffect(() => {
164
153
  let cancelled = false
@@ -188,9 +177,7 @@ export const PktConsent = forwardRef<HTMLElement, IPktConsent>(function PktConse
188
177
  }
189
178
 
190
179
  triggerInitRef.current = () => {
191
- window.document.dispatchEvent(
192
- new Event('CookieBannerReady', { bubbles: true, cancelable: true }),
193
- )
180
+ window.document.dispatchEvent(new Event('CookieBannerReady', { bubbles: true, cancelable: true }))
194
181
 
195
182
  const banner = window.cookieBanner
196
183
  const events = window.__cookieEvents
@@ -232,6 +219,7 @@ export const PktConsent = forwardRef<HTMLElement, IPktConsent>(function PktConse
232
219
  setTimeout(() => window.cookieBanner?.openCookieModal())
233
220
  }
234
221
 
222
+ // aria-haspopup: triggeren åpner consent-dialogen, den navigerer ikke
235
223
  if (triggerType === 'link') {
236
224
  return (
237
225
  <a
@@ -239,6 +227,7 @@ export const PktConsent = forwardRef<HTMLElement, IPktConsent>(function PktConse
239
227
  ref={ref as Ref<HTMLAnchorElement>}
240
228
  href="#"
241
229
  className="pkt-link"
230
+ aria-haspopup="dialog"
242
231
  onClick={openModal}
243
232
  >
244
233
  {resolvedTriggerText}
@@ -252,9 +241,9 @@ export const PktConsent = forwardRef<HTMLElement, IPktConsent>(function PktConse
252
241
  ref={ref as Ref<HTMLAnchorElement>}
253
242
  href="#"
254
243
  className="pkt-footer__link"
244
+ aria-haspopup="dialog"
255
245
  onClick={openModal}
256
246
  >
257
- <PktIcon name="chevron-right" className="pkt-footer__link-icon" />
258
247
  {resolvedTriggerText}
259
248
  </a>
260
249
  )
@@ -266,6 +255,7 @@ export const PktConsent = forwardRef<HTMLElement, IPktConsent>(function PktConse
266
255
  skin="tertiary"
267
256
  variant="icon-only"
268
257
  iconName="cookie"
258
+ aria-haspopup="dialog"
269
259
  onClick={openModal}
270
260
  >
271
261
  {resolvedTriggerText}
@@ -273,7 +263,7 @@ export const PktConsent = forwardRef<HTMLElement, IPktConsent>(function PktConse
273
263
  )
274
264
  }
275
265
  return (
276
- <PktButton ref={ref as Ref<HTMLButtonElement>} onClick={openModal}>
266
+ <PktButton ref={ref as Ref<HTMLButtonElement>} aria-haspopup="dialog" onClick={openModal}>
277
267
  {resolvedTriggerText}
278
268
  </PktButton>
279
269
  )
@@ -49,6 +49,17 @@ describe('PktDatepicker', () => {
49
49
  expect(outer).toBeInTheDocument()
50
50
  })
51
51
 
52
+ test.each([
53
+ ['small', 'small'],
54
+ ['xsmall', 'xsmall'],
55
+ ] as const)('applies inputSize="%s" classes on root, input and calendar button', (size, sizeClass) => {
56
+ const { container } = createDatepickerTest({ inputSize: size })
57
+
58
+ expect(container.querySelector(`.pkt-datepicker--${sizeClass}`)).toBeTruthy()
59
+ expect(container.querySelector(`.pkt-input--${sizeClass}`)).toBeTruthy()
60
+ expect(container.querySelector(`.pkt-btn--${sizeClass}`)).toBeTruthy()
61
+ })
62
+
52
63
  test('renders datepicker inputs container', () => {
53
64
  const { container } = createDatepickerTest()
54
65
 
@@ -12,7 +12,9 @@ export type { IPktDatepicker } from './types'
12
12
  export const PktDatepicker = forwardRef<HTMLDivElement, IPktDatepicker>((props, ref) => {
13
13
  const state = useDatepickerState(props, ref)
14
14
 
15
- const outerClasses = ['pkt-datepicker', state.className].filter(Boolean).join(' ')
15
+ const outerClasses = ['pkt-datepicker', `pkt-datepicker--${state.inputSize}`, state.className]
16
+ .filter(Boolean)
17
+ .join(' ')
16
18
 
17
19
  return (
18
20
  <div className={outerClasses} id={state.id || undefined}>
@@ -34,6 +36,7 @@ export const PktDatepicker = forwardRef<HTMLDivElement, IPktDatepicker>((props,
34
36
  requiredText={state.requiredText}
35
37
  tagText={state.tagText}
36
38
  useWrapper={state.useWrapper}
39
+ size={state.inputSize}
37
40
  counter={state.hasCounter}
38
41
  counterCurrent={state.values.length}
39
42
  counterMaxLength={state.maxlength}
@@ -52,6 +52,11 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
52
52
  toggleCalendar()
53
53
  }
54
54
 
55
+ const handleInputTouchEnd = (e: React.TouchEvent<HTMLInputElement>) => {
56
+ e.preventDefault()
57
+ toggleCalendar()
58
+ }
59
+
55
60
  const renderCalendarButton = () => (
56
61
  <button
57
62
  ref={btnRef}
@@ -69,7 +74,13 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
69
74
  if (multiple) {
70
75
  return (
71
76
  <>
72
- <DateTags dates={values} dateformat={dateformat} idBase={id} strings={strings} onDateRemoved={handleTagRemoved} />
77
+ <DateTags
78
+ dates={values}
79
+ dateformat={dateformat}
80
+ idBase={id}
81
+ strings={strings}
82
+ onDateRemoved={handleTagRemoved}
83
+ />
73
84
  <div className="pkt-datepicker__inputs">
74
85
  <div className="pkt-input__container">
75
86
  <input
@@ -86,6 +97,7 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
86
97
  aria-invalid={hasError}
87
98
  aria-errormessage={hasError ? `${id}-error` : undefined}
88
99
  onClick={handleInputClick}
100
+ onTouchEnd={handleInputTouchEnd}
89
101
  onKeyDown={handleMultipleKeydown}
90
102
  onFocus={handleFocus}
91
103
  onBlur={handleMultipleBlur}
@@ -121,6 +133,7 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
121
133
  aria-invalid={hasError}
122
134
  aria-errormessage={hasError ? `${id}-error` : undefined}
123
135
  onClick={handleInputClick}
136
+ onTouchEnd={handleInputTouchEnd}
124
137
  onKeyDown={handleRangeFromKeydown}
125
138
  onFocus={handleFocus}
126
139
  onBlur={handleRangeBlur}
@@ -144,6 +157,7 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
144
157
  aria-label={`${label} ${strings.generic?.to ?? 'Til'}`}
145
158
  aria-invalid={hasError}
146
159
  onClick={handleInputClick}
160
+ onTouchEnd={handleInputTouchEnd}
147
161
  onKeyDown={handleRangeToKeydown}
148
162
  onFocus={handleFocus}
149
163
  onBlur={handleRangeBlur}
@@ -177,6 +191,7 @@ export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
177
191
  aria-invalid={hasError}
178
192
  aria-errormessage={hasError ? `${id}-error` : undefined}
179
193
  onClick={handleInputClick}
194
+ onTouchEnd={handleInputTouchEnd}
180
195
  onKeyDown={handleSingleKeydown}
181
196
  onFocus={handleFocus}
182
197
  onBlur={undefined}
@@ -1,5 +1,6 @@
1
1
  import type { ChangeEvent, InputHTMLAttributes, ReactNode } from 'react'
2
2
  import type { IDatepickerStrings } from 'shared-types/datepicker'
3
+ import type { TPktInputSize } from 'shared-types/size'
3
4
 
4
5
  export interface IPktDatepicker extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
5
6
  id: string
@@ -23,6 +24,7 @@ export interface IPktDatepicker extends Omit<InputHTMLAttributes<HTMLInputElemen
23
24
 
24
25
  // InputWrapper props
25
26
  fullwidth?: boolean
27
+ inputSize?: TPktInputSize
26
28
  hasFieldset?: boolean
27
29
  inline?: boolean
28
30
  helptext?: string | ReactNode
@@ -76,6 +78,7 @@ export interface IDatepickerState {
76
78
  name?: string
77
79
  placeholder?: string
78
80
  fullwidth: boolean
81
+ inputSize: TPktInputSize
79
82
  hasFieldset: boolean
80
83
  inline: boolean
81
84
  helptext?: string | ReactNode
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
2
2
  import { defaultSingleStrings, defaultRangeStrings } from 'shared-types/datepicker'
3
+ import type { TPktInputSize } from 'shared-types/size'
3
4
  import { valueToArray } from 'shared-utils/value-utils'
4
5
  import { isIOS } from 'shared-utils/device-utils'
5
6
  import { newDate, filterSelectableDates } from 'shared-utils/date-utils'
@@ -35,6 +36,7 @@ export function useDatepickerState(props: IPktDatepicker, ref: React.ForwardedRe
35
36
  calendarOpen: calendarOpenProp,
36
37
  timezone,
37
38
  fullwidth = false,
39
+ inputSize = 'medium' as TPktInputSize,
38
40
  hasFieldset = false,
39
41
  inline = false,
40
42
  helptext,
@@ -110,20 +112,25 @@ export function useDatepickerState(props: IPktDatepicker, ref: React.ForwardedRe
110
112
  const maxStr = typeof max === 'string' ? max : max !== undefined ? String(max) : undefined
111
113
 
112
114
  const inputClasses = useMemo(() => {
113
- const classMap = getDatepickerInputClasses(fullwidth, showRangeLabels, multiple, range, readOnly, inputType)
115
+ const classMap = getDatepickerInputClasses(
116
+ fullwidth,
117
+ showRangeLabels,
118
+ multiple,
119
+ range,
120
+ readOnly,
121
+ inputType,
122
+ inputSize,
123
+ )
114
124
  return Object.entries(classMap)
115
125
  .filter(([, v]) => v)
116
126
  .map(([k]) => k)
117
127
  .join(' ')
118
- }, [fullwidth, showRangeLabels, multiple, range, readOnly, inputType])
128
+ }, [fullwidth, showRangeLabels, multiple, range, readOnly, inputType, inputSize])
119
129
 
120
- const buttonClasses = useMemo(() => {
121
- const classMap = getDatepickerButtonClasses()
122
- return Object.entries(classMap)
123
- .filter(([, v]) => v)
124
- .map(([k]) => k)
125
- .join(' ')
126
- }, [])
130
+ const buttonClasses = Object.entries(getDatepickerButtonClasses(inputSize))
131
+ .filter(([, v]) => v)
132
+ .map(([k]) => k)
133
+ .join(' ')
127
134
 
128
135
  const rangeLabelClasses = useMemo(() => {
129
136
  const classMap = getRangeLabelClasses(showRangeLabels)
@@ -476,6 +483,7 @@ export function useDatepickerState(props: IPktDatepicker, ref: React.ForwardedRe
476
483
  name,
477
484
  placeholder,
478
485
  fullwidth,
486
+ inputSize,
479
487
  hasFieldset,
480
488
  inline,
481
489
  helptext,
@@ -61,11 +61,10 @@ export { formatFileSize, parseFileSize } from 'shared-utils/fileupload'
61
61
  /**
62
62
  * Shared props for `PktFileUpload` (both `form` and `custom` strategies).
63
63
  */
64
- interface IBaseFileUploadProps
65
- extends Omit<
66
- InputHTMLAttributes<HTMLInputElement>,
67
- 'checked' | 'size' | 'type' | 'value' | 'onChange' | 'width' | 'defaultValue'
68
- > {
64
+ interface IBaseFileUploadProps extends Omit<
65
+ InputHTMLAttributes<HTMLInputElement>,
66
+ 'checked' | 'size' | 'type' | 'value' | 'onChange' | 'width' | 'defaultValue'
67
+ > {
69
68
  /** Called with the next file list when files are added/removed/updated. */
70
69
  onFilesChanged?: (files: TFileItemList) => void
71
70
  /** Allow selecting/dropping multiple files. */
@@ -120,10 +119,20 @@ interface IBaseFileUploadProps
120
119
  label?: string
121
120
  /** Help text under the label. */
122
121
  helptext?: string | ReactNode
123
- /** Show "Valgfritt" tag in wrapper. */
122
+ /** Expandable help text (dropdown). */
123
+ helptextDropdown?: string | ReactNode
124
+ /** Button label for expandable help text. */
125
+ helptextDropdownButton?: string
126
+ /** Shows an "optional" tag. */
124
127
  optionalTag?: boolean
125
- /** Show "Må fylles ut" tag in wrapper. */
128
+ /** Label text for the optional tag. */
129
+ optionalText?: string
130
+ /** Shows a "required" tag. */
126
131
  requiredTag?: boolean
132
+ /** Label text for the required tag. */
133
+ requiredText?: string
134
+ /** Extra tag next to the label. */
135
+ tagText?: string | null
127
136
  /** Marks the upload as required. Native validation is used in `form`; submit validation in `custom`. */
128
137
  required?: boolean
129
138
  /** Enable image preview modal (only applies to thumbnail renderer). */
@@ -131,8 +140,7 @@ interface IBaseFileUploadProps
131
140
  }
132
141
 
133
142
  interface IFileInputFileUploadProps
134
- extends IBaseFileUploadProps,
135
- Omit<HTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'defaultValue'> {
143
+ extends IBaseFileUploadProps, Omit<HTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'defaultValue'> {
136
144
  uploadStrategy?: 'form'
137
145
  onFileUploadRequested?: never
138
146
  }
@@ -174,8 +182,13 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
174
182
  disabled = false,
175
183
  label,
176
184
  helptext,
185
+ helptextDropdown,
186
+ helptextDropdownButton,
177
187
  optionalTag,
188
+ optionalText,
178
189
  requiredTag,
190
+ requiredText,
191
+ tagText,
179
192
  required = false,
180
193
  enableImagePreview = false,
181
194
  ['aria-describedby']: ariaDescribedByProp,
@@ -412,10 +425,15 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
412
425
  forId={id}
413
426
  label={label}
414
427
  helptext={helptext}
428
+ helptextDropdown={helptextDropdown}
429
+ helptextDropdownButton={helptextDropdownButton}
415
430
  disabled={disabled}
416
431
  hasError={hasError}
417
432
  optionalTag={optionalTag}
433
+ optionalText={optionalText}
418
434
  requiredTag={requiredTag}
435
+ requiredText={requiredText}
436
+ tagText={tagText}
419
437
  className={classNames('pkt-fileupload-wrapper', { 'pkt-fileupload-wrapper--full-width': fullwidth })}
420
438
  >
421
439
  {fileUploadContent}
@@ -0,0 +1,46 @@
1
+ import '@testing-library/jest-dom'
2
+
3
+ import { render } from '@testing-library/react'
4
+ import { axe, toHaveNoViolations } from 'jest-axe'
5
+ import { describe, expect, test } from 'vitest'
6
+
7
+ import { EXAMPLE_HEADER_FOOTER_DATA } from 'shared-utils/header-footer'
8
+ import type { IPktFooterColumn, IPktFooterLink } from 'shared-types'
9
+ import { PktFooter } from './Footer'
10
+ import { PktFooterSimple } from '../footerSimple/FooterSimple'
11
+
12
+ expect.extend(toHaveNoViolations)
13
+
14
+ const COLUMNS: IPktFooterColumn[] = [
15
+ {
16
+ title: 'Overskrift 1',
17
+ text: 'Beskrivende tekst om løsningen.',
18
+ links: [{ text: 'Lenke 1', href: 'https://example.test/lenke-1' }],
19
+ },
20
+ {
21
+ title: 'Overskrift 2',
22
+ links: [{ text: 'Lenke 2', href: 'https://example.test/lenke-2' }],
23
+ },
24
+ ]
25
+
26
+ const LINKS: IPktFooterLink[] = [
27
+ { text: 'Lenke 1', href: 'https://example.test/lenke-1' },
28
+ { text: 'Lenke 2', href: 'https://example.test/lenke-2' },
29
+ ]
30
+
31
+ describe('PktFooter tilgjengelighet', () => {
32
+ test('kun global footer har ingen a11y-avvik', async () => {
33
+ const { container } = render(<PktFooter data={EXAMPLE_HEADER_FOOTER_DATA} />)
34
+ expect(await axe(container)).toHaveNoViolations()
35
+ })
36
+
37
+ test('footer med custom kolonner har ingen a11y-avvik', async () => {
38
+ const { container } = render(<PktFooter columns={COLUMNS} data={EXAMPLE_HEADER_FOOTER_DATA} />)
39
+ expect(await axe(container)).toHaveNoViolations()
40
+ })
41
+
42
+ test('kompakt footer har ingen a11y-avvik', async () => {
43
+ const { container } = render(<PktFooterSimple links={LINKS} data={EXAMPLE_HEADER_FOOTER_DATA} />)
44
+ expect(await axe(container)).toHaveNoViolations()
45
+ })
46
+ })