@oslokommune/punkt-react 16.26.5 → 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 (98) hide show
  1. package/CHANGELOG.md +93 -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/footer/Footer.accessibility.test.d.ts +0 -0
  8. package/dist/components/footer/Footer.d.ts +12 -34
  9. package/dist/components/footer/Footer.test.d.ts +1 -0
  10. package/dist/components/footer/FooterGlobal.d.ts +23 -0
  11. package/dist/components/footerSimple/FooterSimple.d.ts +19 -16
  12. package/dist/components/footerSimple/FooterSimple.test.d.ts +1 -0
  13. package/dist/components/header/Header.d.ts +15 -3
  14. package/dist/components/header/HeaderGlobal.accessibility.test.d.ts +0 -0
  15. package/dist/components/header/HeaderGlobal.d.ts +2 -0
  16. package/dist/components/header/HeaderGlobal.test.d.ts +0 -0
  17. package/dist/components/header/HeaderService.accessibility.test.d.ts +0 -0
  18. package/dist/components/header/types.d.ts +30 -2
  19. package/dist/components/header-menu/HeaderMenu.d.ts +2 -4
  20. package/dist/components/index.d.ts +4 -0
  21. package/dist/components/inputwrapper/InputWrapper.d.ts +1 -0
  22. package/dist/components/searchinput/SearchInput.d.ts +1 -0
  23. package/dist/components/select/Select.d.ts +4 -7
  24. package/dist/components/textarea/Textarea.d.ts +2 -0
  25. package/dist/components/textinput/Textinput.d.ts +1 -0
  26. package/dist/components/timepicker/types.d.ts +3 -0
  27. package/dist/components/timepicker/useTimepickerState.d.ts +2 -0
  28. package/dist/punkt-react.cjs +1 -1
  29. package/dist/punkt-react.js +3188 -2729
  30. package/dist/shared-types/footer.d.ts +95 -0
  31. package/dist/shared-types/{header-menu.d.ts → header-footer.d.ts} +51 -4
  32. package/dist/shared-types/header.d.ts +1 -1
  33. package/dist/shared-types/index.d.ts +6 -4
  34. package/dist/shared-types/searchinput.d.ts +2 -0
  35. package/dist/shared-types/select.d.ts +3 -2
  36. package/dist/shared-types/size.d.ts +4 -0
  37. package/dist/shared-types/timepicker.d.ts +0 -6
  38. package/dist/shared-utils/datepicker-utils.d.ts +5 -4
  39. package/dist/shared-utils/fileupload/focus-trap.d.ts +4 -10
  40. package/dist/shared-utils/focus-trap/index.d.ts +22 -0
  41. package/dist/shared-utils/{header-menu → header-footer}/example-data.d.ts +5 -4
  42. package/dist/shared-utils/header-footer/external-domain.d.ts +26 -0
  43. package/dist/shared-utils/{header-menu → header-footer}/fetch-header-footer-data.d.ts +6 -2
  44. package/dist/shared-utils/{header-menu → header-footer}/index.d.ts +2 -1
  45. package/dist/shared-utils/{header-menu → header-footer}/select-megamenu.d.ts +1 -1
  46. package/dist/shared-utils/scroll-lock/index.d.ts +24 -0
  47. package/package.json +4 -4
  48. package/src/components/accordion/Accordion.test.tsx +51 -0
  49. package/src/components/accordion/Accordion.tsx +3 -3
  50. package/src/components/accordion/AccordionItem.tsx +7 -14
  51. package/src/components/alert/Alert.test.tsx +45 -9
  52. package/src/components/alert/Alert.tsx +13 -3
  53. package/src/components/button/Button.tsx +2 -2
  54. package/src/components/combobox/Combobox.core.test.tsx +14 -0
  55. package/src/components/combobox/Combobox.tsx +1 -0
  56. package/src/components/combobox/ComboboxInput.tsx +2 -0
  57. package/src/components/combobox/types.ts +2 -0
  58. package/src/components/combobox/useComboboxState.ts +2 -0
  59. package/src/components/consent/Consent.test.tsx +8 -14
  60. package/src/components/consent/Consent.tsx +9 -19
  61. package/src/components/datepicker/Datepicker.core.test.tsx +11 -0
  62. package/src/components/datepicker/Datepicker.tsx +4 -1
  63. package/src/components/datepicker/DatepickerInputs.tsx +16 -1
  64. package/src/components/datepicker/types.ts +3 -0
  65. package/src/components/datepicker/useDatepickerState.ts +17 -9
  66. package/src/components/footer/Footer.accessibility.test.tsx +46 -0
  67. package/src/components/footer/Footer.test.tsx +192 -86
  68. package/src/components/footer/Footer.tsx +169 -159
  69. package/src/components/footer/FooterGlobal.tsx +181 -0
  70. package/src/components/footerSimple/FooterSimple.test.tsx +49 -46
  71. package/src/components/footerSimple/FooterSimple.tsx +117 -78
  72. package/src/components/header/Header.tsx +17 -4
  73. package/src/components/header/HeaderGlobal.accessibility.test.tsx +86 -0
  74. package/src/components/header/HeaderGlobal.test.tsx +254 -0
  75. package/src/components/header/HeaderGlobal.tsx +467 -0
  76. package/src/components/header/HeaderService.accessibility.test.tsx +70 -0
  77. package/src/components/header/HeaderService.test.tsx +29 -16
  78. package/src/components/header/HeaderService.tsx +24 -84
  79. package/src/components/header/types.ts +35 -0
  80. package/src/components/header-menu/HeaderMenu.test.tsx +16 -39
  81. package/src/components/header-menu/HeaderMenu.tsx +62 -70
  82. package/src/components/index.ts +4 -0
  83. package/src/components/inputwrapper/InputWrapper.tsx +6 -1
  84. package/src/components/modal/Modal.tsx +7 -11
  85. package/src/components/searchinput/SearchInput.test.tsx +10 -0
  86. package/src/components/searchinput/SearchInput.tsx +19 -7
  87. package/src/components/select/Select.test.tsx +18 -0
  88. package/src/components/select/Select.tsx +8 -4
  89. package/src/components/textarea/Textarea.test.tsx +10 -0
  90. package/src/components/textarea/Textarea.tsx +5 -1
  91. package/src/components/textinput/Textinput.test.tsx +28 -0
  92. package/src/components/textinput/Textinput.tsx +5 -2
  93. package/src/components/timepicker/Timepicker.test.tsx +10 -0
  94. package/src/components/timepicker/Timepicker.tsx +8 -7
  95. package/src/components/timepicker/types.ts +8 -5
  96. package/src/components/timepicker/useTimepickerState.ts +4 -0
  97. package/dist/shared-utils/modal-scroll-lock.d.ts +0 -10
  98. /package/dist/shared-utils/{header-menu → header-footer}/icon-map.d.ts +0 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * footer.ts
