@oslokommune/punkt-react 16.16.2 → 16.17.1
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.
- package/CHANGELOG.md +35 -0
- package/dist/{dialog-polyfill.esm-BmVHGRTX-CuhXqEqJ.js → dialog-polyfill.esm-DrODM08x-C8rdFsL8.js} +1 -1
- package/dist/index.d.ts +27 -0
- package/dist/punkt-react.es.js +1742 -1160
- package/dist/punkt-react.umd.js +226 -88
- package/dist/shared-types/header-menu.d.ts +108 -0
- package/dist/shared-types/index.d.ts +2 -0
- package/dist/shared-types/skin.d.ts +1 -1
- package/dist/shared-utils/header-menu/example-data.d.ts +11 -0
- package/dist/shared-utils/header-menu/fetch-header-footer-data.d.ts +12 -0
- package/dist/shared-utils/header-menu/icon-map.d.ts +34 -0
- package/dist/shared-utils/header-menu/index.d.ts +11 -0
- package/dist/shared-utils/header-menu/select-megamenu.d.ts +16 -0
- package/package.json +4 -4
- package/src/components/accordion/AccordionItem.tsx +20 -1
- package/src/components/header-menu/HeaderMenu.test.tsx +199 -0
- package/src/components/header-menu/HeaderMenu.tsx +305 -0
- package/src/components/index.ts +1 -0
- package/src/components/interfaces.ts +1 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* header-menu.ts
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the global header mega menu, shared between
|
|
5
|
+
* @oslokommune/punkt-elements and (eventually) @oslokommune/punkt-react.
|
|
6
|
+
*
|
|
7
|
+
* The shape mirrors the live API response from
|
|
8
|
+
* https://cdn.web.oslo.kommune.no/header-footer/header-footer.json
|
|
9
|
+
* which the legacy oslo.kommune.no header consumes.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Locale keys present in the live payload. Typed as a string union with a
|
|
13
|
+
* `string & {}` escape hatch so additional locales can be added without
|
|
14
|
+
* a type-level breaking change.
|
|
15
|
+
*/
|
|
16
|
+
export type THeaderMenuLocale = 'nb-NO' | 'en-GB' | (string & {});
|
|
17
|
+
/** Plain link without an icon. */
|
|
18
|
+
export interface IHeaderMenuLink {
|
|
19
|
+
text: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Link with an icon name. The icon name is the raw value from the API
|
|
24
|
+
* (legacy ODS naming) and gets mapped to a Punkt icon name inside the
|
|
25
|
+
* component via `mapOdsIcon`.
|
|
26
|
+
*
|
|
27
|
+
* `IconType` is generic so the Elements package can narrow it to
|
|
28
|
+
* `PktIconName` while shared code keeps it as `string`.
|
|
29
|
+
*/
|
|
30
|
+
export interface IHeaderMenuIconLink<IconType = string> extends IHeaderMenuLink {
|
|
31
|
+
icon: IconType;
|
|
32
|
+
}
|
|
33
|
+
/** CTA button at the top of the menu (e.g. "Min side"). */
|
|
34
|
+
export interface IHeaderMenuButton<IconType = string> extends IHeaderMenuLink {
|
|
35
|
+
iconName?: IconType;
|
|
36
|
+
}
|
|
37
|
+
/** Featured services grid: a heading and 1–N icon-prefixed links. */
|
|
38
|
+
export interface IHeaderMenuServices<IconType = string> {
|
|
39
|
+
title: string;
|
|
40
|
+
links: IHeaderMenuIconLink<IconType>[];
|
|
41
|
+
}
|
|
42
|
+
/** Categorised column section: a heading and a flat list of links. */
|
|
43
|
+
export interface IHeaderMenuSection {
|
|
44
|
+
title: string;
|
|
45
|
+
links: IHeaderMenuLink[];
|
|
46
|
+
}
|
|
47
|
+
/** The `megamenu` slice of the API payload. */
|
|
48
|
+
export interface IMegaMenu<IconType = string> {
|
|
49
|
+
buttons?: IHeaderMenuButton<IconType>[];
|
|
50
|
+
services: IHeaderMenuServices<IconType>;
|
|
51
|
+
sections: IHeaderMenuSection[];
|
|
52
|
+
links: IHeaderMenuLink[];
|
|
53
|
+
some: IHeaderMenuLink[];
|
|
54
|
+
}
|
|
55
|
+
/** The `footer` slice — kept here so consumers can reuse one fetch for both. */
|
|
56
|
+
export interface IHeaderFooter {
|
|
57
|
+
sections: IHeaderMenuSection[];
|
|
58
|
+
links: IHeaderMenuLink[];
|
|
59
|
+
some: IHeaderMenuLink[];
|
|
60
|
+
}
|
|
61
|
+
/** Per-locale slice of the API response. */
|
|
62
|
+
export interface IHeaderFooterLocaleData<IconType = string> {
|
|
63
|
+
megamenu: IMegaMenu<IconType>;
|
|
64
|
+
footer: IHeaderFooter;
|
|
65
|
+
header?: {
|
|
66
|
+
contact: IHeaderMenuLink;
|
|
67
|
+
};
|
|
68
|
+
search?: {
|
|
69
|
+
input: {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
placeholder: string;
|
|
73
|
+
autocomplete?: string;
|
|
74
|
+
};
|
|
75
|
+
texts: Record<string, string>;
|
|
76
|
+
endpoint: string;
|
|
77
|
+
hiddenFields?: Array<{
|
|
78
|
+
name: string;
|
|
79
|
+
value: string;
|
|
80
|
+
}>;
|
|
81
|
+
};
|
|
82
|
+
i18n?: {
|
|
83
|
+
menu: string;
|
|
84
|
+
search: string;
|
|
85
|
+
navAriaLabel: string;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Top-level API response keyed by locale. */
|
|
89
|
+
export type THeaderFooterApi<IconType = string> = Record<THeaderMenuLocale, IHeaderFooterLocaleData<IconType>>;
|
|
90
|
+
/**
|
|
91
|
+
* Public props for the pkt-header-menu element / future React component.
|
|
92
|
+
* Fields are intentionally framework-agnostic — Elements adds Lit-specific
|
|
93
|
+
* pieces (Booleanish converters, etc.) on top.
|
|
94
|
+
*/
|
|
95
|
+
export interface IPktHeaderMenu<IconType = string> {
|
|
96
|
+
/** Endpoint to fetch the header/footer payload from. */
|
|
97
|
+
dataUrl?: string;
|
|
98
|
+
/** Pre-fetched payload. If supplied, the component skips its own fetch. */
|
|
99
|
+
data?: THeaderFooterApi<IconType>;
|
|
100
|
+
/** Locale key to select from the payload. Defaults to `nb-NO`. */
|
|
101
|
+
locale?: THeaderMenuLocale;
|
|
102
|
+
/** Whether the menu is visible. Controlled by the parent header. */
|
|
103
|
+
open?: boolean;
|
|
104
|
+
/** Pixel breakpoint below which sections collapse into accordions. */
|
|
105
|
+
mobileBreakpoint?: number;
|
|
106
|
+
}
|
|
107
|
+
/** Default URL of the live header/footer endpoint. */
|
|
108
|
+
export declare const DEFAULT_HEADER_FOOTER_URL = "https://cdn.web.oslo.kommune.no/header-footer/header-footer.json";
|
|
@@ -4,6 +4,8 @@ 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
9
|
export type { TLayout, TVerticalPosition, THorizontalPosition, TPosition, TPositionWithCenter, } from './layout';
|
|
8
10
|
export type { THTMLButtonType, TSelectionMode } from './form';
|
|
9
11
|
export type { TSize3, TSize5, TSize7 } from './size';
|
|
@@ -17,7 +17,7 @@ export type TMessageboxSkin = 'beige' | 'blue' | 'red' | 'green';
|
|
|
17
17
|
/**
|
|
18
18
|
* Accordion component skin variants
|
|
19
19
|
*/
|
|
20
|
-
export type TAccordionSkin = 'borderless' | 'outlined' | 'beige' | 'blue';
|
|
20
|
+
export type TAccordionSkin = 'borderless' | 'outlined' | 'beige' | 'blue' | 'plus-minus';
|
|
21
21
|
/**
|
|
22
22
|
* Card component skin variants
|
|
23
23
|
*/
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { THeaderFooterApi } from '../../shared-types/header-menu';
|
|
2
|
+
/**
|
|
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).
|
|
7
|
+
*
|
|
8
|
+
* Tests use focused per-test fixtures with smaller shapes — this
|
|
9
|
+
* dataset is for visual demos only.
|
|
10
|
+
*/
|
|
11
|
+
export declare const EXAMPLE_HEADER_FOOTER_DATA: THeaderFooterApi;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DEFAULT_HEADER_FOOTER_URL, type THeaderFooterApi } from '../../shared-types/header-menu';
|
|
2
|
+
export { DEFAULT_HEADER_FOOTER_URL };
|
|
3
|
+
/**
|
|
4
|
+
* Fetch and parse the Oslo kommune header/footer JSON payload.
|
|
5
|
+
*
|
|
6
|
+
* Throws on HTTP errors and on invalid JSON. The caller is responsible
|
|
7
|
+
* for surfacing or hiding errors — the Lit element, for example, hides
|
|
8
|
+
* the menu and dispatches a `data-error` event.
|
|
9
|
+
*
|
|
10
|
+
* `signal` lets callers abort the fetch when the component disconnects.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fetchHeaderFooterData<IconType = string>(url?: string, signal?: AbortSignal): Promise<THeaderFooterApi<IconType>>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renames from legacy oslo.kommune.no design-system icon names ("ods")
|
|
3
|
+
* to current Punkt icon names. Only entries where the name actually
|
|
4
|
+
* changed are listed — `mapOdsIcon` returns the input unchanged for any
|
|
5
|
+
* name not in this map, so identity entries would just be noise.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ICON_MAP_ODS_TO_PUNKT: Readonly<Record<string, string>>;
|
|
8
|
+
/**
|
|
9
|
+
* Translate a legacy ODS icon name to a Punkt icon name. Falls back to
|
|
10
|
+
* the input string when no mapping exists, so unknown icons render as-is
|
|
11
|
+
* rather than blowing up.
|
|
12
|
+
*/
|
|
13
|
+
export declare function mapOdsIcon(name: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Map of known social-media hostnames to Punkt icon names. Hostnames are
|
|
16
|
+
* stored without the `www.` / `m.` subdomain prefix — `deriveSocialIcon`
|
|
17
|
+
* normalises before looking up.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SOCIAL_HOSTNAME_TO_ICON: Readonly<Record<string, string>>;
|
|
20
|
+
/**
|
|
21
|
+
* The header/footer API ships social-media entries as `{ text, url }` only
|
|
22
|
+
* (no `icon` field). Resolve an icon name from the entry — preferring the
|
|
23
|
+
* URL's hostname because it's stable across locales (`facebook.com` is the
|
|
24
|
+
* same regardless of how the link is labelled).
|
|
25
|
+
*
|
|
26
|
+
* If the URL doesn't parse or its host isn't a known platform, fall back
|
|
27
|
+
* to deriving from the label: lower-case, slugify, run through
|
|
28
|
+
* `mapOdsIcon` so legacy spellings like "LinkedIn" / "linked-in" still
|
|
29
|
+
* resolve to `linkedin`.
|
|
30
|
+
*
|
|
31
|
+
* Returns `undefined` when neither input yields a usable icon — callers
|
|
32
|
+
* should then skip the icon rather than rendering a broken one.
|
|
33
|
+
*/
|
|
34
|
+
export declare function deriveSocialIcon(url?: string, text?: string): string | undefined;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header menu helper exports
|
|
3
|
+
*
|
|
4
|
+
* Shared between @oslokommune/punkt-elements and (later) @oslokommune/punkt-react
|
|
5
|
+
* so both implementations stay aligned with the live oslo.kommune.no
|
|
6
|
+
* header/footer API.
|
|
7
|
+
*/
|
|
8
|
+
export { ICON_MAP_ODS_TO_PUNKT, mapOdsIcon, deriveSocialIcon } from './icon-map';
|
|
9
|
+
export { selectLocaleData, selectMegamenu } from './select-megamenu';
|
|
10
|
+
export { DEFAULT_HEADER_FOOTER_URL, fetchHeaderFooterData, } from './fetch-header-footer-data';
|
|
11
|
+
export { EXAMPLE_HEADER_FOOTER_DATA } from './example-data';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IHeaderFooterLocaleData, IMegaMenu, THeaderFooterApi, THeaderMenuLocale } from '../../shared-types/header-menu';
|
|
2
|
+
/**
|
|
3
|
+
* Pick the locale slice from a header/footer payload. Falls back to
|
|
4
|
+
* `nb-NO` when the requested locale is missing — the same fallback the
|
|
5
|
+
* legacy oslo.kommune.no header used.
|
|
6
|
+
*
|
|
7
|
+
* Returns `undefined` when the payload doesn't contain the fallback
|
|
8
|
+
* either (e.g. a malformed response). Callers should treat that as an
|
|
9
|
+
* error state.
|
|
10
|
+
*/
|
|
11
|
+
export declare function selectLocaleData<IconType = string>(api: THeaderFooterApi<IconType> | undefined, locale?: THeaderMenuLocale): IHeaderFooterLocaleData<IconType> | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Convenience wrapper that returns the `megamenu` slice for the given
|
|
14
|
+
* locale.
|
|
15
|
+
*/
|
|
16
|
+
export declare function selectMegamenu<IconType = string>(api: THeaderFooterApi<IconType> | undefined, locale?: THeaderMenuLocale): IMegaMenu<IconType> | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oslokommune/punkt-react",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.17.1",
|
|
4
4
|
"description": "React komponentbibliotek til Punkt, et designsystem laget av Oslo Origo",
|
|
5
5
|
"homepage": "https://punkt.oslo.kommune.no",
|
|
6
6
|
"author": "Team Designsystem, Oslo Origo",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@lit-labs/ssr-dom-shim": "^1.2.1",
|
|
41
41
|
"@lit/react": "^1.0.7",
|
|
42
|
-
"@oslokommune/punkt-elements": "^16.
|
|
42
|
+
"@oslokommune/punkt-elements": "^16.17.0",
|
|
43
43
|
"classnames": "^2.5.1",
|
|
44
44
|
"prettier": "^3.3.3",
|
|
45
45
|
"react-hook-form": "^7.53.0"
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@eslint/eslintrc": "^3.3.3",
|
|
51
51
|
"@eslint/js": "^9.37.0",
|
|
52
52
|
"@oslokommune/punkt-assets": "^16.16.1",
|
|
53
|
-
"@oslokommune/punkt-css": "^16.
|
|
53
|
+
"@oslokommune/punkt-css": "^16.17.1",
|
|
54
54
|
"@testing-library/jest-dom": "^6.5.0",
|
|
55
55
|
"@testing-library/react": "^16.0.1",
|
|
56
56
|
"@testing-library/user-event": "^14.5.2",
|
|
@@ -109,5 +109,5 @@
|
|
|
109
109
|
"url": "https://github.com/oslokommune/punkt/issues"
|
|
110
110
|
},
|
|
111
111
|
"license": "MIT",
|
|
112
|
-
"gitHead": "
|
|
112
|
+
"gitHead": "9c280105a8c6698188d458d289d56d8d5e6b8b86"
|
|
113
113
|
}
|
|
@@ -96,7 +96,26 @@ export const PktAccordionItem = forwardRef<HTMLDetailsElement, IPktAccordionItem
|
|
|
96
96
|
>
|
|
97
97
|
<summary className="pkt-accordion-item__title" id={`pkt-accordion-item-summary-${id}`}>
|
|
98
98
|
{title}
|
|
99
|
-
|
|
99
|
+
{skin === 'plus-minus' ? (
|
|
100
|
+
<>
|
|
101
|
+
<PktIcon
|
|
102
|
+
name="plus-sign"
|
|
103
|
+
className="pkt-accordion-item__icon pkt-accordion-item__icon--plus"
|
|
104
|
+
aria-hidden="true"
|
|
105
|
+
/>
|
|
106
|
+
<PktIcon
|
|
107
|
+
name="minus-sign"
|
|
108
|
+
className="pkt-accordion-item__icon pkt-accordion-item__icon--minus"
|
|
109
|
+
aria-hidden="true"
|
|
110
|
+
/>
|
|
111
|
+
</>
|
|
112
|
+
) : (
|
|
113
|
+
<PktIcon
|
|
114
|
+
name="chevron-thin-down"
|
|
115
|
+
className="pkt-accordion-item__icon"
|
|
116
|
+
aria-hidden="true"
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
100
119
|
</summary>
|
|
101
120
|
<div className="pkt-accordion-item__content" id={`pkt-accordion-item__content-${id}`} role="region">
|
|
102
121
|
<div className="pkt-accordion-item__content-inner">{children}</div>
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
|
|
3
|
+
import { render, waitFor } from '@testing-library/react'
|
|
4
|
+
import { axe, toHaveNoViolations } from 'jest-axe'
|
|
5
|
+
import type { THeaderFooterApi } from 'shared-types'
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { PktHeaderMenu } from './HeaderMenu'
|
|
9
|
+
|
|
10
|
+
expect.extend(toHaveNoViolations)
|
|
11
|
+
|
|
12
|
+
const LOCAL_DATA: THeaderFooterApi = {
|
|
13
|
+
'nb-NO': {
|
|
14
|
+
megamenu: {
|
|
15
|
+
buttons: [{ text: 'Min side', url: 'https://www.oslo.kommune.no/min-side/' }],
|
|
16
|
+
services: {
|
|
17
|
+
title: 'Tjenester og tilbud',
|
|
18
|
+
links: [
|
|
19
|
+
{ icon: '24h', text: 'Døgnåpne tjenester', url: 'https://example.test/24h' },
|
|
20
|
+
{ icon: 'fire-emblem', text: 'Brannvern', url: 'https://example.test/brannvern' },
|
|
21
|
+
{ icon: 'unknown-icon', text: 'Ukjent', url: 'https://example.test/ukjent' },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
sections: [
|
|
25
|
+
{
|
|
26
|
+
title: 'Oslo vokser',
|
|
27
|
+
links: [{ text: 'Byutvikling', url: 'https://example.test/byutvikling' }],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: 'Politikk og innsyn',
|
|
31
|
+
links: [{ text: 'Politikk', url: 'https://example.test/politikk' }],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
links: [
|
|
35
|
+
{ text: 'Kontakt', url: 'https://example.test/kontakt' },
|
|
36
|
+
{ text: 'English', url: 'https://example.test/english' },
|
|
37
|
+
],
|
|
38
|
+
some: [
|
|
39
|
+
{ text: 'Facebook', url: 'https://www.facebook.com/Oslo/' },
|
|
40
|
+
{ text: 'LinkedIn', url: 'https://www.linkedin.com/company/oslo-kommune' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
footer: { sections: [], links: [], some: [] },
|
|
44
|
+
i18n: { menu: 'Meny', search: 'Søk', navAriaLabel: 'Oslo kommune hovedmeny' },
|
|
45
|
+
},
|
|
46
|
+
'en-GB': {
|
|
47
|
+
megamenu: {
|
|
48
|
+
services: {
|
|
49
|
+
title: 'Services and information',
|
|
50
|
+
links: [{ icon: '24h', text: '24h', url: 'https://example.test/en/24h' }],
|
|
51
|
+
},
|
|
52
|
+
sections: [],
|
|
53
|
+
links: [],
|
|
54
|
+
some: [],
|
|
55
|
+
},
|
|
56
|
+
footer: { sections: [], links: [], some: [] },
|
|
57
|
+
i18n: { menu: 'Menu', search: 'Search', navAriaLabel: 'Oslo kommune main menu' },
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
vi.restoreAllMocks()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('PktHeaderMenu rendering', () => {
|
|
66
|
+
test('renders services, sections and footer when given pre-fetched data', () => {
|
|
67
|
+
const { container } = render(<PktHeaderMenu data={LOCAL_DATA} open />)
|
|
68
|
+
expect(container.querySelector('.pkt-header-menu__services-title')?.textContent).toContain(
|
|
69
|
+
'Tjenester og tilbud',
|
|
70
|
+
)
|
|
71
|
+
expect(container.querySelectorAll('.pkt-header-menu__service')).toHaveLength(3)
|
|
72
|
+
expect(container.querySelectorAll('.pkt-header-menu__section')).toHaveLength(2)
|
|
73
|
+
expect(container.querySelectorAll('.pkt-header-menu__footer-link')).toHaveLength(2)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('does not fetch when data prop is supplied', () => {
|
|
77
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('should not run'))
|
|
78
|
+
render(<PktHeaderMenu data={LOCAL_DATA} open />)
|
|
79
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('maps ODS icon names on service links', async () => {
|
|
83
|
+
const { container } = render(<PktHeaderMenu data={LOCAL_DATA} open />)
|
|
84
|
+
await window.customElements.whenDefined('pkt-icon')
|
|
85
|
+
const icons = container.querySelectorAll<HTMLElement>('.pkt-header-menu__service-icon')
|
|
86
|
+
await waitFor(() => expect(icons[0]).toHaveAttribute('name', '24h'))
|
|
87
|
+
expect(icons[1]).toHaveAttribute('name', 'shield-fire')
|
|
88
|
+
// Unmapped passes through
|
|
89
|
+
expect(icons[2]).toHaveAttribute('name', 'unknown-icon')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('reflects the open prop to a host class', () => {
|
|
93
|
+
const { container, rerender } = render(<PktHeaderMenu data={LOCAL_DATA} open />)
|
|
94
|
+
const root = container.firstChild as HTMLElement
|
|
95
|
+
expect(root).toHaveClass('pkt-header-menu')
|
|
96
|
+
expect(root).toHaveClass('pkt-header-menu--open')
|
|
97
|
+
rerender(<PktHeaderMenu data={LOCAL_DATA} open={false} />)
|
|
98
|
+
expect(root).not.toHaveClass('pkt-header-menu--open')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('uses the locale-specific nav aria-label', () => {
|
|
102
|
+
const { container } = render(<PktHeaderMenu data={LOCAL_DATA} locale="nb-NO" open />)
|
|
103
|
+
expect(container.querySelector('nav')?.getAttribute('aria-label')).toBe(
|
|
104
|
+
'Oslo kommune hovedmeny',
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('switches content when locale changes', () => {
|
|
109
|
+
const { container, rerender } = render(
|
|
110
|
+
<PktHeaderMenu data={LOCAL_DATA} locale="nb-NO" open />,
|
|
111
|
+
)
|
|
112
|
+
expect(container.querySelector('.pkt-header-menu__services-title')?.textContent).toContain(
|
|
113
|
+
'Tjenester og tilbud',
|
|
114
|
+
)
|
|
115
|
+
rerender(<PktHeaderMenu data={LOCAL_DATA} locale="en-GB" open />)
|
|
116
|
+
expect(container.querySelector('.pkt-header-menu__services-title')?.textContent).toContain(
|
|
117
|
+
'Services and information',
|
|
118
|
+
)
|
|
119
|
+
expect(container.querySelector('nav')?.getAttribute('aria-label')).toBe(
|
|
120
|
+
'Oslo kommune main menu',
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('renders social links with derived aria-labels and icons', async () => {
|
|
125
|
+
const { container } = render(<PktHeaderMenu data={LOCAL_DATA} open />)
|
|
126
|
+
await window.customElements.whenDefined('pkt-icon')
|
|
127
|
+
const social = container.querySelectorAll<HTMLElement>('.pkt-header-menu__social-link')
|
|
128
|
+
expect(social).toHaveLength(2)
|
|
129
|
+
expect(social[0]).toHaveAttribute('aria-label', 'Facebook')
|
|
130
|
+
expect(social[1]).toHaveAttribute('aria-label', 'LinkedIn')
|
|
131
|
+
await waitFor(() =>
|
|
132
|
+
expect(social[0].querySelector('pkt-icon')).toHaveAttribute('name', 'facebook'),
|
|
133
|
+
)
|
|
134
|
+
expect(social[1].querySelector('pkt-icon')).toHaveAttribute('name', 'linkedin')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('calls onDataLoaded with the supplied payload', async () => {
|
|
138
|
+
const onDataLoaded = vi.fn()
|
|
139
|
+
render(<PktHeaderMenu data={LOCAL_DATA} open onDataLoaded={onDataLoaded} />)
|
|
140
|
+
await waitFor(() => expect(onDataLoaded).toHaveBeenCalledWith(LOCAL_DATA))
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('PktHeaderMenu fetching', () => {
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() =>
|
|
147
|
+
Promise.resolve(
|
|
148
|
+
new Response(JSON.stringify(LOCAL_DATA), {
|
|
149
|
+
status: 200,
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
}),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('fetches the default URL on mount when no data is supplied', async () => {
|
|
157
|
+
const { container } = render(<PktHeaderMenu open />)
|
|
158
|
+
await waitFor(() => expect(globalThis.fetch).toHaveBeenCalled())
|
|
159
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
160
|
+
'https://cdn.web.oslo.kommune.no/header-footer/header-footer.json',
|
|
161
|
+
expect.any(Object),
|
|
162
|
+
)
|
|
163
|
+
await waitFor(() =>
|
|
164
|
+
expect(container.querySelector('.pkt-header-menu__services-title')?.textContent).toContain(
|
|
165
|
+
'Tjenester og tilbud',
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('honors a custom data-url', async () => {
|
|
171
|
+
render(<PktHeaderMenu dataUrl="https://custom.test/payload.json" open />)
|
|
172
|
+
await waitFor(() =>
|
|
173
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
174
|
+
'https://custom.test/payload.json',
|
|
175
|
+
expect.any(Object),
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('calls onDataError when the fetch fails', async () => {
|
|
181
|
+
vi.restoreAllMocks()
|
|
182
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
183
|
+
new Response('boom', { status: 500 }) as unknown as Response,
|
|
184
|
+
)
|
|
185
|
+
const onDataError = vi.fn()
|
|
186
|
+
const { container } = render(<PktHeaderMenu open onDataError={onDataError} />)
|
|
187
|
+
await waitFor(() => expect(onDataError).toHaveBeenCalledTimes(1))
|
|
188
|
+
expect((onDataError.mock.calls[0][0] as Error).message).toMatch(/HTTP 500/)
|
|
189
|
+
expect(container.querySelector('.pkt-header-menu__error')).not.toBeNull()
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('PktHeaderMenu accessibility', () => {
|
|
194
|
+
test('has no axe violations with the local-data payload', async () => {
|
|
195
|
+
const { container } = render(<PktHeaderMenu data={LOCAL_DATA} open />)
|
|
196
|
+
const results = await axe(container)
|
|
197
|
+
expect(results).toHaveNoViolations()
|
|
198
|
+
})
|
|
199
|
+
})
|