@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.
Files changed (240) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/accordion-CbXDxOho.cjs +47 -0
  3. package/dist/{accordion-CBdIk9gw.js → accordion-DEUHQ4JD.js} +45 -35
  4. package/dist/{alert-HHTGY0sp.js → alert-CbtVGrHD.js} +31 -17
  5. package/dist/{alert-B8dLsiTz.cjs → alert-CkUnbPdA.cjs} +5 -5
  6. package/dist/base-elements/input-element.d.ts +1 -0
  7. package/dist/button-Cod17rKo.js +115 -0
  8. package/dist/{button-B5tQpKeG.cjs → button-gvDXtBMC.cjs} +5 -2
  9. package/dist/{calendar-DIzGBdbh.cjs → calendar-BVXW8DqC.cjs} +1 -1
  10. package/dist/{calendar-CjlUA-Lb.js → calendar-CjC4qlh6.js} +1 -1
  11. package/dist/{checkbox-Bt_sC_90.cjs → checkbox-8uxbeU30.cjs} +1 -1
  12. package/dist/{checkbox-D03BC2Va.js → checkbox-DJDphhqz.js} +1 -1
  13. package/dist/{combobox-CoQBTWqL.js → combobox-Du5yr-xM.js} +16 -2
  14. package/dist/combobox-hKrbYLUe.cjs +136 -0
  15. package/dist/components/accordion/accordion-context.d.ts +7 -0
  16. package/dist/components/accordion/accordion.d.ts +2 -0
  17. package/dist/components/accordion/accordionitem.d.ts +1 -0
  18. package/dist/components/alert/alert.d.ts +5 -0
  19. package/dist/components/alert/alert.test.d.ts +1 -0
  20. package/dist/components/button/button.d.ts +4 -1
  21. package/dist/components/datepicker/datepicker-base.d.ts +2 -0
  22. package/dist/components/datepicker/datepicker-utils.d.ts +2 -2
  23. package/dist/components/footer/footer-base.d.ts +44 -0
  24. package/dist/components/footer/footer-simple.d.ts +20 -0
  25. package/dist/components/footer/footer.accessibility.test.d.ts +0 -0
  26. package/dist/components/footer/footer.d.ts +22 -0
  27. package/dist/components/footer/footer.test.d.ts +0 -0
  28. package/dist/components/footer/index.d.ts +3 -0
  29. package/dist/components/footer/types.d.ts +16 -0
  30. package/dist/components/header/header-chrome-controller.d.ts +44 -0
  31. package/dist/components/header/header-global.accessibility.test.d.ts +0 -0
  32. package/dist/components/header/header-global.d.ts +78 -0
  33. package/dist/components/header/header-global.test.d.ts +0 -0
  34. package/dist/components/header/header-service.accessibility.test.d.ts +0 -0
  35. package/dist/components/header/header-service.d.ts +6 -21
  36. package/dist/components/header/header-service.test.d.ts +1 -1
  37. package/dist/components/header/header.d.ts +11 -3
  38. package/dist/components/header/header.test.d.ts +0 -0
  39. package/dist/components/header/index.d.ts +2 -1
  40. package/dist/components/header/types.d.ts +28 -2
  41. package/dist/components/header-menu/header-menu.d.ts +4 -5
  42. package/dist/components/header-menu/types.d.ts +1 -1
  43. package/dist/components/index.d.ts +5 -3
  44. package/dist/components/input-wrapper/input-wrapper.d.ts +2 -0
  45. package/dist/components/input-wrapper/input-wrapper.test.d.ts +1 -0
  46. package/dist/components/searchinput/searchinput.d.ts +5 -0
  47. package/dist/components/select/index.d.ts +1 -1
  48. package/dist/components/select/select.d.ts +2 -2
  49. package/dist/components/select/select.test.d.ts +1 -0
  50. package/dist/components/textarea/textarea.test.d.ts +2 -0
  51. package/dist/components/textinput/textinput.test.d.ts +1 -0
  52. package/dist/components/timepicker/timepicker.d.ts +2 -0
  53. package/dist/components/timepicker/timepicker.test.d.ts +3 -0
  54. package/dist/consent--Nw43WFU.cjs +18 -0
  55. package/dist/{consent-D5sY6JK3.js → consent-B2hSTCGC.js} +14 -7
  56. package/dist/consume-BiOhrhEy.js +167 -0
  57. package/dist/consume-DKf5SmP9.cjs +1 -0
  58. package/dist/{converters-BKGBRBpS.js → converters-BBfsEJV5.js} +25 -25
  59. package/dist/{converters-9jHlXDyy.cjs → converters-Cd0fPEly.cjs} +1 -1
  60. package/dist/{datepicker-BE5Ll0W4.js → datepicker--Fq8q-J3.js} +30 -12
  61. package/dist/{datepicker-x4xM3KQz.cjs → datepicker-CEoBYTBU.cjs} +11 -7
  62. package/dist/docs/components/alert.d.ts +3 -0
  63. package/dist/docs/components/footer.d.ts +9 -0
  64. package/dist/docs/components/form-sizes.d.ts +10 -0
  65. package/dist/docs/components/header-global-full.d.ts +20 -0
  66. package/dist/docs/components/header-global.d.ts +14 -0
  67. package/dist/docs/components/searchinput.d.ts +6 -3
  68. package/dist/fetch-header-footer-data-Pc32etrk.js +88 -0
  69. package/dist/fetch-header-footer-data-xohWFmYC.cjs +1 -0
  70. package/dist/fileupload-CGKUJrlj.cjs +401 -0
  71. package/dist/{fileupload-2hf8YNhO.js → fileupload-D4gQN64T.js} +156 -176
  72. package/dist/focus-trap-DG3pEuyJ.js +60 -0
  73. package/dist/focus-trap-DSv4Ifqm.cjs +1 -0
  74. package/dist/footer-BIrJeRuZ.js +303 -0
  75. package/dist/footer-Ber-jqM1.cjs +114 -0
  76. package/dist/footer.d.ts +1 -0
  77. package/dist/header-BFk1an4z.cjs +465 -0
  78. package/dist/header-Deg_LGJo.js +1287 -0
  79. package/dist/header-menu-BHqK4QA6.cjs +126 -0
  80. package/dist/{header-menu-C8b5vd3n.js → header-menu-GFtJKM4j.js} +25 -97
  81. package/dist/{helptext-OfqH5npD.js → helptext-C_brWCIT.js} +1 -1
  82. package/dist/{helptext-eckq4JEX.cjs → helptext-Cq5rNaAC.cjs} +1 -1
  83. package/dist/{input-element-ZGOWsvKM.cjs → input-element-B0qLbEdF.cjs} +1 -1
  84. package/dist/{input-element-BFHpeoV8.js → input-element-C0RnXi-J.js} +5 -2
  85. package/dist/{input-wrapper-DwwE0JcO.js → input-wrapper-BweP6t3O.js} +12 -0
  86. package/dist/{input-wrapper-BRjQB4zM.cjs → input-wrapper-DPkkiG7S.cjs} +13 -11
  87. package/dist/{input-wrapper-DaA98B5l.js → input-wrapper-fWV5P4MN.js} +22 -15
  88. package/dist/{input-wrapper-LcZjpt0L.cjs → input-wrapper-wh3fWIhk.cjs} +1 -1
  89. package/dist/modal-CPnFteYS.cjs +35 -0
  90. package/dist/{modal-C6pScpdG.js → modal-xNK_wpHk.js} +11 -20
  91. package/dist/pkt-accordion.cjs +1 -1
  92. package/dist/pkt-accordion.js +1 -1
  93. package/dist/pkt-alert.cjs +1 -1
  94. package/dist/pkt-alert.js +1 -1
  95. package/dist/pkt-button.cjs +1 -1
  96. package/dist/pkt-button.js +1 -1
  97. package/dist/pkt-calendar.cjs +1 -1
  98. package/dist/pkt-calendar.js +1 -1
  99. package/dist/pkt-checkbox.cjs +1 -1
  100. package/dist/pkt-checkbox.js +1 -1
  101. package/dist/pkt-combobox.cjs +1 -1
  102. package/dist/pkt-combobox.js +1 -1
  103. package/dist/pkt-consent.cjs +1 -1
  104. package/dist/pkt-consent.js +1 -1
  105. package/dist/pkt-datepicker.cjs +1 -1
  106. package/dist/pkt-datepicker.js +1 -1
  107. package/dist/pkt-fileupload.cjs +1 -1
  108. package/dist/pkt-fileupload.js +1 -1
  109. package/dist/pkt-footer.cjs +1 -0
  110. package/dist/pkt-footer.js +2 -0
  111. package/dist/pkt-header-menu.cjs +1 -1
  112. package/dist/pkt-header-menu.js +1 -1
  113. package/dist/pkt-header.cjs +1 -1
  114. package/dist/pkt-header.js +2 -2
  115. package/dist/pkt-helptext.cjs +1 -1
  116. package/dist/pkt-helptext.js +1 -1
  117. package/dist/pkt-index.cjs +1 -1
  118. package/dist/pkt-index.js +27 -26
  119. package/dist/pkt-input-wrapper.cjs +1 -1
  120. package/dist/pkt-input-wrapper.js +1 -1
  121. package/dist/pkt-modal.cjs +1 -1
  122. package/dist/pkt-modal.js +1 -1
  123. package/dist/pkt-radiobutton.cjs +1 -1
  124. package/dist/pkt-radiobutton.js +1 -1
  125. package/dist/pkt-searchinput.cjs +1 -1
  126. package/dist/pkt-searchinput.js +1 -1
  127. package/dist/pkt-select.cjs +1 -1
  128. package/dist/pkt-select.js +1 -1
  129. package/dist/pkt-tabs.cjs +1 -1
  130. package/dist/pkt-tabs.js +1 -1
  131. package/dist/pkt-textarea.cjs +1 -1
  132. package/dist/pkt-textarea.js +1 -1
  133. package/dist/pkt-textinput.cjs +1 -1
  134. package/dist/pkt-textinput.js +1 -1
  135. package/dist/pkt-timepicker.cjs +1 -1
  136. package/dist/pkt-timepicker.js +1 -1
  137. package/dist/{radiobutton-DGO6H2Ki.js → radiobutton-C6htwDrT.js} +1 -1
  138. package/dist/{radiobutton-DY1WWepJ.cjs → radiobutton-DjWBoSHv.cjs} +1 -1
  139. package/dist/scroll-lock-BN8lOhV5.cjs +1 -0
  140. package/dist/scroll-lock-DAbNKAUy.js +18 -0
  141. package/dist/searchinput-CfUVfpqL.js +156 -0
  142. package/dist/searchinput-DGjeN1YI.cjs +60 -0
  143. package/dist/{select-D4_HQFkh.cjs → select-CZvAW7W2.cjs} +2 -1
  144. package/dist/{select-BwHUIlwQ.js → select-CsBXXbLP.js} +4 -3
  145. package/dist/setupTests.d.ts +0 -0
  146. package/dist/shared-types/footer.d.ts +95 -0
  147. package/dist/shared-types/{header-menu.d.ts → header-footer.d.ts} +51 -4
  148. package/dist/shared-types/header.d.ts +1 -1
  149. package/dist/shared-types/index.d.ts +6 -4
  150. package/dist/shared-types/searchinput.d.ts +2 -0
  151. package/dist/shared-types/select.d.ts +3 -2
  152. package/dist/shared-types/size.d.ts +4 -0
  153. package/dist/shared-types/timepicker.d.ts +0 -6
  154. package/dist/shared-utils/datepicker-utils.d.ts +5 -4
  155. package/dist/shared-utils/fileupload/focus-trap.d.ts +4 -10
  156. package/dist/shared-utils/focus-trap/index.d.ts +22 -0
  157. package/dist/shared-utils/{header-menu → header-footer}/example-data.d.ts +5 -4
  158. package/dist/shared-utils/header-footer/external-domain.d.ts +26 -0
  159. package/dist/shared-utils/{header-menu → header-footer}/fetch-header-footer-data.d.ts +6 -2
  160. package/dist/shared-utils/{header-menu → header-footer}/index.d.ts +2 -1
  161. package/dist/shared-utils/{header-menu → header-footer}/select-megamenu.d.ts +1 -1
  162. package/dist/shared-utils/scroll-lock/index.d.ts +24 -0
  163. package/dist/tabs-BqrKLCjd.js +214 -0
  164. package/dist/tabs-q62MK_-r.cjs +40 -0
  165. package/dist/{textarea-D6M7ZhtA.js → textarea-Cm392CEI.js} +5 -3
  166. package/dist/{textarea-Bt_Zs8gq.cjs → textarea-sTcZxOIB.cjs} +3 -2
  167. package/dist/{textinput-EDKjcUdg.cjs → textinput-DqJbTglM.cjs} +3 -2
  168. package/dist/{textinput-oU2EvFsi.js → textinput-TSPcBcih.js} +5 -3
  169. package/dist/{timepicker-CgOL2sd9.cjs → timepicker-CqFYMV8q.cjs} +8 -7
  170. package/dist/{timepicker-_G0V_HV3.js → timepicker-DWbgTDKq.js} +10 -9
  171. package/package.json +3 -3
  172. package/src/components/accordion/accordion-context.ts +8 -0
  173. package/src/components/accordion/accordion.test.ts +44 -0
  174. package/src/components/accordion/accordion.ts +13 -3
  175. package/src/components/accordion/accordionitem.ts +9 -1
  176. package/src/components/alert/alert.test.ts +51 -2
  177. package/src/components/alert/alert.ts +11 -5
  178. package/src/components/breadcrumbs/breadcrumbs.test.ts +11 -3
  179. package/src/components/button/button.test.ts +23 -0
  180. package/src/components/button/button.ts +28 -3
  181. package/src/components/combobox/combobox.core.test.ts +20 -0
  182. package/src/components/combobox/combobox.ts +2 -0
  183. package/src/components/consent/consent.test.ts +6 -3
  184. package/src/components/consent/consent.ts +15 -7
  185. package/src/components/datepicker/datepicker-base.ts +5 -1
  186. package/src/components/datepicker/datepicker-utils.test.ts +10 -0
  187. package/src/components/datepicker/datepicker.core.test.ts +17 -0
  188. package/src/components/datepicker/datepicker.ts +8 -0
  189. package/src/components/footer/footer-base.ts +238 -0
  190. package/src/components/footer/footer-simple.ts +71 -0
  191. package/src/components/footer/footer.accessibility.test.ts +66 -0
  192. package/src/components/footer/footer.test.ts +238 -0
  193. package/src/components/footer/footer.ts +81 -0
  194. package/src/components/footer/index.ts +4 -0
  195. package/src/components/footer/types.ts +21 -0
  196. package/src/components/header/header-chrome-controller.ts +123 -0
  197. package/src/components/header/header-global.accessibility.test.ts +110 -0
  198. package/src/components/header/header-global.test.ts +329 -0
  199. package/src/components/header/header-global.ts +648 -0
  200. package/src/components/header/header-service.accessibility.test.ts +79 -0
  201. package/src/components/header/header-service.test.ts +32 -12
  202. package/src/components/header/header-service.ts +38 -162
  203. package/src/components/header/header-user-menu.ts +0 -1
  204. package/src/components/header/header.test.ts +84 -0
  205. package/src/components/header/header.ts +52 -5
  206. package/src/components/header/index.ts +6 -0
  207. package/src/components/header/types.ts +38 -0
  208. package/src/components/header-menu/header-menu.test.ts +131 -10
  209. package/src/components/header-menu/header-menu.ts +23 -24
  210. package/src/components/header-menu/types.ts +1 -1
  211. package/src/components/index.ts +18 -2
  212. package/src/components/input-wrapper/input-wrapper.test.ts +45 -0
  213. package/src/components/input-wrapper/input-wrapper.ts +17 -9
  214. package/src/components/modal/modal.ts +8 -9
  215. package/src/components/searchinput/searchinput.test.ts +43 -0
  216. package/src/components/searchinput/searchinput.ts +29 -12
  217. package/src/components/select/index.ts +1 -1
  218. package/src/components/select/select.test.ts +28 -0
  219. package/src/components/select/select.ts +4 -3
  220. package/src/components/textarea/textarea.test.ts +16 -0
  221. package/src/components/textarea/textarea.ts +3 -1
  222. package/src/components/textinput/textinput.test.ts +28 -0
  223. package/src/components/textinput/textinput.ts +3 -1
  224. package/src/components/timepicker/timepicker.test.ts +26 -0
  225. package/src/components/timepicker/timepicker.ts +12 -6
  226. package/dist/accordion-FISAiAbX.cjs +0 -47
  227. package/dist/button-C_YzTSuU.js +0 -96
  228. package/dist/combobox-D6H9rWPV.cjs +0 -135
  229. package/dist/consent-X3wvhteO.cjs +0 -10
  230. package/dist/fileupload-BLVrslUX.cjs +0 -401
  231. package/dist/header-CFrsaq0c.cjs +0 -310
  232. package/dist/header-D_xvPR4R.js +0 -801
  233. package/dist/header-menu-DMdP_c6X.cjs +0 -126
  234. package/dist/modal-DKU7PnjJ.cjs +0 -35
  235. package/dist/searchinput-CyO5k4Q4.js +0 -148
  236. package/dist/searchinput-DG6YetL1.cjs +0 -60
  237. package/dist/shared-utils/modal-scroll-lock.d.ts +0 -10
  238. package/dist/tabs-CGo0ODPa.cjs +0 -40
  239. package/dist/tabs-ChdR0D1C.js +0 -379
  240. /package/dist/shared-utils/{header-menu → header-footer}/icon-map.d.ts +0 -0
