@oslokommune/punkt-elements 16.26.7 → 17.0.2
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 +110 -0
- package/dist/accordion-CbXDxOho.cjs +47 -0
- package/dist/{accordion-CBdIk9gw.js → accordion-DEUHQ4JD.js} +45 -35
- package/dist/{alert-HHTGY0sp.js → alert-CbtVGrHD.js} +31 -17
- package/dist/{alert-B8dLsiTz.cjs → alert-CkUnbPdA.cjs} +5 -5
- package/dist/base-elements/input-element.d.ts +1 -0
- package/dist/button-Cod17rKo.js +115 -0
- package/dist/{button-B5tQpKeG.cjs → button-gvDXtBMC.cjs} +5 -2
- package/dist/{calendar-DIzGBdbh.cjs → calendar-BVXW8DqC.cjs} +1 -1
- package/dist/{calendar-CjlUA-Lb.js → calendar-CjC4qlh6.js} +1 -1
- package/dist/{checkbox-Bt_sC_90.cjs → checkbox-8uxbeU30.cjs} +1 -1
- package/dist/{checkbox-D03BC2Va.js → checkbox-DJDphhqz.js} +1 -1
- package/dist/{combobox-CoQBTWqL.js → combobox-Du5yr-xM.js} +16 -2
- package/dist/combobox-hKrbYLUe.cjs +136 -0
- package/dist/components/accordion/accordion-context.d.ts +7 -0
- package/dist/components/accordion/accordion.d.ts +2 -0
- package/dist/components/accordion/accordionitem.d.ts +1 -0
- package/dist/components/alert/alert.d.ts +5 -0
- package/dist/components/alert/alert.test.d.ts +1 -0
- package/dist/components/button/button.d.ts +4 -1
- package/dist/components/datepicker/datepicker-base.d.ts +2 -0
- package/dist/components/datepicker/datepicker-utils.d.ts +2 -2
- package/dist/components/footer/footer-base.d.ts +44 -0
- package/dist/components/footer/footer-simple.d.ts +20 -0
- package/dist/components/footer/footer.accessibility.test.d.ts +0 -0
- package/dist/components/footer/footer.d.ts +22 -0
- package/dist/components/footer/footer.test.d.ts +0 -0
- package/dist/components/footer/index.d.ts +3 -0
- package/dist/components/footer/types.d.ts +16 -0
- package/dist/components/header/header-chrome-controller.d.ts +44 -0
- package/dist/components/header/header-global.accessibility.test.d.ts +0 -0
- package/dist/components/header/header-global.d.ts +78 -0
- package/dist/components/header/header-global.test.d.ts +0 -0
- package/dist/components/header/header-service.accessibility.test.d.ts +0 -0
- package/dist/components/header/header-service.d.ts +6 -21
- package/dist/components/header/header-service.test.d.ts +1 -1
- package/dist/components/header/header.d.ts +11 -3
- package/dist/components/header/header.test.d.ts +0 -0
- package/dist/components/header/index.d.ts +2 -1
- package/dist/components/header/types.d.ts +28 -2
- package/dist/components/header-menu/header-menu.d.ts +4 -5
- package/dist/components/header-menu/types.d.ts +1 -1
- package/dist/components/index.d.ts +5 -3
- package/dist/components/input-wrapper/input-wrapper.d.ts +2 -0
- package/dist/components/input-wrapper/input-wrapper.test.d.ts +1 -0
- package/dist/components/searchinput/searchinput.d.ts +5 -0
- package/dist/components/select/index.d.ts +1 -1
- package/dist/components/select/select.d.ts +2 -2
- package/dist/components/select/select.test.d.ts +1 -0
- package/dist/components/textarea/textarea.test.d.ts +2 -0
- package/dist/components/textinput/textinput.test.d.ts +1 -0
- package/dist/components/timepicker/timepicker.d.ts +2 -0
- package/dist/components/timepicker/timepicker.test.d.ts +3 -0
- package/dist/consent--Nw43WFU.cjs +18 -0
- package/dist/{consent-D5sY6JK3.js → consent-B2hSTCGC.js} +14 -7
- package/dist/consume-BiOhrhEy.js +167 -0
- package/dist/consume-DKf5SmP9.cjs +1 -0
- package/dist/{converters-BKGBRBpS.js → converters-BBfsEJV5.js} +25 -25
- package/dist/{converters-9jHlXDyy.cjs → converters-Cd0fPEly.cjs} +1 -1
- package/dist/{datepicker-BE5Ll0W4.js → datepicker--Fq8q-J3.js} +30 -12
- package/dist/{datepicker-x4xM3KQz.cjs → datepicker-CEoBYTBU.cjs} +11 -7
- package/dist/docs/components/alert.d.ts +3 -0
- package/dist/docs/components/footer.d.ts +9 -0
- package/dist/docs/components/form-sizes.d.ts +10 -0
- package/dist/docs/components/header-global-full.d.ts +20 -0
- package/dist/docs/components/header-global.d.ts +14 -0
- package/dist/docs/components/searchinput.d.ts +6 -3
- package/dist/fetch-header-footer-data-Pc32etrk.js +88 -0
- package/dist/fetch-header-footer-data-xohWFmYC.cjs +1 -0
- package/dist/fileupload-CGKUJrlj.cjs +401 -0
- package/dist/{fileupload-2hf8YNhO.js → fileupload-D4gQN64T.js} +156 -176
- package/dist/focus-trap-DG3pEuyJ.js +60 -0
- package/dist/focus-trap-DSv4Ifqm.cjs +1 -0
- package/dist/footer-BIrJeRuZ.js +303 -0
- package/dist/footer-Ber-jqM1.cjs +114 -0
- package/dist/footer.d.ts +1 -0
- package/dist/header-BFk1an4z.cjs +465 -0
- package/dist/header-Deg_LGJo.js +1287 -0
- package/dist/header-menu-BHqK4QA6.cjs +126 -0
- package/dist/{header-menu-C8b5vd3n.js → header-menu-GFtJKM4j.js} +25 -97
- package/dist/{helptext-OfqH5npD.js → helptext-C_brWCIT.js} +1 -1
- package/dist/{helptext-eckq4JEX.cjs → helptext-Cq5rNaAC.cjs} +1 -1
- package/dist/{input-element-ZGOWsvKM.cjs → input-element-B0qLbEdF.cjs} +1 -1
- package/dist/{input-element-BFHpeoV8.js → input-element-C0RnXi-J.js} +5 -2
- package/dist/{input-wrapper-DwwE0JcO.js → input-wrapper-BweP6t3O.js} +12 -0
- package/dist/{input-wrapper-BRjQB4zM.cjs → input-wrapper-DPkkiG7S.cjs} +13 -11
- package/dist/{input-wrapper-DaA98B5l.js → input-wrapper-fWV5P4MN.js} +22 -15
- package/dist/{input-wrapper-LcZjpt0L.cjs → input-wrapper-wh3fWIhk.cjs} +1 -1
- package/dist/modal-CPnFteYS.cjs +35 -0
- package/dist/{modal-C6pScpdG.js → modal-xNK_wpHk.js} +11 -20
- package/dist/pkt-accordion.cjs +1 -1
- package/dist/pkt-accordion.js +1 -1
- package/dist/pkt-alert.cjs +1 -1
- package/dist/pkt-alert.js +1 -1
- package/dist/pkt-button.cjs +1 -1
- package/dist/pkt-button.js +1 -1
- package/dist/pkt-calendar.cjs +1 -1
- package/dist/pkt-calendar.js +1 -1
- package/dist/pkt-checkbox.cjs +1 -1
- package/dist/pkt-checkbox.js +1 -1
- package/dist/pkt-combobox.cjs +1 -1
- package/dist/pkt-combobox.js +1 -1
- package/dist/pkt-consent.cjs +1 -1
- package/dist/pkt-consent.js +1 -1
- package/dist/pkt-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +1 -1
- package/dist/pkt-fileupload.cjs +1 -1
- package/dist/pkt-fileupload.js +1 -1
- package/dist/pkt-footer.cjs +1 -0
- package/dist/pkt-footer.js +2 -0
- package/dist/pkt-header-menu.cjs +1 -1
- package/dist/pkt-header-menu.js +1 -1
- package/dist/pkt-header.cjs +1 -1
- package/dist/pkt-header.js +2 -2
- package/dist/pkt-helptext.cjs +1 -1
- package/dist/pkt-helptext.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +27 -26
- package/dist/pkt-input-wrapper.cjs +1 -1
- package/dist/pkt-input-wrapper.js +1 -1
- package/dist/pkt-modal.cjs +1 -1
- package/dist/pkt-modal.js +1 -1
- package/dist/pkt-radiobutton.cjs +1 -1
- package/dist/pkt-radiobutton.js +1 -1
- package/dist/pkt-searchinput.cjs +1 -1
- package/dist/pkt-searchinput.js +1 -1
- package/dist/pkt-select.cjs +1 -1
- package/dist/pkt-select.js +1 -1
- package/dist/pkt-tabs.cjs +1 -1
- package/dist/pkt-tabs.js +1 -1
- package/dist/pkt-textarea.cjs +1 -1
- package/dist/pkt-textarea.js +1 -1
- package/dist/pkt-textinput.cjs +1 -1
- package/dist/pkt-textinput.js +1 -1
- package/dist/pkt-timepicker.cjs +1 -1
- package/dist/pkt-timepicker.js +1 -1
- package/dist/{radiobutton-DGO6H2Ki.js → radiobutton-C6htwDrT.js} +1 -1
- package/dist/{radiobutton-DY1WWepJ.cjs → radiobutton-DjWBoSHv.cjs} +1 -1
- package/dist/scroll-lock-BN8lOhV5.cjs +1 -0
- package/dist/scroll-lock-DAbNKAUy.js +18 -0
- package/dist/searchinput-CfUVfpqL.js +156 -0
- package/dist/searchinput-DGjeN1YI.cjs +60 -0
- package/dist/{select-D4_HQFkh.cjs → select-CZvAW7W2.cjs} +2 -1
- package/dist/{select-BwHUIlwQ.js → select-CsBXXbLP.js} +4 -3
- package/dist/setupTests.d.ts +0 -0
- 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/dist/tabs-BqrKLCjd.js +214 -0
- package/dist/tabs-q62MK_-r.cjs +40 -0
- package/dist/{textarea-D6M7ZhtA.js → textarea-Cm392CEI.js} +5 -3
- package/dist/{textarea-Bt_Zs8gq.cjs → textarea-sTcZxOIB.cjs} +3 -2
- package/dist/{textinput-EDKjcUdg.cjs → textinput-DqJbTglM.cjs} +3 -2
- package/dist/{textinput-oU2EvFsi.js → textinput-TSPcBcih.js} +5 -3
- package/dist/{timepicker-CgOL2sd9.cjs → timepicker-CqFYMV8q.cjs} +8 -7
- package/dist/{timepicker-_G0V_HV3.js → timepicker-DWbgTDKq.js} +10 -9
- package/package.json +3 -3
- package/src/components/accordion/accordion-context.ts +8 -0
- package/src/components/accordion/accordion.test.ts +44 -0
- package/src/components/accordion/accordion.ts +13 -3
- package/src/components/accordion/accordionitem.ts +9 -1
- package/src/components/alert/alert.test.ts +51 -2
- package/src/components/alert/alert.ts +11 -5
- package/src/components/breadcrumbs/breadcrumbs.test.ts +11 -3
- package/src/components/button/button.test.ts +23 -0
- package/src/components/button/button.ts +28 -3
- package/src/components/combobox/combobox.core.test.ts +20 -0
- package/src/components/combobox/combobox.ts +2 -0
- package/src/components/consent/consent.test.ts +6 -3
- package/src/components/consent/consent.ts +15 -7
- package/src/components/datepicker/datepicker-base.ts +5 -1
- package/src/components/datepicker/datepicker-utils.test.ts +10 -0
- package/src/components/datepicker/datepicker.core.test.ts +17 -0
- package/src/components/datepicker/datepicker.ts +8 -0
- package/src/components/footer/footer-base.ts +238 -0
- package/src/components/footer/footer-simple.ts +71 -0
- package/src/components/footer/footer.accessibility.test.ts +66 -0
- package/src/components/footer/footer.test.ts +238 -0
- package/src/components/footer/footer.ts +81 -0
- package/src/components/footer/index.ts +4 -0
- package/src/components/footer/types.ts +21 -0
- package/src/components/header/header-chrome-controller.ts +123 -0
- package/src/components/header/header-global.accessibility.test.ts +110 -0
- package/src/components/header/header-global.test.ts +329 -0
- package/src/components/header/header-global.ts +648 -0
- package/src/components/header/header-service.accessibility.test.ts +79 -0
- package/src/components/header/header-service.test.ts +32 -12
- package/src/components/header/header-service.ts +38 -162
- package/src/components/header/header-user-menu.ts +0 -1
- package/src/components/header/header.test.ts +84 -0
- package/src/components/header/header.ts +52 -5
- package/src/components/header/index.ts +6 -0
- package/src/components/header/types.ts +38 -0
- package/src/components/header-menu/header-menu.test.ts +131 -10
- package/src/components/header-menu/header-menu.ts +23 -24
- package/src/components/header-menu/types.ts +1 -1
- package/src/components/index.ts +18 -2
- package/src/components/input-wrapper/input-wrapper.test.ts +45 -0
- package/src/components/input-wrapper/input-wrapper.ts +17 -9
- package/src/components/modal/modal.ts +8 -9
- package/src/components/searchinput/searchinput.test.ts +43 -0
- package/src/components/searchinput/searchinput.ts +29 -12
- package/src/components/select/index.ts +1 -1
- package/src/components/select/select.test.ts +28 -0
- package/src/components/select/select.ts +4 -3
- package/src/components/textarea/textarea.test.ts +16 -0
- package/src/components/textarea/textarea.ts +3 -1
- package/src/components/textinput/textinput.test.ts +28 -0
- package/src/components/textinput/textinput.ts +3 -1
- package/src/components/timepicker/timepicker.test.ts +26 -0
- package/src/components/timepicker/timepicker.ts +12 -6
- package/dist/accordion-FISAiAbX.cjs +0 -47
- package/dist/button-C_YzTSuU.js +0 -96
- package/dist/combobox-D6H9rWPV.cjs +0 -135
- package/dist/consent-X3wvhteO.cjs +0 -10
- package/dist/fileupload-BLVrslUX.cjs +0 -401
- package/dist/header-CFrsaq0c.cjs +0 -310
- package/dist/header-D_xvPR4R.js +0 -801
- package/dist/header-menu-DMdP_c6X.cjs +0 -126
- package/dist/modal-DKU7PnjJ.cjs +0 -35
- package/dist/searchinput-CyO5k4Q4.js +0 -148
- package/dist/searchinput-DG6YetL1.cjs +0 -60
- package/dist/shared-utils/modal-scroll-lock.d.ts +0 -10
- package/dist/tabs-CGo0ODPa.cjs +0 -40
- package/dist/tabs-ChdR0D1C.js +0 -379
- /package/dist/shared-utils/{header-menu → header-footer}/icon-map.d.ts +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { html, nothing, type PropertyValues, type TemplateResult } from 'lit'
|
|
2
|
+
import { property, state } from 'lit/decorators.js'
|
|
3
|
+
import { ifDefined } from 'lit/directives/if-defined.js'
|
|
4
|
+
import {
|
|
5
|
+
deriveSocialIcon,
|
|
6
|
+
fetchHeaderFooterData,
|
|
7
|
+
getExternalDomainLabel,
|
|
8
|
+
getLinkDomainLabel,
|
|
9
|
+
selectLocaleData,
|
|
10
|
+
} from 'shared-utils/header-footer'
|
|
11
|
+
import {
|
|
12
|
+
type Booleanish,
|
|
13
|
+
booleanishConverter,
|
|
14
|
+
type IHeaderFooter,
|
|
15
|
+
type IHeaderMenuLink,
|
|
16
|
+
type IHeaderMenuSection,
|
|
17
|
+
type IPktFooterLink,
|
|
18
|
+
type THeaderFooterApi,
|
|
19
|
+
type THeaderMenuLocale,
|
|
20
|
+
} from 'shared-types'
|
|
21
|
+
import { PktElement } from '@/base-elements/element'
|
|
22
|
+
|
|
23
|
+
import '@/components/consent'
|
|
24
|
+
import '@/components/icon'
|
|
25
|
+
|
|
26
|
+
const CONSENT_TRIGGER_URL = '#cb-trigger'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Shared base for `pkt-footer` and `pkt-footer-simple`.
|
|
30
|
+
*
|
|
31
|
+
* - Fetching and caching global footer data
|
|
32
|
+
* - Rendering the global footer band (blue)
|
|
33
|
+
* - Rendering the consent dialog (if `includeConsent` is set)
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export abstract class PktFooterBaseElement<T = {}> extends PktElement<T> {
|
|
37
|
+
@property({ type: String, attribute: 'data-url' }) dataUrl?: string
|
|
38
|
+
@property({ type: Object, attribute: false }) data?: THeaderFooterApi
|
|
39
|
+
@property({ type: String }) locale: THeaderMenuLocale = 'nb-NO'
|
|
40
|
+
|
|
41
|
+
@property({ type: Boolean, attribute: 'skip-global', converter: booleanishConverter })
|
|
42
|
+
skipGlobal: Booleanish = false
|
|
43
|
+
@property({ type: Boolean, attribute: 'open-links-in-new-tab', converter: booleanishConverter })
|
|
44
|
+
openLinksInNewTab: Booleanish = false
|
|
45
|
+
|
|
46
|
+
@property({ type: Boolean, attribute: 'include-consent', converter: booleanishConverter })
|
|
47
|
+
includeConsent: Booleanish = false
|
|
48
|
+
@property({ type: String, attribute: 'hotjar-id' }) hotjarId: string | null = null
|
|
49
|
+
@property({ type: String, attribute: 'google-analytics-id' }) googleAnalyticsId: string | null =
|
|
50
|
+
null
|
|
51
|
+
@property({ type: Boolean, attribute: 'dev-mode', converter: booleanishConverter })
|
|
52
|
+
devMode: Booleanish = false
|
|
53
|
+
@property({ type: String, attribute: 'cookie-domain' }) cookieDomain: string | null = null
|
|
54
|
+
@property({ type: String, attribute: 'cookie-secure' }) cookieSecure: string | null = null
|
|
55
|
+
@property({ type: String, attribute: 'cookie-expiry-days' }) cookieExpiryDays: string | null =
|
|
56
|
+
null
|
|
57
|
+
|
|
58
|
+
@state() private fetchedData?: THeaderFooterApi
|
|
59
|
+
|
|
60
|
+
private abortController?: AbortController
|
|
61
|
+
|
|
62
|
+
connectedCallback() {
|
|
63
|
+
super.connectedCallback()
|
|
64
|
+
if (!this.data && !this.skipGlobal) {
|
|
65
|
+
void this.loadData()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
disconnectedCallback() {
|
|
70
|
+
super.disconnectedCallback()
|
|
71
|
+
this.abortController?.abort()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
updated(changedProperties: PropertyValues) {
|
|
75
|
+
super.updated(changedProperties)
|
|
76
|
+
if (
|
|
77
|
+
changedProperties.has('dataUrl') &&
|
|
78
|
+
changedProperties.get('dataUrl') !== undefined &&
|
|
79
|
+
!this.data
|
|
80
|
+
) {
|
|
81
|
+
void this.loadData()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async loadData() {
|
|
86
|
+
this.abortController?.abort()
|
|
87
|
+
this.abortController = new AbortController()
|
|
88
|
+
try {
|
|
89
|
+
this.fetchedData = await fetchHeaderFooterData<string>(
|
|
90
|
+
this.dataUrl,
|
|
91
|
+
this.abortController.signal,
|
|
92
|
+
)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if ((error as Error).name === 'AbortError') return
|
|
95
|
+
console.warn('Failed to fetch header/footer data:', error)
|
|
96
|
+
this.dispatchEvent(
|
|
97
|
+
new CustomEvent('data-error', { detail: { error }, bubbles: true, composed: true }),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private get globalFooterData(): IHeaderFooter | undefined {
|
|
103
|
+
return selectLocaleData(this.data ?? this.fetchedData, this.locale)?.footer
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected linkTarget(link: IPktFooterLink): { target: string; rel?: string } {
|
|
107
|
+
const newTab = link.openInNewTab || this.openLinksInNewTab
|
|
108
|
+
return newTab ? { target: '_blank', rel: 'noopener noreferrer' } : { target: '_self' }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** "(topdomain.tld)" display for external links */
|
|
112
|
+
protected renderLinkDomain(link: IPktFooterLink, className = 'pkt-footer__link-domain') {
|
|
113
|
+
if (!link.external) return nothing
|
|
114
|
+
const domain = getLinkDomainLabel(link.href)
|
|
115
|
+
if (!domain) return nothing
|
|
116
|
+
return html` <span class=${className}>(${domain})</span>`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
protected renderCustomLink(link: IPktFooterLink): TemplateResult {
|
|
120
|
+
const { target, rel } = this.linkTarget(link)
|
|
121
|
+
return html`
|
|
122
|
+
<a href=${link.href} target=${target} rel=${ifDefined(rel)}
|
|
123
|
+
>${link.text}${this.renderLinkDomain(link)}</a
|
|
124
|
+
>
|
|
125
|
+
`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
protected renderConsent(): TemplateResult | typeof nothing {
|
|
129
|
+
if (!this.includeConsent) return nothing
|
|
130
|
+
return html`
|
|
131
|
+
<pkt-consent
|
|
132
|
+
.triggerType=${'footerlink'}
|
|
133
|
+
.hotjarId=${this.hotjarId}
|
|
134
|
+
.googleAnalyticsId=${this.googleAnalyticsId}
|
|
135
|
+
.devMode=${Boolean(this.devMode)}
|
|
136
|
+
.cookieDomain=${this.cookieDomain}
|
|
137
|
+
.cookieSecure=${this.cookieSecure}
|
|
138
|
+
.cookieExpiryDays=${this.cookieExpiryDays}
|
|
139
|
+
></pkt-consent>
|
|
140
|
+
`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private renderGlobalSectionLink(link: IHeaderMenuLink): TemplateResult | typeof nothing {
|
|
144
|
+
if (link.url === CONSENT_TRIGGER_URL) {
|
|
145
|
+
// Punkt's own consent dialog replaces the CDN cookie-banner trigger.
|
|
146
|
+
if (!this.includeConsent) return nothing
|
|
147
|
+
return html`
|
|
148
|
+
<li>
|
|
149
|
+
<pkt-consent
|
|
150
|
+
.triggerType=${'footerlink'}
|
|
151
|
+
.triggerText=${link.text}
|
|
152
|
+
.hotjarId=${this.hotjarId}
|
|
153
|
+
.googleAnalyticsId=${this.googleAnalyticsId}
|
|
154
|
+
.devMode=${Boolean(this.devMode)}
|
|
155
|
+
.cookieDomain=${this.cookieDomain}
|
|
156
|
+
.cookieSecure=${this.cookieSecure}
|
|
157
|
+
.cookieExpiryDays=${this.cookieExpiryDays}
|
|
158
|
+
></pkt-consent>
|
|
159
|
+
</li>
|
|
160
|
+
`
|
|
161
|
+
}
|
|
162
|
+
const domain = getExternalDomainLabel(link.url)
|
|
163
|
+
return html`
|
|
164
|
+
<li>
|
|
165
|
+
<a href=${link.url}
|
|
166
|
+
>${link.text}${domain
|
|
167
|
+
? html` <span class="pkt-footer__link-domain">(${domain})</span>`
|
|
168
|
+
: nothing}</a
|
|
169
|
+
>
|
|
170
|
+
</li>
|
|
171
|
+
`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private renderGlobalSection(section: IHeaderMenuSection): TemplateResult {
|
|
175
|
+
return html`
|
|
176
|
+
<section>
|
|
177
|
+
<h2>${section.title}</h2>
|
|
178
|
+
<ul class="pkt-footer__link-list">
|
|
179
|
+
${section.links.map((link) => this.renderGlobalSectionLink(link))}
|
|
180
|
+
</ul>
|
|
181
|
+
</section>
|
|
182
|
+
`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private renderSomeLink(entry: IHeaderMenuLink): TemplateResult {
|
|
186
|
+
const icon = deriveSocialIcon(entry.url, entry.text)
|
|
187
|
+
if (!icon) {
|
|
188
|
+
return html`<li><a href=${entry.url}>${entry.text}</a></li>`
|
|
189
|
+
}
|
|
190
|
+
return html`
|
|
191
|
+
<li>
|
|
192
|
+
<a class="pkt-footer__some-link" href=${entry.url} aria-label=${entry.text}>
|
|
193
|
+
<pkt-icon class="pkt-footer__some-icon" name=${icon}></pkt-icon>
|
|
194
|
+
</a>
|
|
195
|
+
</li>
|
|
196
|
+
`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private renderGlobalBottom(footer: IHeaderFooter): TemplateResult | typeof nothing {
|
|
200
|
+
const hasLinks = footer.links?.length > 0
|
|
201
|
+
const hasSome = footer.some?.length > 0
|
|
202
|
+
if (!hasLinks && !hasSome) return nothing
|
|
203
|
+
return html`
|
|
204
|
+
<div class="pkt-footer__bottom">
|
|
205
|
+
${hasLinks
|
|
206
|
+
? html`
|
|
207
|
+
<ul class="pkt-footer__bottom-links">
|
|
208
|
+
${footer.links.map((link) => html`<li><a href=${link.url}>${link.text}</a></li>`)}
|
|
209
|
+
</ul>
|
|
210
|
+
`
|
|
211
|
+
: nothing}
|
|
212
|
+
${hasSome
|
|
213
|
+
? html`
|
|
214
|
+
<ul class="pkt-footer__some">
|
|
215
|
+
${footer.some.map((entry) => this.renderSomeLink(entry))}
|
|
216
|
+
</ul>
|
|
217
|
+
`
|
|
218
|
+
: nothing}
|
|
219
|
+
</div>
|
|
220
|
+
`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
protected renderGlobalFooter(): TemplateResult | typeof nothing {
|
|
224
|
+
if (this.skipGlobal) return nothing
|
|
225
|
+
const footer = this.globalFooterData
|
|
226
|
+
if (!footer) return nothing
|
|
227
|
+
return html`
|
|
228
|
+
<div class="pkt-footer__band pkt-footer__band--global">
|
|
229
|
+
<div class="pkt-footer__inner">
|
|
230
|
+
<div class="pkt-footer__sections">
|
|
231
|
+
${footer.sections.map((section) => this.renderGlobalSection(section))}
|
|
232
|
+
${this.renderGlobalBottom(footer)}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
`
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { html, nothing, type TemplateResult } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
import { ifDefined } from 'lit/directives/if-defined.js'
|
|
4
|
+
import type { IPktFooterLink } from 'shared-types'
|
|
5
|
+
import { PktFooterBaseElement } from './footer-base'
|
|
6
|
+
import type { IPktFooterSimple } from './types'
|
|
7
|
+
|
|
8
|
+
import '@/components/icon'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PktFooterSimple — kompakt footer: en sentrert rad med pil-lenker i det
|
|
12
|
+
* grå sekundær-båndet, stablet oppå den globale (blå) footeren. Uten
|
|
13
|
+
* `links` rendres kun den globale footeren.
|
|
14
|
+
*/
|
|
15
|
+
export class PktFooterSimple
|
|
16
|
+
extends PktFooterBaseElement<IPktFooterSimple>
|
|
17
|
+
implements IPktFooterSimple
|
|
18
|
+
{
|
|
19
|
+
@property({ type: Array }) links?: IPktFooterLink[]
|
|
20
|
+
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
super.connectedCallback()
|
|
23
|
+
this.classList.add('pkt-footer-simple-host')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private renderLink(link: IPktFooterLink): TemplateResult {
|
|
27
|
+
const { target, rel } = this.linkTarget(link)
|
|
28
|
+
return html`
|
|
29
|
+
<a class="pkt-footer-simple__link" href=${link.href} target=${target} rel=${ifDefined(rel)}>
|
|
30
|
+
<pkt-icon class="pkt-footer-simple__link-icon" name="arrow-link"></pkt-icon>
|
|
31
|
+
<span>${link.text}${this.renderLinkDomain(link, 'pkt-footer-simple__link-domain')}</span>
|
|
32
|
+
</a>
|
|
33
|
+
`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render() {
|
|
37
|
+
const hasCustomContent = !!this.links?.length
|
|
38
|
+
// Med skipGlobal rendres consent-lenken i sekundær-båndet i stedet
|
|
39
|
+
// for i den globale footeren.
|
|
40
|
+
const consentInSecondary = this.skipGlobal && this.includeConsent
|
|
41
|
+
return html`
|
|
42
|
+
<footer class="pkt-footer-simple">
|
|
43
|
+
${hasCustomContent || consentInSecondary
|
|
44
|
+
? html`
|
|
45
|
+
<div class="pkt-footer__band pkt-footer__band--secondary">
|
|
46
|
+
<div class="pkt-footer__inner pkt-footer__inner--compact">
|
|
47
|
+
<ul class="pkt-footer-simple__list">
|
|
48
|
+
${this.links?.map((link) => html`<li>${this.renderLink(link)}</li>`)}
|
|
49
|
+
${consentInSecondary ? html`<li>${this.renderConsent()}</li>` : nothing}
|
|
50
|
+
</ul>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
`
|
|
54
|
+
: nothing}
|
|
55
|
+
${this.renderGlobalFooter()}
|
|
56
|
+
</footer>
|
|
57
|
+
`
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
declare global {
|
|
62
|
+
interface HTMLElementTagNameMap {
|
|
63
|
+
'pkt-footer-simple': PktFooterSimple
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
customElement('pkt-footer-simple')(PktFooterSimple)
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.warn('Forsøker å definere <pkt-footer-simple>, men den er allerede definert')
|
|
71
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { axe, toHaveNoViolations } from 'jest-axe'
|
|
3
|
+
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import './footer'
|
|
6
|
+
import './footer-simple'
|
|
7
|
+
import { PktFooter } from './footer'
|
|
8
|
+
import { PktFooterSimple } from './footer-simple'
|
|
9
|
+
import { EXAMPLE_HEADER_FOOTER_DATA } from 'shared-utils/header-footer'
|
|
10
|
+
import type { IPktFooterColumn, IPktFooterLink } from 'shared-types'
|
|
11
|
+
|
|
12
|
+
expect.extend(toHaveNoViolations)
|
|
13
|
+
|
|
14
|
+
const COLUMNS: IPktFooterColumn[] = [
|
|
15
|
+
{
|
|
16
|
+
title: 'Overskrift 1',
|
|
17
|
+
text: 'Beskrivende tekst om løsningen.',
|
|
18
|
+
links: [{ text: 'Lenke 1', href: 'https://example.test/lenke-1' }],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
title: 'Overskrift 2',
|
|
22
|
+
links: [{ text: 'Lenke 2', href: 'https://example.test/lenke-2' }],
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const SIMPLE_LINKS: IPktFooterLink[] = [
|
|
27
|
+
{ text: 'Lenke 1', href: 'https://example.test/lenke-1' },
|
|
28
|
+
{ text: 'Lenke 2', href: 'https://example.test/lenke-2' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
document.body.innerHTML = ''
|
|
33
|
+
vi.restoreAllMocks()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const mount = async <T extends PktFooter | PktFooterSimple>(
|
|
37
|
+
tag: 'pkt-footer' | 'pkt-footer-simple',
|
|
38
|
+
props: Record<string, unknown> = {},
|
|
39
|
+
): Promise<T> => {
|
|
40
|
+
const el = document.createElement(tag) as T
|
|
41
|
+
el.data = EXAMPLE_HEADER_FOOTER_DATA
|
|
42
|
+
Object.assign(el, props)
|
|
43
|
+
document.body.appendChild(el)
|
|
44
|
+
await customElements.whenDefined(tag)
|
|
45
|
+
await el.updateComplete
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
47
|
+
await el.updateComplete
|
|
48
|
+
return el
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('PktFooter tilgjengelighet', () => {
|
|
52
|
+
test('kun global footer har ingen a11y-avvik', async () => {
|
|
53
|
+
const el = await mount('pkt-footer')
|
|
54
|
+
expect(await axe(el)).toHaveNoViolations()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('footer med custom kolonner har ingen a11y-avvik', async () => {
|
|
58
|
+
const el = await mount('pkt-footer', { columns: COLUMNS })
|
|
59
|
+
expect(await axe(el)).toHaveNoViolations()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('kompakt footer har ingen a11y-avvik', async () => {
|
|
63
|
+
const el = await mount('pkt-footer-simple', { links: SIMPLE_LINKS })
|
|
64
|
+
expect(await axe(el)).toHaveNoViolations()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import './footer'
|
|
5
|
+
import './footer-simple'
|
|
6
|
+
import { PktFooter } from './footer'
|
|
7
|
+
import { PktFooterSimple } from './footer-simple'
|
|
8
|
+
import { EXAMPLE_HEADER_FOOTER_DATA } from 'shared-utils/header-footer'
|
|
9
|
+
import type { IPktFooterColumn, IPktFooterLink } from 'shared-types'
|
|
10
|
+
|
|
11
|
+
const COLUMNS: IPktFooterColumn[] = [
|
|
12
|
+
{
|
|
13
|
+
title: 'Overskrift 1',
|
|
14
|
+
text: 'Her kan du skrive en lengre tekst som beskriver løsningen.',
|
|
15
|
+
links: [{ text: 'Lenke 1', href: 'https://example.test/lenke-1' }],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
title: 'Overskrift 2',
|
|
19
|
+
links: [
|
|
20
|
+
{ text: 'Lenke 2', href: 'https://example.test/lenke-2', external: true },
|
|
21
|
+
{ text: 'Lenke 3', href: 'https://example.test/lenke-3', openInNewTab: true },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const SIMPLE_LINKS: IPktFooterLink[] = [
|
|
27
|
+
{ text: 'Lenke 1', href: 'https://example.test/lenke-1' },
|
|
28
|
+
{ text: 'Lenke 2', href: 'https://example.test/lenke-2' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const mountFooter = async (props: Record<string, unknown> = {}): Promise<PktFooter> => {
|
|
32
|
+
const el = document.createElement('pkt-footer') as PktFooter
|
|
33
|
+
el.data = EXAMPLE_HEADER_FOOTER_DATA
|
|
34
|
+
Object.assign(el, props)
|
|
35
|
+
document.body.appendChild(el)
|
|
36
|
+
await customElements.whenDefined('pkt-footer')
|
|
37
|
+
await el.updateComplete
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
39
|
+
await el.updateComplete
|
|
40
|
+
return el
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const mountSimple = async (props: Record<string, unknown> = {}): Promise<PktFooterSimple> => {
|
|
44
|
+
const el = document.createElement('pkt-footer-simple') as PktFooterSimple
|
|
45
|
+
el.data = EXAMPLE_HEADER_FOOTER_DATA
|
|
46
|
+
Object.assign(el, props)
|
|
47
|
+
document.body.appendChild(el)
|
|
48
|
+
await customElements.whenDefined('pkt-footer-simple')
|
|
49
|
+
await el.updateComplete
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
51
|
+
await el.updateComplete
|
|
52
|
+
return el
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
document.body.innerHTML = ''
|
|
57
|
+
vi.restoreAllMocks()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('PktFooter global footer', () => {
|
|
61
|
+
test('rendrer kun global footer uten custom innhold', async () => {
|
|
62
|
+
const el = await mountFooter()
|
|
63
|
+
expect(el.querySelector('.pkt-footer__band--secondary')).toBeNull()
|
|
64
|
+
expect(el.querySelector('.pkt-footer__band--global')).not.toBeNull()
|
|
65
|
+
const titles = [
|
|
66
|
+
...el.querySelectorAll('.pkt-footer__band--global h2'),
|
|
67
|
+
].map((t) => t.textContent)
|
|
68
|
+
expect(titles).toEqual(['Kontakt', 'Jobb i Oslo kommune', 'Om nettstedet'])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('rendrer bunnrad med språklenke og some-ikoner', async () => {
|
|
72
|
+
const el = await mountFooter()
|
|
73
|
+
const bottom = el.querySelector('.pkt-footer__band--global .pkt-footer__bottom')
|
|
74
|
+
expect(bottom?.querySelector('a')?.textContent).toContain('English')
|
|
75
|
+
const someLinks = [...el.querySelectorAll('.pkt-footer__band--global .pkt-footer__some-link')]
|
|
76
|
+
expect(someLinks).toHaveLength(3)
|
|
77
|
+
expect(someLinks[0].getAttribute('aria-label')).toBe('Facebook')
|
|
78
|
+
expect(someLinks[0].querySelector('pkt-icon')?.getAttribute('name')).toBe('facebook')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('merker eksterne lenker med toppdomene i parentes', async () => {
|
|
82
|
+
const el = await mountFooter()
|
|
83
|
+
const jobLink = [...el.querySelectorAll('.pkt-footer__band--global a')].find(
|
|
84
|
+
(link) => link.textContent?.includes('Ledige stillinger'),
|
|
85
|
+
)
|
|
86
|
+
expect(jobLink?.querySelector('.pkt-footer__link-domain')?.textContent).toBe('(webcruiter.com)')
|
|
87
|
+
const internalLink = [
|
|
88
|
+
...el.querySelectorAll('.pkt-footer__band--global a'),
|
|
89
|
+
].find((link) => link.textContent?.includes('Tilgjengelighet'))
|
|
90
|
+
expect(internalLink?.querySelector('.pkt-footer__link-domain')).toBeNull()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('skjuler #cb-trigger-lenken uten includeConsent', async () => {
|
|
94
|
+
const el = await mountFooter()
|
|
95
|
+
expect(el.textContent).not.toContain('Innstillinger for informasjonskapsler')
|
|
96
|
+
expect(el.querySelector('pkt-consent')).toBeNull()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('erstatter #cb-trigger med pkt-consent når includeConsent er satt', async () => {
|
|
100
|
+
const el = await mountFooter({ includeConsent: true })
|
|
101
|
+
const consent = el.querySelector('pkt-consent')
|
|
102
|
+
expect(consent).not.toBeNull()
|
|
103
|
+
expect(consent?.closest('.pkt-footer__band--global')).not.toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('bruker valgt locale', async () => {
|
|
107
|
+
const el = await mountFooter({ locale: 'en-GB' })
|
|
108
|
+
const titles = [
|
|
109
|
+
...el.querySelectorAll('.pkt-footer__band--global h2'),
|
|
110
|
+
].map((t) => t.textContent)
|
|
111
|
+
expect(titles).toContain('Contact us')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('skipGlobal dropper global footer og henter ikke data', async () => {
|
|
115
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('should not run'))
|
|
116
|
+
const el = document.createElement('pkt-footer') as PktFooter
|
|
117
|
+
el.skipGlobal = true
|
|
118
|
+
el.columns = COLUMNS
|
|
119
|
+
document.body.appendChild(el)
|
|
120
|
+
await customElements.whenDefined('pkt-footer')
|
|
121
|
+
await el.updateComplete
|
|
122
|
+
expect(el.querySelector('.pkt-footer__band--global')).toBeNull()
|
|
123
|
+
expect(el.querySelector('.pkt-footer__band--secondary')).not.toBeNull()
|
|
124
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('PktFooter henting av data', () => {
|
|
129
|
+
test('henter data fra endepunkt når data ikke er satt', async () => {
|
|
130
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
131
|
+
new Response(JSON.stringify(EXAMPLE_HEADER_FOOTER_DATA), {
|
|
132
|
+
status: 200,
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
const el = document.createElement('pkt-footer') as PktFooter
|
|
137
|
+
document.body.appendChild(el)
|
|
138
|
+
await customElements.whenDefined('pkt-footer')
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
140
|
+
await el.updateComplete
|
|
141
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
142
|
+
'https://cdn.web.oslo.kommune.no/header-footer/header-footer.json',
|
|
143
|
+
)
|
|
144
|
+
expect(el.querySelector('.pkt-footer__band--global')).not.toBeNull()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('rendrer ingenting og emitter data-error når hentingen feiler', async () => {
|
|
148
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
149
|
+
new Response('boom', { status: 500 }) as unknown as Response,
|
|
150
|
+
)
|
|
151
|
+
const el = document.createElement('pkt-footer') as PktFooter
|
|
152
|
+
const events: CustomEvent[] = []
|
|
153
|
+
el.addEventListener('data-error', (e) => events.push(e as CustomEvent))
|
|
154
|
+
document.body.appendChild(el)
|
|
155
|
+
await customElements.whenDefined('pkt-footer')
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
157
|
+
await el.updateComplete
|
|
158
|
+
expect(events).toHaveLength(1)
|
|
159
|
+
expect(el.querySelector('.pkt-footer__band--global')).toBeNull()
|
|
160
|
+
expect(el.querySelector('footer')?.textContent?.trim()).toBe('')
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('PktFooter sekundær footer (custom innhold)', () => {
|
|
165
|
+
test('rendrer kolonner med tittel, tekst og lenker', async () => {
|
|
166
|
+
const el = await mountFooter({ columns: COLUMNS })
|
|
167
|
+
const columns = [...el.querySelectorAll('.pkt-footer__band--secondary section')]
|
|
168
|
+
expect(columns).toHaveLength(2)
|
|
169
|
+
expect(columns[0].querySelector('h2')?.textContent).toBe('Overskrift 1')
|
|
170
|
+
expect(columns[0].querySelector('p')?.textContent).toContain(
|
|
171
|
+
'lengre tekst',
|
|
172
|
+
)
|
|
173
|
+
expect(columns[1].querySelector('p')).toBeNull()
|
|
174
|
+
const link = columns[0].querySelector<HTMLAnchorElement>('a')
|
|
175
|
+
expect(link?.getAttribute('href')).toBe('https://example.test/lenke-1')
|
|
176
|
+
expect(link?.getAttribute('target')).toBe('_self')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('eksterne og nye-fane-lenker får riktige attributter', async () => {
|
|
180
|
+
const el = await mountFooter({ columns: COLUMNS })
|
|
181
|
+
const links = [
|
|
182
|
+
...el.querySelectorAll<HTMLAnchorElement>('.pkt-footer__band--secondary a'),
|
|
183
|
+
]
|
|
184
|
+
// external-flagget viser toppdomenet i parentes bak lenketeksten
|
|
185
|
+
const external = links.find((l) => l.textContent?.includes('Lenke 2'))
|
|
186
|
+
expect(external?.querySelector('.pkt-footer__link-domain')?.textContent).toBe('(example.test)')
|
|
187
|
+
const newTab = links.find((l) => l.textContent?.includes('Lenke 3'))
|
|
188
|
+
expect(newTab?.querySelector('.pkt-footer__link-domain')).toBeNull()
|
|
189
|
+
expect(newTab?.getAttribute('target')).toBe('_blank')
|
|
190
|
+
expect(newTab?.getAttribute('rel')).toBe('noopener noreferrer')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('openLinksInNewTab åpner alle custom lenker i ny fane', async () => {
|
|
194
|
+
const el = await mountFooter({ columns: COLUMNS, openLinksInNewTab: true })
|
|
195
|
+
const links = [
|
|
196
|
+
...el.querySelectorAll<HTMLAnchorElement>('.pkt-footer__band--secondary a'),
|
|
197
|
+
]
|
|
198
|
+
expect(links.every((l) => l.getAttribute('target') === '_blank')).toBe(true)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('global footer rendres under sekundær footer', async () => {
|
|
202
|
+
const el = await mountFooter({ columns: COLUMNS })
|
|
203
|
+
const footer = el.querySelector('footer.pkt-footer')
|
|
204
|
+
const children = [...(footer?.children ?? [])].map((c) => c.className)
|
|
205
|
+
expect(children[0]).toContain('pkt-footer__band--secondary')
|
|
206
|
+
expect(children[1]).toContain('pkt-footer__band--global')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('consent rendres i sekundær footer ved skipGlobal + includeConsent', async () => {
|
|
210
|
+
const el = await mountFooter({ columns: COLUMNS, skipGlobal: true, includeConsent: true })
|
|
211
|
+
const consent = el.querySelector('pkt-consent')
|
|
212
|
+
expect(consent).not.toBeNull()
|
|
213
|
+
expect(consent?.closest('.pkt-footer__band--secondary')).not.toBeNull()
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('PktFooterSimple', () => {
|
|
218
|
+
test('rendrer kompakt lenkerad med pil-ikoner + global footer', async () => {
|
|
219
|
+
const el = await mountSimple({ links: SIMPLE_LINKS })
|
|
220
|
+
const links = [...el.querySelectorAll<HTMLAnchorElement>('.pkt-footer-simple__link')]
|
|
221
|
+
expect(links).toHaveLength(2)
|
|
222
|
+
expect(links[0].querySelector('pkt-icon')?.getAttribute('name')).toBe('arrow-link')
|
|
223
|
+
expect(el.querySelector('.pkt-footer__band--global')).not.toBeNull()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('rendrer kun global footer uten links', async () => {
|
|
227
|
+
const el = await mountSimple()
|
|
228
|
+
expect(el.querySelector('.pkt-footer-simple .pkt-footer__band--secondary')).toBeNull()
|
|
229
|
+
expect(el.querySelector('.pkt-footer__band--global')).not.toBeNull()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('consent rendres som listepunkt ved skipGlobal + includeConsent', async () => {
|
|
233
|
+
const el = await mountSimple({ links: SIMPLE_LINKS, skipGlobal: true, includeConsent: true })
|
|
234
|
+
expect(el.querySelector('.pkt-footer__band--global')).toBeNull()
|
|
235
|
+
const consent = el.querySelector('pkt-consent')
|
|
236
|
+
expect(consent?.closest('.pkt-footer-simple__list')).not.toBeNull()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { html, nothing, type TemplateResult } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
import type { IPktFooterColumn } from 'shared-types'
|
|
4
|
+
import { PktFooterBaseElement } from './footer-base'
|
|
5
|
+
import type { IPktFooter } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PktFooter — footer med valgfritt custom innhold (grått sekundær-bånd)
|
|
9
|
+
* stablet oppå den globale (blå) footeren. Uten `columns` rendres kun den
|
|
10
|
+
* globale footeren. Den globale henter innhold fra Oslo kommunes felles
|
|
11
|
+
* header/footer-endepunkt (samme som `pkt-header type="global"`).
|
|
12
|
+
*/
|
|
13
|
+
export class PktFooter extends PktFooterBaseElement<IPktFooter> implements IPktFooter {
|
|
14
|
+
@property({ type: Array }) columns?: IPktFooterColumn[]
|
|
15
|
+
|
|
16
|
+
connectedCallback() {
|
|
17
|
+
super.connectedCallback()
|
|
18
|
+
this.classList.add('pkt-footer-host')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private renderColumn(column: IPktFooterColumn): TemplateResult {
|
|
22
|
+
return html`
|
|
23
|
+
<section>
|
|
24
|
+
<h2>${column.title}</h2>
|
|
25
|
+
${column.text ? html`<p>${column.text}</p>` : nothing}
|
|
26
|
+
${column.links?.length
|
|
27
|
+
? html`
|
|
28
|
+
<ul class="pkt-footer__link-list">
|
|
29
|
+
${column.links.map((link) => html`<li>${this.renderCustomLink(link)}</li>`)}
|
|
30
|
+
</ul>
|
|
31
|
+
`
|
|
32
|
+
: nothing}
|
|
33
|
+
</section>
|
|
34
|
+
`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private renderSecondary(): TemplateResult {
|
|
38
|
+
// Med skipGlobal rendres consent-lenken i sekundær-båndet i stedet
|
|
39
|
+
// for i den globale footeren.
|
|
40
|
+
const consentInSecondary = this.skipGlobal && this.includeConsent
|
|
41
|
+
return html`
|
|
42
|
+
<div class="pkt-footer__band pkt-footer__band--secondary">
|
|
43
|
+
<div class="pkt-footer__inner">
|
|
44
|
+
${this.columns?.length
|
|
45
|
+
? html`
|
|
46
|
+
<div class="pkt-footer__sections">
|
|
47
|
+
${this.columns.map((column) => this.renderColumn(column))}
|
|
48
|
+
</div>
|
|
49
|
+
`
|
|
50
|
+
: nothing}
|
|
51
|
+
${consentInSecondary
|
|
52
|
+
? html`<div class="pkt-footer__consent">${this.renderConsent()}</div>`
|
|
53
|
+
: nothing}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render() {
|
|
60
|
+
const hasCustomContent = !!this.columns?.length
|
|
61
|
+
const consentInSecondary = this.skipGlobal && this.includeConsent
|
|
62
|
+
return html`
|
|
63
|
+
<footer class="pkt-footer">
|
|
64
|
+
${hasCustomContent || consentInSecondary ? this.renderSecondary() : nothing}
|
|
65
|
+
${this.renderGlobalFooter()}
|
|
66
|
+
</footer>
|
|
67
|
+
`
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare global {
|
|
72
|
+
interface HTMLElementTagNameMap {
|
|
73
|
+
'pkt-footer': PktFooter
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
customElement('pkt-footer')(PktFooter)
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.warn('Forsøker å definere <pkt-footer>, men den er allerede definert')
|
|
81
|
+
}
|