@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.
- package/CHANGELOG.md +111 -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/fileupload/FileUpload.d.ts +12 -2
- 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 +3191 -2727
- 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/fileupload/FileUpload.tsx +27 -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,35 +1,69 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { FC, type HTMLAttributes } from 'react'
|
|
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 IPktFooterLink,
|
|
9
|
+
type IPktFooterSimpleProps,
|
|
10
|
+
} from 'shared-types'
|
|
11
|
+
import { getLinkDomainLabel } from 'shared-utils/header-footer'
|
|
5
12
|
import { PktConsent } from '../consent/Consent'
|
|
6
13
|
import { PktIcon } from '../icon/Icon'
|
|
14
|
+
import { FooterGlobal, useGlobalFooterData, type TFooterConsentConfig } from '../footer/FooterGlobal'
|
|
7
15
|
|
|
8
|
-
export interface IPktFooterSimple
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
export interface IPktFooterSimple
|
|
17
|
+
extends IPktFooterSimpleProps<string>, TFooterConsentConfig, Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
18
|
+
onDataError?: (error: Error) => void
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated The privacy link now comes from the global footer's payload.
|
|
21
|
+
* Only rendered (in the secondary footer) when explicitly set to a
|
|
22
|
+
* non-default value.
|
|
23
|
+
*/
|
|
15
24
|
personvernOgInfoLink?: string
|
|
25
|
+
/**
|
|
26
|
+
* @deprecated The accessibility link now comes from the global footer's
|
|
27
|
+
* payload. Only rendered (in the secondary footer) when explicitly set
|
|
28
|
+
* to a non-default value.
|
|
29
|
+
*/
|
|
16
30
|
tilgjengelighetLink?: string
|
|
17
|
-
openLinksInNewTab?: boolean
|
|
18
|
-
includeConsent?: boolean
|
|
19
|
-
hotjarId?: string | null
|
|
20
|
-
googleAnalyticsId?: string | null
|
|
21
|
-
devMode?: boolean
|
|
22
|
-
cookieDomain?: string | null
|
|
23
|
-
cookieSecure?: string | null
|
|
24
|
-
cookieExpiryDays?: string | null
|
|
25
|
-
onToggleConsent?: (e: CustomEvent) => void
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
const SimpleLink: FC<{ link: IPktFooterLink; openLinksInNewTab: boolean }> = ({ link, openLinksInNewTab }) => {
|
|
34
|
+
const newTab = link.openInNewTab || openLinksInNewTab
|
|
35
|
+
// external = vis toppdomenet i parentes bak lenketeksten
|
|
36
|
+
const domain = link.external ? getLinkDomainLabel(link.href) : undefined
|
|
37
|
+
return (
|
|
38
|
+
<a
|
|
39
|
+
className="pkt-footer-simple__link"
|
|
40
|
+
href={link.href}
|
|
41
|
+
target={newTab ? '_blank' : '_self'}
|
|
42
|
+
rel={newTab ? 'noopener noreferrer' : undefined}
|
|
43
|
+
>
|
|
44
|
+
<PktIcon className="pkt-footer-simple__link-icon" name="arrow-link" />
|
|
45
|
+
<span>
|
|
46
|
+
{link.text}
|
|
47
|
+
{domain && <span className="pkt-footer-simple__link-domain"> ({domain})</span>}
|
|
48
|
+
</span>
|
|
49
|
+
</a>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* PktFooterSimple — kompakt footer: en sentrert rad med pil-lenker i det
|
|
55
|
+
* grå sekundær-båndet, stablet oppå den globale (blå) footeren. Uten
|
|
56
|
+
* `links` rendres kun den globale footeren.
|
|
57
|
+
*/
|
|
28
58
|
export const PktFooterSimple: FC<IPktFooterSimple> = ({
|
|
29
|
-
links
|
|
59
|
+
links,
|
|
60
|
+
personvernOgInfoLink,
|
|
61
|
+
tilgjengelighetLink,
|
|
62
|
+
skipGlobal = false,
|
|
63
|
+
data,
|
|
64
|
+
dataUrl,
|
|
65
|
+
locale = 'nb-NO',
|
|
30
66
|
openLinksInNewTab = false,
|
|
31
|
-
personvernOgInfoLink = 'https://www.oslo.kommune.no/personvern-og-informasjonskapsler/',
|
|
32
|
-
tilgjengelighetLink = 'https://www.oslo.kommune.no/tilgjengelighet/',
|
|
33
67
|
includeConsent = false,
|
|
34
68
|
hotjarId = null,
|
|
35
69
|
googleAnalyticsId = null,
|
|
@@ -37,68 +71,73 @@ export const PktFooterSimple: FC<IPktFooterSimple> = ({
|
|
|
37
71
|
cookieDomain = null,
|
|
38
72
|
cookieSecure = null,
|
|
39
73
|
cookieExpiryDays = null,
|
|
40
|
-
onToggleConsent
|
|
41
|
-
|
|
42
|
-
console.log(e.detail)
|
|
43
|
-
},
|
|
74
|
+
onToggleConsent,
|
|
75
|
+
onDataError,
|
|
44
76
|
className,
|
|
77
|
+
...rest
|
|
45
78
|
}) => {
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (personvernOgInfoLink || tilgjengelighetLink) {
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.warn(
|
|
83
|
+
'PktFooterSimple: The "personvernOgInfoLink"/"tilgjengelighetLink" props are deprecated — the links now come from the global footer. Overridden values are rendered in the secondary footer.',
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}, [personvernOgInfoLink, tilgjengelighetLink])
|
|
87
|
+
|
|
88
|
+
const globalFooter = useGlobalFooterData({ data, dataUrl, locale, skipGlobal, onDataError })
|
|
89
|
+
|
|
90
|
+
// Deprecated lenker rendres kun når de er eksplisitt overstyrt til noe
|
|
91
|
+
// annet enn default — ellers dekkes de av den globale footeren.
|
|
92
|
+
const effectiveLinks = useMemo(() => {
|
|
93
|
+
const legacyLinks: IPktFooterLink[] = []
|
|
94
|
+
if (personvernOgInfoLink && personvernOgInfoLink !== DEFAULT_FOOTER_PERSONVERN_URL) {
|
|
95
|
+
legacyLinks.push({ text: 'Personvern og informasjonskapsler', href: personvernOgInfoLink })
|
|
96
|
+
}
|
|
97
|
+
if (tilgjengelighetLink && tilgjengelighetLink !== DEFAULT_FOOTER_TILGJENGELIGHET_URL) {
|
|
98
|
+
legacyLinks.push({ text: 'Tilgjengelighet', href: tilgjengelighetLink })
|
|
99
|
+
}
|
|
100
|
+
return [...(links ?? []), ...legacyLinks]
|
|
101
|
+
}, [links, personvernOgInfoLink, tilgjengelighetLink])
|
|
102
|
+
|
|
103
|
+
const consent: TFooterConsentConfig = {
|
|
104
|
+
hotjarId,
|
|
105
|
+
googleAnalyticsId,
|
|
106
|
+
devMode,
|
|
107
|
+
cookieDomain,
|
|
108
|
+
cookieSecure,
|
|
109
|
+
cookieExpiryDays,
|
|
110
|
+
onToggleConsent,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Med skipGlobal rendres consent-lenken i sekundær-båndet i stedet for i
|
|
114
|
+
// den globale footeren.
|
|
115
|
+
const consentInSecondary = skipGlobal && includeConsent
|
|
116
|
+
const hasSecondary = effectiveLinks.length > 0 || consentInSecondary
|
|
117
|
+
|
|
46
118
|
const classNames = [className, 'pkt-footer-simple'].filter(Boolean).join(' ')
|
|
119
|
+
|
|
47
120
|
return (
|
|
48
|
-
<footer className={classNames}
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
target={openLinksInNewTab ? '_blank' : '_self'}
|
|
69
|
-
rel={openLinksInNewTab ? 'noopener noreferrer' : undefined}
|
|
70
|
-
>
|
|
71
|
-
<PktIcon className="pkt-footer-simple__link-icon" name="chevron-right"></PktIcon>
|
|
72
|
-
Personvern og informasjonskapsler
|
|
73
|
-
</a>
|
|
74
|
-
</li>
|
|
75
|
-
<li className="pkt-footer-simple__list-item">
|
|
76
|
-
<a
|
|
77
|
-
className="pkt-footer-simple__link"
|
|
78
|
-
href={tilgjengelighetLink}
|
|
79
|
-
target={openLinksInNewTab ? '_blank' : '_self'}
|
|
80
|
-
rel={openLinksInNewTab ? 'noopener noreferrer' : undefined}
|
|
81
|
-
>
|
|
82
|
-
<PktIcon className="pkt-footer-simple__link-icon" name="chevron-right"></PktIcon>
|
|
83
|
-
Tilgjengelighet
|
|
84
|
-
</a>
|
|
85
|
-
</li>
|
|
86
|
-
{includeConsent && (
|
|
87
|
-
<li className="pkt-footer-simple__list-item">
|
|
88
|
-
<PktConsent
|
|
89
|
-
triggerType="footerlink"
|
|
90
|
-
hotjarId={hotjarId}
|
|
91
|
-
googleAnalyticsId={googleAnalyticsId}
|
|
92
|
-
devMode={devMode}
|
|
93
|
-
cookieDomain={cookieDomain}
|
|
94
|
-
cookieSecure={cookieSecure}
|
|
95
|
-
cookieExpiryDays={cookieExpiryDays}
|
|
96
|
-
onToggleConsent={onToggleConsent}
|
|
97
|
-
/>
|
|
98
|
-
</li>
|
|
99
|
-
)}
|
|
100
|
-
</ul>
|
|
101
|
-
</div>
|
|
121
|
+
<footer className={classNames} {...rest}>
|
|
122
|
+
{hasSecondary && (
|
|
123
|
+
<div className="pkt-footer__band pkt-footer__band--secondary">
|
|
124
|
+
<div className="pkt-footer__inner pkt-footer__inner--compact">
|
|
125
|
+
<ul className="pkt-footer-simple__list">
|
|
126
|
+
{effectiveLinks.map((link) => (
|
|
127
|
+
<li key={`${link.href}|${link.text}`}>
|
|
128
|
+
<SimpleLink link={link} openLinksInNewTab={openLinksInNewTab} />
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
{consentInSecondary && (
|
|
132
|
+
<li>
|
|
133
|
+
<PktConsent triggerType="footerlink" {...consent} />
|
|
134
|
+
</li>
|
|
135
|
+
)}
|
|
136
|
+
</ul>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
{globalFooter && <FooterGlobal footer={globalFooter} includeConsent={includeConsent} consent={consent} />}
|
|
102
141
|
</footer>
|
|
103
142
|
)
|
|
104
143
|
}
|
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import { ForwardedRef, forwardRef } from 'react'
|
|
4
4
|
|
|
5
|
+
import { PktHeaderGlobal } from './HeaderGlobal'
|
|
5
6
|
import { PktHeaderService } from './HeaderService'
|
|
6
|
-
import { IPktHeader } from './types'
|
|
7
|
+
import { IPktHeader, IPktHeaderGlobal } from './types'
|
|
7
8
|
|
|
8
9
|
export * from './types'
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Props for `<PktHeader>`. The `type` prop selects the variant:
|
|
13
|
+
* the default `service` header (a single service's header) or the
|
|
14
|
+
* `global` (kommune-wide) header driven by the shared header/footer
|
|
15
|
+
* payload.
|
|
16
|
+
*/
|
|
17
|
+
export type IPktHeaderProps = (IPktHeader & { type?: 'service' }) | (IPktHeaderGlobal & { type: 'global' })
|
|
18
|
+
|
|
10
19
|
/**
|
|
11
20
|
* PktHeader - Main header component for Oslo kommune services
|
|
12
21
|
*
|
|
@@ -17,10 +26,14 @@ export * from './types'
|
|
|
17
26
|
* - Responsive mobile menu
|
|
18
27
|
* - Fixed positioning with scroll-to-hide
|
|
19
28
|
*
|
|
20
|
-
*
|
|
29
|
+
* The `type` prop switches between the `service` header (default) and
|
|
30
|
+
* the `global` (kommune-wide) header.
|
|
21
31
|
*/
|
|
22
|
-
export const PktHeader = forwardRef((props:
|
|
23
|
-
|
|
32
|
+
export const PktHeader = forwardRef((props: IPktHeaderProps, ref: ForwardedRef<HTMLElement>) => {
|
|
33
|
+
if (props.type === 'global') {
|
|
34
|
+
return <PktHeaderGlobal {...props} ref={ref as ForwardedRef<HTMLDivElement>} />
|
|
35
|
+
}
|
|
36
|
+
return <PktHeaderService {...props} ref={ref as ForwardedRef<HTMLDivElement>} />
|
|
24
37
|
})
|
|
25
38
|
|
|
26
39
|
PktHeader.displayName = 'PktHeader'
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react'
|
|
4
|
+
import { axe, toHaveNoViolations } from 'jest-axe'
|
|
5
|
+
import { EXAMPLE_HEADER_FOOTER_DATA } from 'shared-utils/header-footer'
|
|
6
|
+
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { PktHeaderGlobal } from './HeaderGlobal'
|
|
9
|
+
|
|
10
|
+
expect.extend(toHaveNoViolations)
|
|
11
|
+
|
|
12
|
+
// jsdom reports offsetWidth as 0 (compact). Zero the breakpoints to force the
|
|
13
|
+
// full desktop layout; omit to exercise the compact/mobile layout.
|
|
14
|
+
const DESKTOP = { mobileBreakpoint: 0, tabletBreakpoint: 0 }
|
|
15
|
+
|
|
16
|
+
const renderGlobal = (props = {}) =>
|
|
17
|
+
render(<PktHeaderGlobal data={EXAMPLE_HEADER_FOOTER_DATA} position="relative" {...props} />)
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.unstubAllGlobals()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('PktHeaderGlobal – tilgjengelighet', () => {
|
|
24
|
+
test('ingen brudd i standardvisning (desktop)', async () => {
|
|
25
|
+
const { container } = renderGlobal(DESKTOP)
|
|
26
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('ingen brudd i kompakt visning (mobil)', async () => {
|
|
30
|
+
const { container } = renderGlobal()
|
|
31
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('ingen brudd med innlogget bruker', async () => {
|
|
35
|
+
const { container } = renderGlobal({
|
|
36
|
+
...DESKTOP,
|
|
37
|
+
user: { name: 'Aksel Web Opdahl' },
|
|
38
|
+
logOutButtonPlacement: 'userMenu',
|
|
39
|
+
logOut: vi.fn(),
|
|
40
|
+
})
|
|
41
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('ingen brudd med åpen megameny', async () => {
|
|
45
|
+
const { container } = renderGlobal(DESKTOP)
|
|
46
|
+
fireEvent.click(container.querySelector('#pkt-header-global-menu-toggle') as Element)
|
|
47
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('ingen brudd med åpent søke-overlay', async () => {
|
|
51
|
+
const { container } = renderGlobal()
|
|
52
|
+
fireEvent.click(container.querySelector('#pkt-header-global-search-toggle') as Element)
|
|
53
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('ingen brudd med åpen brukermeny', async () => {
|
|
57
|
+
const { container } = renderGlobal({
|
|
58
|
+
...DESKTOP,
|
|
59
|
+
user: { name: 'Aksel Web Opdahl' },
|
|
60
|
+
logOutButtonPlacement: 'userMenu',
|
|
61
|
+
logOut: vi.fn(),
|
|
62
|
+
})
|
|
63
|
+
fireEvent.click(container.querySelector('.pkt-header-global__user-button') as Element)
|
|
64
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('ingen brudd når menydata feiler', async () => {
|
|
68
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network')))
|
|
69
|
+
const { container, findByRole } = render(<PktHeaderGlobal position="relative" {...DESKTOP} />)
|
|
70
|
+
const menuButton = await findByRole('button', { name: /Meny/ })
|
|
71
|
+
fireEvent.click(menuButton)
|
|
72
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('menyknappen har aria-expanded og aria-controls', () => {
|
|
76
|
+
const { container } = renderGlobal(DESKTOP)
|
|
77
|
+
const toggle = container.querySelector('#pkt-header-global-menu-toggle') as HTMLElement
|
|
78
|
+
expect(toggle).toHaveAttribute('aria-controls', 'pkt-header-global-menu')
|
|
79
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'false')
|
|
80
|
+
|
|
81
|
+
fireEvent.click(toggle)
|
|
82
|
+
expect(
|
|
83
|
+
container.querySelector('#pkt-header-global-menu-toggle'),
|
|
84
|
+
).toHaveAttribute('aria-expanded', 'true')
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
|
|
3
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
4
|
+
import { EXAMPLE_HEADER_FOOTER_DATA } from 'shared-utils/header-footer'
|
|
5
|
+
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { PktHeaderGlobal } from './HeaderGlobal'
|
|
8
|
+
|
|
9
|
+
const renderGlobal = (props = {}) =>
|
|
10
|
+
render(<PktHeaderGlobal data={EXAMPLE_HEADER_FOOTER_DATA} position="relative" {...props} />)
|
|
11
|
+
|
|
12
|
+
// jsdom reports offsetWidth as 0, which puts the header in compact mode.
|
|
13
|
+
// Force the desktop (full) layout by zeroing the breakpoints.
|
|
14
|
+
const DESKTOP = { mobileBreakpoint: 0, tabletBreakpoint: 0 }
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.unstubAllGlobals()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('PktHeaderGlobal', () => {
|
|
21
|
+
test('viser søkefelt, kontaktlenke og menyknapp (desktop)', () => {
|
|
22
|
+
const { container } = renderGlobal(DESKTOP)
|
|
23
|
+
|
|
24
|
+
expect(container.querySelector('input[name="q"]')).toBeInTheDocument()
|
|
25
|
+
|
|
26
|
+
const contact = screen.getByRole('link', { name: 'Kontakt' })
|
|
27
|
+
expect(contact).toHaveAttribute('href', 'https://www.oslo.kommune.no/kontakt')
|
|
28
|
+
|
|
29
|
+
expect(screen.getByRole('button', { name: /Meny/ })).toBeInTheDocument()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('søkefeltet er en GET-form mot søke-endepunktet (desktop)', () => {
|
|
33
|
+
const { container } = renderGlobal(DESKTOP)
|
|
34
|
+
const form = container.querySelector('form[role="search"]')
|
|
35
|
+
expect(form).toHaveAttribute('action', 'https://www.oslo.kommune.no/sokeresultat/')
|
|
36
|
+
expect(form).toHaveAttribute('method', 'get')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('mobil: søkeknappen åpner et søkefelt-overlay', () => {
|
|
40
|
+
const { container } = renderGlobal()
|
|
41
|
+
// Compact layout: inline search is replaced by a round toggle.
|
|
42
|
+
expect(container.querySelector('.pkt-header-global__search-panel')).not.toBeInTheDocument()
|
|
43
|
+
const searchToggle = container.querySelector('#pkt-header-global-search-toggle')
|
|
44
|
+
expect(searchToggle).toBeInTheDocument()
|
|
45
|
+
|
|
46
|
+
fireEvent.click(searchToggle as Element)
|
|
47
|
+
|
|
48
|
+
const panel = container.querySelector('.pkt-header-global__search-panel')
|
|
49
|
+
expect(panel).toBeInTheDocument()
|
|
50
|
+
expect(panel?.querySelector('input[name="q"]')).toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('mobil: åpning av søk gir fokus til søkefeltet', () => {
|
|
54
|
+
const { container } = renderGlobal()
|
|
55
|
+
fireEvent.click(container.querySelector('#pkt-header-global-search-toggle') as Element)
|
|
56
|
+
|
|
57
|
+
const input = container.querySelector('#pkt-header-global-search-panel input') as HTMLInputElement
|
|
58
|
+
expect(input).toBeTruthy()
|
|
59
|
+
expect(document.activeElement).toBe(input)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('mobil: Tab ut av søke-overlay lukker det og flytter fokus til søkeknappen', () => {
|
|
63
|
+
const { container } = renderGlobal()
|
|
64
|
+
const searchToggle = container.querySelector('#pkt-header-global-search-toggle') as HTMLElement
|
|
65
|
+
fireEvent.click(searchToggle)
|
|
66
|
+
|
|
67
|
+
const panel = container.querySelector('#pkt-header-global-search-panel') as HTMLElement
|
|
68
|
+
const focusables = panel.querySelectorAll<HTMLElement>('input, button')
|
|
69
|
+
const last = focusables[focusables.length - 1]
|
|
70
|
+
act(() => last.focus())
|
|
71
|
+
|
|
72
|
+
fireEvent.keyDown(document, { key: 'Tab' })
|
|
73
|
+
|
|
74
|
+
expect(container.querySelector('.pkt-header-global__search-panel')).not.toBeInTheDocument()
|
|
75
|
+
expect(searchToggle.contains(document.activeElement) || document.activeElement === searchToggle).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('mobil: fokus utenfor søke-overlay lukker det', () => {
|
|
79
|
+
const { container } = renderGlobal()
|
|
80
|
+
fireEvent.click(container.querySelector('#pkt-header-global-search-toggle') as Element)
|
|
81
|
+
expect(container.querySelector('.pkt-header-global__search-panel')).toBeInTheDocument()
|
|
82
|
+
|
|
83
|
+
const menuToggle = container.querySelector('#pkt-header-global-menu-toggle') as HTMLElement
|
|
84
|
+
act(() => menuToggle.focus())
|
|
85
|
+
fireEvent.focusIn(menuToggle)
|
|
86
|
+
|
|
87
|
+
expect(container.querySelector('.pkt-header-global__search-panel')).not.toBeInTheDocument()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('mobil: åpning av søk lukker megamenyen', () => {
|
|
91
|
+
const { container } = renderGlobal()
|
|
92
|
+
fireEvent.click(screen.getByRole('button', { name: /Meny/ }))
|
|
93
|
+
expect(screen.getByText('Tjenester og tilbud')).toBeInTheDocument()
|
|
94
|
+
|
|
95
|
+
fireEvent.click(container.querySelector('#pkt-header-global-search-toggle') as Element)
|
|
96
|
+
expect(screen.queryByText('Tjenester og tilbud')).not.toBeInTheDocument()
|
|
97
|
+
expect(container.querySelector('.pkt-header-global__search-panel')).toBeInTheDocument()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('menyknappen åpner megamenyen', () => {
|
|
101
|
+
renderGlobal()
|
|
102
|
+
expect(screen.queryByText('Tjenester og tilbud')).not.toBeInTheDocument()
|
|
103
|
+
|
|
104
|
+
fireEvent.click(screen.getByRole('button', { name: /Meny/ }))
|
|
105
|
+
|
|
106
|
+
expect(screen.getByText('Tjenester og tilbud')).toBeInTheDocument()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('tastatur: Tab fra menyknappen flytter fokus inn i megamenyen', () => {
|
|
110
|
+
const { container } = renderGlobal(DESKTOP)
|
|
111
|
+
const toggle = container.querySelector('#pkt-header-global-menu-toggle') as HTMLElement
|
|
112
|
+
fireEvent.click(toggle)
|
|
113
|
+
act(() => toggle.focus())
|
|
114
|
+
expect(document.activeElement).toBe(toggle)
|
|
115
|
+
|
|
116
|
+
fireEvent.keyDown(document, { key: 'Tab' })
|
|
117
|
+
|
|
118
|
+
const menu = container.querySelector('#pkt-header-global-menu') as HTMLElement
|
|
119
|
+
const firstFocusable = menu.querySelector('a[href], button') as HTMLElement
|
|
120
|
+
expect(document.activeElement).toBe(firstFocusable)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('mobil: Tab fra menyknappen flytter fokus inn i megamenyen (accordion)', () => {
|
|
124
|
+
const original = window.matchMedia
|
|
125
|
+
// Force the mega menu into its mobile accordion layout.
|
|
126
|
+
window.matchMedia = vi.fn().mockReturnValue({
|
|
127
|
+
matches: true,
|
|
128
|
+
addEventListener: vi.fn(),
|
|
129
|
+
removeEventListener: vi.fn(),
|
|
130
|
+
addListener: vi.fn(),
|
|
131
|
+
removeListener: vi.fn(),
|
|
132
|
+
}) as unknown as typeof window.matchMedia
|
|
133
|
+
try {
|
|
134
|
+
const { container } = renderGlobal()
|
|
135
|
+
const toggle = container.querySelector('#pkt-header-global-menu-toggle') as HTMLElement
|
|
136
|
+
fireEvent.click(toggle)
|
|
137
|
+
act(() => toggle.focus())
|
|
138
|
+
|
|
139
|
+
fireEvent.keyDown(document, { key: 'Tab' })
|
|
140
|
+
|
|
141
|
+
const menu = container.querySelector('#pkt-header-global-menu') as HTMLElement
|
|
142
|
+
const firstSummary = menu.querySelector('summary') as HTMLElement
|
|
143
|
+
expect(firstSummary).toBeTruthy()
|
|
144
|
+
expect(document.activeElement).toBe(firstSummary)
|
|
145
|
+
} finally {
|
|
146
|
+
window.matchMedia = original
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('tastatur: Tab ut av megamenyen lukker den og flytter fokus til menyknappen', () => {
|
|
151
|
+
const { container } = renderGlobal(DESKTOP)
|
|
152
|
+
const toggle = container.querySelector('#pkt-header-global-menu-toggle') as HTMLElement
|
|
153
|
+
fireEvent.click(toggle)
|
|
154
|
+
|
|
155
|
+
const menu = container.querySelector('#pkt-header-global-menu') as HTMLElement
|
|
156
|
+
const focusables = menu.querySelectorAll<HTMLElement>('a[href], button')
|
|
157
|
+
const last = focusables[focusables.length - 1]
|
|
158
|
+
act(() => last.focus())
|
|
159
|
+
|
|
160
|
+
fireEvent.keyDown(document, { key: 'Tab' })
|
|
161
|
+
|
|
162
|
+
expect(container.querySelector('#pkt-header-global-menu')).not.toBeInTheDocument()
|
|
163
|
+
expect(toggle.contains(document.activeElement) || document.activeElement === toggle).toBe(true)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('tastatur: fokus utenfor megamenyen lukker den', () => {
|
|
167
|
+
const { container } = renderGlobal(DESKTOP)
|
|
168
|
+
fireEvent.click(container.querySelector('#pkt-header-global-menu-toggle') as Element)
|
|
169
|
+
expect(screen.getByText('Tjenester og tilbud')).toBeInTheDocument()
|
|
170
|
+
|
|
171
|
+
const contact = container.querySelector('.pkt-header-global__contact') as HTMLElement
|
|
172
|
+
act(() => contact.focus())
|
|
173
|
+
fireEvent.focusIn(contact)
|
|
174
|
+
|
|
175
|
+
expect(screen.queryByText('Tjenester og tilbud')).not.toBeInTheDocument()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('viser feilmelding i menyen når data ikke kan lastes', async () => {
|
|
179
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network')))
|
|
180
|
+
render(<PktHeaderGlobal position="relative" {...DESKTOP} />)
|
|
181
|
+
|
|
182
|
+
const menuButton = await screen.findByRole('button', { name: /Meny/ })
|
|
183
|
+
await waitFor(() => expect(menuButton).not.toBeDisabled())
|
|
184
|
+
|
|
185
|
+
fireEvent.click(menuButton)
|
|
186
|
+
expect(screen.getByText('Kunne ikke laste meny.')).toBeInTheDocument()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('brukerknappen åpner brukermenyen', () => {
|
|
190
|
+
renderGlobal({ user: { name: 'Aksel Web Opdahl' }, logOutButtonPlacement: 'userMenu', logOut: vi.fn() })
|
|
191
|
+
|
|
192
|
+
const userButton = screen.getByRole('button', { name: /Aksel Web Opdahl/ })
|
|
193
|
+
fireEvent.click(userButton)
|
|
194
|
+
|
|
195
|
+
expect(screen.getByText('Pålogget som')).toBeInTheDocument()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('kaller onDataLoaded når data er forhåndshentet', () => {
|
|
199
|
+
const onDataLoaded = vi.fn()
|
|
200
|
+
renderGlobal({ onDataLoaded })
|
|
201
|
+
expect(onDataLoaded).toHaveBeenCalledWith(EXAMPLE_HEADER_FOOTER_DATA)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('logo lenker til www.oslo.kommune.no når logoLink er satt', () => {
|
|
205
|
+
renderGlobal({ logoLink: 'https://www.oslo.kommune.no' })
|
|
206
|
+
const logoLink = screen.getByRole('link', { name: 'Til hovedsiden' })
|
|
207
|
+
expect(logoLink).toHaveAttribute('href', 'https://www.oslo.kommune.no')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('skjuler kontaktlenken når bruker er innlogget', () => {
|
|
211
|
+
const { container } = renderGlobal({ ...DESKTOP, user: { name: 'Aksel Web Opdahl' } })
|
|
212
|
+
expect(container.querySelector('.pkt-header-global__contact')).not.toBeInTheDocument()
|
|
213
|
+
expect(container.querySelector('.pkt-header-global__user-button')).toBeInTheDocument()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('megameny-overlegget forankres til headerens faktiske posisjon', () => {
|
|
217
|
+
const { container } = renderGlobal({ ...DESKTOP, position: 'relative' })
|
|
218
|
+
const header = container.querySelector('.pkt-header-global') as HTMLElement
|
|
219
|
+
// Simulate a header sitting 32px down the page (relative/sticky position).
|
|
220
|
+
header.getBoundingClientRect = () =>
|
|
221
|
+
({ bottom: 120, top: 32, left: 16, right: 1296, width: 1280, height: 88 }) as DOMRect
|
|
222
|
+
|
|
223
|
+
fireEvent.click(container.querySelector('#pkt-header-global-menu-toggle') as Element)
|
|
224
|
+
|
|
225
|
+
const overlay = container.querySelector('#pkt-header-global-menu') as HTMLElement
|
|
226
|
+
// The CSS adds the design offset (104/88px) on top of this anchor.
|
|
227
|
+
expect(overlay.style.getPropertyValue('--pkt-header-global-top')).toBe('32px')
|
|
228
|
+
expect(overlay.style.getPropertyValue('--pkt-header-global-left')).toBe('16px')
|
|
229
|
+
expect(overlay.style.getPropertyValue('--pkt-header-global-width')).toBe('1280px')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('position="sticky" gir sticky-klasse uten wrapper/spacer', () => {
|
|
233
|
+
const { container } = renderGlobal({ ...DESKTOP, position: 'sticky' })
|
|
234
|
+
const header = container.querySelector('.pkt-header-global')
|
|
235
|
+
expect(header).toHaveClass('pkt-header-global--sticky')
|
|
236
|
+
expect(header).not.toHaveClass('pkt-header-global--fixed')
|
|
237
|
+
expect(container.querySelector('.pkt-header-global-wrapper')).toBeNull()
|
|
238
|
+
expect(container.querySelector('.pkt-header-global-spacer')).toBeNull()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('logoen lenker til www.oslo.kommune.no som standard', () => {
|
|
242
|
+
const { container } = renderGlobal(DESKTOP)
|
|
243
|
+
const logo = container.querySelector('.pkt-header-global__logo a')
|
|
244
|
+
expect(logo).toHaveAttribute('href', 'https://www.oslo.kommune.no')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('logoClick kalles ved klikk på logoen', () => {
|
|
248
|
+
// preventDefault: jsdom kan ikke navigere
|
|
249
|
+
const onClick = vi.fn((e: React.MouseEvent) => e.preventDefault())
|
|
250
|
+
const { container } = renderGlobal({ ...DESKTOP, logoClick: onClick })
|
|
251
|
+
fireEvent.click(container.querySelector('.pkt-header-global__logo a') as Element)
|
|
252
|
+
expect(onClick).toHaveBeenCalledTimes(1)
|
|
253
|
+
})
|
|
254
|
+
})
|