@oslokommune/punkt-elements 12.31.2 → 12.32.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.
Files changed (159) hide show
  1. package/dist/{alert-BDxxRqgi.cjs → alert-D5G5UJuo.cjs} +1 -1
  2. package/dist/{alert-D0S57u0r.js → alert-gERpVuB7.js} +6 -6
  3. package/dist/alert.d.ts +32 -0
  4. package/dist/{backlink-CzpB-ih9.js → backlink-DdNgqA56.js} +2 -2
  5. package/dist/{backlink-Dn4DfWVR.cjs → backlink-Q2HTG9jm.cjs} +1 -1
  6. package/dist/backlink.d.ts +32 -0
  7. package/dist/{button-Beo3c7cx.cjs → button-BPyZeW73.cjs} +1 -1
  8. package/dist/{button-9NwGr-OS.js → button-rArIL0-j.js} +3 -3
  9. package/dist/button.d.ts +32 -0
  10. package/dist/{calendar-BbZNxsKY.js → calendar-1ryAEdX3.js} +4 -4
  11. package/dist/{calendar-CxBo98iI.cjs → calendar-2DqPLXdD.cjs} +1 -1
  12. package/dist/calendar.d.ts +32 -0
  13. package/dist/{card-DQfNKnKl.js → card-BbMBpvJt.js} +7 -7
  14. package/dist/{card-C63x_nll.cjs → card-ZX09f_ka.cjs} +1 -1
  15. package/dist/card.d.ts +32 -0
  16. package/dist/{checkbox-9Zjy_NU7.cjs → checkbox-BnDG6wIO.cjs} +1 -1
  17. package/dist/{checkbox-CzDpR6_8.js → checkbox-Ceui2TLp.js} +4 -4
  18. package/dist/checkbox.d.ts +32 -0
  19. package/dist/{class-map-DWtqmIRS.js → class-map-ChuDQU5C.js} +1 -1
  20. package/dist/{class-map-Dj5mbCUg.cjs → class-map-D4rXyUxT.cjs} +1 -1
  21. package/dist/combobox-DH-YlbNh.cjs +115 -0
  22. package/dist/combobox-DbO6I0GT.js +694 -0
  23. package/dist/combobox.d.ts +47 -0
  24. package/dist/{datepicker-CYUvRGhE.js → datepicker-8MOgQsyL.js} +144 -196
  25. package/dist/datepicker-BRH-59Q9.cjs +153 -0
  26. package/dist/datepicker.d.ts +32 -0
  27. package/dist/directive-helpers-D7XIyCQ_.js +45 -0
  28. package/dist/directive-helpers-mGjAtADc.cjs +5 -0
  29. package/dist/{element-CzFXQBoS.cjs → element-BBo3JZk5.cjs} +1 -1
  30. package/dist/{element-C7XjZtLU.js → element-G8JoS0Lj.js} +6 -0
  31. package/dist/{helptext-B9kxDc2b.cjs → helptext-B4Uc-d56.cjs} +2 -2
  32. package/dist/{helptext-CqnoPodd.js → helptext-Y4cSgTkd.js} +10 -10
  33. package/dist/helptext.d.ts +32 -0
  34. package/dist/{icon-BEUgV9Wo.js → icon-BJnwW0eh.js} +1 -1
  35. package/dist/{icon-BOKusjAA.cjs → icon-BTUCDPN5.cjs} +1 -1
  36. package/dist/icon.d.ts +32 -0
  37. package/dist/{if-defined-CpIkv1A4.cjs → if-defined-C1ZDVzYn.cjs} +1 -1
  38. package/dist/{if-defined-eRX4e5zO.js → if-defined-rCqT8Od1.js} +1 -1
  39. package/dist/index.d.ts +208 -14
  40. package/dist/input-element-AhnBdCb8.cjs +1 -0
  41. package/dist/{input-element-BK8UkQli.js → input-element-DM2uSYaW.js} +22 -19
  42. package/dist/input-wrapper-BdZxmQyO.cjs +52 -0
  43. package/dist/input-wrapper-DQmYzhcy.js +185 -0
  44. package/dist/input-wrapper.d.ts +32 -0
  45. package/dist/{link-D3U0Jkz8.js → link-C3lW3z8X.js} +5 -5
  46. package/dist/{link-1iq0Pmuf.cjs → link-DOVlsg2S.cjs} +1 -1
  47. package/dist/link.d.ts +32 -0
  48. package/dist/{linkcard-CRpo3tiw.js → linkcard-CvqqyHVW.js} +4 -4
  49. package/dist/{linkcard-2WzDJPZz.cjs → linkcard-DDD92XfD.cjs} +1 -1
  50. package/dist/linkcard.d.ts +32 -0
  51. package/dist/listbox-BTVnrHWv.cjs +95 -0
  52. package/dist/listbox-DX-Euxdm.js +360 -0
  53. package/dist/listbox.d.ts +47 -0
  54. package/dist/{loader-B1edLWTg.js → loader-BudoV0yd.js} +6 -6
  55. package/dist/{loader-DI74pe25.cjs → loader-Csq0Yd1k.cjs} +1 -1
  56. package/dist/loader.d.ts +32 -0
  57. package/dist/{messagebox-DQpEMkS2.cjs → messagebox-8GwnNqb0.cjs} +1 -1
  58. package/dist/{messagebox-KP-8-tA9.js → messagebox-D6uexEhg.js} +6 -6
  59. package/dist/messagebox.d.ts +32 -0
  60. package/dist/{modal-ytIJwfr3.cjs → modal-CLixB4Dz.cjs} +1 -1
  61. package/dist/{modal-kPX8nO_L.js → modal-DnYn2Rlg.js} +6 -6
  62. package/dist/modal.d.ts +32 -0
  63. package/dist/pkt-alert.cjs +1 -1
  64. package/dist/pkt-alert.js +1 -1
  65. package/dist/pkt-backlink.cjs +1 -1
  66. package/dist/pkt-backlink.js +1 -1
  67. package/dist/pkt-button.cjs +1 -1
  68. package/dist/pkt-button.js +1 -1
  69. package/dist/pkt-calendar.cjs +1 -1
  70. package/dist/pkt-calendar.js +1 -1
  71. package/dist/pkt-card.cjs +1 -1
  72. package/dist/pkt-card.js +1 -1
  73. package/dist/pkt-checkbox.cjs +1 -1
  74. package/dist/pkt-checkbox.js +1 -1
  75. package/dist/pkt-combobox.cjs +1 -0
  76. package/dist/pkt-combobox.js +6 -0
  77. package/dist/pkt-datepicker.cjs +1 -1
  78. package/dist/pkt-datepicker.js +1 -1
  79. package/dist/pkt-helptext.cjs +1 -1
  80. package/dist/pkt-helptext.js +1 -1
  81. package/dist/pkt-icon.cjs +1 -1
  82. package/dist/pkt-icon.js +1 -1
  83. package/dist/pkt-index.cjs +3 -3
  84. package/dist/pkt-index.js +45 -43
  85. package/dist/pkt-input-wrapper.cjs +1 -1
  86. package/dist/pkt-input-wrapper.js +1 -1
  87. package/dist/pkt-link.cjs +1 -1
  88. package/dist/pkt-link.js +1 -1
  89. package/dist/pkt-linkcard.cjs +1 -1
  90. package/dist/pkt-linkcard.js +1 -1
  91. package/dist/pkt-listbox.cjs +1 -0
  92. package/dist/pkt-listbox.js +6 -0
  93. package/dist/pkt-loader.cjs +1 -1
  94. package/dist/pkt-loader.js +1 -1
  95. package/dist/pkt-messagebox.cjs +1 -1
  96. package/dist/pkt-messagebox.js +1 -1
  97. package/dist/pkt-modal.cjs +1 -1
  98. package/dist/pkt-modal.js +1 -1
  99. package/dist/pkt-options-controller-BtU1zEtG.cjs +1 -0
  100. package/dist/pkt-options-controller-CZplGTgu.js +38 -0
  101. package/dist/pkt-progressbar.cjs +1 -1
  102. package/dist/pkt-progressbar.js +2 -2
  103. package/dist/pkt-radiobutton.cjs +1 -1
  104. package/dist/pkt-radiobutton.js +1 -1
  105. package/dist/pkt-select.cjs +1 -1
  106. package/dist/pkt-select.js +1 -1
  107. package/dist/pkt-slot-controller-CqNvEpFd.cjs +1 -0
  108. package/dist/{pkt-slot-controller-Clbye6cM.js → pkt-slot-controller-D1DakVrU.js} +17 -7
  109. package/dist/pkt-tag.cjs +1 -1
  110. package/dist/pkt-tag.js +1 -1
  111. package/dist/pkt-textarea.cjs +1 -1
  112. package/dist/pkt-textarea.js +1 -1
  113. package/dist/pkt-textinput.cjs +1 -1
  114. package/dist/pkt-textinput.js +1 -1
  115. package/dist/{progressbar-B6A9UVXS.cjs → progressbar-BW_icNId.cjs} +1 -1
  116. package/dist/{progressbar-D0nxLqHu.js → progressbar-czvExwTL.js} +5 -5
  117. package/dist/progressbar.d.ts +32 -0
  118. package/dist/{radiobutton-BWyQgR_x.cjs → radiobutton-BeSuCrbp.cjs} +1 -1
  119. package/dist/{radiobutton-DLWjvLBO.js → radiobutton-DKo27Stm.js} +5 -5
  120. package/dist/radiobutton.d.ts +32 -0
  121. package/dist/ref-DsoUUoPU.cjs +9 -0
  122. package/dist/ref-cRTOoM4R.js +102 -0
  123. package/dist/repeat-CArTw6-s.js +61 -0
  124. package/dist/repeat-kruY8poV.cjs +5 -0
  125. package/dist/select-DxHhPEgD.js +118 -0
  126. package/dist/select-lvFnhEVg.cjs +49 -0
  127. package/dist/select.d.ts +32 -0
  128. package/dist/{state-HNj0_316.cjs → state-BILlRnrD.cjs} +1 -1
  129. package/dist/{state-CDQk0DFQ.js → state-gfUuUqVg.js} +1 -1
  130. package/dist/{tag-CmFcSdOV.js → tag-CWx1XsGR.js} +7 -7
  131. package/dist/{tag-BeLSOjNh.cjs → tag-DThwKsrg.cjs} +1 -1
  132. package/dist/tag.d.ts +32 -0
  133. package/dist/{textarea-BPqWCymU.js → textarea-BNNDbxxO.js} +18 -17
  134. package/dist/{textarea-VG-UTMLP.cjs → textarea-CDsLbogK.cjs} +6 -6
  135. package/dist/textarea.d.ts +32 -0
  136. package/dist/{textinput-CEP7QA3E.cjs → textinput-C3C088Ki.cjs} +5 -4
  137. package/dist/{textinput-VD74aGzx.js → textinput-M8I4dfoP.js} +25 -24
  138. package/dist/textinput.d.ts +32 -0
  139. package/package.json +3 -3
  140. package/src/components/combobox/combobox.ts +873 -0
  141. package/src/components/combobox/countrycodes.json +927 -0
  142. package/src/components/combobox/index.ts +6 -0
  143. package/src/components/datepicker/datepicker.ts +2 -0
  144. package/src/components/helptext/helptext.ts +1 -1
  145. package/src/components/index.ts +7 -0
  146. package/src/components/input-wrapper/input-wrapper.ts +42 -34
  147. package/src/components/listbox/index.ts +4 -0
  148. package/src/components/listbox/listbox.ts +474 -0
  149. package/src/components/select/select.ts +7 -0
  150. package/src/components/textinput/textinput.ts +3 -2
  151. package/dist/datepicker-FuAL0uNU.cjs +0 -155
  152. package/dist/input-element-Dtyuf6s8.cjs +0 -1
  153. package/dist/input-wrapper-Bo2_t6pA.cjs +0 -50
  154. package/dist/input-wrapper-DaZZq8c0.js +0 -172
  155. package/dist/pkt-slot-controller-Oc32unDk.cjs +0 -1
  156. package/dist/ref-2anvRHT4.cjs +0 -13
  157. package/dist/ref-DbOSDQbk.js +0 -143
  158. package/dist/select-CzuxXKll.js +0 -150
  159. package/dist/select-DZL6aa2s.cjs +0 -48