3
+ *
4
+ * Framework-agnostic prop types for the reworked footer components
5
+ * (`PktFooter` / `pkt-footer` and `PktFooterSimple` / `pkt-footer-simple`).
6
+ *
7
+ * Both components render an optional grey "secondary" band with the
8
+ * consumer's custom content, stacked on top of the global (blue) footer
9
+ * whose content comes from the same CDN payload as the global header —
10
+ * see `header-footer.ts`.
11
+ */
12
+ import type { THeaderFooterApi, THeaderMenuLocale } from './header-footer';
13
+ /** Custom link in the secondary (grey) footer. */
14
+ export interface IPktFooterLink {
15
+ href: string;
16
+ text: string;
17
+ /**
18
+ * Mark the link as external: the link's top domain is shown in
19
+ * parentheses after the text ("Ledige stillinger (webcruiter.com)"),
20
+ * per Punkt's link-labelling guidance.
21
+ */
22
+ external?: boolean;
23
+ openInNewTab?: boolean;
24
+ }
25
+ /** Column of custom content in the secondary footer (1–3 per footer). */
26
+ export interface IPktFooterColumn {
27
+ title: string;
28
+ /** Optional free-text paragraph shown above the links. */
29
+ text?: string;
30
+ links?: IPktFooterLink[];
31
+ }
32
+ /**
33
+ * @deprecated The social-media row now comes from the global footer's
34
+ * payload. Only rendered when explicitly supplied.
35
+ */
36
+ export interface IPktFooterSocialLink {
37
+ href: string;
38
+ iconName?: string;
39
+ language?: string;
40
+ openInNewTab?: boolean;
41
+ }
42
+ /** Props shared by `PktFooter` and `PktFooterSimple` in both frameworks. */
43
+ export interface IPktFooterBase<IconType = string> {
44
+ /** Pre-fetched header/footer payload. If supplied, the component skips its own fetch. */
45
+ data?: THeaderFooterApi<IconType>;
46
+ /** Endpoint to fetch the header/footer payload from. */
47
+ dataUrl?: string;
48
+ /** Locale key to select from the payload. Defaults to `nb-NO`. */
49
+ locale?: THeaderMenuLocale;
50
+ /** Skip the global (blue) footer entirely, e.g. when CSP blocks the CDN. Default: false */
51
+ skipGlobal?: boolean;
52
+ /** Open custom links in a new tab. Applies to custom content only. Default: false */
53
+ openLinksInNewTab?: boolean;
54
+ /**
55
+ * Replace the payload's cookie-settings link (`#cb-trigger`) with
56
+ * Punkt's own consent dialog. When unset the cookie-settings link is
57
+ * hidden. Default: false
58
+ */
59
+ includeConsent?: boolean;
60
+ hotjarId?: string | null;
61
+ googleAnalyticsId?: string | null;
62
+ devMode?: boolean;
63
+ cookieDomain?: string | null;
64
+ cookieSecure?: string | null;
65
+ cookieExpiryDays?: string | null;
66
+ }
67
+ export interface IPktFooterProps<IconType = string> extends IPktFooterBase<IconType> {
68
+ /** Custom content columns for the secondary (grey) footer. */
69
+ columns?: IPktFooterColumn[];
70
+ /** @deprecated Use `columns`. Mapped to the first entry of `columns`. */
71
+ columnOne?: IPktFooterColumn;
72
+ /** @deprecated Use `columns`. Mapped to the second entry of `columns`. */
73
+ columnTwo?: IPktFooterColumn;
74
+ /**
75
+ * @deprecated The privacy link now comes from the global footer's payload.
76
+ * Only rendered (in the secondary footer) when explicitly set to a
77
+ * non-default value.
78
+ */
79
+ personvernOgInfoLink?: string;
80
+ /**
81
+ * @deprecated The accessibility link now comes from the global footer's
82
+ * payload. Only rendered (in the secondary footer) when explicitly set
83
+ * to a non-default value.
84
+ */
85
+ tilgjengelighetLink?: string;
86
+ /** @deprecated The social-media row now comes from the global footer's payload. */
87
+ socialLinks?: IPktFooterSocialLink[];
88
+ }
89
+ export interface IPktFooterSimpleProps<IconType = string> extends IPktFooterBase<IconType> {
90
+ /** Custom links shown as a compact row in the secondary (grey) footer. */
91
+ links?: IPktFooterLink[];
92
+ }
93
+ /** Historical defaults for the deprecated link props — used to detect explicit overrides. */
94
+ export declare const DEFAULT_FOOTER_PERSONVERN_URL = "https://www.oslo.kommune.no/personvern-og-informasjonskapsler/";
95
+ export declare const DEFAULT_FOOTER_TILGJENGELIGHET_URL = "https://www.oslo.kommune.no/tilgjengelighet/";
@@ -1,18 +1,20 @@
1
1
  /**
2
- * header-menu.ts
2
+ * header-footer.ts
3
3
  *
4
- * Type definitions for the global header mega menu, shared between
5
- * @oslokommune/punkt-elements and (eventually) @oslokommune/punkt-react.
4
+ * Type definitions for the global header/footer payload (mega menu,
5
+ * footer, search and contact), shared between @oslokommune/punkt-elements
6
+ * and @oslokommune/punkt-react.
6
7
  *
7
8
  * The shape mirrors the live API response from
8
9
  * https://cdn.web.oslo.kommune.no/header-footer/header-footer.json
9
- * which the legacy oslo.kommune.no header consumes.
10
+ * which the "live" oslo.kommune.no header consumes.
10
11
  */
