@oslokommune/punkt-elements 16.26.4 → 16.26.6

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 (29) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/{combobox-B9YSsRZB.js → combobox-1xG07Oce.js} +2 -2
  3. package/dist/{combobox-BhIqExJZ.cjs → combobox-BjBNyEly.cjs} +1 -1
  4. package/dist/components/fileupload/fileupload-base.d.ts +10 -0
  5. package/dist/components/fileupload/fileupload-types.d.ts +10 -0
  6. package/dist/docs/components/select.d.ts +3 -0
  7. package/dist/{fileupload-DpvQ3e_T.js → fileupload-2hf8YNhO.js} +414 -92
  8. package/dist/{fileupload-BMHJaAKR.cjs → fileupload-BLVrslUX.cjs} +42 -37
  9. package/dist/pkt-combobox.cjs +1 -1
  10. package/dist/pkt-combobox.js +1 -1
  11. package/dist/pkt-fileupload.cjs +1 -1
  12. package/dist/pkt-fileupload.js +1 -1
  13. package/dist/pkt-index.cjs +1 -1
  14. package/dist/pkt-index.js +3 -3
  15. package/dist/pkt-options-controller-CPrfqepu.cjs +1 -0
  16. package/dist/{pkt-options-controller-Cis62vGe.js → pkt-options-controller-DMxYwnnj.js} +32 -9
  17. package/dist/pkt-select.cjs +1 -1
  18. package/dist/pkt-select.js +1 -1
  19. package/dist/{select-BVPcJEIz.cjs → select-C3Q_bGZb.cjs} +1 -1
  20. package/dist/{select-C1--vTZ2.js → select-YyR17Ml6.js} +1 -1
  21. package/package.json +2 -2
  22. package/src/components/combobox/combobox.selection.test.ts +51 -0
  23. package/src/components/combobox/combobox.ts +3 -0
  24. package/src/components/fileupload/fileupload-base.ts +14 -0
  25. package/src/components/fileupload/fileupload-types.ts +10 -0
  26. package/src/components/fileupload/fileupload.test.ts +35 -0
  27. package/src/components/fileupload/fileupload.ts +5 -0
  28. package/src/components/select/select.test.ts +73 -3
  29. package/dist/pkt-options-controller-DHKHMyXM.cjs +0 -1
@@ -540,4 +540,55 @@ describe('PktCombobox', () => {
540
540
  expect(valueEl?.textContent?.trim()).toBe('NO no')
541
541
  })
542
542
  })
543
+
544
+ describe('Programmatic value control', () => {
545
+ test('updates listbox selected state when value is set externally (single)', async () => {
546
+ const container = await createCombobox('id="test" name="test" label="Test" value="apple"')
547
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
548
+ combobox.options = getDefaultOptions()
549
+ await combobox.updateComplete
550
+
551
+ combobox.value = 'banana'
552
+ await combobox.updateComplete
553
+
554
+ const options = combobox['_options']
555
+ expect(options.find((o) => o.value === 'banana')?.selected).toBe(true)
556
+ expect(options.find((o) => o.value === 'apple')?.selected).toBe(false)
557
+
558
+ await openAndWaitForListbox(combobox)
559
+ const selected = combobox.querySelectorAll('[role="option"][aria-selected="true"]')
560
+ expect(selected).toHaveLength(1)
561
+ expect(selected[0].textContent).toContain('Banana')
562
+ })
563
+
564
+ test('updates listbox selected state when value is set externally (multiple)', async () => {
565
+ const container = await createCombobox(
566
+ 'id="test" name="test" label="Test" multiple value="apple,banana"',
567
+ )
568
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
569
+ combobox.options = getDefaultOptions()
570
+ await combobox.updateComplete
571
+
572
+ combobox.value = 'cherry,date'
573
+ await combobox.updateComplete
574
+
575
+ const selectedValues = combobox['_options'].filter((o) => o.selected).map((o) => o.value)
576
+ expect(selectedValues).toEqual(['cherry', 'date'])
577
+ })
578
+
579
+ test('clears listbox selected state when value is cleared externally', async () => {
580
+ const container = await createCombobox(
581
+ 'id="test" name="test" label="Test" multiple value="apple,banana"',
582
+ )
583
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
584
+ combobox.options = getDefaultOptions()
585
+ await combobox.updateComplete
586
+
587
+ combobox.value = ''
588
+ await combobox.updateComplete
589
+
590
+ expect(combobox['_value']).toEqual([])
591
+ expect(combobox['_options'].some((o) => o.selected)).toBe(false)
592
+ })
593
+ })
543
594
  })
