@oslokommune/punkt-elements 16.6.2 → 16.7.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.
- package/CHANGELOG.md +18 -0
- package/dist/{accordionitem-D7sWqDM_.cjs → accordionitem-D1Agl1bB.cjs} +1 -1
- package/dist/{accordionitem-Cj74XruR.js → accordionitem-ZlaXvjt5.js} +5 -5
- package/dist/{alert-ca_hFi-u.js → alert-BuAXrnKl.js} +6 -6
- package/dist/{alert-C95U_RZ0.cjs → alert-DFkFNTxh.cjs} +1 -1
- package/dist/{async-directive-CKx6pubI.js → async-directive-CMzWMlZ5.js} +1 -1
- package/dist/{async-directive-B158h3ba.cjs → async-directive-DoMuclel.cjs} +1 -1
- package/dist/{backlink-CQoNgoa-.cjs → backlink-EiAXUUZP.cjs} +1 -1
- package/dist/{backlink-C68H4Z3b.js → backlink-V4DAt17e.js} +3 -3
- package/dist/{button-BRovt5nm.js → button-DEyvt1LI.js} +5 -5
- package/dist/{button-BXN_JPaH.cjs → button-DYv5Dg4M.cjs} +1 -1
- package/dist/{calendar-43Y_6FD8.cjs → calendar-HIE9nxcc.cjs} +1 -1
- package/dist/{calendar-B5Xwl8mV.js → calendar-S63oreVW.js} +4 -4
- package/dist/{card-DfHo45CG.cjs → card-CyeH6l1c.cjs} +1 -1
- package/dist/{card-DvgCTN4B.js → card-DDiLfuO4.js} +7 -7
- package/dist/{checkbox-mmn3v9ry.cjs → checkbox-BLlwTABj.cjs} +1 -1
- package/dist/{checkbox-CbIUMIYt.js → checkbox-ejBUa5qQ.js} +4 -4
- package/dist/{class-map-3ADKve8g.js → class-map-CEnykyO_.js} +1 -1
- package/dist/{class-map-Bokp1SoS.cjs → class-map-CsENJXf1.cjs} +1 -1
- package/dist/{combobox-CwW0md6G.js → combobox-BmxSecfE.js} +13 -13
- package/dist/{combobox-TKL44xnV.cjs → combobox-Hb2V-fPX.cjs} +1 -1
- package/dist/{consent-CgS1L1xF.js → consent-iXyx5v6J.js} +3 -3
- package/dist/{consent-CIhFwuFO.cjs → consent-ruWAEVXW.cjs} +1 -1
- package/dist/{datepicker-qRWWzmbo.js → datepicker-CZl7b8Na.js} +12 -12
- package/dist/{datepicker-oN9L0Wnm.cjs → datepicker-D2H7U6MS.cjs} +1 -1
- package/dist/{directive-helpers-B3Vpl3xV.cjs → directive-helpers-BP2WOx60.cjs} +1 -1
- package/dist/{directive-helpers-CyPtA1_i.js → directive-helpers-rm-28gIj.js} +1 -1
- package/dist/{element-CV9utnHJ.js → element-CPpT3rFQ.js} +68 -67
- package/dist/{element-CMTfByxQ.cjs → element-gR_VtSiD.cjs} +6 -6
- package/dist/{element-with-slot-1djk8KXi.js → element-with-slot-Dqk6-TRR.js} +2 -2
- package/dist/{element-with-slot-yT8k1Z37.cjs → element-with-slot-ZX-up7jO.cjs} +1 -1
- package/dist/{heading-D9R5UBcv.js → heading-CKt7cVn7.js} +1 -1
- package/dist/{heading-B0XRE0lq.cjs → heading-Cvz8m1xL.cjs} +1 -1
- package/dist/{helptext-B366oeR9.cjs → helptext-5Mzp65-F.cjs} +1 -1
- package/dist/{helptext-DepcDYFV.js → helptext-jIoWRcCF.js} +5 -5
- package/dist/{icon-D0IQAVwS.js → icon-BlKA8YuW.js} +1 -1
- package/dist/{icon-Dj0oZZSa.cjs → icon-Bqj9oBbl.cjs} +1 -1
- package/dist/{if-defined-rXcLNTzN.js → if-defined-B0krMLad.js} +1 -1
- package/dist/{if-defined-hKKmbsI8.cjs → if-defined-BOW3ODSj.cjs} +1 -1
- package/dist/index.d.ts +92 -0
- package/dist/{input-element-n5U38pZ1.js → input-element-8nLwuQS4.js} +3 -3
- package/dist/{input-element-Xe9FTi2a.cjs → input-element-DdVhUvva.cjs} +1 -1
- package/dist/{input-wrapper-BdFdJU-k.cjs → input-wrapper-B0MkQ0IZ.cjs} +1 -1
- package/dist/{input-wrapper-B__6jW4U.js → input-wrapper-B2JpK016.js} +7 -7
- package/dist/{link-DLjXMpvi.cjs → link-R_lNhViF.cjs} +1 -1
- package/dist/{link-oGYvBLYL.js → link-Z_GizclR.js} +4 -4
- package/dist/{linkcard-BhVIc18n.js → linkcard-BlQvdB1a.js} +4 -4
- package/dist/{linkcard-C20WC6Nw.cjs → linkcard-C7HbSLlX.cjs} +1 -1
- package/dist/{listbox-CS4WQOuf.js → listbox-BQKDZvZn.js} +5 -5
- package/dist/{listbox-OAVh_U7u.cjs → listbox-CGgV9MAe.cjs} +1 -1
- package/dist/{loader-BHt9i5Xp.js → loader-Ci3CHU8k.js} +5 -5
- package/dist/{loader-DxAGCcNF.cjs → loader-e2gs_1xz.cjs} +1 -1
- package/dist/{messagebox-DkspcMQg.js → messagebox-Bf-XMlvW.js} +5 -5
- package/dist/{messagebox-B8LW0PSQ.cjs → messagebox-D67w71VX.cjs} +1 -1
- package/dist/{modal-mWn5pjs8.cjs → modal-CyvEkbfQ.cjs} +1 -1
- package/dist/{modal-D3u6ueP5.js → modal-DzGe1g8Z.js} +6 -6
- package/dist/pkt-accordion.cjs +1 -1
- package/dist/pkt-accordion.js +2 -2
- package/dist/pkt-alert.cjs +1 -1
- package/dist/pkt-alert.js +1 -1
- package/dist/pkt-backlink.cjs +1 -1
- package/dist/pkt-backlink.js +1 -1
- package/dist/pkt-breadcrumbs.cjs +1 -1
- package/dist/pkt-breadcrumbs.js +2 -2
- package/dist/pkt-button.cjs +1 -1
- package/dist/pkt-button.js +1 -1
- package/dist/pkt-calendar.cjs +1 -1
- package/dist/pkt-calendar.js +1 -1
- package/dist/pkt-card.cjs +1 -1
- package/dist/pkt-card.js +1 -1
- package/dist/pkt-checkbox.cjs +1 -1
- package/dist/pkt-checkbox.js +1 -1
- package/dist/pkt-combobox.cjs +1 -1
- package/dist/pkt-combobox.js +1 -1
- package/dist/pkt-consent.cjs +1 -1
- package/dist/pkt-consent.js +1 -1
- package/dist/pkt-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +2 -2
- package/dist/pkt-header.cjs +1 -1
- package/dist/pkt-header.js +10 -10
- package/dist/pkt-heading.cjs +1 -1
- package/dist/pkt-heading.js +1 -1
- package/dist/pkt-helptext.cjs +1 -1
- package/dist/pkt-helptext.js +1 -1
- package/dist/pkt-icon.cjs +1 -1
- package/dist/pkt-icon.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +33 -31
- package/dist/pkt-input-wrapper.cjs +1 -1
- package/dist/pkt-input-wrapper.js +1 -1
- package/dist/pkt-link.cjs +1 -1
- package/dist/pkt-link.js +1 -1
- package/dist/pkt-linkcard.cjs +1 -1
- package/dist/pkt-linkcard.js +1 -1
- package/dist/pkt-listbox.cjs +1 -1
- package/dist/pkt-listbox.js +1 -1
- package/dist/pkt-loader.cjs +1 -1
- package/dist/pkt-loader.js +1 -1
- package/dist/pkt-messagebox.cjs +1 -1
- package/dist/pkt-messagebox.js +1 -1
- package/dist/pkt-modal.cjs +1 -1
- package/dist/pkt-modal.js +1 -1
- package/dist/{pkt-options-controller-DIeKSpWv.cjs → pkt-options-controller-5LXbEzuk.cjs} +1 -1
- package/dist/{pkt-options-controller-W4ITGQy9.js → pkt-options-controller-B5YalfCb.js} +1 -1
- package/dist/pkt-progressbar.cjs +1 -1
- package/dist/pkt-progressbar.js +2 -2
- package/dist/pkt-radiobutton.cjs +1 -1
- package/dist/pkt-radiobutton.js +1 -1
- package/dist/pkt-select.cjs +1 -1
- package/dist/pkt-select.js +1 -1
- package/dist/pkt-tabs.cjs +1 -1
- package/dist/pkt-tabs.js +2 -2
- package/dist/pkt-tag.cjs +1 -1
- package/dist/pkt-tag.js +1 -1
- package/dist/pkt-textarea.cjs +1 -1
- package/dist/pkt-textarea.js +1 -1
- package/dist/pkt-textinput.cjs +1 -1
- package/dist/pkt-textinput.js +1 -1
- package/dist/pkt-timepicker.cjs +188 -0
- package/dist/pkt-timepicker.js +552 -0
- package/dist/{progressbar-CcX-xiPI.js → progressbar-By7e0lEI.js} +5 -5
- package/dist/{progressbar-CC9Qr82_.cjs → progressbar-DsJ2B9Vm.cjs} +1 -1
- package/dist/{radiobutton-RW3h0tJj.js → radiobutton-6frNG9J_.js} +5 -5
- package/dist/{radiobutton-BUDQYAH-.cjs → radiobutton-Bv-BZ1Hw.cjs} +1 -1
- package/dist/{ref-DBGHvw2G.js → ref-Bg_Swj5w.js} +2 -2
- package/dist/{ref-DahIePMT.cjs → ref-CL-Xk9Xm.cjs} +1 -1
- package/dist/{repeat-DnlRNf63.js → repeat-BD8TKgJb.js} +2 -2
- package/dist/{repeat-C-FJ2vfy.cjs → repeat-Cfq8CDlf.cjs} +1 -1
- package/dist/{select-iXKeM3yz.js → select-CXZt1nHS.js} +8 -8
- package/dist/{select-PuEeK4jQ.cjs → select-WFhS4Vfp.cjs} +1 -1
- package/dist/{state-l4hGZdFJ.js → state-C_1EOo3D.js} +1 -1
- package/dist/{state-BNgpvY-A.cjs → state-Dg-53wQ8.cjs} +1 -1
- package/dist/{tabitem-DZSzwag2.js → tabitem-CB8wkKmv.js} +5 -5
- package/dist/{tabitem-Jenus4Qd.cjs → tabitem-DZpspU4Q.cjs} +1 -1
- package/dist/{tag-CIWBLpjv.js → tag-BuCpqaks.js} +6 -6
- package/dist/{tag-DeE1OpmF.cjs → tag-X6xdjR0N.cjs} +1 -1
- package/dist/{textarea-DR9lVDN-.js → textarea-Bzuymclz.js} +10 -10
- package/dist/{textarea-DtiYQWjy.cjs → textarea-DoeHJ7HR.cjs} +1 -1
- package/dist/{textinput-0Bttvt9V.js → textinput-9lHjs4ce.js} +9 -9
- package/dist/{textinput-cKrjvbJW.cjs → textinput-BNsLd26i.cjs} +1 -1
- package/dist/timepicker.d.ts +1 -0
- package/package.json +2 -3
- package/src/components/index.ts +3 -0
- package/src/components/timepicker/index.ts +2 -0
- package/src/components/timepicker/timepicker.test.ts +453 -0
- package/src/components/timepicker/timepicker.ts +787 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { axe, toHaveNoViolations } from 'jest-axe'
|
|
3
|
+
import { fireEvent } from '@testing-library/dom'
|
|
4
|
+
import { vi } from 'vitest'
|
|
5
|
+
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
|
|
6
|
+
import { CustomElementFor } from '../../tests/component-registry'
|
|
7
|
+
import './timepicker'
|
|
8
|
+
|
|
9
|
+
export interface TimepickerTestConfig extends BaseTestConfig {
|
|
10
|
+
value?: string
|
|
11
|
+
min?: string
|
|
12
|
+
max?: string
|
|
13
|
+
step?: number
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
required?: boolean
|
|
16
|
+
label?: string
|
|
17
|
+
'hide-picker'?: boolean
|
|
18
|
+
'step-arrows'?: boolean
|
|
19
|
+
fullwidth?: boolean
|
|
20
|
+
hasError?: boolean
|
|
21
|
+
errorMessage?: string
|
|
22
|
+
helptext?: string
|
|
23
|
+
id?: string
|
|
24
|
+
name?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const createTimepickerTest = async (config: TimepickerTestConfig = {}) => {
|
|
28
|
+
const { container, element } = await createElementTest<
|
|
29
|
+
CustomElementFor<'pkt-timepicker'>,
|
|
30
|
+
TimepickerTestConfig
|
|
31
|
+
>('pkt-timepicker', config)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
container,
|
|
35
|
+
timepicker: element,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
expect.extend(toHaveNoViolations)
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
document.body.innerHTML = ''
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('PktTimepicker', () => {
|
|
46
|
+
describe('Rendering', () => {
|
|
47
|
+
test('renders without errors', async () => {
|
|
48
|
+
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
|
|
49
|
+
expect(timepicker).toBeInTheDocument()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('renders hours and minutes inputs', async () => {
|
|
53
|
+
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
|
|
54
|
+
const hoursInput = timepicker.querySelector('#test-hours')
|
|
55
|
+
const minutesInput = timepicker.querySelector('#test-minutes')
|
|
56
|
+
expect(hoursInput).toBeInTheDocument()
|
|
57
|
+
expect(minutesInput).toBeInTheDocument()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('renders clock button by default', async () => {
|
|
61
|
+
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
|
|
62
|
+
const button = timepicker.querySelector('.pkt-timepicker__button')
|
|
63
|
+
expect(button).toBeInTheDocument()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('does not render clock button when hide-picker', async () => {
|
|
67
|
+
const { timepicker } = await createTimepickerTest({
|
|
68
|
+
label: 'Tidspunkt',
|
|
69
|
+
id: 'test',
|
|
70
|
+
'hide-picker': true,
|
|
71
|
+
})
|
|
72
|
+
const button = timepicker.querySelector('.pkt-timepicker__button')
|
|
73
|
+
expect(button).not.toBeInTheDocument()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('renders decorative icon when hide-picker', async () => {
|
|
77
|
+
const { timepicker } = await createTimepickerTest({
|
|
78
|
+
label: 'Tidspunkt',
|
|
79
|
+
id: 'test',
|
|
80
|
+
'hide-picker': true,
|
|
81
|
+
})
|
|
82
|
+
const icon = timepicker.querySelector('.pkt-timepicker__icon')
|
|
83
|
+
expect(icon).toBeInTheDocument()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('renders prev/next buttons when step-arrows', async () => {
|
|
87
|
+
const { timepicker } = await createTimepickerTest({
|
|
88
|
+
label: 'Tidspunkt',
|
|
89
|
+
id: 'test',
|
|
90
|
+
'step-arrows': true,
|
|
91
|
+
})
|
|
92
|
+
const prev = timepicker.querySelector('.pkt-timepicker__button--prev')
|
|
93
|
+
const next = timepicker.querySelector('.pkt-timepicker__button--next')
|
|
94
|
+
expect(prev).toBeInTheDocument()
|
|
95
|
+
expect(next).toBeInTheDocument()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('does not render popup when hide-picker', async () => {
|
|
99
|
+
const { timepicker } = await createTimepickerTest({
|
|
100
|
+
label: 'Tidspunkt',
|
|
101
|
+
id: 'test',
|
|
102
|
+
'hide-picker': true,
|
|
103
|
+
})
|
|
104
|
+
const popup = timepicker.querySelector('.pkt-timepicker-popup')
|
|
105
|
+
expect(popup).not.toBeInTheDocument()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('does not render popup when step-arrows', async () => {
|
|
109
|
+
const { timepicker } = await createTimepickerTest({
|
|
110
|
+
label: 'Tidspunkt',
|
|
111
|
+
id: 'test',
|
|
112
|
+
'step-arrows': true,
|
|
113
|
+
})
|
|
114
|
+
const popup = timepicker.querySelector('.pkt-timepicker-popup')
|
|
115
|
+
expect(popup).not.toBeInTheDocument()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('renders hidden popup by default (not open)', async () => {
|
|
119
|
+
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
|
|
120
|
+
const popup = timepicker.querySelector('.pkt-timepicker-popup')
|
|
121
|
+
expect(popup).toBeInTheDocument()
|
|
122
|
+
expect(popup).toHaveAttribute('hidden')
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('Properties', () => {
|
|
127
|
+
test('value sets display inputs correctly', async () => {
|
|
128
|
+
const { timepicker } = await createTimepickerTest({
|
|
129
|
+
label: 'Tidspunkt',
|
|
130
|
+
id: 'test',
|
|
131
|
+
value: '09:30',
|
|
132
|
+
})
|
|
133
|
+
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
|
|
134
|
+
const minutesInput = timepicker.querySelector<HTMLInputElement>('#test-minutes')
|
|
135
|
+
expect(hoursInput?.value).toBe('09')
|
|
136
|
+
expect(minutesInput?.value).toBe('30')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('empty value renders empty display inputs', async () => {
|
|
140
|
+
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
|
|
141
|
+
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
|
|
142
|
+
const minutesInput = timepicker.querySelector<HTMLInputElement>('#test-minutes')
|
|
143
|
+
expect(hoursInput?.value).toBe('')
|
|
144
|
+
expect(minutesInput?.value).toBe('')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('disabled disables all inputs', async () => {
|
|
148
|
+
const { timepicker } = await createTimepickerTest({
|
|
149
|
+
label: 'Tidspunkt',
|
|
150
|
+
id: 'test',
|
|
151
|
+
disabled: true,
|
|
152
|
+
})
|
|
153
|
+
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
|
|
154
|
+
const minutesInput = timepicker.querySelector<HTMLInputElement>('#test-minutes')
|
|
155
|
+
expect(hoursInput).toBeDisabled()
|
|
156
|
+
expect(minutesInput).toBeDisabled()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('disabled disables the clock button', async () => {
|
|
160
|
+
const { timepicker } = await createTimepickerTest({
|
|
161
|
+
label: 'Tidspunkt',
|
|
162
|
+
id: 'test',
|
|
163
|
+
disabled: true,
|
|
164
|
+
})
|
|
165
|
+
const button = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button')
|
|
166
|
+
expect(button).toBeDisabled()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('step-arrows adds pkt-timepicker--stepper class to host', async () => {
|
|
170
|
+
const { timepicker } = await createTimepickerTest({
|
|
171
|
+
label: 'Tidspunkt',
|
|
172
|
+
id: 'test',
|
|
173
|
+
'step-arrows': true,
|
|
174
|
+
})
|
|
175
|
+
expect(timepicker.classList.contains('pkt-timepicker--stepper')).toBe(true)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('fullwidth adds pkt-timepicker--fullwidth class to host', async () => {
|
|
179
|
+
const { timepicker } = await createTimepickerTest({
|
|
180
|
+
label: 'Tidspunkt',
|
|
181
|
+
id: 'test',
|
|
182
|
+
fullwidth: true,
|
|
183
|
+
})
|
|
184
|
+
expect(timepicker.classList.contains('pkt-timepicker--fullwidth')).toBe(true)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('min/max/step forwarded to hidden input', async () => {
|
|
188
|
+
const { timepicker } = await createTimepickerTest({
|
|
189
|
+
label: 'Tidspunkt',
|
|
190
|
+
id: 'test',
|
|
191
|
+
min: '08:00',
|
|
192
|
+
max: '17:00',
|
|
193
|
+
step: 300,
|
|
194
|
+
})
|
|
195
|
+
const hiddenInput = timepicker.querySelector<HTMLInputElement>('#test-input')
|
|
196
|
+
expect(hiddenInput?.getAttribute('min')).toBe('08:00')
|
|
197
|
+
expect(hiddenInput?.getAttribute('max')).toBe('17:00')
|
|
198
|
+
expect(hiddenInput?.getAttribute('step')).toBe('300')
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('Value management', () => {
|
|
203
|
+
test('external value change updates display', async () => {
|
|
204
|
+
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
|
|
205
|
+
timepicker.value = '15:45'
|
|
206
|
+
await timepicker.updateComplete
|
|
207
|
+
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
|
|
208
|
+
const minutesInput = timepicker.querySelector<HTMLInputElement>('#test-minutes')
|
|
209
|
+
expect(hoursInput?.value).toBe('15')
|
|
210
|
+
expect(minutesInput?.value).toBe('45')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('invalid value format clears display', async () => {
|
|
214
|
+
const { timepicker } = await createTimepickerTest({
|
|
215
|
+
label: 'Tidspunkt',
|
|
216
|
+
id: 'test',
|
|
217
|
+
value: '09:30',
|
|
218
|
+
})
|
|
219
|
+
timepicker.value = 'invalid'
|
|
220
|
+
await timepicker.updateComplete
|
|
221
|
+
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
|
|
222
|
+
expect(hoursInput?.value).toBe('')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('hidden input reflects value', async () => {
|
|
226
|
+
const { timepicker } = await createTimepickerTest({
|
|
227
|
+
label: 'Tidspunkt',
|
|
228
|
+
id: 'test',
|
|
229
|
+
value: '09:30',
|
|
230
|
+
})
|
|
231
|
+
const hiddenInput = timepicker.querySelector<HTMLInputElement>('#test-input')
|
|
232
|
+
expect(hiddenInput?.value).toBe('09:30')
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('Events', () => {
|
|
237
|
+
test('change fires when value changes via ArrowUp', async () => {
|
|
238
|
+
const { timepicker } = await createTimepickerTest({
|
|
239
|
+
label: 'Tidspunkt',
|
|
240
|
+
id: 'test',
|
|
241
|
+
value: '09:30',
|
|
242
|
+
})
|
|
243
|
+
const changeHandler = vi.fn()
|
|
244
|
+
timepicker.addEventListener('change', changeHandler)
|
|
245
|
+
timepicker.touched = true
|
|
246
|
+
|
|
247
|
+
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')!
|
|
248
|
+
fireEvent.keyDown(hoursInput, { key: 'ArrowUp' })
|
|
249
|
+
await timepicker.updateComplete
|
|
250
|
+
|
|
251
|
+
expect(changeHandler).toHaveBeenCalled()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('value-change fires with HH:MM detail', async () => {
|
|
255
|
+
const { timepicker } = await createTimepickerTest({
|
|
256
|
+
label: 'Tidspunkt',
|
|
257
|
+
id: 'test',
|
|
258
|
+
value: '09:30',
|
|
259
|
+
})
|
|
260
|
+
const valueChangeHandler = vi.fn()
|
|
261
|
+
timepicker.addEventListener('value-change', valueChangeHandler)
|
|
262
|
+
timepicker.touched = true
|
|
263
|
+
|
|
264
|
+
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')!
|
|
265
|
+
fireEvent.keyDown(hoursInput, { key: 'ArrowUp' })
|
|
266
|
+
await timepicker.updateComplete
|
|
267
|
+
|
|
268
|
+
expect(valueChangeHandler).toHaveBeenCalledWith(
|
|
269
|
+
expect.objectContaining({ detail: '10:30' }),
|
|
270
|
+
)
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('Popup', () => {
|
|
275
|
+
test('clock button opens popup', async () => {
|
|
276
|
+
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
|
|
277
|
+
const button = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button')!
|
|
278
|
+
fireEvent.click(button)
|
|
279
|
+
await timepicker.updateComplete
|
|
280
|
+
const popup = timepicker.querySelector('.pkt-timepicker-popup')
|
|
281
|
+
expect(popup).not.toHaveAttribute('hidden')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('clock button toggles popup closed', async () => {
|
|
285
|
+
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
|
|
286
|
+
const button = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button')!
|
|
287
|
+
fireEvent.click(button)
|
|
288
|
+
await timepicker.updateComplete
|
|
289
|
+
fireEvent.click(button)
|
|
290
|
+
await timepicker.updateComplete
|
|
291
|
+
const popup = timepicker.querySelector('.pkt-timepicker-popup')
|
|
292
|
+
expect(popup).toHaveAttribute('hidden')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('popup renders hour and minute columns', async () => {
|
|
296
|
+
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
|
|
297
|
+
const button = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button')!
|
|
298
|
+
fireEvent.click(button)
|
|
299
|
+
await timepicker.updateComplete
|
|
300
|
+
const cols = timepicker.querySelectorAll('.pkt-timepicker-popup__col')
|
|
301
|
+
expect(cols).toHaveLength(2)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('selecting hour option updates hours display', async () => {
|
|
305
|
+
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
|
|
306
|
+
timepicker.touched = true
|
|
307
|
+
const button = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button')!
|
|
308
|
+
fireEvent.click(button)
|
|
309
|
+
await timepicker.updateComplete
|
|
310
|
+
|
|
311
|
+
const option = timepicker.querySelector<HTMLButtonElement>(
|
|
312
|
+
'[data-type="hour"][data-value="9"]',
|
|
313
|
+
)!
|
|
314
|
+
fireEvent.click(option)
|
|
315
|
+
await timepicker.updateComplete
|
|
316
|
+
|
|
317
|
+
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
|
|
318
|
+
expect(hoursInput?.value).toBe('09')
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
describe('Stepper', () => {
|
|
323
|
+
test('next button increments by step', async () => {
|
|
324
|
+
const { timepicker } = await createTimepickerTest({
|
|
325
|
+
label: 'Tidspunkt',
|
|
326
|
+
id: 'test',
|
|
327
|
+
'step-arrows': true,
|
|
328
|
+
value: '09:00',
|
|
329
|
+
})
|
|
330
|
+
timepicker.touched = true
|
|
331
|
+
const next = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button--next')!
|
|
332
|
+
fireEvent.click(next)
|
|
333
|
+
await timepicker.updateComplete
|
|
334
|
+
expect(timepicker.value).toBe('09:01')
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
test('prev button decrements by step', async () => {
|
|
338
|
+
const { timepicker } = await createTimepickerTest({
|
|
339
|
+
label: 'Tidspunkt',
|
|
340
|
+
id: 'test',
|
|
341
|
+
'step-arrows': true,
|
|
342
|
+
value: '09:05',
|
|
343
|
+
step: 300,
|
|
344
|
+
})
|
|
345
|
+
timepicker.touched = true
|
|
346
|
+
const prev = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button--prev')!
|
|
347
|
+
fireEvent.click(prev)
|
|
348
|
+
await timepicker.updateComplete
|
|
349
|
+
expect(timepicker.value).toBe('09:00')
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
test('next button at 59 minutes rolls over to next hour', async () => {
|
|
353
|
+
const { timepicker } = await createTimepickerTest({
|
|
354
|
+
label: 'Tidspunkt',
|
|
355
|
+
id: 'test',
|
|
356
|
+
'step-arrows': true,
|
|
357
|
+
value: '09:59',
|
|
358
|
+
})
|
|
359
|
+
timepicker.touched = true
|
|
360
|
+
const next = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button--next')!
|
|
361
|
+
fireEvent.click(next)
|
|
362
|
+
await timepicker.updateComplete
|
|
363
|
+
expect(timepicker.value).toBe('10:00')
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
describe('Form integration', () => {
|
|
368
|
+
test('required with empty value fails validation', async () => {
|
|
369
|
+
const { timepicker } = await createTimepickerTest({
|
|
370
|
+
label: 'Tidspunkt',
|
|
371
|
+
id: 'test',
|
|
372
|
+
name: 'tidspunkt',
|
|
373
|
+
required: true,
|
|
374
|
+
})
|
|
375
|
+
expect(timepicker.internals.validity.valid).toBe(false)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
test('form reset clears value', async () => {
|
|
379
|
+
const container = document.createElement('div')
|
|
380
|
+
container.innerHTML = `
|
|
381
|
+
<form>
|
|
382
|
+
<pkt-timepicker id="test" label="Tidspunkt" name="tidspunkt" value="09:30"></pkt-timepicker>
|
|
383
|
+
</form>
|
|
384
|
+
`
|
|
385
|
+
document.body.appendChild(container)
|
|
386
|
+
await customElements.whenDefined('pkt-timepicker')
|
|
387
|
+
const timepicker = container.querySelector('pkt-timepicker') as CustomElementFor<'pkt-timepicker'>
|
|
388
|
+
await timepicker.updateComplete
|
|
389
|
+
|
|
390
|
+
const form = container.querySelector('form')!
|
|
391
|
+
form.reset()
|
|
392
|
+
await timepicker.updateComplete
|
|
393
|
+
|
|
394
|
+
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
|
|
395
|
+
expect(hoursInput?.value).toBe('')
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
describe('Accessibility', () => {
|
|
400
|
+
test('passes axe in default state', async () => {
|
|
401
|
+
const { container } = await createTimepickerTest({
|
|
402
|
+
label: 'Tidspunkt',
|
|
403
|
+
id: 'test-axe',
|
|
404
|
+
})
|
|
405
|
+
const results = await axe(container)
|
|
406
|
+
expect(results).toHaveNoViolations()
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test('passes axe with step-arrows', async () => {
|
|
410
|
+
const { container } = await createTimepickerTest({
|
|
411
|
+
label: 'Tidspunkt',
|
|
412
|
+
id: 'test-axe-stepper',
|
|
413
|
+
'step-arrows': true,
|
|
414
|
+
})
|
|
415
|
+
const results = await axe(container)
|
|
416
|
+
expect(results).toHaveNoViolations()
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
test('passes axe when disabled', async () => {
|
|
420
|
+
const { container } = await createTimepickerTest({
|
|
421
|
+
label: 'Tidspunkt',
|
|
422
|
+
id: 'test-axe-disabled',
|
|
423
|
+
disabled: true,
|
|
424
|
+
})
|
|
425
|
+
const results = await axe(container)
|
|
426
|
+
expect(results).toHaveNoViolations()
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
test('passes axe with hasError and errorMessage', async () => {
|
|
430
|
+
const { container } = await createTimepickerTest({
|
|
431
|
+
label: 'Tidspunkt',
|
|
432
|
+
id: 'test-axe-error',
|
|
433
|
+
hasError: true,
|
|
434
|
+
errorMessage: 'Ugyldig tidspunkt',
|
|
435
|
+
})
|
|
436
|
+
const results = await axe(container)
|
|
437
|
+
expect(results).toHaveNoViolations()
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
test('hours input has correct spinbutton ARIA', async () => {
|
|
441
|
+
const { timepicker } = await createTimepickerTest({
|
|
442
|
+
label: 'Tidspunkt',
|
|
443
|
+
id: 'test',
|
|
444
|
+
value: '09:30',
|
|
445
|
+
})
|
|
446
|
+
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')!
|
|
447
|
+
expect(hoursInput.getAttribute('role')).toBe('spinbutton')
|
|
448
|
+
expect(hoursInput.getAttribute('aria-valuemin')).toBe('0')
|
|
449
|
+
expect(hoursInput.getAttribute('aria-valuemax')).toBe('23')
|
|
450
|
+
expect(hoursInput.getAttribute('aria-valuenow')).toBe('09')
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
})
|