@oslokommune/punkt-react 16.26.5 → 17.0.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 +93 -0
- package/dist/components/accordion/Accordion.d.ts +1 -0
- package/dist/components/alert/Alert.d.ts +9 -0
- package/dist/components/button/Button.d.ts +1 -1
- package/dist/components/combobox/types.d.ts +2 -0
- package/dist/components/datepicker/types.d.ts +3 -0
- package/dist/components/footer/Footer.accessibility.test.d.ts +0 -0
- package/dist/components/footer/Footer.d.ts +12 -34
- package/dist/components/footer/Footer.test.d.ts +1 -0
- package/dist/components/footer/FooterGlobal.d.ts +23 -0
- package/dist/components/footerSimple/FooterSimple.d.ts +19 -16
- package/dist/components/footerSimple/FooterSimple.test.d.ts +1 -0
- package/dist/components/header/Header.d.ts +15 -3
- package/dist/components/header/HeaderGlobal.accessibility.test.d.ts +0 -0
- package/dist/components/header/HeaderGlobal.d.ts +2 -0
- package/dist/components/header/HeaderGlobal.test.d.ts +0 -0
- package/dist/components/header/HeaderService.accessibility.test.d.ts +0 -0
- package/dist/components/header/types.d.ts +30 -2
- package/dist/components/header-menu/HeaderMenu.d.ts +2 -4
- package/dist/components/index.d.ts +4 -0
- package/dist/components/inputwrapper/InputWrapper.d.ts +1 -0
- package/dist/components/searchinput/SearchInput.d.ts +1 -0
- package/dist/components/select/Select.d.ts +4 -7
- package/dist/components/textarea/Textarea.d.ts +2 -0
- package/dist/components/textinput/Textinput.d.ts +1 -0
- package/dist/components/timepicker/types.d.ts +3 -0
- package/dist/components/timepicker/useTimepickerState.d.ts +2 -0
- package/dist/punkt-react.cjs +1 -1
- package/dist/punkt-react.js +3188 -2729
- package/dist/shared-types/footer.d.ts +95 -0
- package/dist/shared-types/{header-menu.d.ts → header-footer.d.ts} +51 -4
- package/dist/shared-types/header.d.ts +1 -1
- package/dist/shared-types/index.d.ts +6 -4
- package/dist/shared-types/searchinput.d.ts +2 -0
- package/dist/shared-types/select.d.ts +3 -2
- package/dist/shared-types/size.d.ts +4 -0
- package/dist/shared-types/timepicker.d.ts +0 -6
- package/dist/shared-utils/datepicker-utils.d.ts +5 -4
- package/dist/shared-utils/fileupload/focus-trap.d.ts +4 -10
- package/dist/shared-utils/focus-trap/index.d.ts +22 -0
- package/dist/shared-utils/{header-menu → header-footer}/example-data.d.ts +5 -4
- package/dist/shared-utils/header-footer/external-domain.d.ts +26 -0
- package/dist/shared-utils/{header-menu → header-footer}/fetch-header-footer-data.d.ts +6 -2
- package/dist/shared-utils/{header-menu → header-footer}/index.d.ts +2 -1
- package/dist/shared-utils/{header-menu → header-footer}/select-megamenu.d.ts +1 -1
- package/dist/shared-utils/scroll-lock/index.d.ts +24 -0
- package/package.json +4 -4
- package/src/components/accordion/Accordion.test.tsx +51 -0
- package/src/components/accordion/Accordion.tsx +3 -3
- package/src/components/accordion/AccordionItem.tsx +7 -14
- package/src/components/alert/Alert.test.tsx +45 -9
- package/src/components/alert/Alert.tsx +13 -3
- package/src/components/button/Button.tsx +2 -2
- package/src/components/combobox/Combobox.core.test.tsx +14 -0
- package/src/components/combobox/Combobox.tsx +1 -0
- package/src/components/combobox/ComboboxInput.tsx +2 -0
- package/src/components/combobox/types.ts +2 -0
- package/src/components/combobox/useComboboxState.ts +2 -0
- package/src/components/consent/Consent.test.tsx +8 -14
- package/src/components/consent/Consent.tsx +9 -19
- package/src/components/datepicker/Datepicker.core.test.tsx +11 -0
- package/src/components/datepicker/Datepicker.tsx +4 -1
- package/src/components/datepicker/DatepickerInputs.tsx +16 -1
- package/src/components/datepicker/types.ts +3 -0
- package/src/components/datepicker/useDatepickerState.ts +17 -9
- package/src/components/footer/Footer.accessibility.test.tsx +46 -0
- package/src/components/footer/Footer.test.tsx +192 -86
- package/src/components/footer/Footer.tsx +169 -159
- package/src/components/footer/FooterGlobal.tsx +181 -0
- package/src/components/footerSimple/FooterSimple.test.tsx +49 -46
- package/src/components/footerSimple/FooterSimple.tsx +117 -78
- package/src/components/header/Header.tsx +17 -4
- package/src/components/header/HeaderGlobal.accessibility.test.tsx +86 -0
- package/src/components/header/HeaderGlobal.test.tsx +254 -0
- package/src/components/header/HeaderGlobal.tsx +467 -0
- package/src/components/header/HeaderService.accessibility.test.tsx +70 -0
- package/src/components/header/HeaderService.test.tsx +29 -16
- package/src/components/header/HeaderService.tsx +24 -84
- package/src/components/header/types.ts +35 -0
- package/src/components/header-menu/HeaderMenu.test.tsx +16 -39
- package/src/components/header-menu/HeaderMenu.tsx +62 -70
- package/src/components/index.ts +4 -0
- package/src/components/inputwrapper/InputWrapper.tsx +6 -1
- package/src/components/modal/Modal.tsx +7 -11
- package/src/components/searchinput/SearchInput.test.tsx +10 -0
- package/src/components/searchinput/SearchInput.tsx +19 -7
- package/src/components/select/Select.test.tsx +18 -0
- package/src/components/select/Select.tsx +8 -4
- package/src/components/textarea/Textarea.test.tsx +10 -0
- package/src/components/textarea/Textarea.tsx +5 -1
- package/src/components/textinput/Textinput.test.tsx +28 -0
- package/src/components/textinput/Textinput.tsx +5 -2
- package/src/components/timepicker/Timepicker.test.tsx +10 -0
- package/src/components/timepicker/Timepicker.tsx +8 -7
- package/src/components/timepicker/types.ts +8 -5
- package/src/components/timepicker/useTimepickerState.ts +4 -0
- package/dist/shared-utils/modal-scroll-lock.d.ts +0 -10
- /package/dist/shared-utils/{header-menu → header-footer}/icon-map.d.ts +0 -0
|
@@ -1,55 +1,71 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { FC,
|
|
3
|
+
import { FC, useEffect, useMemo, type HTMLAttributes } from 'react'
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_FOOTER_PERSONVERN_URL,
|
|
7
|
+
DEFAULT_FOOTER_TILGJENGELIGHET_URL,
|
|
8
|
+
type IPktFooterColumn,
|
|
9
|
+
type IPktFooterLink,
|
|
10
|
+
type IPktFooterProps,
|
|
11
|
+
} from 'shared-types'
|
|
12
|
+
import { getLinkDomainLabel } from 'shared-utils/header-footer'
|
|
5
13
|
import { PktConsent } from '../consent/Consent'
|
|
6
14
|
import { PktIcon } from '../icon/Icon'
|
|
15
|
+
import { FooterGlobal, useGlobalFooterData, type TFooterConsentConfig } from './FooterGlobal'
|
|
7
16
|
|
|
8
|
-
interface
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
openInNewTab?: boolean
|
|
17
|
+
export interface IPktFooter
|
|
18
|
+
extends IPktFooterProps<string>, TFooterConsentConfig, Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
19
|
+
/** Kalles når henting fra endepunktet feiler (den globale footeren skjules stille). */
|
|
20
|
+
onDataError?: (error: Error) => void
|
|
13
21
|
}
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const CustomLink: FC<{ link: IPktFooterLink; openLinksInNewTab: boolean }> = ({ link, openLinksInNewTab }) => {
|
|
24
|
+
const newTab = link.openInNewTab || openLinksInNewTab
|
|
25
|
+
// external = vis toppdomenet i parentes bak lenketeksten
|
|
26
|
+
const domain = link.external ? getLinkDomainLabel(link.href) : undefined
|
|
27
|
+
return (
|
|
28
|
+
<li>
|
|
29
|
+
<a href={link.href} target={newTab ? '_blank' : '_self'} rel={newTab ? 'noopener noreferrer' : undefined}>
|
|
30
|
+
{link.text}
|
|
31
|
+
{domain && <span className="pkt-footer__link-domain"> ({domain})</span>}
|
|
32
|
+
</a>
|
|
33
|
+
</li>
|
|
34
|
+
)
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
cookieExpiryDays?: string | null
|
|
42
|
-
onToggleConsent?: (e: CustomEvent) => void
|
|
43
|
-
}
|
|
37
|
+
const Column: FC<{ column: IPktFooterColumn; openLinksInNewTab: boolean }> = ({ column, openLinksInNewTab }) => (
|
|
38
|
+
<section>
|
|
39
|
+
<h2>{column.title}</h2>
|
|
40
|
+
{column.text && <p>{column.text}</p>}
|
|
41
|
+
{!!column.links?.length && (
|
|
42
|
+
<ul className="pkt-footer__link-list">
|
|
43
|
+
{column.links.map((link) => (
|
|
44
|
+
<CustomLink key={`${link.href}|${link.text}`} link={link} openLinksInNewTab={openLinksInNewTab} />
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
)}
|
|
48
|
+
</section>
|
|
49
|
+
)
|
|
44
50
|
|
|
51
|
+
/**
|
|
52
|
+
* PktFooter — footer med valgfritt custom innhold (grått sekundær-bånd)
|
|
53
|
+
* stablet oppå den globale (blå) footeren. Uten custom innhold rendres kun
|
|
54
|
+
* den globale. Den globale henter innhold fra Oslo kommunes felles
|
|
55
|
+
* header/footer-endepunkt (samme som PktHeader type="global").
|
|
56
|
+
*/
|
|
45
57
|
export const PktFooter: FC<IPktFooter> = ({
|
|
58
|
+
columns,
|
|
46
59
|
columnOne,
|
|
47
60
|
columnTwo,
|
|
61
|
+
personvernOgInfoLink,
|
|
62
|
+
tilgjengelighetLink,
|
|
48
63
|
socialLinks,
|
|
49
|
-
|
|
64
|
+
skipGlobal = false,
|
|
65
|
+
data,
|
|
66
|
+
dataUrl,
|
|
67
|
+
locale = 'nb-NO',
|
|
50
68
|
openLinksInNewTab = false,
|
|
51
|
-
personvernOgInfoLink = 'https://www.oslo.kommune.no/personvern-og-informasjonskapsler/',
|
|
52
|
-
tilgjengelighetLink = 'https://www.oslo.kommune.no/tilgjengelighet/',
|
|
53
69
|
includeConsent = false,
|
|
54
70
|
hotjarId = null,
|
|
55
71
|
googleAnalyticsId = null,
|
|
@@ -57,135 +73,129 @@ export const PktFooter: FC<IPktFooter> = ({
|
|
|
57
73
|
cookieDomain = null,
|
|
58
74
|
cookieSecure = null,
|
|
59
75
|
cookieExpiryDays = null,
|
|
60
|
-
onToggleConsent
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
onToggleConsent,
|
|
77
|
+
onDataError,
|
|
78
|
+
className,
|
|
79
|
+
...rest
|
|
64
80
|
}) => {
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (columnOne || columnTwo) {
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.warn(
|
|
85
|
+
'PktFooter: The "columnOne"/"columnTwo" props are deprecated and will be removed in a future version. Please use "columns" instead.',
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}, [columnOne, columnTwo])
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (personvernOgInfoLink || tilgjengelighetLink) {
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.warn(
|
|
94
|
+
'PktFooter: The "personvernOgInfoLink"/"tilgjengelighetLink" props are deprecated — the links now come from the global footer. Overridden values are rendered in the secondary footer.',
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}, [personvernOgInfoLink, tilgjengelighetLink])
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (socialLinks) {
|
|
101
|
+
// eslint-disable-next-line no-console
|
|
102
|
+
console.warn(
|
|
103
|
+
'PktFooter: The "socialLinks" prop is deprecated and will be removed in a future version — the social media links now come from the global footer.',
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}, [socialLinks])
|
|
107
|
+
|
|
108
|
+
const globalFooter = useGlobalFooterData({ data, dataUrl, locale, skipGlobal, onDataError })
|
|
109
|
+
|
|
110
|
+
// Deprecated «Om nettstedet»-lenker rendres kun når de er eksplisitt
|
|
111
|
+
// overstyrt til noe annet enn default — ellers dekkes de av den globale
|
|
112
|
+
// footeren.
|
|
113
|
+
const effectiveColumns = useMemo(() => {
|
|
114
|
+
const base = columns?.length ? columns : ([columnOne, columnTwo].filter(Boolean) as IPktFooterColumn[])
|
|
115
|
+
const legacyLinks: IPktFooterLink[] = []
|
|
116
|
+
if (personvernOgInfoLink && personvernOgInfoLink !== DEFAULT_FOOTER_PERSONVERN_URL) {
|
|
117
|
+
legacyLinks.push({ text: 'Personvern og informasjonskapsler', href: personvernOgInfoLink })
|
|
118
|
+
}
|
|
119
|
+
if (tilgjengelighetLink && tilgjengelighetLink !== DEFAULT_FOOTER_TILGJENGELIGHET_URL) {
|
|
120
|
+
legacyLinks.push({ text: 'Tilgjengelighet', href: tilgjengelighetLink })
|
|
121
|
+
}
|
|
122
|
+
if (!legacyLinks.length) return base
|
|
123
|
+
return [...base, { title: 'Om nettstedet', links: legacyLinks }]
|
|
124
|
+
}, [columns, columnOne, columnTwo, personvernOgInfoLink, tilgjengelighetLink])
|
|
125
|
+
|
|
126
|
+
const consent: TFooterConsentConfig = {
|
|
127
|
+
hotjarId,
|
|
128
|
+
googleAnalyticsId,
|
|
129
|
+
devMode,
|
|
130
|
+
cookieDomain,
|
|
131
|
+
cookieSecure,
|
|
132
|
+
cookieExpiryDays,
|
|
133
|
+
onToggleConsent,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Med skipGlobal rendres consent-lenken i sekundær-båndet i stedet for i
|
|
137
|
+
// den globale footeren.
|
|
138
|
+
const consentInSecondary = skipGlobal && includeConsent
|
|
139
|
+
const languageLinks = socialLinks?.filter((link) => link.language) ?? []
|
|
140
|
+
const iconLinks = socialLinks?.filter((link) => link.iconName) ?? []
|
|
141
|
+
const hasSecondary = effectiveColumns.length > 0 || consentInSecondary || !!socialLinks?.length
|
|
142
|
+
|
|
65
143
|
const classNames = [className, 'pkt-footer'].filter(Boolean).join(' ')
|
|
66
144
|
|
|
67
145
|
return (
|
|
68
|
-
<footer className={classNames}
|
|
69
|
-
|
|
70
|
-
<div className="pkt-
|
|
71
|
-
<div className="pkt-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<li className="pkt-footer__list-item" key={`links-${index}`}>
|
|
77
|
-
<a
|
|
78
|
-
className={`pkt-footer__link ${link.external ? 'pkt-link--external' : ''}`}
|
|
79
|
-
href={link.href}
|
|
80
|
-
target={link.openInNewTab || openLinksInNewTab ? '_blank' : '_self'}
|
|
81
|
-
rel={link.openInNewTab || openLinksInNewTab ? 'noopener noreferrer' : undefined}
|
|
82
|
-
>
|
|
83
|
-
<PktIcon className="pkt-footer__link-icon" name="chevron-right" />
|
|
84
|
-
{link.text}
|
|
85
|
-
</a>
|
|
86
|
-
</li>
|
|
87
|
-
))}
|
|
88
|
-
</ul>
|
|
89
|
-
</div>
|
|
90
|
-
<div className="pkt-footer__column pkt-cell pkt-cell--span12 pkt-cell--span4-laptop-up">
|
|
91
|
-
<h2 className="pkt-footer__title">{columnTwo.title}</h2>
|
|
92
|
-
<ul className="pkt-footer__list">
|
|
93
|
-
{columnTwo.text && <li className="pkt-footer__text">{columnTwo.text}</li>}
|
|
94
|
-
{columnTwo.links?.map((link, index) => (
|
|
95
|
-
<li className="pkt-footer__list-item" key={`links-${index}`}>
|
|
96
|
-
<a
|
|
97
|
-
className={`pkt-footer__link ${link.external ? 'pkt-link--external' : ''}`}
|
|
98
|
-
href={link.href}
|
|
99
|
-
target={link.openInNewTab || openLinksInNewTab ? '_blank' : '_self'}
|
|
100
|
-
rel={link.openInNewTab || openLinksInNewTab ? 'noopener noreferrer' : undefined}
|
|
101
|
-
>
|
|
102
|
-
<PktIcon className="pkt-footer__link-icon" name="chevron-right" />
|
|
103
|
-
{link.text}
|
|
104
|
-
</a>
|
|
105
|
-
</li>
|
|
106
|
-
))}
|
|
107
|
-
</ul>
|
|
108
|
-
</div>
|
|
109
|
-
<div className="pkt-footer__column pkt-cell pkt-cell--span12 pkt-cell--span4-laptop-up">
|
|
110
|
-
<h2 className="pkt-footer__title">Om nettstedet</h2>
|
|
111
|
-
<ul className="pkt-footer__list">
|
|
112
|
-
<li className="pkt-footer__list-item">
|
|
113
|
-
<a
|
|
114
|
-
className="pkt-footer__link"
|
|
115
|
-
href={personvernOgInfoLink}
|
|
116
|
-
target={openLinksInNewTab ? '_blank' : '_self'}
|
|
117
|
-
rel={openLinksInNewTab ? 'noopener noreferrer' : undefined}
|
|
118
|
-
>
|
|
119
|
-
<PktIcon className="pkt-footer__link-icon" name="chevron-right" />
|
|
120
|
-
Personvern og informasjonskapsler
|
|
121
|
-
</a>
|
|
122
|
-
</li>
|
|
123
|
-
<li className="pkt-footer__list-item">
|
|
124
|
-
<a
|
|
125
|
-
className="pkt-footer__link"
|
|
126
|
-
href={tilgjengelighetLink}
|
|
127
|
-
target={openLinksInNewTab ? '_blank' : '_self'}
|
|
128
|
-
rel={openLinksInNewTab ? 'noopener noreferrer' : undefined}
|
|
129
|
-
>
|
|
130
|
-
<PktIcon className="pkt-footer__link-icon" name="chevron-right" />
|
|
131
|
-
Tilgjengelighet
|
|
132
|
-
</a>
|
|
133
|
-
</li>
|
|
134
|
-
{includeConsent && (
|
|
135
|
-
<li className="pkt-footer__list-item">
|
|
136
|
-
<PktConsent
|
|
137
|
-
triggerType="footerlink"
|
|
138
|
-
hotjarId={hotjarId}
|
|
139
|
-
googleAnalyticsId={googleAnalyticsId}
|
|
140
|
-
devMode={devMode}
|
|
141
|
-
cookieDomain={cookieDomain}
|
|
142
|
-
cookieSecure={cookieSecure}
|
|
143
|
-
cookieExpiryDays={cookieExpiryDays}
|
|
144
|
-
onToggleConsent={onToggleConsent}
|
|
145
|
-
/>
|
|
146
|
-
</li>
|
|
147
|
-
)}
|
|
148
|
-
</ul>
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
{socialLinks && (
|
|
152
|
-
<div className="pkt-footer__social" aria-label="standard lenker">
|
|
153
|
-
<div className="pkt-footer__social-languages">
|
|
154
|
-
{socialLinks
|
|
155
|
-
.filter((link) => link.language)
|
|
156
|
-
.map((link, index) => (
|
|
157
|
-
<div key={`sociallinks-language-${index}`} className="pkt-footer__social-language">
|
|
158
|
-
<a
|
|
159
|
-
href={link.href}
|
|
160
|
-
aria-label={`til ${link.language} versjon av nettsiden`}
|
|
161
|
-
target={link.openInNewTab || openLinksInNewTab ? '_blank' : '_self'}
|
|
162
|
-
rel={link.openInNewTab || openLinksInNewTab ? 'noopener noreferrer' : undefined}
|
|
163
|
-
>
|
|
164
|
-
{link.language}
|
|
165
|
-
</a>
|
|
166
|
-
</div>
|
|
146
|
+
<footer className={classNames} {...rest}>
|
|
147
|
+
{hasSecondary && (
|
|
148
|
+
<div className="pkt-footer__band pkt-footer__band--secondary">
|
|
149
|
+
<div className="pkt-footer__inner">
|
|
150
|
+
{effectiveColumns.length > 0 && (
|
|
151
|
+
<div className="pkt-footer__sections">
|
|
152
|
+
{effectiveColumns.map((column) => (
|
|
153
|
+
<Column key={column.title} column={column} openLinksInNewTab={openLinksInNewTab} />
|
|
167
154
|
))}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
{consentInSecondary && (
|
|
158
|
+
<div className="pkt-footer__consent">
|
|
159
|
+
<PktConsent triggerType="footerlink" {...consent} />
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
{!!socialLinks?.length && (
|
|
163
|
+
<div className="pkt-footer__bottom">
|
|
164
|
+
<ul className="pkt-footer__bottom-links">
|
|
165
|
+
{languageLinks.map((link) => (
|
|
166
|
+
<li key={`${link.href}|${link.language}`}>
|
|
167
|
+
<a
|
|
168
|
+
href={link.href}
|
|
169
|
+
aria-label={`til ${link.language} versjon av nettsiden`}
|
|
170
|
+
target={link.openInNewTab || openLinksInNewTab ? '_blank' : '_self'}
|
|
171
|
+
rel={link.openInNewTab || openLinksInNewTab ? 'noopener noreferrer' : undefined}
|
|
172
|
+
>
|
|
173
|
+
{link.language}
|
|
174
|
+
</a>
|
|
175
|
+
</li>
|
|
176
|
+
))}
|
|
177
|
+
</ul>
|
|
178
|
+
<ul className="pkt-footer__some">
|
|
179
|
+
{iconLinks.map((link) => (
|
|
180
|
+
<li key={`${link.href}|${link.iconName}`}>
|
|
181
|
+
<a
|
|
182
|
+
className="pkt-footer__some-link"
|
|
183
|
+
href={link.href}
|
|
184
|
+
aria-label={`til ${link.iconName}`}
|
|
185
|
+
target={link.openInNewTab || openLinksInNewTab ? '_blank' : '_self'}
|
|
186
|
+
rel={link.openInNewTab || openLinksInNewTab ? 'noopener noreferrer' : undefined}
|
|
187
|
+
>
|
|
188
|
+
<PktIcon className="pkt-footer__some-icon" name={link.iconName} />
|
|
189
|
+
</a>
|
|
190
|
+
</li>
|
|
191
|
+
))}
|
|
192
|
+
</ul>
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
186
195
|
</div>
|
|
187
|
-
|
|
188
|
-
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
{globalFooter && <FooterGlobal footer={globalFooter} includeConsent={includeConsent} consent={consent} />}
|
|
189
199
|
</footer>
|
|
190
200
|
)
|
|
191
201
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
deriveSocialIcon,
|
|
7
|
+
fetchHeaderFooterData,
|
|
8
|
+
getExternalDomainLabel,
|
|
9
|
+
selectLocaleData,
|
|
10
|
+
} from 'shared-utils/header-footer'
|
|
11
|
+
import type {
|
|
12
|
+
IHeaderFooter,
|
|
13
|
+
IHeaderMenuLink,
|
|
14
|
+
IHeaderMenuSection,
|
|
15
|
+
THeaderFooterApi,
|
|
16
|
+
THeaderMenuLocale,
|
|
17
|
+
} from 'shared-types'
|
|
18
|
+
import { PktConsent, type IPktConsent } from '../consent/Consent'
|
|
19
|
+
import { PktIcon } from '../icon/Icon'
|
|
20
|
+
|
|
21
|
+
/** The payload marks the cookie-settings entry with this pseudo-URL. */
|
|
22
|
+
const CONSENT_TRIGGER_URL = '#cb-trigger'
|
|
23
|
+
|
|
24
|
+
export type TFooterConsentConfig = Pick<
|
|
25
|
+
IPktConsent,
|
|
26
|
+
| 'hotjarId'
|
|
27
|
+
| 'googleAnalyticsId'
|
|
28
|
+
| 'devMode'
|
|
29
|
+
| 'cookieDomain'
|
|
30
|
+
| 'cookieSecure'
|
|
31
|
+
| 'cookieExpiryDays'
|
|
32
|
+
| 'onToggleConsent'
|
|
33
|
+
>
|
|
34
|
+
|
|
35
|
+
interface IUseGlobalFooterDataOptions {
|
|
36
|
+
data?: THeaderFooterApi<string>
|
|
37
|
+
dataUrl?: string
|
|
38
|
+
locale?: THeaderMenuLocale
|
|
39
|
+
skipGlobal?: boolean
|
|
40
|
+
onDataError?: (error: Error) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useGlobalFooterData({
|
|
44
|
+
data,
|
|
45
|
+
dataUrl,
|
|
46
|
+
locale = 'nb-NO',
|
|
47
|
+
skipGlobal = false,
|
|
48
|
+
onDataError,
|
|
49
|
+
}: IUseGlobalFooterDataOptions): IHeaderFooter | undefined {
|
|
50
|
+
const [fetchedData, setFetchedData] = useState<THeaderFooterApi<string>>()
|
|
51
|
+
const onDataErrorRef = useRef(onDataError)
|
|
52
|
+
onDataErrorRef.current = onDataError
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (data || skipGlobal) return
|
|
56
|
+
const controller = new AbortController()
|
|
57
|
+
fetchHeaderFooterData<string>(dataUrl, controller.signal)
|
|
58
|
+
.then((payload) => {
|
|
59
|
+
if (!controller.signal.aborted) setFetchedData(payload)
|
|
60
|
+
})
|
|
61
|
+
.catch((error: Error) => {
|
|
62
|
+
if (error.name === 'AbortError') return
|
|
63
|
+
// eslint-disable-next-line no-console -- Error on fetching data
|
|
64
|
+
console.warn('Failed to fetch header/footer data:', error)
|
|
65
|
+
onDataErrorRef.current?.(error)
|
|
66
|
+
})
|
|
67
|
+
return () => controller.abort()
|
|
68
|
+
}, [data, dataUrl, skipGlobal])
|
|
69
|
+
|
|
70
|
+
const effectiveData = data ?? fetchedData
|
|
71
|
+
return useMemo(() => {
|
|
72
|
+
if (skipGlobal) return undefined
|
|
73
|
+
return selectLocaleData(effectiveData, locale)?.footer
|
|
74
|
+
}, [effectiveData, locale, skipGlobal])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ISectionLinkProps {
|
|
78
|
+
link: IHeaderMenuLink
|
|
79
|
+
includeConsent: boolean
|
|
80
|
+
consent: TFooterConsentConfig
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const SectionLink: FC<ISectionLinkProps> = ({ link, includeConsent, consent }) => {
|
|
84
|
+
if (link.url === CONSENT_TRIGGER_URL) {
|
|
85
|
+
// Punkt's own consent dialog replaces the CDN cookie-banner trigger.
|
|
86
|
+
if (!includeConsent) return null
|
|
87
|
+
return (
|
|
88
|
+
<li>
|
|
89
|
+
<PktConsent triggerType="footerlink" triggerText={link.text} {...consent} />
|
|
90
|
+
</li>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
const domain = getExternalDomainLabel(link.url)
|
|
94
|
+
return (
|
|
95
|
+
<li>
|
|
96
|
+
<a href={link.url}>
|
|
97
|
+
{link.text}
|
|
98
|
+
{domain && <span className="pkt-footer__link-domain"> ({domain})</span>}
|
|
99
|
+
</a>
|
|
100
|
+
</li>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const Section: FC<{ section: IHeaderMenuSection } & Omit<ISectionLinkProps, 'link'>> = ({
|
|
105
|
+
section,
|
|
106
|
+
includeConsent,
|
|
107
|
+
consent,
|
|
108
|
+
}) => (
|
|
109
|
+
<section>
|
|
110
|
+
<h2>{section.title}</h2>
|
|
111
|
+
<ul className="pkt-footer__link-list">
|
|
112
|
+
{section.links.map((link) => (
|
|
113
|
+
<SectionLink key={`${link.url}|${link.text}`} link={link} includeConsent={includeConsent} consent={consent} />
|
|
114
|
+
))}
|
|
115
|
+
</ul>
|
|
116
|
+
</section>
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const SomeLink: FC<{ entry: IHeaderMenuLink }> = ({ entry }) => {
|
|
120
|
+
const icon = deriveSocialIcon(entry.url, entry.text)
|
|
121
|
+
if (!icon) {
|
|
122
|
+
return (
|
|
123
|
+
<li>
|
|
124
|
+
<a href={entry.url}>{entry.text}</a>
|
|
125
|
+
</li>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
return (
|
|
129
|
+
<li>
|
|
130
|
+
<a className="pkt-footer__some-link" href={entry.url} aria-label={entry.text}>
|
|
131
|
+
<PktIcon className="pkt-footer__some-icon" name={icon} />
|
|
132
|
+
</a>
|
|
133
|
+
</li>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface IFooterGlobalProps {
|
|
138
|
+
footer: IHeaderFooter
|
|
139
|
+
includeConsent?: boolean
|
|
140
|
+
consent?: TFooterConsentConfig
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The global (blue) footer band, fed by the shared header/footer payload.
|
|
145
|
+
* Internal — rendered by PktFooter / PktFooterSimple, not exported.
|
|
146
|
+
*/
|
|
147
|
+
export const FooterGlobal: FC<IFooterGlobalProps> = ({ footer, includeConsent = false, consent = {} }) => {
|
|
148
|
+
const hasLinks = footer.links?.length > 0
|
|
149
|
+
const hasSome = footer.some?.length > 0
|
|
150
|
+
return (
|
|
151
|
+
<div className="pkt-footer__band pkt-footer__band--global">
|
|
152
|
+
<div className="pkt-footer__inner">
|
|
153
|
+
<div className="pkt-footer__sections">
|
|
154
|
+
{footer.sections.map((section) => (
|
|
155
|
+
<Section key={section.title} section={section} includeConsent={includeConsent} consent={consent} />
|
|
156
|
+
))}
|
|
157
|
+
{(hasLinks || hasSome) && (
|
|
158
|
+
<div className="pkt-footer__bottom">
|
|
159
|
+
{hasLinks && (
|
|
160
|
+
<ul className="pkt-footer__bottom-links">
|
|
161
|
+
{footer.links.map((link) => (
|
|
162
|
+
<li key={`${link.url}|${link.text}`}>
|
|
163
|
+
<a href={link.url}>{link.text}</a>
|
|
164
|
+
</li>
|
|
165
|
+
))}
|
|
166
|
+
</ul>
|
|
167
|
+
)}
|
|
168
|
+
{hasSome && (
|
|
169
|
+
<ul className="pkt-footer__some">
|
|
170
|
+
{footer.some.map((entry) => (
|
|
171
|
+
<SomeLink key={`${entry.url}|${entry.text}`} entry={entry} />
|
|
172
|
+
))}
|
|
173
|
+
</ul>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
@@ -1,55 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
import '
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import { render } from '../../utils/test-utils'
|
|
4
|
+
import { EXAMPLE_HEADER_FOOTER_DATA } from 'shared-utils/header-footer'
|
|
5
|
+
import type { IPktFooterLink } from 'shared-types'
|
|
7
6
|
import { PktFooterSimple } from './FooterSimple'
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
const LINKS: IPktFooterLink[] = [
|
|
9
|
+
{ text: 'Lenke 1', href: 'https://example.test/lenke-1' },
|
|
10
|
+
{ text: 'Lenke 2', href: 'https://example.test/lenke-2', external: true },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.restoreAllMocks()
|
|
15
|
+
})
|
|
10
16
|
|
|
11
17
|
describe('PktFooterSimple', () => {
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
expect(getByText('Tilgjengelighet').getAttribute('href')).toEqual('https://www.oslo.kommune.no/tilgjengelighet/')
|
|
45
|
-
})
|
|
18
|
+
test('rendrer kompakt lenkerad med pil-ikoner + global footer', () => {
|
|
19
|
+
const { container } = render(<PktFooterSimple links={LINKS} data={EXAMPLE_HEADER_FOOTER_DATA} />)
|
|
20
|
+
const links = [...container.querySelectorAll('.pkt-footer-simple__link')]
|
|
21
|
+
expect(links).toHaveLength(2)
|
|
22
|
+
expect(links[0].querySelector('.pkt-footer-simple__link-icon')).not.toBeNull()
|
|
23
|
+
expect(container.querySelector('.pkt-footer__band--global')).not.toBeNull()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('rendrer kun global footer uten links', () => {
|
|
27
|
+
const { container } = render(<PktFooterSimple data={EXAMPLE_HEADER_FOOTER_DATA} />)
|
|
28
|
+
expect(container.querySelector('.pkt-footer-simple .pkt-footer__band--secondary')).toBeNull()
|
|
29
|
+
expect(container.querySelector('.pkt-footer__band--global')).not.toBeNull()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('eksterne lenker viser toppdomenet i parentes', () => {
|
|
33
|
+
render(<PktFooterSimple links={LINKS} data={EXAMPLE_HEADER_FOOTER_DATA} />)
|
|
34
|
+
const external = screen.getByRole('link', { name: /Lenke 2/ })
|
|
35
|
+
expect(external.querySelector('.pkt-footer-simple__link-domain')?.textContent?.trim()).toBe('(example.test)')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('skipGlobal dropper global footer og henter ikke data', () => {
|
|
39
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('should not run'))
|
|
40
|
+
const { container } = render(<PktFooterSimple links={LINKS} skipGlobal />)
|
|
41
|
+
expect(container.querySelector('.pkt-footer__band--global')).toBeNull()
|
|
42
|
+
expect(container.querySelector('.pkt-footer-simple .pkt-footer__band--secondary')).not.toBeNull()
|
|
43
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('consent rendres som listepunkt ved skipGlobal + includeConsent', () => {
|
|
47
|
+
const { container } = render(<PktFooterSimple links={LINKS} skipGlobal includeConsent />)
|
|
48
|
+
const consent = container.querySelector('.pkt-footer-simple__list a.pkt-footer__link')
|
|
49
|
+
expect(consent).not.toBeNull()
|
|
46
50
|
})
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})
|
|
52
|
+
test('openLinksInNewTab åpner lenker i ny fane', () => {
|
|
53
|
+
render(<PktFooterSimple links={LINKS} data={EXAMPLE_HEADER_FOOTER_DATA} openLinksInNewTab />)
|
|
54
|
+
const link = screen.getByRole('link', { name: 'Lenke 1' })
|
|
55
|
+
expect(link).toHaveAttribute('target', '_blank')
|
|
56
|
+
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
|
54
57
|
})
|
|
55
58
|
})
|