@@ -154,6 +154,9 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
154
154
  const newInternal = this.parseValue()
155
155
  if (newInternal.join(',') !== this._value.join(',')) {
156
156
  this._value = newInternal
157
+ this.options.forEach((o) => (o.selected = newInternal.includes(o.value)))
158
+ this._options.forEach((o) => (o.selected = newInternal.includes(o.value)))
159
+ this._options = [...this._options]
157
160
  }
158
161
  this.updateMaxReached()
159
162
  this.syncValueAndDispatch(oldInternal)
@@ -14,6 +14,7 @@ import {
14
14
 
15
15
  import { PktElement } from '@/base-elements/element'
16
16
  import converters from '../../helpers/converters'
17
+ import specs from 'componentSpecs/fileupload.json'
17
18
 
18
19
  import type {
19
20
  FileItem,
@@ -41,6 +42,11 @@ export abstract class PktFileUploadBase
41
42
  @property({ type: String }) label: string = ''
42
43
  /** Optional help text shown under label. */
43
44
  @property({ type: String }) helptext: string = ''
45
+ /** Expandable help text (dropdown) shown by `pkt-input-wrapper`. */
46
+ @property({ type: String, attribute: 'helptext-dropdown' }) helptextDropdown: string = ''
47
+ /** Button label for expandable help text. */
48
+ @property({ type: String, attribute: 'helptext-dropdown-button' })
49
+ helptextDropdownButton: string = specs.props.helptextDropdownButton.default
44
50
  /** Mark input as required in form strategy. */
45
51
  @property({ type: Boolean, reflect: true }) required = false
46
52
  /** Allow selecting/dropping multiple files. */
@@ -85,8 +91,16 @@ export abstract class PktFileUploadBase
85
91
  @property({ type: String, attribute: 'error-message' }) errorMessage = ''
86
92
  /** Show "Valgfritt" tag in the input wrapper. */
87
93
  @property({ type: Boolean, attribute: 'optional-tag' }) optionalTag = false
94
+ /** Label text for the optional tag. */
95
+ @property({ type: String, attribute: 'optional-text' })
96
+ optionalText: string = specs.props.optionalText.default
88
97
  /** Show "Må fylles ut" tag in the input wrapper. */
89
98
  @property({ type: Boolean, attribute: 'required-tag' }) requiredTag = false
99
+ /** Label text for the required tag. */
100
+ @property({ type: String, attribute: 'required-text' })
101
+ requiredText: string = specs.props.requiredText.default
102
+ /** Extra tag next to the label. */
103
+ @property({ type: String, attribute: 'tag-text' }) tagText: string | null = null
90
104
  /** Trailing characters to keep when middle-truncating long filenames. Set to `0` to disable. */
91
105
  @property({ type: Number, attribute: 'truncate-tail' }) truncateTail: number = 4
92
106
  /** Controlled mode source of truth. Parent owns file list and updates this prop from events. */
@@ -55,6 +55,10 @@ export interface IPktFileUpload {
55
55
  name?: string
56
56
  label?: string
57
57
  helptext?: string
58
+ /** Expandable help text (dropdown) in input-wrapper. */
59
+ helptextDropdown?: string
60
+ /** Button label for expandable help text. */
61
+ helptextDropdownButton?: string
58
62
  required?: boolean
59
63
  multiple?: boolean
60
64
  disabled?: boolean
@@ -73,8 +77,14 @@ export interface IPktFileUpload {
73
77
  errorMessage?: string
74
78
  /** Show "Valgfritt" tag in input-wrapper. */
75
79
  optionalTag?: boolean
80
+ /** Label text for the optional tag. */
81
+ optionalText?: string
76
82
  /** Show "Må fylles ut" tag in input-wrapper. */
77
83
  requiredTag?: boolean
84
+ /** Label text for the required tag. */
85
+ requiredText?: string
86
+ /** Extra tag next to the label. */
87
+ tagText?: string | null
78
88
  /** Trailing characters to keep when truncating long filenames. */
79
89
  truncateTail?: number
80
90
  /** Transfer states keyed by `fileId` (custom strategy progress/error/cancel rendering). */
@@ -875,6 +875,41 @@ describe('PktFileUpload', () => {
875
875
  expect(requiredWrapper?.requiredTag).toBe(true)
876
876
  })
877
877
 
878
+ test('forwards tag texts and expandable helptext to pkt-input-wrapper', async () => {
879
+ const { fileupload } = await createFileUploadTest({
880
+ label: 'Vedlegg',
881
+ optionalTag: true,
882
+ optionalText: 'Frivillig',
883
+ tagText: 'Nytt',
884
+ helptextDropdown: 'Utfyllende informasjon om vedlegg',
885
+ helptextDropdownButton: 'Vis mer',
886
+ })
887
+
888
+ const wrapper = fileupload.querySelector<
889
+ HTMLElement & {
890
+ optionalText?: string
891
+ tagText?: string | null
892
+ helptextDropdown?: string
893
+ helptextDropdownButton?: string
894
+ }
895
+ >('pkt-input-wrapper')
896
+ expect(wrapper?.optionalText).toBe('Frivillig')
897
+ expect(wrapper?.tagText).toBe('Nytt')
898
+ expect(wrapper?.helptextDropdown).toBe('Utfyllende informasjon om vedlegg')
899
+ expect(wrapper?.helptextDropdownButton).toBe('Vis mer')
900
+ expect(wrapper?.textContent).toContain('Frivillig')
901
+ expect(wrapper?.textContent).toContain('Nytt')
902
+
903
+ const { fileupload: required } = await createFileUploadTest({
904
+ label: 'Vedlegg',
905
+ requiredTag: true,
906
+ requiredText: 'Obligatorisk',
907
+ })
908
+ const requiredWrapper = required.querySelector<HTMLElement & { requiredText?: string }>('pkt-input-wrapper')
909
+ expect(requiredWrapper?.requiredText).toBe('Obligatorisk')
910
+ expect(requiredWrapper?.textContent).toContain('Obligatorisk')
911
+ })
912
+
878
913
  test('default id is unique per instance', async () => {
879
914
  const { fileupload: first } = await createFileUploadTest()
880
915
  const { fileupload: second } = await createFileUploadTest()
@@ -68,10 +68,15 @@ export class PktFileUpload extends PktFileUploadBase implements IPktFileUpload {
68
68
  .forId=${`${this.id}-input`}
69
69
  .label=${this.label}
70
70
  .helptext=${this.helptext}
71
+ .helptextDropdown=${this.helptextDropdown}
72
+ .helptextDropdownButton=${this.helptextDropdownButton}
71
73
  ?disabled=${this.disabled}
72
74
  ?hasError=${this.hasEffectiveError}
73
75
  ?optionalTag=${this.optionalTag}
76
+ .optionalText=${this.optionalText}
74
77
  ?requiredTag=${this.requiredTag}
78
+ .requiredText=${this.requiredText}
79
+ .tagText=${this.tagText}
75
80
  class=${classMap({
76
81
  'pkt-fileupload-wrapper': true,
77
82
  'pkt-fileupload-wrapper--full-width': this.fullwidth,
@@ -199,18 +199,88 @@ describe('pkt-select', () => {
199
199
  test('handles useWrapper false', async () => {
200
200
  const { select } = await createSelectTest()
201
201
 
202
- // Set useWrapper property directly on the element
203
202
  select.useWrapper = false
204
203
  await select.updateComplete
205
204
 
206
- // When useWrapper is false, verify the behavior
207
205
  expect(select.useWrapper).toBe(false)
208
206
 
209
- // Confirm that there's a label with class pkt-sr-only in the markup
210
207
  const srOnlyLabel = select.querySelector('label.pkt-sr-only')
211
208
  expect(srOnlyLabel).toBeInTheDocument()
212
209
  })
213
210
 
211
+ test('does not duplicate slotted options when moved in the DOM (issue #3544)', async () => {
212
+ const zoneA = document.createElement('div')
213
+ const zoneB = document.createElement('div')
214
+ document.body.append(zoneA, zoneB)
215
+
216
+ zoneA.innerHTML = `
217
+ <pkt-select label="Kategori" id="reconnect-select">
218
+ <option value="">Velg…</option>
219
+ <option value="en">En</option>
220
+ <option value="to">To</option>
221
+ </pkt-select>
222
+ `
223
+ const select = zoneA.querySelector('pkt-select')!
224
+ await select.updateComplete
225
+ expect(select.querySelectorAll('select option')).toHaveLength(3)
226
+
227
+ zoneB.appendChild(select)
228
+ await select.updateComplete
229
+ expect(select.querySelectorAll('select option')).toHaveLength(3)
230
+
231
+ zoneA.appendChild(select)
232
+ await select.updateComplete
233
+ expect(select.querySelectorAll('select option')).toHaveLength(3)
234
+ })
235
+
236
+ test('updates rendered option when disabled is toggled on a slotted option', async () => {
237
+ const wrapper = document.createElement('div')
238
+ document.body.append(wrapper)
239
+ wrapper.innerHTML = `
240
+ <pkt-select label="Frukt" id="toggle-disabled-select">
241
+ <option value="">Velg…</option>
242
+ <option value="banan">Banan</option>
243
+ </pkt-select>
244
+ `
245
+ const select = wrapper.querySelector('pkt-select')!
246
+ await select.updateComplete
247
+
248
+ const slotted = select.querySelector(':scope > option[value="banan"]')!
249
+ const rendered = () => select.querySelector('select option[value="banan"]') as HTMLOptionElement
250
+
251
+ expect(rendered().disabled).toBe(false)
252
+
253
+ slotted.toggleAttribute('disabled')
254
+ await new Promise((resolve) => setTimeout(resolve, 0))
255
+ await select.updateComplete
256
+ expect(rendered().disabled).toBe(true)
257
+
258
+ slotted.toggleAttribute('disabled')
259
+ await new Promise((resolve) => setTimeout(resolve, 0))
260
+ await select.updateComplete
261
+ expect(rendered().disabled).toBe(false)
262
+ })
263
+
264
+ test('updates rendered option when the text of a slotted option changes', async () => {
265
+ const wrapper = document.createElement('div')
266
+ document.body.append(wrapper)
267
+ wrapper.innerHTML = `
268
+ <pkt-select label="Frukt" id="change-label-select">
269
+ <option value="banan">Banan</option>
270
+ </pkt-select>
271
+ `
272
+ const select = wrapper.querySelector('pkt-select')!
273
+ await select.updateComplete
274
+
275
+ const slotted = select.querySelector(':scope > option[value="banan"]')!
276
+ slotted.textContent = 'Gul banan'
277
+ await new Promise((resolve) => setTimeout(resolve, 0))
278
+ await select.updateComplete
279
+
280
+ const rendered = select.querySelector('select option[value="banan"]')
281
+ expect(rendered?.textContent?.trim()).toBe('Gul banan')
282
+ })
283
+
214
284
  test('meets accessibility standards', async () => {
215
285
  const options: TSelectOption[] = [
216
286
  { value: '1', label: 'Option 1' },
@@ -1 +0,0 @@
1
- const e=require("./element-with-slot-BY7Jc-q8.cjs");var t=class{constructor(e){this.nodes=[],this.options=[],this.generation=0,this.host=e,this.host.addController(this),this.nodes=[],this.options=[],this.observer=new MutationObserver(e=>this.handleMutations(e))}hostConnected(){Array.from(this.host.childNodes).forEach(e=>{(e.nodeName===`OPTION`||e.nodeName===`DATA`)&&this.addNode(e)}),this.createOptions(),this.observer.observe(this.host,{childList:!0,subtree:!1,attributes:!0})}hostDisconnected(){this.observer.disconnect()}addNode(e){(e.nodeName===`OPTION`||e.nodeName===`DATA`)&&(e.hasAttribute(`hidden`)&&!e.hasAttribute(`data-skip`)&&e.setAttribute(`data-hidden`,`true`),!e.hasAttribute(`hidden`)&&e.hasAttribute(`data-skip`)&&e.hasAttribute(`data-hidden`)&&e.removeAttribute(`data-hidden`),e.setAttribute(`class`,`pkt-hide`),e.setAttribute(`data-skip`,`true`),e.setAttribute(`hidden`,`true`),this.nodes.push(e))}createOptions(){this.options=this.nodes.map(e=>({value:e.hasAttribute(`value`)?e.getAttribute(`value`)??``:e.textContent??``,label:e.textContent||e.getAttribute(`value`)||``,selected:e.hasAttribute(`selected`),disabled:e.hasAttribute(`disabled`),hidden:e.hasAttribute(`data-hidden`)})),this.generation++,this.host.requestUpdate()}handleMutations(t){let n=!1;t.forEach(t=>{t.type===`childList`&&(t.addedNodes.forEach(t=>{e.r(t)&&(this.addNode(t),n=!0)}),t.removedNodes.forEach(t=>{e.r(t)&&(this.nodes=this.nodes.filter(e=>e!==t),n=!0)})),t.type===`attributes`&&e.r(t.target)&&(this.addNode(t.target),n=!0)}),n&&this.createOptions()}};Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return t}});