@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.
- package/CHANGELOG.md +34 -0
- package/dist/{combobox-B9YSsRZB.js → combobox-1xG07Oce.js} +2 -2
- package/dist/{combobox-BhIqExJZ.cjs → combobox-BjBNyEly.cjs} +1 -1
- package/dist/components/fileupload/fileupload-base.d.ts +10 -0
- package/dist/components/fileupload/fileupload-types.d.ts +10 -0
- package/dist/docs/components/select.d.ts +3 -0
- package/dist/{fileupload-DpvQ3e_T.js → fileupload-2hf8YNhO.js} +414 -92
- package/dist/{fileupload-BMHJaAKR.cjs → fileupload-BLVrslUX.cjs} +42 -37
- package/dist/pkt-combobox.cjs +1 -1
- package/dist/pkt-combobox.js +1 -1
- package/dist/pkt-fileupload.cjs +1 -1
- package/dist/pkt-fileupload.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +3 -3
- package/dist/pkt-options-controller-CPrfqepu.cjs +1 -0
- package/dist/{pkt-options-controller-Cis62vGe.js → pkt-options-controller-DMxYwnnj.js} +32 -9
- package/dist/pkt-select.cjs +1 -1
- package/dist/pkt-select.js +1 -1
- package/dist/{select-BVPcJEIz.cjs → select-C3Q_bGZb.cjs} +1 -1
- package/dist/{select-C1--vTZ2.js → select-YyR17Ml6.js} +1 -1
- package/package.json +2 -2
- package/src/components/combobox/combobox.selection.test.ts +51 -0
- package/src/components/combobox/combobox.ts +3 -0
- package/src/components/fileupload/fileupload-base.ts +14 -0
- package/src/components/fileupload/fileupload-types.ts +10 -0
- package/src/components/fileupload/fileupload.test.ts +35 -0
- package/src/components/fileupload/fileupload.ts +5 -0
- package/src/components/select/select.test.ts +73 -3
- 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}});
|