11
12
  /**
12
13
  * Locale keys present in the live payload. Typed as a string union with a
13
14
  * `string & {}` escape hatch so additional locales can be added without
14
15
  * a type-level breaking change.
15
16
  */
17
+ import type { Representing, TLogOutButtonPlacement, THeaderPosition, THeaderScrollBehavior, User, UserMenuItem } from './header';
16
18
  export type THeaderMenuLocale = 'nb-NO' | 'en-GB' | (string & {});
17
19
  /** Plain link without an icon. */
18
20
  export interface IHeaderMenuLink {
@@ -104,5 +106,50 @@ export interface IPktHeaderMenu<IconType = string> {
104
106
  /** Pixel breakpoint below which sections collapse into accordions. */
105
107
  mobileBreakpoint?: number;
106
108
  }
109
+ /**
110
+ * Framework-agnostic props for the global (kommune-wide) header
111
+ * (`pkt-header-global` / `PktHeaderGlobal`). Elements narrows the
112
+ * boolean fields to `Booleanish` and React adds callback/className
113
+ * props on top — see each package's `header/types.ts`.
114
+ */
115
+ export interface IPktHeaderGlobal<IconType = string> {
116
+ /** Endpoint to fetch the header/footer payload from. */
117
+ dataUrl?: string;
118
+ /** Pre-fetched payload. If supplied, the component (and its menu) skips its own fetch. */
119
+ data?: THeaderFooterApi<IconType>;
120
+ /** Locale key to select from the payload. Defaults to `nb-NO`. */
121
+ locale?: THeaderMenuLocale;
122
+ /** Logo link URL. When omitted the logo is rendered without a link. */
123
+ logoLink?: string;
124
+ /** Override the CDN path the logo assets load from. Can also be set
125
+ * globally via `window.pktLogoPath`. */
126
+ logoPath?: string;
127
+ /** Show the search field. Default: true */
128
+ showSearch?: boolean;
129
+ /** Placeholder for the search field. Falls back to the payload's search placeholder, then 'Søk'. */
130
+ searchPlaceholder?: string;
131
+ /** Show the contact link (from the payload's `header.contact`). Default: true */
132
+ showContact?: boolean;
133
+ /** User object for the logged-in user. When set, the user menu is shown. */
134
+ user?: User;
135
+ /** User menu items. */
136
+ userMenu?: UserMenuItem<IconType>[];
137
+ /** Representation object. */
138
+ representing?: Representing;
139
+ /** Allow the user to change representation. */
140
+ canChangeRepresentation?: boolean;
141
+ /** Logout button placement. */
142
+ logOutButtonPlacement?: TLogOutButtonPlacement;
143
+ /** Header position. Default: 'fixed' */
144
+ position?: THeaderPosition;
145
+ /** Scroll behavior. Default: 'hide' */
146
+ scrollBehavior?: THeaderScrollBehavior;
147
+ /** Width below which the header switches to mobile layout. Default: 768 */
148
+ mobileBreakpoint?: number;
149
+ /** Width below which the header switches to tablet layout. Default: 1024 */
150
+ tabletBreakpoint?: number;
151
+ }
107
152
  /** Default URL of the live header/footer endpoint. */
108
153
  export declare const DEFAULT_HEADER_FOOTER_URL = "https://cdn.web.oslo.kommune.no/header-footer/header-footer.json";
154
+ /** Default logo link for both header variants (unless overridden or hidden). */
155
+ export declare const DEFAULT_HEADER_LOGO_LINK = "https://www.oslo.kommune.no";
@@ -81,7 +81,7 @@ export type TLogOutButtonPlacement = 'userMenu' | 'header' | 'both' | 'none';
81
81
  /**
82
82
  * Position options for the header
83
83
  */
84
- export type THeaderPosition = 'fixed' | 'relative';
84
+ export type THeaderPosition = 'fixed' | 'sticky' | 'relative';
85
85
  /**
86
86
  * Scroll behavior options for the header
87
87
  */
@@ -4,12 +4,13 @@ export type { TAriaBoolean, TAriaBooleanMixed, TAriaCurrent, TAriaHasPopup, TAri
4
4
  export type { THeadingLevel, THeadingSize, THeadingWeight } from './heading';
5
5
  export type { User, Representing, UserMenuItem, InternalMenuItemBase, InternalMenuLink, InternalMenuButton, TInternalMenuItem, TSlotMenuVariant, THeaderMenu, TLogOutButtonPlacement, THeaderPosition, THeaderScrollBehavior, } from './header';
6
6
  export { convertUserMenuItem } from './header';
7
- export type { THeaderMenuLocale, IHeaderMenuLink, IHeaderMenuIconLink, IHeaderMenuButton, IHeaderMenuServices, IHeaderMenuSection, IMegaMenu, IHeaderFooter, IHeaderFooterLocaleData, THeaderFooterApi, IPktHeaderMenu, } from './header-menu';
8
- export { DEFAULT_HEADER_FOOTER_URL } from './header-menu';
7
+ export type { THeaderMenuLocale, IHeaderMenuLink, IHeaderMenuIconLink, IHeaderMenuButton, IHeaderMenuServices, IHeaderMenuSection, IMegaMenu, IHeaderFooter, IHeaderFooterLocaleData, THeaderFooterApi, IPktHeaderMenu, IPktHeaderGlobal, } from './header-footer';
8
+ export { DEFAULT_HEADER_FOOTER_URL, DEFAULT_HEADER_LOGO_LINK } from './header-footer';
9
+ export type { IPktFooterLink, IPktFooterColumn, IPktFooterSocialLink, IPktFooterBase, IPktFooterProps, IPktFooterSimpleProps, } from './footer';
10
+ export { DEFAULT_FOOTER_PERSONVERN_URL, DEFAULT_FOOTER_TILGJENGELIGHET_URL } from './footer';
9
11
  export type { TLayout, TVerticalPosition, THorizontalPosition, TPosition, TPositionWithCenter, } from './layout';
10
12
  export type { THTMLButtonType, TSelectionMode } from './form';
11
- export type { TSelectOption } from './select';
12
- export type { TSize3, TSize5, TSize7 } from './size';
13
+ export type { TPktInputSize, TSize3, TSize5, TSize7 } from './size';
13
14
  export type { TAlertSkin, TMessageboxSkin, TAccordionSkin, TCardSkin, TTagSkin } from './skin';
14
15
  export type { IDateConstraints, ICalendarGridDimensions, IGridCellPosition, TDateRangeMap, ISelectionState, ISelectionOptions, } from './calendar';
15
16
  export type { IDatepickerStrings } from './datepicker';
@@ -18,6 +19,7 @@ export type { ITimepickerStrings } from './timepicker';
18
19
  export { defaultTimepickerStrings } from './timepicker';
19
20
  export type { TProgressbarRole, TProgressbarSkin, TProgressbarStatusPlacement, TProgressbarStatusType, TProgressbarTitlePosition, } from './progressbar';
20
21
  export type { IPktComboboxOption, TPktComboboxTagSkin, TPktComboboxDisplayValue, TPktComboboxTagPlacement, } from './combobox';
22
+ export type { IPktSelectOption, TSelectOption } from './select';
21
23
  export type { TPktSearchInputAppearance, TPktSearchInputMethod, IPktSearchInputSuggestion, IPktSearchInput, } from './searchinput';
22
24
  export type { TUploadStrategy as TFileUploadStrategy, TFileUploadItemRenderer, TFilesChangedReason, TFileValidator, TTransferProgress, IFileItem, IFileTransfer, IFileValidateDetail, ITransferCancelledDetail, IFilesChangedDetail, IFileUploadStrings, } from './fileupload';
23
25
  export { defaultFileUploadStrings } from './fileupload';
@@ -5,6 +5,7 @@
5
5
  * stay aligned. React extends suggestions with `ReactNode` and optional
6
6
  * `onClick` in the component layer — see `@oslokommune/punkt-react`.
7
7
  */
8
+ import type { TPktInputSize } from './size';
8
9
  export type TPktSearchInputAppearance = 'local' | 'local-with-button' | 'global';
9
10
  export type TPktSearchInputMethod = 'get' | 'post' | 'dialog';
10
11
  /**
@@ -28,6 +29,7 @@ export interface IPktSearchInputSuggestion {
28
29
  */
29
30
  export interface IPktSearchInput {
30
31
  appearance?: TPktSearchInputAppearance;
32
+ inputSize?: TPktInputSize;
31
33
  disabled?: boolean;
32
34
  fullwidth?: boolean;
33
35
  id: string;
@@ -3,10 +3,11 @@
3
3
  * Lives in shared-types so both `@oslokommune/punkt-elements` and
4
4
  * `@oslokommune/punkt-react` can re-export the same canonical type.
5
5
  */
6
- export type TSelectOption = {
6
+ export interface IPktSelectOption {
7
7
  value: string;
8
8
  label: string;
9
9
  selected?: boolean;
10
10
  disabled?: boolean;
11
11
  hidden?: boolean;
12
- };
12
+ }
13
+ export type TSelectOption = IPktSelectOption;
@@ -16,3 +16,7 @@ export type TSize5 = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
16
16
  * Full 7-level size scale
17
17
  */
18
18
  export type TSize7 = 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge';
19
+ /**
20
+ * Sizes used in our input components
21
+ */
22
+ export type TPktInputSize = 'xsmall' | 'small' | 'medium';
@@ -1,9 +1,3 @@
1
- /**
2
- * timepicker.ts
3
- *
4
- * Type definitions for the timepicker component.
5
- * Used by both Elements and React implementations.
6
- */
7
1
  /**
8
2
  * Translation strings for the timepicker component.
9
3
  * Matches the shape of the `timepicker` key in no.json.
@@ -1,3 +1,4 @@
1
+ import type { TPktInputSize } from '../shared-types/size';
1
2
  /**
2
3
  * Shared datepicker utility functions used by both Elements and React packages.
3
4
  * Only framework-agnostic functions belong here.
@@ -23,11 +24,11 @@ export declare const filterDates: (dates: string[], min?: string | null, max?: s
23
24
  /**
24
25
  * Generates input CSS classes for datepicker.
25
26
  */
26
- export declare const getDatepickerInputClasses: (fullwidth: boolean, showRangeLabels: boolean, multiple: boolean, range: boolean, readonly?: boolean, inputType?: string) => Record<string, boolean>;
27
+ export declare const getDatepickerInputClasses: (fullwidth: boolean, showRangeLabels: boolean, multiple: boolean, range: boolean, readonly?: boolean, inputType?: string, inputSize?: TPktInputSize) => Record<string, boolean>;
27
28
  /**
28
29
  * Generates button CSS classes for calendar toggle button.
29
30
  */
30
- export declare const getDatepickerButtonClasses: () => Record<string, boolean>;
31
+ export declare const getDatepickerButtonClasses: (inputSize?: TPktInputSize) => Record<string, boolean>;
31
32
  /**
32
33
  * Generates range label CSS classes.
33
34
  */
@@ -62,8 +63,8 @@ declare const _default: {
62
63
  validateRangeOrder: (values: string[]) => boolean;
63
64
  sortDates: (dates: string[]) => string[];
64
65
  filterDates: (dates: string[], min?: string | null, max?: string | null, excludedDates?: string[], excludedWeekdays?: string[]) => string[];
65
- getDatepickerInputClasses: (fullwidth: boolean, showRangeLabels: boolean, multiple: boolean, range: boolean, readonly?: boolean, inputType?: string) => Record<string, boolean>;
66
- getDatepickerButtonClasses: () => Record<string, boolean>;
66
+ getDatepickerInputClasses: (fullwidth: boolean, showRangeLabels: boolean, multiple: boolean, range: boolean, readonly?: boolean, inputType?: string, inputSize?: TPktInputSize) => Record<string, boolean>;
67
+ getDatepickerButtonClasses: (inputSize?: TPktInputSize) => Record<string, boolean>;
67
68
  getRangeLabelClasses: (showRangeLabels: boolean) => Record<string, boolean>;
68
69
  processDateSelection: (detail: string | string[], multiple: boolean, range: boolean) => string;
69
70
  handleCalendarPosition: (popup: HTMLElement | null, input: HTMLElement | null, hasCounter?: boolean) => void;
@@ -1,12 +1,6 @@
1
1
  /**
2
- * Build a list of focusable elements inside a container (same selector used by
3
- * Lit and React fileupload preview modals). Hidden elements are filtered out.
2
+ * Re-export of the generic focus-trap helpers, kept here for backwards
3
+ * compatibility with existing `shared-utils/fileupload` imports. The canonical
4
+ * implementation now lives in `shared-utils/focus-trap`.
4
5
  */
5
- export declare function getFocusableElements(container: HTMLElement | null | undefined): HTMLElement[];
6
- /**
7
- * Keyboard handler that wraps Tab / Shift-Tab focus around the first and last
8
- * focusable elements inside a container. Returns `true` if the event was handled.
9
- *
10
- * Call from a `keydown` listener on the modal root.
11
- */
12
- export declare function trapTabInside(event: KeyboardEvent, container: HTMLElement | null | undefined): boolean;
6
+ export { getFocusableElements, trapTabInside } from '../focus-trap';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Build a list of focusable elements inside a container (same selector used by
3
+ * the fileupload preview modals and the global header mega menu). Hidden
4
+ * elements are filtered out.
5
+ */
6
+ export declare function getFocusableElements(container: HTMLElement | null | undefined): HTMLElement[];
7
+ /**
8
+ * Like {@link getFocusableElements}, but descends open shadow roots, treats a
9
+ * native `<summary>` as focusable, and skips content hidden inside a collapsed
10
+ * `<details>`. Returned in flattened tree order. Use this when the container may
11
+ * hold web components or `<details>` accordions (e.g. the header mega menu).
12
+ */
13
+ export declare function getFocusableElementsDeep(container: HTMLElement | null | undefined): HTMLElement[];
14
+ /** Resolve the genuinely focused element, piercing nested open shadow roots. */
15
+ export declare function getDeepActiveElement(doc?: Document): HTMLElement | null;
16
+ /**
17
+ * Keyboard handler that wraps Tab / Shift-Tab focus around the first and last
18
+ * focusable elements inside a container. Returns `true` if the event was handled.
19
+ *
20
+ * Call from a `keydown` listener on the modal root.
21
+ */
22
+ export declare function trapTabInside(event: KeyboardEvent, container: HTMLElement | null | undefined): boolean;
@@ -1,9 +1,10 @@
1
- import type { THeaderFooterApi } from '../../shared-types/header-menu';
1
+ import type { THeaderFooterApi } from '../../shared-types/header-footer';
2
2
  /**
3
3
  * Example header/footer payload used by the dev/demo pages in both
4
- * Punkt Elements and Punkt React. Mirrors the live Oslo kommune
5
- * payload closely enough to exercise every region of the mega menu
6
- * (services grid, four section columns, footer + social links).
4
+ * Punkt Elements and Punkt React. Verbatim copy of the live Oslo
5
+ * kommune payload from
6
+ * https://cdn.web.oslo.kommune.no/header-footer/header-footer.json
7
+ * (hentet 2026-07-06).
7
8
  *
8
9
  * Tests use focused per-test fixtures with smaller shapes — this
9
10
  * dataset is for visual demos only.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * External-link labelling for the global footer.
3
+ *
4
+ * Punkt's link-labelling guidance (punkt.oslo.kommune.no/god-praksis/merking-av-lenker/)
5
+ * prefers spelling out the destination domain in parentheses after the link
6
+ * text — "Ledige stillinger (webcruiter.com)" — over an external-link icon.
7
+ */
8
+ /**
9
+ * Return the top domain of an absolute http(s) URL, or `undefined` for
10
+ * relative URLs, fragments, and non-web schemes (`mailto:`, `tel:`).
11
+ * Unlike `getExternalDomainLabel` this does no internal-host check — use
12
+ * it when the consumer has explicitly flagged the link as external.
13
+ */
14
+ export declare function getLinkDomainLabel(url: string | undefined): string | undefined;
15
+ /**
16
+ * Return the top domain to display after an external link's text, or
17
+ * `undefined` when the link needs no label:
18
+ *
19
+ * - relative URLs and fragments (`/jobb`, `#cb-trigger`) — same site
20
+ * - non-http(s) schemes (`mailto:`, `tel:`) — not web navigation
21
+ * - hosts on `oslo.kommune.no` (or a subdomain) — internal
22
+ *
23
+ * `internalHost` can widen what counts as internal, e.g. for consumers
24
+ * hosting the payload themselves.
25
+ */
26
+ export declare function getExternalDomainLabel(url: string | undefined, internalHost?: string): string | undefined;
@@ -1,5 +1,7 @@
1
- import { DEFAULT_HEADER_FOOTER_URL, type THeaderFooterApi } from '../../shared-types/header-menu';
1
+ import { DEFAULT_HEADER_FOOTER_URL, type THeaderFooterApi } from '../../shared-types/header-footer';
2
2
  export { DEFAULT_HEADER_FOOTER_URL };
3
+ /** Clear the shared payload cache. Intended for tests. */
4
+ export declare function resetHeaderFooterDataCache(): void;
3
5
  /**
4
6
  * Fetch and parse the Oslo kommune header/footer JSON payload.
5
7
  *
@@ -7,6 +9,8 @@ export { DEFAULT_HEADER_FOOTER_URL };
7
9
  * for surfacing or hiding errors — the Lit element, for example, hides
8
10
  * the menu and dispatches a `data-error` event.
9
11
  *
10
- * `signal` lets callers abort the fetch when the component disconnects.
12
+ * `signal` lets callers abort their own wait when the component
13
+ * disconnects; the underlying request is shared and keeps running for
14
+ * any other component awaiting the same URL.
11
15
  */
12
16
  export declare function fetchHeaderFooterData<IconType = string>(url?: string, signal?: AbortSignal): Promise<THeaderFooterApi<IconType>>;
@@ -7,5 +7,6 @@
7
7
  */
8
8
  export { ICON_MAP_ODS_TO_PUNKT, mapOdsIcon, deriveSocialIcon } from './icon-map';
9
9
  export { selectLocaleData, selectMegamenu } from './select-megamenu';
10
- export { DEFAULT_HEADER_FOOTER_URL, fetchHeaderFooterData, } from './fetch-header-footer-data';
10
+ export { getExternalDomainLabel, getLinkDomainLabel } from './external-domain';
11
+ export { DEFAULT_HEADER_FOOTER_URL, fetchHeaderFooterData, resetHeaderFooterDataCache, } from './fetch-header-footer-data';
11
12
  export { EXAMPLE_HEADER_FOOTER_DATA } from './example-data';
@@ -1,4 +1,4 @@
1
- import type { IHeaderFooterLocaleData, IMegaMenu, THeaderFooterApi, THeaderMenuLocale } from '../../shared-types/header-menu';
1
+ import type { IHeaderFooterLocaleData, IMegaMenu, THeaderFooterApi, THeaderMenuLocale } from '../../shared-types/header-footer';
2
2
  /**
3
3
  * Pick the locale slice from a header/footer payload. Falls back to
4
4
  * `nb-NO` when the requested locale is missing — the same fallback the
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared body scroll-lock used by every overlay in the design system (modal,
3
+ * service/global header menus, search and user dropdowns).
4
+ *
5
+ * Strategy: an iOS-safe `position: fixed` lock — `overflow: hidden` alone is
6
+ * ignored by iOS Safari. The current scroll position is saved, the body is
7
+ * pinned, scrollbar width is reserved to avoid a layout shift, and the scroll
8
+ * position is restored on release.
9
+ *
10
+ * Reference-counted so several open overlays don't unlock the page when one of
11
+ * them closes. Each caller holds a token (see {@link createScrollLockToken}) so
12
+ * repeated lock/unlock calls from the same source are idempotent — only the
13
+ * first lock and the last unlock touch the DOM.
14
+ */
15
+ export interface ScrollLockToken {
16
+ locked: boolean;
17
+ }
18
+ export declare const createScrollLockToken: () => ScrollLockToken;
19
+ /**
20
+ * Lock or unlock body scroll for a single source. Pass a token created with
21
+ * {@link createScrollLockToken} so the call is idempotent — locking an already
22
+ * locked token (or unlocking an unlocked one) is a no-op.
23
+ */
24
+ export declare const setScrollLock: (token: ScrollLockToken, shouldLock: boolean) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-react",
3
- "version": "16.26.5",
3
+ "version": "17.0.0",
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",
@@ -44,8 +44,8 @@
44
44
  "@eslint/compat": "^2.1.0",
45
45
  "@eslint/eslintrc": "^3.3.5",
46
46
  "@eslint/js": "^10.0.1",
47
- "@oslokommune/punkt-assets": "^16.26.2",
48
- "@oslokommune/punkt-css": "^16.26.3",
47
+ "@oslokommune/punkt-assets": "^17.0.0",
48
+ "@oslokommune/punkt-css": "^17.0.0",
49
49
  "@testing-library/jest-dom": "^6.9.1",
50
50
  "@testing-library/react": "^16.3.2",
51
51
  "@testing-library/user-event": "^14.6.1",
@@ -101,5 +101,5 @@
101
101
  "url": "https://github.com/oslokommune/punkt/issues"
102
102
  },
103
103
  "license": "MIT",
104
- "gitHead": "0a1e6fef1813b2de00696cbdd075f060fdc1e22c"
104
+ "gitHead": "16ef60f40da1a44e59bb72380712e4636768e2c5"
105
105
  }
@@ -76,6 +76,57 @@ describe('PktAccordion', () => {
76
76
  expect(ref.current?.tagName).toBe('DIV')
77
77
  expect(ref.current).toHaveClass('pkt-accordion')
78
78
  })
79
+
80
+ test('propagates plus-minus skin to items', () => {
81
+ const { container } = render(
82
+ <PktAccordion skin="plus-minus">
83
+ <PktAccordionItem title="Title" id="item1">
84
+ Content
85
+ </PktAccordionItem>
86
+ </PktAccordion>,
87
+ )
88
+
89
+ expect(container.querySelector('.pkt-accordion-item__icon--plus')).toBeInTheDocument()
90
+ expect(container.querySelector('.pkt-accordion-item__icon--minus')).toBeInTheDocument()
91
+ expect(container.querySelector('[name="chevron-thin-down"]')).not.toBeInTheDocument()
92
+ })
93
+
94
+ test('updates item icons when skin changes', () => {
95
+ const { container, rerender } = render(
96
+ <PktAccordion skin="outlined">
97
+ <PktAccordionItem title="Title" id="item1">
98
+ Content
99
+ </PktAccordionItem>
100
+ </PktAccordion>,
101
+ )
102
+
103
+ expect(container.querySelector('[name="chevron-thin-down"]')).toBeInTheDocument()
104
+ expect(container.querySelector('.pkt-accordion-item__icon--plus')).not.toBeInTheDocument()
105
+
106
+ rerender(
107
+ <PktAccordion skin="plus-minus">
108
+ <PktAccordionItem title="Title" id="item1">
109
+ Content
110
+ </PktAccordionItem>
111
+ </PktAccordion>,
112
+ )
113
+
114
+ expect(container.querySelector('[name="chevron-thin-down"]')).not.toBeInTheDocument()
115
+ expect(container.querySelector('.pkt-accordion-item__icon--plus')).toBeInTheDocument()
116
+ })
117
+
118
+ test('item skin prop overrides accordion skin', () => {
119
+ const { container } = render(
120
+ <PktAccordion skin="plus-minus">
121
+ <PktAccordionItem title="Title" id="item1" skin="outlined">
122
+ Content
123
+ </PktAccordionItem>
124
+ </PktAccordion>,
125
+ )
126
+
127
+ expect(container.querySelector('[name="chevron-thin-down"]')).toBeInTheDocument()
128
+ expect(container.querySelector('.pkt-accordion-item__icon--plus')).not.toBeInTheDocument()
129
+ })
79
130
  })