@@ -0,0 +1,6 @@
1
+ import { PktCombobox } from './combobox'
2
+
3
+ export type { IPktCombobox, IPktComboboxOption, TPktComboboxTagPlacement } from './combobox'
4
+
5
+ export { PktCombobox }
6
+ export default PktCombobox
@@ -512,6 +512,8 @@ export class PktDatepicker extends PktInputElement {
512
512
  .counterMaxLength=${this.maxlength}
513
513
  ?disabled=${this.disabled}
514
514
  ?hasError=${this.hasError}
515
+ ?hasFieldset=${this.hasFieldset}
516
+ ?inline=${this.inline}
515
517
  ?required=${this.required}
516
518
  ?optionalTag=${this.optionalTag}
517
519
  ?requiredTag=${this.requiredTag}
@@ -74,7 +74,7 @@ export class PktHelptext extends PktElement {
74
74
  }
75
75
 
76
76
  const helptextElement = () => {
77
- return html`<div>
77
+ return html`<div class="pkt-inputwrapper__helptext-container">
78
78
  <div class="pkt-inputwrapper__helptext" id="${this.forId}-helptext">
79
79
  <div class="pkt-contents" ${ref(this.defaultSlot)} name="helptext"></div>
80
80
  ${this.helptext && unsafeHTML(this.helptext)}
@@ -4,6 +4,7 @@ export { PktBackLink } from '@/components/backlink'
4
4
  export { PktButton } from '@/components/button'
5
5
  export { PktCalendar } from '@/components/calendar'
6
6
  export { PktCard } from '@/components/card'
7
+ export { PktCombobox } from './combobox'
7
8
  export { PktCheckbox } from '@/components/checkbox'
8
9
  export { PktComponent } from '../base-elements/component-template.js'
9
10
  export { PktDatepicker } from '@/components/datepicker/datepicker.js'
@@ -46,6 +47,12 @@ export type {
46
47
  TProgressbarTitlePosition,
47
48
  } from '@/components/progressbar'
48
49
 
50
+ export type {
51
+ IPktCombobox,
52
+ IPktComboboxOption,
53
+ TPktComboboxTagPlacement,
54
+ } from '@/components/combobox'
55
+
49
56
  export type { TTagSkin, TTagType } from '@/components/tag'
50
57
 
51
58
  export type { TSelectOption } from '@/components/select'
@@ -11,6 +11,7 @@ import specs from 'componentSpecs/input-wrapper.json'
11
11
  import '@/components/helptext'
12
12
  import '@/components/icon'
13
13
 
14
+ type TCounterPosition = 'top' | 'bottom'
14
15
  type Props = ElementProps<
15
16
  PktInputWrapper,
16
17
  | 'forId'
@@ -39,7 +40,7 @@ type Props = ElementProps<
39
40
  export class PktInputWrapper extends PktElement<Props> {
40
41
  defaultSlot: Ref<HTMLElement> = createRef()
41
42
  helptextSlot: Ref<HTMLElement> = createRef()
42
- @state() hasHelptextSlot: boolean = false
43
+ @state() hasHelptextSlot: boolean = true
43
44
 
44
45
  constructor() {
45
46
  super()
@@ -57,6 +58,8 @@ export class PktInputWrapper extends PktElement<Props> {
57
58
  @property({ type: Boolean }) counter: boolean = specs.props.counter.default
58
59
  @property({ type: Number }) counterCurrent: number = 0
59
60
  @property({ type: Number }) counterMaxLength: number = 0
61
+ @property({ type: String }) counterError: string | null = null
62
+ @property({ type: String, reflect: false }) counterPosition: TCounterPosition = 'bottom'
60
63
  @property({ type: Boolean }) optionalTag: boolean = specs.props.optionalTag.default
61
64
  @property({ type: String }) optionalText: string = specs.props.optionalText.default
62
65
  @property({ type: Boolean }) requiredTag: boolean = specs.props.requiredTag.default
@@ -70,12 +73,12 @@ export class PktInputWrapper extends PktElement<Props> {
70
73
  @property({ type: Boolean }) useWrapper: boolean = specs.props.useWrapper.default
71
74
  @property({ type: String, reflect: true }) role: string | null = 'group'
72
75
 
73
- public updateSlots(): void {
74
- this.hasHelptextSlot = this.slotController.filledSlots.has('helptext')
76
+ public updateSlots(filledSlots: Set<string | null | undefined>): void {
77
+ this.hasHelptextSlot = filledSlots.has('helptext')
78
+ this.requestUpdate()
75
79
  }
76
80
 
77
81
  protected updated(changedProperties: PropertyValues): void {
78
- this.updateSlots()
79
82
  super.updated(changedProperties)
80
83
  }
81
84
 
@@ -95,14 +98,6 @@ export class PktInputWrapper extends PktElement<Props> {
95
98
  'pkt-tag--beige': !this.optionalTag && this.requiredTag,
96
99
  }
97
100
 
98
- const hasDropdown = !!this.helptextDropdown
99
-
100
- const wrapperClasses = {
101
- 'pkt-inputwrapper__label': true,
102
- 'pkt-inputwrapper__fieldset': this.hasFieldset,
103
- 'pkt-inputwrapper__legend': this.hasFieldset,
104
- }
105
-
106
101
  const describedBy = this.ariaDescribedby
107
102
  ? this.ariaDescribedby
108
103
  : this.helptext
@@ -126,13 +121,22 @@ export class PktInputWrapper extends PktElement<Props> {
126
121
  const labelElement = () => {
127
122
  if (this.useWrapper) {
128
123
  if (this.hasFieldset) {
129
- return html`<legend class="pkt-inputwrapper__legend" id="${this.forId}-label">
124
+ return html`<legend
125
+ class="pkt-inputwrapper__legend"
126
+ id="${this.forId}-label"
127
+ @click=${this.handleLabelClick}
128
+ >
130
129
  ${this.label} ${tagElement()}
131
130
  </legend>`
132
- } else if (hasDropdown) {
133
- return html`<h2>${this.label} ${tagElement()}</h2>`
134
131
  } else {
135
- return html`<span>${this.label} ${tagElement()}</span>`
132
+ return html`<label
133
+ class="pkt-inputwrapper__label"
134
+ for="${this.forId}"
135
+ aria-describedby="${describedBy}"
136
+ id="${this.forId}-label"
137
+ @click=${this.handleLabelClick}
138
+ >${this.label}${tagElement()}</label
139
+ >`
136
140
  }
137
141
  } else {
138
142
  return html`<label
@@ -147,6 +151,9 @@ export class PktInputWrapper extends PktElement<Props> {
147
151
  }
148
152
 
149
153
  const helptextElement = () => {
154
+ if (!this.helptext && !this.hasHelptextSlot && !this.helptextDropdown) {
155
+ return nothing
156
+ }
150
157
  return html`
151
158
  <pkt-helptext
152
159
  .forId=${this.forId}
@@ -166,7 +173,7 @@ export class PktInputWrapper extends PktElement<Props> {
166
173
  const counterElement = () => {
167
174
  if (this.counter) {
168
175
  return html`<div class="pkt-input__counter" aria-live="polite" aria-atomic="true">
169
- ${this.counterCurrent || 0}
176
+ ${this.counterError ? this.counterError : nothing} ${this.counterCurrent || 0}
170
177
  ${this.counterMaxLength ? `/${this.counterMaxLength}` : nothing}
171
178
  </div>`
172
179
  } else {
@@ -194,31 +201,18 @@ export class PktInputWrapper extends PktElement<Props> {
194
201
  const inputContent = () => {
195
202
  return html`
196
203
  ${labelElement()} ${helptextElement()}
197
- ${hasDropdown
198
- ? html`<label for="${this.forId}" class="pkt-sr-only" aria-describedby="${describedBy}"
199
- >${this.label}</label
200
- >`
201
- : nothing}
204
+ ${this.counterPosition === 'top' ? counterElement() : nothing}
202
205
  <div class="pkt-contents" ${ref(this.defaultSlot)}></div>
203
- ${counterElement()} ${errorElement()}
206
+ ${this.counterPosition === 'bottom' ? counterElement() : nothing} ${errorElement()}
204
207
  `
205
208
  }
206
209
 
207
210
  const wrapperElement = () => {
208
211
  return this.hasFieldset
209
- ? html`<fieldset class=${classMap(wrapperClasses)} aria-describedby="${describedBy}">
212
+ ? html`<fieldset class="pkt-inputwrapper__fieldset" aria-describedby="${describedBy}">
210
213
  ${inputContent()}
211
214
  </fieldset>`
212
- : hasDropdown
213
- ? html`<div class=${classMap(wrapperClasses)}>${inputContent()}</div>`
214
- : html`<label
215
- class=${classMap(wrapperClasses)}
216
- for="${this.forId}"
217
- aria-describedby="${describedBy}"
218
- id="${this.forId}-label"
219
- >
220
- ${inputContent()}
221
- </label>`
215
+ : html`<div class="pkt-inputwrapper__fieldset">${inputContent()}</div>`
222
216
  }
223
217
 
224
218
  return html`<div class=${classMap(classes)}>${wrapperElement()}</div> `
@@ -232,4 +226,18 @@ export class PktInputWrapper extends PktElement<Props> {
232
226
  }),
233
227
  )
234
228
  }
229
+
230
+ private handleLabelClick(e: MouseEvent) {
231
+ if (this.disabled) {
232
+ e.preventDefault()
233
+ e.stopImmediatePropagation()
234
+ }
235
+ this.dispatchEvent(
236
+ new CustomEvent('labelClick', {
237
+ bubbles: true,
238
+ composed: true,
239
+ detail: 'label clicked',
240
+ }),
241
+ )
242
+ }
235
243
  }
@@ -0,0 +1,4 @@
1
+ import { PktListbox } from './listbox'
2
+
3
+ export { PktListbox }
4
+ export default PktListbox
@@ -0,0 +1,474 @@
1
+ import { html, nothing, PropertyValues } from 'lit'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+ import { ifDefined } from 'lit/directives/if-defined.js'
4
+ import strings from '@/translations/no.json'
5
+ import { PktElement } from '@/base-elements/element'
6
+ import { repeat } from 'lit/directives/repeat.js'
7
+ import { classMap } from 'lit/directives/class-map.js'
8
+ import { IPktComboboxOption } from '@/components/combobox/combobox'
9
+ import { uuidish } from '@/utils/stringutils'
10
+
11
+ declare global {
12
+ interface HTMLElementTagNameMap {
13
+ 'pkt-listbox': PktListbox
14
+ }
15
+ }
16
+
17
+ export interface IPktListbox {
18
+ options: IPktComboboxOption[]
19
+ isOpen: boolean
20
+ disabled: boolean
21
+ includeSearch: boolean
22
+ isMultiSelect: boolean
23
+ allowUserInput: boolean
24
+ maxIsReached: boolean
25
+ customUserInput: string | null
26
+ searchPlaceholder: string | null
27
+ searchValue: string | null
28
+ maxLength: number
29
+ userMessage: string | null
30
+ }
31
+
32
+ @customElement('pkt-listbox')
33
+ export class PktListbox extends PktElement implements IPktListbox {
34
+ @property({ type: String }) id: string = uuidish()
35
+ @property({ type: String }) label: string | null = null
36
+ @property({ type: Array }) options: IPktComboboxOption[] = []
37
+ @property({ type: Boolean, reflect: true }) isOpen: boolean = false
38
+ @property({ type: Boolean }) disabled: boolean = false
39
+ @property({ type: Boolean }) includeSearch: boolean = false
40
+ @property({ type: Boolean }) isMultiSelect: boolean = false
41
+ @property({ type: Boolean }) allowUserInput: boolean = false
42
+ @property({ type: Boolean }) maxIsReached: boolean = false
43
+ @property({ type: String }) customUserInput: string | null = null
44
+ @property({ type: String }) searchPlaceholder: string | null = null
45
+ @property({ type: String }) searchValue: string | null = null
46
+ @property({ type: Number }) maxLength: number = 0
47
+ @property({ type: String }) userMessage: string | null = null
48
+
49
+ private _selectedOptions: number = 0
50
+
51
+ @state() private _filteredOptions: IPktComboboxOption[] = []
52
+
53
+ // Lifecycle methods
54
+ connectedCallback(): void {
55
+ super.connectedCallback()
56
+ if (this.includeSearch && !this.searchValue) {
57
+ this.searchValue = ''
58
+ }
59
+ if (this.options.length > 0) {
60
+ this.filterOptions()
61
+ }
62
+ this.setAttribute('tabindex', '-1')
63
+ this.addEventListener('focus', this.focusFirstOrSelectedOption)
64
+ }
65
+
66
+ updated(changedProperties: PropertyValues) {
67
+ if (changedProperties.has('options') || changedProperties.has('searchValue')) {
68
+ this.filterOptions()
69
+ }
70
+ super.updated(changedProperties)
71
+ }
72
+
73
+ attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
74
+ if (name === 'options' || name === 'searchValue' || name === 'search-value') {
75
+ this.filterOptions()
76
+ }
77
+ super.attributeChangedCallback(name, _old, value)
78
+ }
79
+
80
+ // Render methods
81
+ render() {
82
+ return html`
83
+ <div
84
+ class=${classMap({
85
+ 'pkt-listbox': true,
86
+ 'pkt-listbox__open': this.isOpen,
87
+ 'pkt-txt-16-light': true,
88
+ })}
89
+ role="listbox"
90
+ aria-label=${ifDefined(this.label)}
91
+ >
92
+ <div class="pkt-listbox__banners">
93
+ ${this.renderMaximumReachedBanner()} ${this.renderUserMessage()}
94
+ ${this.renderNewOptionBanner()} ${this.renderSearch()}
95
+ </div>
96
+ <ul class="pkt-listbox__options" role="presentation">
97
+ ${this.renderList()}
98
+ </ul>
99
+ </div>
100
+ `
101
+ }
102
+
103
+ renderCheckboxOrCheckIcon(option: IPktComboboxOption, index: number) {
104
+ return this.isMultiSelect
105
+ ? html`
106
+ <input
107
+ class="pkt-input-check__input-checkbox"
108
+ type="checkbox"
109
+ role="presentation"
110
+ tabindex="-1"
111
+ value=${option.value}
112
+ .checked=${option.selected}
113
+ aria-labelledby=${this.id + '-option-label-' + index}
114
+ ?disabled=${this.disabled || option.disabled || (this.maxIsReached && !option.selected)}
115
+ />
116
+ `
117
+ : option.selected
118
+ ? html`<pkt-icon name="check-big"></pkt-icon>`
119
+ : nothing
120
+ }
121
+
122
+ renderList() {
123
+ return html`
124
+ ${repeat(
125
+ this._filteredOptions,
126
+ (option) => option.value,
127
+ (option, index) => html`
128
+ <li
129
+ @click=${() => {
130
+ this.toggleOption(option)
131
+ }}
132
+ aria-selected=${option.selected ? 'true' : 'false'}
133
+ @keydown=${this.handleOptionKeydown}
134
+ class=${classMap({
135
+ 'pkt-listbox__option': true,
136
+ 'pkt-listbox__option--selected': Boolean(!this.isMultiSelect && option.selected),
137
+ 'pkt-listbox__option--checkBox': this.isMultiSelect,
138
+ })}
139
+ tabindex="${this.disabled || option.disabled ? '-1' : '0'}"
140
+ data-index=${index}
141
+ data-value=${option.value}
142
+ data-selected=${option.selected ? 'true' : 'false'}
143
+ ?data-disabled=${this.disabled ||
144
+ option.disabled ||
145
+ (this.maxIsReached && !option.selected)}
146
+ role="option"
147
+ id=${`${this.id}-${index}`}
148
+ >
149
+ ${this.renderCheckboxOrCheckIcon(option, index)}
150
+ <span class="pkt-listbox__option-label" id=${this.id + '-option-label-' + index}>
151
+ ${option.prefix
152
+ ? html`<span class="pkt-listbox__option-prefix">${option.prefix}</span>`
153
+ : nothing}
154
+ ${option.label || option.value}
155
+ </span>
156
+ ${option.description
157
+ ? html`<span class="pkt-listbox__option-description pkt-txt-14-light"
158
+ >${option.description}</span
159
+ >`
160
+ : nothing}
161
+ </li>
162
+ `,
163
+ )}
164
+ `
165
+ }
166
+
167
+ renderNewOptionBanner() {
168
+ return this.allowUserInput && this.customUserInput
169
+ ? html`
170
+ <div
171
+ class="pkt-listbox__banner pkt-listbox__banner--new-option pkt-listbox__option"
172
+ data-type="new-option"
173
+ data-value=${this.customUserInput}
174
+ data-selected="false"
175
+ tabindex="0"
176
+ @click=${() =>
177
+ this.toggleOption({
178
+ value: this.customUserInput || '',
179
+ })}
180
+ @keydown=${this.handleOptionKeydown}
181
+ >
182
+ <pkt-icon class="pkt-listbox__banner-icon" name="plus-sign" size="large"></pkt-icon>
183
+ Legg til “${this.customUserInput}”
184
+ </div>
185
+ `
186
+ : nothing
187
+ }
188
+
189
+ renderMaximumReachedBanner() {
190
+ this._selectedOptions = this.options.filter((option) => option.selected).length
191
+
192
+ return this.isMultiSelect && this._selectedOptions > 0 && this.maxLength > 0
193
+ ? html`
194
+ <div class="pkt-listbox__banner pkt-listbox__banner--maximum-reached">
195
+ ${this._selectedOptions} av maks ${this.maxLength} mulige er valgt.
196
+ </div>
197
+ `
198
+ : nothing
199
+ }
200
+
201
+ renderUserMessage() {
202
+ return this.userMessage
203
+ ? html`<div class="pkt-listbox__banner pkt-listbox__banner--user-message">
204
+ <pkt-icon
205
+ class="pkt-listbox__banner-icon"
206
+ name="exclamation-mark-circle"
207
+ size="large"
208
+ ></pkt-icon>
209
+ ${this.userMessage}
210
+ </div>`
211
+ : nothing
212
+ }
213
+
214
+ renderSearch() {
215
+ return this.includeSearch
216
+ ? html`
217
+ <div class="pkt-listbox__search">
218
+ <span class="pkt-listbox__search-icon">
219
+ <pkt-icon name="magnifying-glass-small" size="large"></pkt-icon>
220
+ </span>
221
+ <input
222
+ class="pkt-txt-16-light"
223
+ type="text"
224
+ aria-label="Søk i listen"
225
+ form=""
226
+ placeholder=${this.searchPlaceholder || strings.forms.search.placeholder}
227
+ @input=${this.handleSearchInput}
228
+ @keydown=${this.handleSearchKeydown}
229
+ .value=${this.searchValue}
230
+ data-type="searchbox"
231
+ ?disabled=${this.disabled}
232
+ ?readonly=${this.disabled}
233
+ role="searchbox"
234
+ />
235
+ </div>
236
+ `
237
+ : nothing
238
+ }
239
+
240
+ // Event handlers
241
+ handleSearchInput(e: InputEvent) {
242
+ this.searchValue = (e.target as HTMLInputElement).value
243
+ this.dispatchEvent(
244
+ new CustomEvent('search', {
245
+ detail: this.searchValue,
246
+ bubbles: false,
247
+ }),
248
+ )
249
+ }
250
+
251
+ handleSearchKeydown(e: KeyboardEvent) {
252
+ switch (e.key) {
253
+ case 'Enter':
254
+ e.preventDefault()
255
+ break
256
+ case 'ArrowUp':
257
+ case 'Escape':
258
+ this.closeOptions()
259
+ e.preventDefault()
260
+ break
261
+ case 'ArrowDown':
262
+ case 'Tab':
263
+ this.focusFirstOrSelectedOption()
264
+ break
265
+ }
266
+ }
267
+
268
+ handleOptionKeydown(e: KeyboardEvent) {
269
+ const target = e.currentTarget as HTMLElement
270
+ const value = target.dataset.value
271
+ const itemType = target.dataset.type
272
+ const isValueSelected = target.dataset.selected === 'true'
273
+
274
+ if (
275
+ !this.getOptionElements().length &&
276
+ (!this.customUserInput || (!this.allowUserInput && this.customUserInput)) &&
277
+ itemType !== 'new-option' &&
278
+ itemType !== 'searchbox'
279
+ ) {
280
+ return
281
+ }
282
+
283
+ switch (e.key) {
284
+ case ' ':
285
+ case 'Enter':
286
+ this.toggleOption(target)
287
+ e.preventDefault()
288
+ break
289
+
290
+ case 'Backspace':
291
+ if (value) {
292
+ if (isValueSelected) {
293
+ this.toggleOption(target)
294
+ } else {
295
+ this.closeOptions()
296
+ }
297
+ }
298
+ e.preventDefault()
299
+ break
300
+
301
+ case 'Escape':
302
+ case 'Tab':
303
+ this.closeOptions()
304
+ break
305
+
306
+ case 'ArrowDown':
307
+ if (e.altKey) {
308
+ this.focusLastOption()
309
+ } else {
310
+ if (itemType === 'searchbox' || itemType === 'new-option') {
311
+ this.focusFirstOption()
312
+ } else {
313
+ this.focusNextOption(target)
314
+ }
315
+ }
316
+ e.preventDefault()
317
+ break
318
+
319
+ case 'ArrowUp':
320
+ if (e.altKey) {
321
+ this.focusFirstOption()
322
+ } else {
323
+ if (target.dataset.index === '0' && this.includeSearch) {
324
+ const searchInput = this.querySelector('[role="searchbox"]') as HTMLElement
325
+ searchInput && searchInput.focus()
326
+ } else if (target.dataset.index === '0' && this.customUserInput) {
327
+ const newOption = this.querySelector('[data-type="new-option"]') as HTMLElement
328
+ newOption && newOption.focus()
329
+ } else {
330
+ this.focusPreviousOption(target)
331
+ }
332
+ }
333
+ e.preventDefault()
334
+ break
335
+
336
+ case 'Home':
337
+ this.focusFirstOption()
338
+ e.preventDefault()
339
+ break
340
+
341
+ case 'End':
342
+ this.focusLastOption()
343
+ e.preventDefault()
344
+ break
345
+
346
+ default:
347
+ if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
348
+ this.selectAll()
349
+ e.preventDefault()
350
+ }
351
+ if (this.isLetterOrSpace(e.key)) {
352
+ this.handleTypeAhead(e.key)
353
+ e.preventDefault()
354
+ }
355
+ break
356
+ }
357
+ }
358
+
359
+ // Focus management methods
360
+ focusAndScrollIntoView(el: HTMLElement) {
361
+ el.scrollIntoView({ block: 'nearest' })
362
+ window.setTimeout(() => el.focus(), 0)
363
+ }
364
+
365
+ focusNextOption(target: HTMLElement) {
366
+ const nextOption = target.nextElementSibling as HTMLElement
367
+ nextOption && this.focusAndScrollIntoView(nextOption)
368
+ }
369
+
370
+ focusPreviousOption(target: HTMLElement) {
371
+ const previousOption = target.previousElementSibling as HTMLElement
372
+ if (target.dataset.index === '0' && this.includeSearch) {
373
+ const searchInput = this.querySelector('[role="searchbox"]') as HTMLElement
374
+ searchInput && this.focusAndScrollIntoView(searchInput)
375
+ } else if (previousOption) {
376
+ this.focusAndScrollIntoView(previousOption)
377
+ }
378
+ }
379
+
380
+ focusFirstOption() {
381
+ const firstOption = this.getOptionElements()[0]
382
+ firstOption && this.focusAndScrollIntoView(firstOption)
383
+ }
384
+
385
+ focusLastOption() {
386
+ const lastOption = this.getOptionElements().pop()
387
+ lastOption && this.focusAndScrollIntoView(lastOption)
388
+ }
389
+
390
+ focusFirstOrSelectedOption() {
391
+ if (this.disabled) return
392
+ const selectedOption = this.getOptionElements().find(
393
+ (option) => option.dataset.selected === 'true',
394
+ )
395
+ if (this.allowUserInput && this.customUserInput) {
396
+ const newOption = this.querySelector('[data-type="new-option"]') as HTMLElement
397
+ this.focusAndScrollIntoView(newOption)
398
+ } else if (selectedOption) {
399
+ this.focusAndScrollIntoView(selectedOption)
400
+ } else if (this.includeSearch && !(document.activeElement instanceof HTMLInputElement)) {
401
+ const searchInput = this.querySelector('[role="searchbox"]') as HTMLElement
402
+ window.setTimeout(() => searchInput.focus(), 0)
403
+ } else {
404
+ this.focusFirstOption()
405
+ }
406
+ }
407
+
408
+ // Event dispatching methods
409
+ toggleOption(option: IPktComboboxOption | HTMLElement) {
410
+ const optionDisabled = option instanceof HTMLElement ? option.dataset.disabled : option.disabled
411
+ if (this.disabled || optionDisabled) return
412
+ const value = option instanceof HTMLElement ? option.dataset.value : option.value
413
+ this.dispatchEvent(
414
+ new CustomEvent('option-toggle', {
415
+ detail: value,
416
+ bubbles: false,
417
+ }),
418
+ )
419
+ }
420
+
421
+ selectAll() {
422
+ this.dispatchEvent(new CustomEvent('select-all', { bubbles: false }))
423
+ }
424
+
425
+ closeOptions() {
426
+ this.dispatchEvent(new CustomEvent('close-options', { bubbles: false }))
427
+ }
428
+
429
+ // Filtering and typeahead methods
430
+
431
+ filterOptions() {
432
+ if (this.searchValue) {
433
+ this._filteredOptions = this.options.filter((option) => {
434
+ const fulltext = option.label + option.value
435
+ return fulltext.toLowerCase().includes(this.searchValue?.toLowerCase() || '')
436
+ })
437
+ } else {
438
+ this._filteredOptions = [...this.options]
439
+ }
440
+ }
441
+
442
+ isLetterOrSpace(char: string): boolean {
443
+ return /^[\p{L} ]$/u.test(char)
444
+ }
445
+
446
+ handleTypeAhead(char: string) {
447
+ this.typeAheadString += char.toLowerCase()
448
+
449
+ if (this.typeAheadTimeout) {
450
+ clearTimeout(this.typeAheadTimeout)
451
+ }
452
+
453
+ this.typeAheadTimeout = window.setTimeout(() => {
454
+ this.typeAheadString = ''
455
+ }, 500)
456
+
457
+ const options = this.getOptionElements()
458
+ const match = options.find((option) =>
459
+ option.textContent?.trim().toLowerCase().startsWith(this.typeAheadString),
460
+ )
461
+
462
+ match && this.focusAndScrollIntoView(match)
463
+ }
464
+
465
+ // DOM helper methods
466
+ getOptionElements() {
467
+ if (!this._filteredOptions.length) {
468
+ return []
469
+ }
470
+ return Array.from(
471
+ this.querySelectorAll('[role="option"]:not([data-disabled])') || [],
472
+ ) as HTMLElement[]
473
+ }
474
+ }