@@ -0,0 +1,648 @@
1
+ import { PktElement } from '@/base-elements/element'
2
+ import { html, nothing, type PropertyValues, type TemplateResult } from 'lit'
3
+ import { customElement, property, state } from 'lit/decorators.js'
4
+ import { classMap } from 'lit/directives/class-map.js'
5
+ import { ifDefined } from 'lit/directives/if-defined.js'
6
+ import { createRef, type Ref, ref } from 'lit/directives/ref.js'
7
+ import { fetchHeaderFooterData, selectLocaleData } from 'shared-utils/header-footer'
8
+ import { getDeepActiveElement, getFocusableElementsDeep } from 'shared-utils/focus-trap'
9
+ import { type IHeaderFooterLocaleData, DEFAULT_HEADER_LOGO_LINK } from 'shared-types'
10
+ import {
11
+ type Booleanish,
12
+ booleanishConverter,
13
+ type IPktHeaderGlobal,
14
+ type Representing,
15
+ type TGlobalHeaderMenu,
16
+ type THeaderFooterApi,
17
+ type THeaderMenuLocale,
18
+ type THeaderPosition,
19
+ type THeaderScrollBehavior,
20
+ type TLogOutButtonPlacement,
21
+ type User,
22
+ type UserMenuItem,
23
+ } from './types'
24
+ import { formatLastLoggedIn } from './header-utils'
25
+ import { HeaderChromeController } from './header-chrome-controller'
26
+
27
+ import '@/components/button'
28
+ import '@/components/icon'
29
+ import '@/components/searchinput'
30
+ import '@/components/header-menu'
31
+ import './header-user-menu'
32
+
33
+ const defaultLogoPath = 'https://punkt-cdn.oslo.kommune.no/latest/logos/'
34
+
35
+ if (typeof window !== 'undefined') {
36
+ window.pktLogoPath = window.pktLogoPath || defaultLogoPath
37
+ }
38
+
39
+ const MENU_ID = 'pkt-header-global-menu'
40
+ const MENU_TOGGLE_ID = 'pkt-header-global-menu-toggle'
41
+ const SEARCH_PANEL_ID = 'pkt-header-global-search-panel'
42
+ const SEARCH_TOGGLE_ID = 'pkt-header-global-search-toggle'
43
+
44
+ export class PktHeaderGlobal extends PktElement<IPktHeaderGlobal> implements IPktHeaderGlobal {
45
+ @property({ type: String, attribute: 'data-url' }) dataUrl?: string
46
+ @property({ type: Object, attribute: false }) data?: THeaderFooterApi
47
+ @property({ type: String }) locale: THeaderMenuLocale = 'nb-NO'
48
+ @property({ type: String, attribute: 'logo-link' }) logoLink: string = DEFAULT_HEADER_LOGO_LINK
49
+ @property({ type: String, attribute: 'logo-path' }) logoPath: string | undefined =
50
+ typeof window !== 'undefined' ? window.pktLogoPath : defaultLogoPath
51
+ @property({ type: String, attribute: 'search-placeholder' }) searchPlaceholder?: string
52
+ @property({ type: String }) position: THeaderPosition = 'fixed'
53
+ @property({ type: String, attribute: 'scroll-behavior' }) scrollBehavior: THeaderScrollBehavior =
54
+ 'hide'
55
+ @property({ type: Number, attribute: 'mobile-breakpoint' }) mobileBreakpoint = 768
56
+ @property({ type: Number, attribute: 'tablet-breakpoint' }) tabletBreakpoint = 1024
57
+ @property({ type: String, attribute: 'log-out-button-placement' })
58
+ logOutButtonPlacement: TLogOutButtonPlacement = 'none'
59
+
60
+ @property({ type: Boolean, attribute: 'show-search', converter: booleanishConverter })
61
+ showSearch: Booleanish = true
62
+ @property({ type: Boolean, attribute: 'show-contact', converter: booleanishConverter })
63
+ showContact: Booleanish = true
64
+ @property({
65
+ type: Boolean,
66
+ attribute: 'can-change-representation',
67
+ converter: booleanishConverter,
68
+ })
69
+ canChangeRepresentation: Booleanish = false
70
+ @property({ type: Boolean, attribute: 'has-log-out', converter: booleanishConverter })
71
+ hasLogOut: Booleanish = false
72
+
73
+ @property({ type: Object }) user?: User
74
+ @property({ type: Array, attribute: 'user-menu' }) userMenu?: UserMenuItem[]
75
+ @property({ type: Object }) representing?: Representing
76
+
77
+ @state() private openMenu: TGlobalHeaderMenu = 'none'
78
+ @state() private fetchedData?: THeaderFooterApi
79
+ @state() private loadError = false
80
+ @state() private headerAnchor = { top: 0, left: 0, width: 0 }
81
+
82
+ private headerRef: Ref<HTMLElement> = createRef()
83
+ private menuContainerRef: Ref<HTMLElement> = createRef()
84
+ private userContainerRef: Ref<HTMLElement> = createRef()
85
+ private abortController?: AbortController
86
+
87
+ private chrome = new HeaderChromeController(this, () => ({
88
+ mobileBreakpoint: this.mobileBreakpoint,
89
+ tabletBreakpoint: this.tabletBreakpoint,
90
+ position: this.position,
91
+ scrollBehavior: this.scrollBehavior,
92
+ }))
93
+
94
+ private get isMobile(): boolean {
95
+ return this.chrome.isMobile
96
+ }
97
+ private get isTablet(): boolean {
98
+ return this.chrome.isTablet
99
+ }
100
+ private get isHidden(): boolean {
101
+ return this.chrome.isHidden
102
+ }
103
+
104
+ connectedCallback() {
105
+ super.connectedCallback()
106
+ this.classList.add('pkt-header-global-host')
107
+ if (this.data) {
108
+ this.emitDataLoaded(this.data)
109
+ } else {
110
+ void this.loadData()
111
+ }
112
+ }
113
+
114
+ disconnectedCallback() {
115
+ super.disconnectedCallback()
116
+ this.abortController?.abort()
117
+ document.removeEventListener('mousedown', this.handleClickOutside)
118
+ document.removeEventListener('keydown', this.handleEscapeKey)
119
+ document.removeEventListener('keydown', this.handleOverlayKeydown)
120
+ document.removeEventListener('focusin', this.handleOverlayFocusIn)
121
+ }
122
+
123
+ firstUpdated() {
124
+ this.chrome.observe(this.headerRef.value)
125
+ }
126
+
127
+ willUpdate(changedProperties: PropertyValues) {
128
+ super.willUpdate(changedProperties)
129
+
130
+ if (this.openMenu === 'menu' || this.openMenu === 'search') {
131
+ const rect = this.headerRef.value?.getBoundingClientRect()
132
+ if (rect) {
133
+ this.headerAnchor = { top: rect.top, left: rect.left, width: rect.width }
134
+ }
135
+ }
136
+ }
137
+
138
+ private get overlayAnchorStyle(): string {
139
+ const { top, left, width } = this.headerAnchor
140
+ return `--pkt-header-global-top: ${top}px; --pkt-header-global-left: ${left}px; --pkt-header-global-width: ${width}px`
141
+ }
142
+
143
+ updated(changedProperties: PropertyValues) {
144
+ super.updated(changedProperties)
145
+
146
+ if (changedProperties.has('mobileBreakpoint') || changedProperties.has('tabletBreakpoint')) {
147
+ this.chrome.refreshBreakpoints()
148
+ }
149
+
150
+ if (
151
+ changedProperties.has('dataUrl') &&
152
+ changedProperties.get('dataUrl') !== undefined &&
153
+ !this.data
154
+ ) {
155
+ void this.loadData()
156
+ }
157
+
158
+ if (changedProperties.has('data') && this.data) {
159
+ this.emitDataLoaded(this.data)
160
+ }
161
+
162
+ if (changedProperties.has('openMenu')) {
163
+ const previous = changedProperties.get('openMenu') as TGlobalHeaderMenu | undefined
164
+ if (this.openMenu !== 'none' && (previous === 'none' || previous === undefined)) {
165
+ document.addEventListener('mousedown', this.handleClickOutside)
166
+ document.addEventListener('keydown', this.handleEscapeKey)
167
+ } else if (this.openMenu === 'none' && previous && previous !== 'none') {
168
+ document.removeEventListener('mousedown', this.handleClickOutside)
169
+ document.removeEventListener('keydown', this.handleEscapeKey)
170
+ this.chrome.restoreFocus()
171
+ }
172
+
173
+ const wasOverlay = previous === 'menu' || previous === 'search'
174
+ const isOverlay = this.openMenu === 'menu' || this.openMenu === 'search'
175
+ if (isOverlay && !wasOverlay) {
176
+ document.addEventListener('keydown', this.handleOverlayKeydown)
177
+ document.addEventListener('focusin', this.handleOverlayFocusIn)
178
+ } else if (!isOverlay && wasOverlay) {
179
+ document.removeEventListener('keydown', this.handleOverlayKeydown)
180
+ document.removeEventListener('focusin', this.handleOverlayFocusIn)
181
+ }
182
+ if (this.openMenu === 'search' && previous !== 'search') {
183
+ requestAnimationFrame(() => {
184
+ this.querySelector<HTMLInputElement>(`#${SEARCH_PANEL_ID} input`)?.focus()
185
+ })
186
+ }
187
+
188
+ this.chrome.pauseScrollHide = this.openMenu !== 'none'
189
+ this.chrome.setScrollLock(this.openMenu === 'menu' || this.openMenu === 'search')
190
+ }
191
+ }
192
+
193
+ private async loadData() {
194
+ this.abortController?.abort()
195
+ this.abortController = new AbortController()
196
+ this.loadError = false
197
+ try {
198
+ const data = await fetchHeaderFooterData<string>(this.dataUrl, this.abortController.signal)
199
+ this.fetchedData = data
200
+ this.emitDataLoaded(data)
201
+ } catch (error) {
202
+ if ((error as Error).name === 'AbortError') return
203
+ this.loadError = true
204
+ this.dispatchEvent(
205
+ new CustomEvent('data-error', { detail: { error }, bubbles: true, composed: true }),
206
+ )
207
+ }
208
+ }
209
+
210
+ private emitDataLoaded(data: THeaderFooterApi) {
211
+ this.dispatchEvent(
212
+ new CustomEvent('data-loaded', { detail: { data }, bubbles: true, composed: true }),
213
+ )
214
+ }
215
+
216
+ private get effectiveData(): THeaderFooterApi | undefined {
217
+ return this.data ?? this.fetchedData
218
+ }
219
+
220
+ private get localeData(): IHeaderFooterLocaleData | undefined {
221
+ return selectLocaleData(this.effectiveData, this.locale)
222
+ }
223
+
224
+ private get shouldHideOnScroll(): boolean {
225
+ return this.scrollBehavior === 'hide'
226
+ }
227
+
228
+ private get isFixed(): boolean {
229
+ return this.position === 'fixed'
230
+ }
231
+
232
+ private get isSticky(): boolean {
233
+ return this.position === 'sticky'
234
+ }
235
+
236
+ private toggleMenu(mode: TGlobalHeaderMenu) {
237
+ if (this.openMenu === mode) {
238
+ this.openMenu = 'none'
239
+ } else {
240
+ this.chrome.captureFocus()
241
+ this.openMenu = mode
242
+ }
243
+ }
244
+
245
+ private handleClickOutside = (event: MouseEvent) => {
246
+ const target = event.target as Element
247
+ if (
248
+ this.openMenu === 'user' &&
249
+ !target.closest('.pkt-header-global__user-container') &&
250
+ !target.closest('.pkt-header-global__user-menu')
251
+ ) {
252
+ this.openMenu = 'none'
253
+ }
254
+ if (
255
+ this.openMenu === 'menu' &&
256
+ !target.closest('.pkt-header-menu') &&
257
+ !target.closest(`#${MENU_TOGGLE_ID}`)
258
+ ) {
259
+ this.openMenu = 'none'
260
+ }
261
+ if (
262
+ this.openMenu === 'search' &&
263
+ !target.closest('.pkt-header-global__search-panel-inner') &&
264
+ !target.closest(`#${SEARCH_TOGGLE_ID}`)
265
+ ) {
266
+ this.openMenu = 'none'
267
+ }
268
+ }
269
+
270
+ private handleEscapeKey = (event: KeyboardEvent) => {
271
+ if (event.key === 'Escape' && this.openMenu !== 'none') {
272
+ event.preventDefault()
273
+ this.chrome.flagRestoreFocus()
274
+ this.openMenu = 'none'
275
+ }
276
+ }
277
+
278
+ private get activeOverlay(): { panel: HTMLElement | null; toggle: HTMLElement | null } | null {
279
+ if (this.openMenu === 'menu') {
280
+ return {
281
+ panel: this.menuContainerRef.value ?? null,
282
+ toggle: this.querySelector<HTMLElement>(`#${MENU_TOGGLE_ID}`),
283
+ }
284
+ }
285
+ if (this.openMenu === 'search') {
286
+ return {
287
+ panel: this.querySelector<HTMLElement>(`#${SEARCH_PANEL_ID}`),
288
+ toggle: this.querySelector<HTMLElement>(`#${SEARCH_TOGGLE_ID}`),
289
+ }
290
+ }
291
+ return null
292
+ }
293
+
294
+ private focusToggle(toggle: HTMLElement | null) {
295
+ ;(toggle?.querySelector<HTMLElement>('button') ?? toggle)?.focus()
296
+ }
297
+
298
+ private handleOverlayKeydown = (event: KeyboardEvent) => {
299
+ if (event.key !== 'Tab') return
300
+ const overlay = this.activeOverlay
301
+ if (!overlay || !overlay.panel || !overlay.toggle) return
302
+ const { panel, toggle } = overlay
303
+ const focusables = getFocusableElementsDeep(panel)
304
+ const active = getDeepActiveElement()
305
+
306
+ if (active && toggle.contains(active)) {
307
+ if (!event.shiftKey && focusables.length > 0) {
308
+ event.preventDefault()
309
+ focusables[0].focus()
310
+ }
311
+ return
312
+ }
313
+
314
+ if (focusables.length === 0) return
315
+ const first = focusables[0]
316
+ const last = focusables[focusables.length - 1]
317
+ const tabbingForwardOut = !event.shiftKey && active === last
318
+ const tabbingBackwardOut = event.shiftKey && active === first
319
+ if (tabbingForwardOut || tabbingBackwardOut) {
320
+ event.preventDefault()
321
+ this.openMenu = 'none'
322
+ this.focusToggle(toggle)
323
+ }
324
+ }
325
+
326
+ private handleOverlayFocusIn = (event: FocusEvent) => {
327
+ const overlay = this.activeOverlay
328
+ if (!overlay || !overlay.panel) return
329
+ const target = event.target as Node | null
330
+ if (!target) return
331
+ if (overlay.panel.contains(target) || overlay.toggle?.contains(target)) return
332
+ this.openMenu = 'none'
333
+ }
334
+
335
+ private handleSearch(query: string) {
336
+ this.dispatchEvent(
337
+ new CustomEvent('search', { detail: { query }, bubbles: true, composed: true }),
338
+ )
339
+ }
340
+
341
+ private handleLogout() {
342
+ this.dispatchEvent(new CustomEvent('log-out', { bubbles: true, composed: true }))
343
+ }
344
+
345
+ private handleChangeRepresentation() {
346
+ this.dispatchEvent(new CustomEvent('change-representation', { bubbles: true, composed: true }))
347
+ }
348
+
349
+ private get formattedLastLoggedIn(): string | undefined {
350
+ return formatLastLoggedIn(this.user?.lastLoggedIn)
351
+ }
352
+
353
+ private get showLogoutInUserMenu(): boolean {
354
+ return (
355
+ this.hasLogOut &&
356
+ (this.logOutButtonPlacement === 'userMenu' || this.logOutButtonPlacement === 'both')
357
+ )
358
+ }
359
+
360
+ private handleLogoClick(e: Event) {
361
+ this.dispatchEvent(
362
+ new CustomEvent('logo-click', {
363
+ bubbles: true,
364
+ composed: true,
365
+ detail: { originalEvent: e },
366
+ }),
367
+ )
368
+ }
369
+
370
+ private renderLogo() {
371
+ const logoIcon = html`
372
+ <pkt-icon name="oslologo" aria-hidden="true" path=${ifDefined(this.logoPath)}></pkt-icon>
373
+ `
374
+ return html`
375
+ <span class="pkt-header-global__logo">
376
+ <a href=${this.logoLink} aria-label="Til hovedsiden" @click=${this.handleLogoClick}>
377
+ ${logoIcon}
378
+ </a>
379
+ </span>
380
+ `
381
+ }
382
+
383
+ private renderSearch(localeData: IHeaderFooterLocaleData): TemplateResult | typeof nothing {
384
+ if (!this.showSearch) return nothing
385
+ const search = localeData.search
386
+ if (!search) return nothing
387
+ const placeholder = this.searchPlaceholder ?? search.input.placeholder
388
+ return html`
389
+ <div class="pkt-header-global__search">
390
+ <pkt-searchinput
391
+ appearance="global"
392
+ input-size=${this.isTablet ? 'small' : 'medium'}
393
+ fullwidth
394
+ id=${search.input.id || 'pkt-header-global-search'}
395
+ name=${ifDefined(search.input.name)}
396
+ placeholder=${ifDefined(placeholder)}
397
+ aria-label=${ifDefined(localeData.i18n?.search)}
398
+ action=${search.endpoint}
399
+ method="get"
400
+ @pkt-search=${(e: CustomEvent<{ value: string }>) => this.handleSearch(e.detail.value)}
401
+ ></pkt-searchinput>
402
+ </div>
403
+ `
404
+ }
405
+
406
+ private renderContact(localeData: IHeaderFooterLocaleData): TemplateResult | typeof nothing {
407
+ // Hide the contact link when the user menu is showing (a logged-in user).
408
+ if (!this.showContact || this.user) return nothing
409
+ const contact = localeData.header?.contact
410
+ const classes = classMap({
411
+ 'pkt-header-global__contact': true,
412
+ 'pkt-btn': true,
413
+ 'pkt-btn--secondary': true,
414
+ 'pkt-btn--label-only': true,
415
+ 'pkt-btn--small': this.isMobile || this.isTablet,
416
+ 'pkt-btn--medium': !this.isMobile && !this.isTablet,
417
+ })
418
+ if (!contact) return nothing
419
+ return html`
420
+ <a class=${classes} href=${contact.url}>
421
+ <span class="pkt-btn__text">${contact.text}</span>
422
+ </a>
423
+ `
424
+ }
425
+
426
+ private get isLoading(): boolean {
427
+ return !this.localeData && !this.loadError
428
+ }
429
+
430
+ private renderMenuToggle(localeData?: IHeaderFooterLocaleData) {
431
+ const menuText = localeData?.i18n?.menu || 'Meny'
432
+ const isOpen = this.openMenu === 'menu'
433
+ return html`
434
+ <pkt-button
435
+ id=${MENU_TOGGLE_ID}
436
+ class="pkt-header-global__menu-toggle"
437
+ skin=${this.isMobile ? 'tertiary' : 'secondary'}
438
+ variant=${this.isMobile ? 'icon-only' : 'icon-right'}
439
+ iconName=${isOpen ? 'close' : 'menu'}
440
+ size=${this.isTablet ? 'small' : 'medium'}
441
+ state=${isOpen ? 'active' : 'normal'}
442
+ aria-expanded=${isOpen}
443
+ aria-controls=${MENU_ID}
444
+ ?disabled=${this.isLoading}
445
+ @click=${() => this.toggleMenu('menu')}
446
+ >
447
+ ${menuText}
448
+ </pkt-button>
449
+ `
450
+ }
451
+
452
+ private renderSearchToggle(localeData: IHeaderFooterLocaleData): TemplateResult | typeof nothing {
453
+ if (!this.showSearch || !localeData.search) return nothing
454
+ const isOpen = this.openMenu === 'search'
455
+ return html`
456
+ <pkt-button
457
+ id=${SEARCH_TOGGLE_ID}
458
+ class="pkt-header-global__search-toggle"
459
+ skin="primary"
460
+ color=${ifDefined(isOpen ? undefined : 'yellow')}
461
+ variant="icon-only"
462
+ iconName=${isOpen ? 'close' : 'magnifying-glass-big'}
463
+ size=${this.isMobile ? 'small' : 'medium'}
464
+ state=${isOpen ? 'active' : 'normal'}
465
+ aria-expanded=${isOpen}
466
+ aria-controls=${SEARCH_PANEL_ID}
467
+ @click=${() => this.toggleMenu('search')}
468
+ >
469
+ ${localeData.i18n?.search || 'Søk'}
470
+ </pkt-button>
471
+ `
472
+ }
473
+
474
+ private renderSearchPanel(localeData: IHeaderFooterLocaleData): TemplateResult | typeof nothing {
475
+ const search = localeData.search
476
+ if (this.openMenu !== 'search' || !this.showSearch || !search) return nothing
477
+ const placeholder = this.searchPlaceholder ?? search.input.placeholder
478
+ const classes = classMap({
479
+ 'pkt-header-global__search-panel': true,
480
+ [`pkt-header-global__search-panel--${this.overlayBreakpointClass}`]:
481
+ !!this.overlayBreakpointClass,
482
+ })
483
+ return html`
484
+ <div class=${classes} id=${SEARCH_PANEL_ID} style=${this.overlayAnchorStyle}>
485
+ <div class="pkt-header-global__search-panel-inner">
486
+ <pkt-searchinput
487
+ appearance="global"
488
+ fullwidth
489
+ id=${search.input.id || 'pkt-header-global-search'}
490
+ name=${ifDefined(search.input.name)}
491
+ placeholder=${ifDefined(placeholder)}
492
+ aria-label=${ifDefined(localeData.i18n?.search || 'Søk')}
493
+ action=${search.endpoint}
494
+ method="get"
495
+ @pkt-search=${(e: CustomEvent<{ value: string }>) => this.handleSearch(e.detail.value)}
496
+ ></pkt-searchinput>
497
+ </div>
498
+ </div>
499
+ `
500
+ }
501
+
502
+ private renderUser() {
503
+ if (!this.user) return nothing
504
+ const isOpen = this.openMenu === 'user'
505
+ const userMenuClasses = classMap({
506
+ 'pkt-header-global__user-menu': true,
507
+ 'is-open': isOpen,
508
+ })
509
+ return html`
510
+ <div class="pkt-header-global__user">
511
+ <div
512
+ class="pkt-header-global__user-container ${isOpen ? 'is-open' : ''}"
513
+ ${ref(this.userContainerRef)}
514
+ >
515
+ <pkt-button
516
+ class="pkt-header-global__user-button"
517
+ skin="secondary"
518
+ size=${this.isTablet ? 'small' : 'medium'}
519
+ state=${isOpen ? 'active' : 'normal'}
520
+ variant="icons-right-and-left"
521
+ iconName="user"
522
+ secondIconName=${isOpen ? 'chevron-thin-up' : 'chevron-thin-down'}
523
+ @click=${() => this.toggleMenu('user')}
524
+ >
525
+ <span class="pkt-sr-only">Brukermeny: </span>
526
+ <span translate="no">${this.representing?.name || this.user.name}</span>
527
+ </pkt-button>
528
+ ${isOpen
529
+ ? html`
530
+ <div class=${userMenuClasses}>
531
+ <pkt-header-user-menu
532
+ .user=${this.user}
533
+ formatted-last-logged-in=${this.formattedLastLoggedIn || nothing}
534
+ .representing=${this.representing}
535
+ .userMenu=${this.userMenu}
536
+ ?can-change-representation=${this.canChangeRepresentation}
537
+ ?logout-on-click=${this.showLogoutInUserMenu}
538
+ @change-representation=${() => this.handleChangeRepresentation()}
539
+ @log-out=${() => this.handleLogout()}
540
+ ></pkt-header-user-menu>
541
+ </div>
542
+ `
543
+ : nothing}
544
+ </div>
545
+ </div>
546
+ `
547
+ }
548
+
549
+ private get overlayBreakpointClass(): string {
550
+ if (this.isMobile) return 'mobile'
551
+ if (this.isTablet) return 'tablet'
552
+ return ''
553
+ }
554
+
555
+ private renderMenu() {
556
+ if (this.openMenu !== 'menu') return nothing
557
+ const classes = classMap({
558
+ 'pkt-header-global__menu': true,
559
+ [`pkt-header-global__menu--${this.overlayBreakpointClass}`]: !!this.overlayBreakpointClass,
560
+ })
561
+ return html`
562
+ <div
563
+ class=${classes}
564
+ id=${MENU_ID}
565
+ style=${this.overlayAnchorStyle}
566
+ ${ref(this.menuContainerRef)}
567
+ >
568
+ ${this.localeData
569
+ ? html`
570
+ <pkt-header-menu
571
+ .data=${this.effectiveData}
572
+ locale=${this.locale}
573
+ open
574
+ mobile-breakpoint=${this.mobileBreakpoint}
575
+ ></pkt-header-menu>
576
+ `
577
+ : html`
578
+ <nav class="pkt-header-menu pkt-header-menu--open">
579
+ <p class="pkt-header-menu__error">Kunne ikke laste meny.</p>
580
+ </nav>
581
+ `}
582
+ </div>
583
+ `
584
+ }
585
+
586
+ private renderHeader() {
587
+ const headerClasses = classMap({
588
+ 'pkt-header-global': true,
589
+ 'pkt-header-global--mobile': this.isMobile,
590
+ 'pkt-header-global--tablet': this.isTablet,
591
+ 'pkt-header-global--fixed': this.isFixed,
592
+ 'pkt-header-global--sticky': this.isSticky,
593
+ 'pkt-header-global--scroll-to-hide': this.shouldHideOnScroll,
594
+ 'pkt-header-global--hidden': this.isHidden,
595
+ })
596
+
597
+ const localeData = this.localeData
598
+ const compact = this.isMobile
599
+
600
+ return html`
601
+ <header class=${headerClasses} ${ref(this.headerRef)}>
602
+ <div class="pkt-header-global__logo-area">${this.renderLogo()}</div>
603
+ <div class="pkt-header-global__content">
604
+ ${!compact && localeData ? this.renderSearch(localeData) : nothing}
605
+ ${compact && localeData ? this.renderSearchToggle(localeData) : nothing}
606
+ ${this.renderMenuToggle(localeData)}
607
+ ${!compact && localeData ? this.renderContact(localeData) : nothing}
608
+ </div>
609
+ ${this.renderUser()}
610
+ </header>
611
+ `
612
+ }
613
+
614
+ render() {
615
+ const headerElement = this.renderHeader()
616
+ const localeData = this.localeData
617
+ const menu = this.renderMenu()
618
+ const searchPanel = localeData ? this.renderSearchPanel(localeData) : nothing
619
+ if (this.isFixed) {
620
+ const spacerClasses = classMap({
621
+ 'pkt-header-global-spacer': true,
622
+ 'pkt-header-global-spacer--mobile': this.isMobile,
623
+ 'pkt-header-global-spacer--tablet': this.isTablet && !this.isMobile,
624
+ 'pkt-header-global-spacer--has-user': !!this.user,
625
+ })
626
+ return html`
627
+ <div class="pkt-header-global-wrapper">
628
+ ${headerElement}
629
+ <div class=${spacerClasses}></div>
630
+ ${searchPanel}${menu}
631
+ </div>
632
+ `
633
+ }
634
+ return html`${headerElement}${searchPanel}${menu}`
635
+ }
636
+ }
637
+
638
+ declare global {
639
+ interface HTMLElementTagNameMap {
640
+ 'pkt-header-global': PktHeaderGlobal
641
+ }
642
+ }
643
+
644
+ try {
645
+ customElement('pkt-header-global')(PktHeaderGlobal)
646
+ } catch (e) {
647
+ console.warn('Forsøker å definere <pkt-header-global>, men den er allerede definert')
648
+ }