80
131
 
81
132
  describe('PktAccordionItem', () => {
@@ -20,8 +20,8 @@ export interface IPktAccordion {
20
20
  className?: string
21
21
  }
22
22
 
23
- // Send name som context til AccordionItem-children
24
- const AccordionContext = createContext<{ name?: string }>({})
23
+ // Send name og skin som context til AccordionItem-children
24
+ const AccordionContext = createContext<{ name?: string; skin?: TPktAccordionSkin }>({})
25
25
 
26
26
  export const useAccordionContext = () => useContext(AccordionContext)
27
27
 
@@ -37,7 +37,7 @@ export const PktAccordion = forwardRef<HTMLDivElement, IPktAccordion>(
37
37
  .join(' ')
38
38
 
39
39
  return (
40
- <AccordionContext.Provider value={{ name }}>
40
+ <AccordionContext.Provider value={{ name, skin }}>
41
41
  <div
42
42
  ref={ref}
43
43
  className={accordionClasses}
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { forwardRef, type MouseEvent, type ReactNode, type SyntheticEvent,useEffect, useState } from 'react'
3
+ import { forwardRef, type MouseEvent, type ReactNode, type SyntheticEvent, useState } from 'react'
4
4
  import type { TAccordionSkin } from 'shared-types'
5
5
 
6
6
  import { PktIcon } from '../icon/Icon'
@@ -40,20 +40,17 @@ export const PktAccordionItem = forwardRef<HTMLDetailsElement, IPktAccordionItem
40
40
  ref,
41
41
  ) => {
42
42
  const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen)
43
- const { name: contextName } = useAccordionContext()
43
+ const { name: contextName, skin: contextSkin } = useAccordionContext()
44
44
 
45
45
  // Bruk name fra props eller fra context
46
46
  const actualName = name || contextName
47
47
 
48
+ // Bruk skin fra props eller fra context
49
+ const actualSkin = skin || contextSkin
50
+
48
51
  // Bruk controlled isOpen om prop, ellers bruk intern state
49
52
  const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen
50
53
 
51
- useEffect(() => {
52
- if (controlledIsOpen === undefined) {
53
- setInternalIsOpen(defaultOpen)
54
- }
55
- }, [defaultOpen, controlledIsOpen])
56
-
57
54
  const handleToggle = (e: SyntheticEvent<HTMLDetailsElement>) => {
58
55
  const detailsElement = e.currentTarget
59
56
  const newOpenState = detailsElement.open
@@ -96,7 +93,7 @@ export const PktAccordionItem = forwardRef<HTMLDetailsElement, IPktAccordionItem
96
93
  >
97
94
  <summary className="pkt-accordion-item__title" id={`pkt-accordion-item-summary-${id}`}>
98
95
  {title}
99
- {skin === 'plus-minus' ? (
96
+ {actualSkin === 'plus-minus' ? (
100
97
  <>
101
98
  <PktIcon
102
99
  name="plus-sign"
@@ -110,11 +107,7 @@ export const PktAccordionItem = forwardRef<HTMLDetailsElement, IPktAccordionItem
110
107
  />
111
108
  </>
112
109
  ) : (
113
- <PktIcon
114
- name="chevron-thin-down"
115
- className="pkt-accordion-item__icon"
116
- aria-hidden="true"
117
- />
110
+ <PktIcon name="chevron-thin-down" className="pkt-accordion-item__icon" aria-hidden="true" />
118
111
  )}
119
112
  </summary>
120
113
  <div className="pkt-accordion-item__content" id={`pkt-accordion-item__content-${id}`} role="region">