@oslokommune/punkt-elements 16.16.1 → 16.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/dist/{accordion-nNH87Y60.js → accordion-CU2tJ5vn.js} +23 -9
- package/dist/{accordion-BB1JrONI.cjs → accordion-DDZkjXFb.cjs} +20 -7
- package/dist/{checkbox-nTbihMxW.cjs → checkbox-BerAYcfK.cjs} +1 -1
- package/dist/{checkbox-Bjp43NxA.js → checkbox-CF5mSBR5.js} +1 -1
- package/dist/{combobox-C4Q6kQEs.cjs → combobox-DYttdLJ4.cjs} +1 -1
- package/dist/{combobox-CL9BLuxt.js → combobox-DjvRMKOD.js} +3 -3
- package/dist/components/header-menu/header-menu.d.ts +41 -0
- package/dist/components/header-menu/header-menu.test.d.ts +0 -0
- package/dist/components/header-menu/index.d.ts +5 -0
- package/dist/components/header-menu/types.d.ts +6 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/searchinput/index.d.ts +4 -0
- package/dist/components/searchinput/searchinput.d.ts +27 -0
- package/dist/components/searchinput/searchinput.test.d.ts +0 -0
- package/dist/{datepicker-DYd3JCs_.cjs → datepicker-D3QQbd_H.cjs} +1 -1
- package/dist/{datepicker-BUK1Qanw.js → datepicker-M_jcY3wp.js} +2 -2
- package/dist/docs/components/header-full.d.ts +13 -0
- package/dist/docs/components/header-menu.d.ts +8 -0
- package/dist/docs/components/searchinput.d.ts +14 -0
- package/dist/{fileupload-DjUAvBcP.cjs → fileupload-BD_0oMrW.cjs} +1 -1
- package/dist/{fileupload-DbmqRkz3.js → fileupload-F9UMO7eS.js} +3 -3
- package/dist/focus-modality-controller-BSZAQk8y.cjs +1 -0
- package/dist/focus-modality-controller-pzSZkDkH.js +29 -0
- package/dist/{header-Dps8o-ZY.cjs → header-DZranHLm.cjs} +2 -2
- package/dist/{header-T5O6sfCN.js → header-j0SSHF4C.js} +4 -3
- package/dist/header-menu-CPI1_5oZ.cjs +126 -0
- package/dist/header-menu-zhGYdnvz.js +319 -0
- package/dist/header-menu.d.ts +1 -0
- package/dist/{helptext-C5rG4VkA.js → helptext-BdqoW19r.js} +1 -1
- package/dist/{helptext-DQgStO4D.cjs → helptext-KWXjKljV.cjs} +1 -1
- package/dist/{input-element-CjebcQd1.js → input-element-B1ciW1N2.js} +1 -1
- package/dist/{input-element-FGcr5hGd.cjs → input-element-D0egtEnr.cjs} +1 -1
- package/dist/{input-wrapper-B_XFi211.cjs → input-wrapper-DOIWggEv.cjs} +1 -1
- package/dist/{input-wrapper-DBpMVSxt.js → input-wrapper-DVU68NcJ.js} +5 -29
- package/dist/{modal-B6591sWa.cjs → modal-C_LxgxzC.cjs} +1 -1
- package/dist/{modal-DyIC3uhs.js → modal-Ca7dETTw.js} +1 -1
- package/dist/pkt-accordion.cjs +1 -1
- package/dist/pkt-accordion.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 +1 -1
- package/dist/pkt-fileupload.cjs +1 -1
- package/dist/pkt-fileupload.js +1 -1
- package/dist/pkt-header-menu.cjs +1 -0
- package/dist/pkt-header-menu.js +2 -0
- package/dist/pkt-header.cjs +1 -1
- package/dist/pkt-header.js +1 -1
- package/dist/pkt-helptext.cjs +1 -1
- package/dist/pkt-helptext.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +24 -22
- 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-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-progressbar.cjs +1 -1
- package/dist/pkt-progressbar.js +1 -1
- package/dist/pkt-radiobutton.cjs +1 -1
- package/dist/pkt-radiobutton.js +1 -1
- package/dist/pkt-searchinput.cjs +1 -0
- package/dist/pkt-searchinput.js +2 -0
- 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 +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 +1 -1
- package/dist/pkt-timepicker.js +1 -1
- package/dist/{radiobutton-DvXr7tDk.cjs → radiobutton-B1Xg6XzP.cjs} +1 -1
- package/dist/{radiobutton-B-8-wWFf.js → radiobutton-BAn1V_mQ.js} +1 -1
- package/dist/searchinput-BVIaoetB.cjs +60 -0
- package/dist/searchinput-CIseS5tL.js +147 -0
- package/dist/searchinput.d.ts +1 -0
- package/dist/{select-D9B8Ighz.js → select-6XCVj1lP.js} +3 -3
- package/dist/{select-B75wzvyz.cjs → select-DO0xGhQQ.cjs} +1 -1
- package/dist/shared-types/header-menu.d.ts +108 -0
- package/dist/shared-types/index.d.ts +3 -0
- package/dist/shared-types/searchinput.d.ts +41 -0
- package/dist/shared-types/skin.d.ts +1 -1
- package/dist/shared-utils/header-menu/example-data.d.ts +11 -0
- package/dist/shared-utils/header-menu/fetch-header-footer-data.d.ts +12 -0
- package/dist/shared-utils/header-menu/icon-map.d.ts +34 -0
- package/dist/shared-utils/header-menu/index.d.ts +11 -0
- package/dist/shared-utils/header-menu/select-megamenu.d.ts +16 -0
- package/dist/tests/component-registry.d.ts +2 -0
- package/dist/{textarea-CjqprL6Y.cjs → textarea-CV6z3LDf.cjs} +1 -1
- package/dist/{textarea-CIWXMaAD.js → textarea-DBSNSvrX.js} +2 -2
- package/dist/{textinput-CxzAxM8E.js → textinput-BZZnFDk3.js} +2 -2
- package/dist/{textinput-B0VWmVIF.cjs → textinput-Dsc-8-BB.cjs} +1 -1
- package/dist/{timepicker-Cv9juQeB.cjs → timepicker-C2Ua6vHY.cjs} +1 -1
- package/dist/{timepicker-DXrpg5C7.js → timepicker-DDQ7CfOy.js} +2 -2
- package/package.json +2 -2
- package/src/components/accordion/accordionitem.ts +22 -5
- package/src/components/header/header-service.ts +1 -0
- package/src/components/header-menu/header-menu.test.ts +314 -0
- package/src/components/header-menu/header-menu.ts +401 -0
- package/src/components/header-menu/index.ts +6 -0
- package/src/components/header-menu/types.ts +31 -0
- package/src/components/index.ts +10 -0
- package/src/components/searchinput/index.ts +10 -0
- package/src/components/searchinput/searchinput.test.ts +312 -0
- package/src/components/searchinput/searchinput.ts +213 -0
- /package/dist/{consent-zh2npq2K.cjs → consent-B5dIDyLD.cjs} +0 -0
- /package/dist/{consent-f_IGyHss.js → consent-DSHrt9eS.js} +0 -0
- /package/dist/{dialog-polyfill.esm-BmVHGRTX.js → dialog-polyfill.esm-B7W3teD0.js} +0 -0
- /package/dist/{dialog-polyfill.esm-CXDsb3l-.cjs → dialog-polyfill.esm-BnAQvMQO.cjs} +0 -0
- /package/dist/{input-wrapper-CYCCx8MP.cjs → input-wrapper-CQGoyLdZ.cjs} +0 -0
- /package/dist/{input-wrapper-kw2y6Jp_.js → input-wrapper-DwSsP39J.js} +0 -0
- /package/dist/{link-B8zYc5WK.cjs → link-DMKjKawh.cjs} +0 -0
- /package/dist/{link-CjJcwcxR.js → link-F51Ho7CV.js} +0 -0
- /package/dist/{linkcard-C4x-NJMi.cjs → linkcard-CBmJbSoV.cjs} +0 -0
- /package/dist/{linkcard-Ddh5R_L-.js → linkcard-E07qJZMP.js} +0 -0
- /package/dist/{loader-CGQXAuyM.cjs → loader-q_eO6zaE.cjs} +0 -0
- /package/dist/{loader-DeizpX45.js → loader-teN65oWo.js} +0 -0
- /package/dist/{messagebox-DqoPqo4K.js → messagebox-Ct9lfe0Z.js} +0 -0
- /package/dist/{messagebox-Ccu29rDv.cjs → messagebox-D1QeQqb8.cjs} +0 -0
- /package/dist/{pkt-options-controller-DbLrdRwH.js → pkt-options-controller-CCB15L_G.js} +0 -0
- /package/dist/{pkt-options-controller-BPBb_O2K.cjs → pkt-options-controller-CeAQ-D-i.cjs} +0 -0
- /package/dist/{progressbar-CrwpnIkv.cjs → progressbar-Ca7pRjmt.cjs} +0 -0
- /package/dist/{progressbar-uC-0W6o4.js → progressbar-o-M17HPA.js} +0 -0
- /package/dist/{tabs-RnDtIFv3.cjs → tabs-CN_RIMWg.cjs} +0 -0
- /package/dist/{tabs-4eWA7CsK.js → tabs-ClI1XQDt.js} +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { fireEvent } from '@testing-library/dom'
|
|
3
|
+
import { axe, toHaveNoViolations } from 'jest-axe'
|
|
4
|
+
import { vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
|
|
7
|
+
import { CustomElementFor } from '../../tests/component-registry'
|
|
8
|
+
import './searchinput'
|
|
9
|
+
|
|
10
|
+
expect.extend(toHaveNoViolations)
|
|
11
|
+
|
|
12
|
+
interface SearchInputTestConfig extends BaseTestConfig {
|
|
13
|
+
id?: string
|
|
14
|
+
name?: string
|
|
15
|
+
appearance?: 'local' | 'local-with-button' | 'global'
|
|
16
|
+
label?: string
|
|
17
|
+
placeholder?: string
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
fullwidth?: boolean
|
|
20
|
+
action?: string
|
|
21
|
+
method?: 'get' | 'post' | 'dialog'
|
|
22
|
+
value?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const createSearchInputTest = async (config: SearchInputTestConfig = {}) => {
|
|
26
|
+
const { container, element } = await createElementTest<
|
|
27
|
+
CustomElementFor<'pkt-searchinput'>,
|
|
28
|
+
SearchInputTestConfig
|
|
29
|
+
>('pkt-searchinput', {
|
|
30
|
+
id: 'test-searchinput',
|
|
31
|
+
...config,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
container,
|
|
36
|
+
searchinput: element,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
document.body.innerHTML = ''
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('PktSearchInput', () => {
|
|
45
|
+
describe('wrapper', () => {
|
|
46
|
+
test('renders form when action exists', async () => {
|
|
47
|
+
const { searchinput } = await createSearchInputTest({ action: '/search', method: 'get' })
|
|
48
|
+
const wrapper = searchinput.querySelector('.pkt-searchinput')
|
|
49
|
+
expect(wrapper?.tagName.toLowerCase()).toBe('form')
|
|
50
|
+
expect(wrapper).toHaveAttribute('role', 'search')
|
|
51
|
+
expect(wrapper).toHaveAttribute('action', '/search')
|
|
52
|
+
expect(wrapper).toHaveAttribute('method', 'get')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('renders div when action does not exist', async () => {
|
|
56
|
+
const { searchinput } = await createSearchInputTest()
|
|
57
|
+
const wrapper = searchinput.querySelector('.pkt-searchinput')
|
|
58
|
+
expect(wrapper?.tagName.toLowerCase()).toBe('div')
|
|
59
|
+
expect(wrapper).toHaveAttribute('role', 'search')
|
|
60
|
+
expect(searchinput.querySelector('form')).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('label', () => {
|
|
65
|
+
test('renders label when label exists', async () => {
|
|
66
|
+
const { searchinput } = await createSearchInputTest({ label: 'Test Label' })
|
|
67
|
+
await searchinput.updateComplete
|
|
68
|
+
const label = searchinput.querySelector('label.pkt-inputwrapper__label')
|
|
69
|
+
expect(label).toBeInTheDocument()
|
|
70
|
+
expect(label?.textContent).toContain('Test Label')
|
|
71
|
+
expect(label).toHaveAttribute('for', 'test-searchinput')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('does not render label when label is absent', async () => {
|
|
75
|
+
const { searchinput } = await createSearchInputTest()
|
|
76
|
+
await searchinput.updateComplete
|
|
77
|
+
expect(searchinput.querySelector('label')).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('input attributes', () => {
|
|
82
|
+
test('input has expected type, name, id, placeholder, disabled', async () => {
|
|
83
|
+
const { searchinput } = await createSearchInputTest({
|
|
84
|
+
id: 'my-search',
|
|
85
|
+
name: 'q',
|
|
86
|
+
placeholder: 'Finn…',
|
|
87
|
+
disabled: true,
|
|
88
|
+
})
|
|
89
|
+
await searchinput.updateComplete
|
|
90
|
+
|
|
91
|
+
const input = searchinput.querySelector('input') as HTMLInputElement
|
|
92
|
+
expect(input.type).toBe('search')
|
|
93
|
+
expect(input.id).toBe('my-search')
|
|
94
|
+
expect(input.name).toBe('q')
|
|
95
|
+
expect(input.placeholder).toBe('Finn…')
|
|
96
|
+
expect(input.disabled).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('name defaults to id when name is not set', async () => {
|
|
100
|
+
const { searchinput } = await createSearchInputTest({ id: 'search-field' })
|
|
101
|
+
await searchinput.updateComplete
|
|
102
|
+
expect((searchinput.querySelector('input') as HTMLInputElement).name).toBe('search-field')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('placeholder defaults to Søk…', async () => {
|
|
106
|
+
const { searchinput } = await createSearchInputTest()
|
|
107
|
+
await searchinput.updateComplete
|
|
108
|
+
expect((searchinput.querySelector('input') as HTMLInputElement).placeholder).toBe('Søk…')
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('appearance and fullwidth classes', () => {
|
|
113
|
+
test('applies appearance modifier and fullwidth on wrapper', async () => {
|
|
114
|
+
const { searchinput } = await createSearchInputTest({
|
|
115
|
+
appearance: 'global',
|
|
116
|
+
fullwidth: true,
|
|
117
|
+
})
|
|
118
|
+
await searchinput.updateComplete
|
|
119
|
+
const wrapper = searchinput.querySelector('.pkt-searchinput')
|
|
120
|
+
expect(wrapper?.classList.contains('pkt-searchinput--global')).toBe(true)
|
|
121
|
+
expect(wrapper?.classList.contains('pkt-searchinput--fullwidth')).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('local uses pkt-input__container; local-with-button uses pkt-searchinput__field', async () => {
|
|
125
|
+
const { searchinput: localEl } = await createSearchInputTest({ appearance: 'local' })
|
|
126
|
+
await localEl.updateComplete
|
|
127
|
+
expect(localEl.querySelector('.pkt-input__container')).toBeTruthy()
|
|
128
|
+
expect(localEl.querySelector('.pkt-searchinput__field')).toBeNull()
|
|
129
|
+
|
|
130
|
+
const { searchinput: lwb } = await createSearchInputTest({
|
|
131
|
+
appearance: 'local-with-button',
|
|
132
|
+
})
|
|
133
|
+
await lwb.updateComplete
|
|
134
|
+
expect(lwb.querySelector('.pkt-searchinput__field')).toBeTruthy()
|
|
135
|
+
expect(lwb.querySelector('.pkt-input__container')).toBeNull()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('pkt-search', () => {
|
|
140
|
+
test('fires on Enter with detail { value }', async () => {
|
|
141
|
+
const { searchinput } = await createSearchInputTest()
|
|
142
|
+
const searchSpy = vi.fn()
|
|
143
|
+
searchinput.addEventListener('pkt-search', searchSpy)
|
|
144
|
+
|
|
145
|
+
const input = searchinput.querySelector('input') as HTMLInputElement
|
|
146
|
+
fireEvent.input(input, { target: { value: 'oslo' } })
|
|
147
|
+
fireEvent.keyDown(input, { key: 'Enter' })
|
|
148
|
+
|
|
149
|
+
expect(searchSpy).toHaveBeenCalledTimes(1)
|
|
150
|
+
expect(searchSpy.mock.calls[0][0].detail).toEqual({ value: 'oslo' })
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('fires on search button click with detail { value }', async () => {
|
|
154
|
+
const { searchinput } = await createSearchInputTest({ value: 'kommune' })
|
|
155
|
+
await searchinput.updateComplete
|
|
156
|
+
const searchSpy = vi.fn()
|
|
157
|
+
searchinput.addEventListener('pkt-search', searchSpy)
|
|
158
|
+
|
|
159
|
+
const button = searchinput.querySelector('.pkt-searchinput__button') as HTMLButtonElement
|
|
160
|
+
fireEvent.click(button)
|
|
161
|
+
|
|
162
|
+
expect(searchSpy).toHaveBeenCalledTimes(1)
|
|
163
|
+
expect(searchSpy.mock.calls[0][0].detail).toEqual({ value: 'kommune' })
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('search submit button classes', () => {
|
|
168
|
+
test('local: tertiary, icon-only, pkt-input-icon', async () => {
|
|
169
|
+
const { searchinput } = await createSearchInputTest({ appearance: 'local' })
|
|
170
|
+
await searchinput.updateComplete
|
|
171
|
+
const button = searchinput.querySelector('.pkt-searchinput__button') as HTMLButtonElement
|
|
172
|
+
expect(button.tagName.toLowerCase()).toBe('button')
|
|
173
|
+
expect(button.classList.contains('pkt-input-icon')).toBe(true)
|
|
174
|
+
expect(button.classList.contains('pkt-btn--tertiary')).toBe(true)
|
|
175
|
+
expect(button.classList.contains('pkt-btn--icon-only')).toBe(true)
|
|
176
|
+
expect(button.classList.contains('pkt-btn--medium')).toBe(true)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('global: primary, yellow', async () => {
|
|
180
|
+
const { searchinput } = await createSearchInputTest({ appearance: 'global' })
|
|
181
|
+
await searchinput.updateComplete
|
|
182
|
+
const button = searchinput.querySelector('.pkt-searchinput__button') as HTMLButtonElement
|
|
183
|
+
expect(button.classList.contains('pkt-btn--primary')).toBe(true)
|
|
184
|
+
expect(button.classList.contains('pkt-btn--yellow')).toBe(true)
|
|
185
|
+
expect(button.classList.contains('pkt-input-icon')).toBe(false)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('suggestions', () => {
|
|
190
|
+
test('renders from suggestions property without mutating the array', async () => {
|
|
191
|
+
const suggestions = [
|
|
192
|
+
{ title: 'A', text: 'tekst', href: 'https://oslo.kommune.no' },
|
|
193
|
+
{ title: 'B', text: 'tekst 2' },
|
|
194
|
+
]
|
|
195
|
+
const snapshot = JSON.stringify(suggestions)
|
|
196
|
+
|
|
197
|
+
const { searchinput } = await createSearchInputTest()
|
|
198
|
+
searchinput.suggestions = suggestions
|
|
199
|
+
await searchinput.updateComplete
|
|
200
|
+
|
|
201
|
+
expect(searchinput.querySelectorAll('.pkt-searchinput__suggestion')).toHaveLength(2)
|
|
202
|
+
expect(JSON.stringify(suggestions)).toBe(snapshot)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('suggestion with href renders as link', async () => {
|
|
206
|
+
const { searchinput } = await createSearchInputTest()
|
|
207
|
+
searchinput.suggestions = [{ title: 'Lenke', href: 'https://oslo.kommune.no' }]
|
|
208
|
+
await searchinput.updateComplete
|
|
209
|
+
|
|
210
|
+
const link = searchinput.querySelector('a.pkt-searchinput__suggestion')
|
|
211
|
+
expect(link).toBeTruthy()
|
|
212
|
+
expect(link?.getAttribute('href')).toBe('https://oslo.kommune.no')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('suggestion without href renders as button type="button"', async () => {
|
|
216
|
+
const { searchinput } = await createSearchInputTest()
|
|
217
|
+
searchinput.suggestions = [{ title: 'Knapp', text: 'beskrivelse' }]
|
|
218
|
+
await searchinput.updateComplete
|
|
219
|
+
|
|
220
|
+
const btn = searchinput.querySelector('button.pkt-searchinput__suggestion')
|
|
221
|
+
expect(btn).toBeTruthy()
|
|
222
|
+
expect(btn?.getAttribute('type')).toBe('button')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('nonInteractive suggestion renders as div and does not emit pkt-suggestion-click', async () => {
|
|
226
|
+
const suggestion = { text: 'Ingen resultater', nonInteractive: true }
|
|
227
|
+
const { searchinput } = await createSearchInputTest()
|
|
228
|
+
searchinput.suggestions = [suggestion]
|
|
229
|
+
await searchinput.updateComplete
|
|
230
|
+
|
|
231
|
+
expect(searchinput.querySelector('div.pkt-searchinput__suggestion')).toBeTruthy()
|
|
232
|
+
expect(searchinput.querySelector('button.pkt-searchinput__suggestion')).toBeNull()
|
|
233
|
+
|
|
234
|
+
const spy = vi.fn()
|
|
235
|
+
searchinput.addEventListener('pkt-suggestion-click', spy)
|
|
236
|
+
const div = searchinput.querySelector('div.pkt-searchinput__suggestion') as HTMLDivElement
|
|
237
|
+
fireEvent.click(div)
|
|
238
|
+
|
|
239
|
+
expect(spy).not.toHaveBeenCalled()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('pkt-suggestion-click fires with detail { index, suggestion } on link click', async () => {
|
|
243
|
+
const suggestion = { title: 'T', href: 'https://oslo.kommune.no' }
|
|
244
|
+
const { searchinput } = await createSearchInputTest()
|
|
245
|
+
searchinput.suggestions = [suggestion]
|
|
246
|
+
await searchinput.updateComplete
|
|
247
|
+
|
|
248
|
+
const spy = vi.fn()
|
|
249
|
+
searchinput.addEventListener('pkt-suggestion-click', spy)
|
|
250
|
+
searchinput.addEventListener('pkt-suggestion-click', (e: Event) => e.preventDefault())
|
|
251
|
+
|
|
252
|
+
const link = searchinput.querySelector('a.pkt-searchinput__suggestion') as HTMLAnchorElement
|
|
253
|
+
fireEvent.click(link)
|
|
254
|
+
|
|
255
|
+
expect(spy).toHaveBeenCalledTimes(1)
|
|
256
|
+
const evt = spy.mock.calls[0][0] as CustomEvent
|
|
257
|
+
expect(evt.detail).toEqual({ index: 0, suggestion })
|
|
258
|
+
expect(evt.cancelable).toBe(true)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('pkt-suggestion-click fires with detail { index, suggestion } on button click', async () => {
|
|
262
|
+
const suggestion = { title: 'X', text: 'y' }
|
|
263
|
+
const { searchinput } = await createSearchInputTest()
|
|
264
|
+
searchinput.suggestions = [suggestion]
|
|
265
|
+
await searchinput.updateComplete
|
|
266
|
+
|
|
267
|
+
const spy = vi.fn()
|
|
268
|
+
searchinput.addEventListener('pkt-suggestion-click', spy)
|
|
269
|
+
|
|
270
|
+
const btn = searchinput.querySelector('button.pkt-searchinput__suggestion') as HTMLButtonElement
|
|
271
|
+
fireEvent.click(btn)
|
|
272
|
+
|
|
273
|
+
expect(spy).toHaveBeenCalledTimes(1)
|
|
274
|
+
expect((spy.mock.calls[0][0] as CustomEvent).detail).toEqual({
|
|
275
|
+
index: 0,
|
|
276
|
+
suggestion,
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('canceling pkt-suggestion-click causes preventDefault on the anchor MouseEvent', async () => {
|
|
281
|
+
const { searchinput } = await createSearchInputTest()
|
|
282
|
+
searchinput.suggestions = [{ title: 'Resultat', href: 'https://oslo.kommune.no' }]
|
|
283
|
+
await searchinput.updateComplete
|
|
284
|
+
|
|
285
|
+
searchinput.addEventListener('pkt-suggestion-click', (e: Event) => e.preventDefault())
|
|
286
|
+
|
|
287
|
+
const link = searchinput.querySelector('a.pkt-searchinput__suggestion') as HTMLAnchorElement
|
|
288
|
+
let seen: MouseEvent | undefined
|
|
289
|
+
link.addEventListener('click', (e: MouseEvent) => {
|
|
290
|
+
seen = e
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
fireEvent.click(link)
|
|
294
|
+
|
|
295
|
+
expect(seen).toBeDefined()
|
|
296
|
+
expect(seen!.defaultPrevented).toBe(true)
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
describe('accessibility', () => {
|
|
301
|
+
test('has no axe violations with label and suggestions', async () => {
|
|
302
|
+
const { searchinput } = await createSearchInputTest({
|
|
303
|
+
label: 'Søk etter noe',
|
|
304
|
+
})
|
|
305
|
+
searchinput.suggestions = [{ title: 'Resultat', text: 'Eksempeltekst' }]
|
|
306
|
+
await searchinput.updateComplete
|
|
307
|
+
|
|
308
|
+
const results = await axe(searchinput)
|
|
309
|
+
expect(results).toHaveNoViolations()
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
})
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { PktElement } from '@/base-elements/element'
|
|
2
|
+
import { html, nothing } from 'lit'
|
|
3
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
4
|
+
import { classMap } from 'lit/directives/class-map.js'
|
|
5
|
+
import { ifDefined } from 'lit/directives/if-defined.js'
|
|
6
|
+
import { FocusModalityController } from '@/controllers/focus-modality-controller'
|
|
7
|
+
|
|
8
|
+
import '@/components/icon'
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
IPktSearchInput,
|
|
12
|
+
IPktSearchInputSuggestion,
|
|
13
|
+
TPktSearchInputAppearance,
|
|
14
|
+
TPktSearchInputMethod,
|
|
15
|
+
} from 'shared-types/searchinput'
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
IPktSearchInput,
|
|
19
|
+
IPktSearchInputSuggestion,
|
|
20
|
+
TPktSearchInputAppearance,
|
|
21
|
+
TPktSearchInputMethod,
|
|
22
|
+
} from 'shared-types/searchinput'
|
|
23
|
+
|
|
24
|
+
export class PktSearchInput extends PktElement<IPktSearchInput> implements IPktSearchInput {
|
|
25
|
+
@property({ type: String }) appearance: TPktSearchInputAppearance = 'local'
|
|
26
|
+
@property({ type: Boolean, reflect: true }) disabled: boolean = false
|
|
27
|
+
@property({ type: Boolean, reflect: true }) fullwidth: boolean = false
|
|
28
|
+
@property({ type: String, reflect: true }) id: string = ''
|
|
29
|
+
@property({ type: String }) label: string | undefined
|
|
30
|
+
@property({ type: String }) name: string | undefined
|
|
31
|
+
@property({ type: String }) placeholder: string = 'Søk…'
|
|
32
|
+
@property({ type: String }) value: string = ''
|
|
33
|
+
@property({ type: Array }) suggestions: IPktSearchInputSuggestion[] | undefined
|
|
34
|
+
@property({ type: String }) action: string | undefined
|
|
35
|
+
@property({ type: String }) method: TPktSearchInputMethod | undefined
|
|
36
|
+
|
|
37
|
+
focusModality = new FocusModalityController(this, '.pkt-searchinput')
|
|
38
|
+
|
|
39
|
+
private get suggestionsId() {
|
|
40
|
+
return `${this.id}-suggestions`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private dispatchSearch(): void {
|
|
44
|
+
this.dispatchEvent(
|
|
45
|
+
new CustomEvent('pkt-search', {
|
|
46
|
+
detail: { value: this.value },
|
|
47
|
+
bubbles: true,
|
|
48
|
+
composed: true,
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private onInput(event: InputEvent): void {
|
|
54
|
+
const target = event.target as HTMLInputElement
|
|
55
|
+
this.value = target.value
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private onInputKeydown(event: KeyboardEvent): void {
|
|
59
|
+
if (event.key !== 'Enter') return
|
|
60
|
+
event.preventDefault()
|
|
61
|
+
this.dispatchSearch()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private onSearchClick(event: Event): void {
|
|
65
|
+
event.preventDefault()
|
|
66
|
+
this.dispatchSearch()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private onSuggestionClick(
|
|
70
|
+
event: Event,
|
|
71
|
+
suggestion: IPktSearchInputSuggestion,
|
|
72
|
+
index: number,
|
|
73
|
+
): void {
|
|
74
|
+
const customEvent = new CustomEvent('pkt-suggestion-click', {
|
|
75
|
+
detail: {
|
|
76
|
+
index,
|
|
77
|
+
suggestion,
|
|
78
|
+
},
|
|
79
|
+
bubbles: true,
|
|
80
|
+
composed: true,
|
|
81
|
+
cancelable: true,
|
|
82
|
+
})
|
|
83
|
+
const allowed = this.dispatchEvent(customEvent)
|
|
84
|
+
if (!allowed) {
|
|
85
|
+
event.preventDefault()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private renderSuggestion(suggestion: IPktSearchInputSuggestion, index: number) {
|
|
90
|
+
const suggestionContent = html`
|
|
91
|
+
${suggestion.title
|
|
92
|
+
? html`<h3 class="pkt-searchinput__suggestion-title">${suggestion.title}</h3>`
|
|
93
|
+
: nothing}
|
|
94
|
+
${suggestion.text
|
|
95
|
+
? html`<p class="pkt-searchinput__suggestion-text">${suggestion.text}</p>`
|
|
96
|
+
: nothing}
|
|
97
|
+
`
|
|
98
|
+
|
|
99
|
+
if (suggestion.href) {
|
|
100
|
+
return html`<a
|
|
101
|
+
href=${suggestion.href}
|
|
102
|
+
class="pkt-searchinput__suggestion"
|
|
103
|
+
@click=${(event: Event) => this.onSuggestionClick(event, suggestion, index)}
|
|
104
|
+
>
|
|
105
|
+
${suggestionContent}
|
|
106
|
+
</a>`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (suggestion.nonInteractive) {
|
|
110
|
+
return html`<div class="pkt-searchinput__suggestion">${suggestionContent}</div>`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return html`<button
|
|
114
|
+
type="button"
|
|
115
|
+
class="pkt-searchinput__suggestion"
|
|
116
|
+
@click=${(event: Event) => this.onSuggestionClick(event, suggestion, index)}
|
|
117
|
+
>
|
|
118
|
+
${suggestionContent}
|
|
119
|
+
</button>`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
render() {
|
|
123
|
+
const wrapperClasses = classMap({
|
|
124
|
+
'pkt-searchinput': true,
|
|
125
|
+
[`pkt-searchinput--${this.appearance}`]: true,
|
|
126
|
+
'pkt-searchinput--fullwidth': this.fullwidth,
|
|
127
|
+
})
|
|
128
|
+
const buttonClasses = classMap({
|
|
129
|
+
'pkt-searchinput__button': true,
|
|
130
|
+
'pkt-input-icon': this.appearance === 'local',
|
|
131
|
+
'pkt-btn': true,
|
|
132
|
+
'pkt-btn--medium': true,
|
|
133
|
+
'pkt-btn--icon-only': true,
|
|
134
|
+
'pkt-btn--tertiary': this.appearance === 'local',
|
|
135
|
+
'pkt-btn--primary': this.appearance === 'global' || this.appearance === 'local-with-button',
|
|
136
|
+
'pkt-btn--yellow': this.appearance === 'global',
|
|
137
|
+
})
|
|
138
|
+
const inputClasses = classMap({
|
|
139
|
+
'pkt-input': true,
|
|
140
|
+
'pkt-input--fullwidth': this.fullwidth,
|
|
141
|
+
})
|
|
142
|
+
const fieldClass =
|
|
143
|
+
this.appearance === 'local' ? 'pkt-input__container' : 'pkt-searchinput__field'
|
|
144
|
+
|
|
145
|
+
const body = html`
|
|
146
|
+
${this.label
|
|
147
|
+
? html`<label for=${ifDefined(this.id || undefined)} class="pkt-inputwrapper__label"
|
|
148
|
+
>${this.label}</label
|
|
149
|
+
>`
|
|
150
|
+
: nothing}
|
|
151
|
+
<div class=${fieldClass}>
|
|
152
|
+
<input
|
|
153
|
+
class=${inputClasses}
|
|
154
|
+
type="search"
|
|
155
|
+
name=${this.name || this.id}
|
|
156
|
+
id=${this.id}
|
|
157
|
+
placeholder=${this.placeholder || 'Søk…'}
|
|
158
|
+
.value=${this.value}
|
|
159
|
+
?disabled=${this.disabled}
|
|
160
|
+
autocomplete="off"
|
|
161
|
+
aria-autocomplete="list"
|
|
162
|
+
aria-controls=${this.suggestionsId}
|
|
163
|
+
@input=${this.onInput}
|
|
164
|
+
@keydown=${this.onInputKeydown}
|
|
165
|
+
/>
|
|
166
|
+
<button
|
|
167
|
+
type="submit"
|
|
168
|
+
class=${buttonClasses}
|
|
169
|
+
?disabled=${this.disabled}
|
|
170
|
+
@click=${this.onSearchClick}
|
|
171
|
+
>
|
|
172
|
+
<pkt-icon class="pkt-btn__icon" name="magnifying-glass-big" />
|
|
173
|
+
<span class="pkt-btn__text">${this.label || this.placeholder || 'Søk…'}</span>
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
${this.suggestions
|
|
177
|
+
? html`<ul
|
|
178
|
+
id=${this.suggestionsId}
|
|
179
|
+
class="pkt-searchinput__suggestions"
|
|
180
|
+
aria-live="assertive"
|
|
181
|
+
>
|
|
182
|
+
${this.suggestions.map(
|
|
183
|
+
(suggestion, index) => html` <li>${this.renderSuggestion(suggestion, index)}</li> `,
|
|
184
|
+
)}
|
|
185
|
+
</ul>`
|
|
186
|
+
: nothing}
|
|
187
|
+
`
|
|
188
|
+
|
|
189
|
+
if (this.action) {
|
|
190
|
+
return html`<form
|
|
191
|
+
role="search"
|
|
192
|
+
class=${wrapperClasses}
|
|
193
|
+
action=${this.action}
|
|
194
|
+
method=${ifDefined(this.method)}
|
|
195
|
+
@submit=${(event: Event) => {
|
|
196
|
+
event.preventDefault()
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
${body}
|
|
200
|
+
</form>`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return html`<div role="search" class=${wrapperClasses}>${body}</div>`
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export default PktSearchInput
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
customElement('pkt-searchinput')(PktSearchInput)
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.warn('Forsøker å definere <pkt-searchinput>, men den er allerede definert')
|
|
213
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|