@oslokommune/punkt-react 15.4.6 → 16.0.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 (29) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/index.d.ts +38 -15
  3. package/dist/punkt-react.es.js +12025 -10664
  4. package/dist/punkt-react.umd.js +562 -549
  5. package/package.json +5 -5
  6. package/src/components/accordion/Accordion.test.tsx +3 -2
  7. package/src/components/alert/Alert.test.tsx +2 -1
  8. package/src/components/backlink/BackLink.test.tsx +2 -1
  9. package/src/components/button/Button.test.tsx +4 -3
  10. package/src/components/calendar/Calendar.interaction.test.tsx +2 -1
  11. package/src/components/checkbox/Checkbox.test.tsx +2 -1
  12. package/src/components/combobox/Combobox.accessibility.test.tsx +277 -0
  13. package/src/components/combobox/Combobox.core.test.tsx +469 -0
  14. package/src/components/combobox/Combobox.interaction.test.tsx +607 -0
  15. package/src/components/combobox/Combobox.selection.test.tsx +548 -0
  16. package/src/components/combobox/Combobox.tsx +59 -54
  17. package/src/components/combobox/ComboboxInput.tsx +140 -0
  18. package/src/components/combobox/ComboboxTags.tsx +110 -0
  19. package/src/components/combobox/Listbox.tsx +172 -0
  20. package/src/components/combobox/types.ts +145 -0
  21. package/src/components/combobox/useComboboxState.ts +1141 -0
  22. package/src/components/datepicker/Datepicker.accessibility.test.tsx +5 -4
  23. package/src/components/datepicker/Datepicker.input.test.tsx +3 -2
  24. package/src/components/datepicker/Datepicker.selection.test.tsx +8 -8
  25. package/src/components/datepicker/Datepicker.validation.test.tsx +2 -1
  26. package/src/components/radio/RadioButton.test.tsx +3 -2
  27. package/src/components/searchinput/SearchInput.test.tsx +6 -5
  28. package/src/components/tabs/Tabs.test.tsx +13 -12
  29. package/src/components/tag/Tag.test.tsx +2 -1
@@ -0,0 +1,548 @@
1
+ import '@testing-library/jest-dom'
2
+
3
+ import { fireEvent, render } from '@testing-library/react'
4
+ import { vi } from 'vitest'
5
+
6
+ import type { IPktComboboxOption } from 'shared-types/combobox'
7
+ import { PktCombobox } from './Combobox'
8
+ import type { IPktCombobox } from './types'
9
+
10
+ const comboboxId = 'test-combobox'
11
+ const label = 'Test Combobox'
12
+
13
+ const getDefaultOptions = (): IPktComboboxOption[] => [
14
+ { value: 'apple', label: 'Apple' },
15
+ { value: 'banana', label: 'Banana' },
16
+ { value: 'cherry', label: 'Cherry' },
17
+ { value: 'date', label: 'Date' },
18
+ ]
19
+
20
+ const createComboboxTest = (props: Partial<IPktCombobox> = {}) => {
21
+ const defaultProps: IPktCombobox = {
22
+ label,
23
+ id: comboboxId,
24
+ ...props,
25
+ }
26
+
27
+ return render(<PktCombobox {...defaultProps} />)
28
+ }
29
+
30
+ const getFormInputValue = (container: HTMLElement) => {
31
+ return (container.querySelector('input.pkt-visually-hidden') as HTMLInputElement)?.value ?? ''
32
+ }
33
+
34
+ const openDropdown = (container: HTMLElement) => {
35
+ const arrowButton = container.querySelector('.pkt-combobox__input')
36
+ fireEvent.click(arrowButton!)
37
+ }
38
+
39
+ const clickOption = (container: HTMLElement, value: string) => {
40
+ const option = container.querySelector(`[data-value="${value}"][role="option"]`)
41
+ fireEvent.click(option!)
42
+ }
43
+
44
+ describe('PktCombobox', () => {
45
+ describe('Single selection', () => {
46
+ test('selects a value by clicking option', () => {
47
+ const handleValueChange = vi.fn()
48
+ const { container } = createComboboxTest({
49
+ options: getDefaultOptions(),
50
+ onValueChange: handleValueChange,
51
+ })
52
+
53
+ openDropdown(container)
54
+ clickOption(container, 'apple')
55
+
56
+ expect(handleValueChange).toHaveBeenCalledWith(['apple'])
57
+ expect(getFormInputValue(container)).toBe('apple')
58
+ })
59
+
60
+ test('replaces current selection when selecting a new value', () => {
61
+ const handleValueChange = vi.fn()
62
+ const { container } = createComboboxTest({
63
+ defaultValue: 'apple',
64
+ options: getDefaultOptions(),
65
+ onValueChange: handleValueChange,
66
+ })
67
+
68
+ expect(getFormInputValue(container)).toBe('apple')
69
+
70
+ openDropdown(container)
71
+ clickOption(container, 'banana')
72
+
73
+ expect(handleValueChange).toHaveBeenCalledWith(['banana'])
74
+ expect(getFormInputValue(container)).toBe('banana')
75
+ })
76
+
77
+ test('closes dropdown after selecting in single mode', () => {
78
+ const { container } = createComboboxTest({
79
+ options: getDefaultOptions(),
80
+ })
81
+
82
+ openDropdown(container)
83
+
84
+ const arrowButton = container.querySelector('.pkt-combobox__input')
85
+ expect(arrowButton?.getAttribute('aria-expanded')).toBe('true')
86
+
87
+ clickOption(container, 'apple')
88
+
89
+ expect(arrowButton?.getAttribute('aria-expanded')).toBe('false')
90
+ })
91
+
92
+ test('deselects value when toggling already selected option', () => {
93
+ const handleValueChange = vi.fn()
94
+ const { container } = createComboboxTest({
95
+ defaultValue: 'apple',
96
+ options: getDefaultOptions(),
97
+ onValueChange: handleValueChange,
98
+ })
99
+
100
+ openDropdown(container)
101
+ clickOption(container, 'apple')
102
+
103
+ expect(handleValueChange).toHaveBeenCalledWith([])
104
+ })
105
+
106
+ test('displays selected value as text in single mode', () => {
107
+ const { container } = createComboboxTest({
108
+ defaultValue: 'apple',
109
+ options: getDefaultOptions(),
110
+ })
111
+
112
+ const valueSpan = container.querySelector('.pkt-combobox__value')
113
+ expect(valueSpan?.textContent?.trim()).toBe('Apple')
114
+ })
115
+
116
+ test('selects option when clicking it in the open dropdown', () => {
117
+ const handleValueChange = vi.fn()
118
+ const { container } = createComboboxTest({
119
+ options: getDefaultOptions(),
120
+ onValueChange: handleValueChange,
121
+ })
122
+
123
+ openDropdown(container)
124
+
125
+ const option = container.querySelector('.pkt-listbox__option')
126
+ expect(option).toBeInTheDocument()
127
+
128
+ fireEvent.click(option!)
129
+
130
+ expect(handleValueChange).toHaveBeenCalledWith(['apple'])
131
+ })
132
+ })
133
+
134
+ describe('Multiple selection', () => {
135
+ test('selects multiple values', () => {
136
+ const handleValueChange = vi.fn()
137
+ const { container } = createComboboxTest({
138
+ multiple: true,
139
+ options: getDefaultOptions(),
140
+ onValueChange: handleValueChange,
141
+ })
142
+
143
+ openDropdown(container)
144
+ clickOption(container, 'apple')
145
+
146
+ expect(handleValueChange).toHaveBeenCalledWith(['apple'])
147
+
148
+ clickOption(container, 'banana')
149
+
150
+ expect(handleValueChange).toHaveBeenCalledWith(['apple', 'banana'])
151
+ })
152
+
153
+ test('keeps dropdown open after selection in multiple mode', () => {
154
+ const { container } = createComboboxTest({
155
+ multiple: true,
156
+ options: getDefaultOptions(),
157
+ })
158
+
159
+ openDropdown(container)
160
+ clickOption(container, 'apple')
161
+
162
+ const arrowButton = container.querySelector('.pkt-combobox__input')
163
+ expect(arrowButton?.getAttribute('aria-expanded')).toBe('true')
164
+ })
165
+
166
+ test('renders selected values as tags in multiple mode', () => {
167
+ const { container } = createComboboxTest({
168
+ multiple: true,
169
+ defaultValue: ['apple', 'banana'],
170
+ options: getDefaultOptions(),
171
+ })
172
+
173
+ const tags = container.querySelectorAll('.pkt-combobox__input .pkt-tag')
174
+ expect(tags.length).toBe(2)
175
+ })
176
+
177
+ test('removes a selected value by clicking its tag close button', () => {
178
+ const handleValueChange = vi.fn()
179
+ const { container } = createComboboxTest({
180
+ multiple: true,
181
+ defaultValue: ['apple', 'banana'],
182
+ options: getDefaultOptions(),
183
+ onValueChange: handleValueChange,
184
+ })
185
+
186
+ const closeButtons = container.querySelectorAll('.pkt-tag__close-btn')
187
+ expect(closeButtons.length).toBe(2)
188
+
189
+ fireEvent.click(closeButtons[0])
190
+
191
+ expect(handleValueChange).toHaveBeenCalledWith(['banana'])
192
+ })
193
+
194
+ test('deselects value when toggling already selected option in multiple mode', () => {
195
+ const handleValueChange = vi.fn()
196
+ const { container } = createComboboxTest({
197
+ multiple: true,
198
+ defaultValue: ['apple', 'banana'],
199
+ options: getDefaultOptions(),
200
+ onValueChange: handleValueChange,
201
+ })
202
+
203
+ openDropdown(container)
204
+ clickOption(container, 'apple')
205
+
206
+ expect(handleValueChange).toHaveBeenCalledWith(['banana'])
207
+ })
208
+
209
+ test('renders tags outside when tagPlacement is outside', () => {
210
+ const { container } = createComboboxTest({
211
+ multiple: true,
212
+ tagPlacement: 'outside',
213
+ defaultValue: ['apple', 'banana'],
214
+ options: getDefaultOptions(),
215
+ })
216
+
217
+ const outsideTags = container.querySelector('.pkt-combobox__tags-outside')
218
+ expect(outsideTags).toBeInTheDocument()
219
+
220
+ const tags = outsideTags?.querySelectorAll('.pkt-tag')
221
+ expect(tags?.length).toBe(2)
222
+ })
223
+
224
+ test('renders value as tag with tagSkinColor', () => {
225
+ const optionsWithTags: IPktComboboxOption[] = [
226
+ { value: 'red', label: 'Red', tagSkinColor: 'red' },
227
+ { value: 'blue', label: 'Blue', tagSkinColor: 'blue' },
228
+ ]
229
+
230
+ const { container } = createComboboxTest({
231
+ multiple: true,
232
+ defaultValue: ['red'],
233
+ options: optionsWithTags,
234
+ })
235
+
236
+ const tag = container.querySelector('.pkt-tag')
237
+ expect(tag).toBeInTheDocument()
238
+ })
239
+
240
+ test('renders checkboxes for multi-select options', () => {
241
+ const { container } = createComboboxTest({
242
+ multiple: true,
243
+ defaultValue: ['apple'],
244
+ options: getDefaultOptions(),
245
+ })
246
+
247
+ const checkboxes = container.querySelectorAll('.pkt-listbox__option input[type="checkbox"]')
248
+ expect(checkboxes.length).toBe(4)
249
+
250
+ const checkedBoxes = container.querySelectorAll('.pkt-listbox__option input[type="checkbox"]:checked')
251
+ expect(checkedBoxes.length).toBe(1)
252
+ })
253
+ })
254
+
255
+ describe('Maxlength enforcement', () => {
256
+ test('prevents selection beyond maxlength', () => {
257
+ const handleValueChange = vi.fn()
258
+ const { container } = createComboboxTest({
259
+ multiple: true,
260
+ maxlength: 2,
261
+ defaultValue: ['apple', 'banana'],
262
+ options: getDefaultOptions(),
263
+ onValueChange: handleValueChange,
264
+ })
265
+
266
+ openDropdown(container)
267
+ clickOption(container, 'date')
268
+
269
+ // Value should not change - cherry was clicked but max is reached
270
+ expect(getFormInputValue(container)).toBe('apple,banana')
271
+ })
272
+
273
+ test('allows deselection when at maxlength', () => {
274
+ const handleValueChange = vi.fn()
275
+ const { container } = createComboboxTest({
276
+ multiple: true,
277
+ maxlength: 2,
278
+ defaultValue: ['apple', 'banana'],
279
+ options: getDefaultOptions(),
280
+ onValueChange: handleValueChange,
281
+ })
282
+
283
+ openDropdown(container)
284
+ clickOption(container, 'apple')
285
+
286
+ expect(handleValueChange).toHaveBeenCalledWith(['banana'])
287
+ })
288
+
289
+ test('shows max reached banner', () => {
290
+ const { container } = createComboboxTest({
291
+ multiple: true,
292
+ maxlength: 2,
293
+ defaultValue: ['apple', 'banana'],
294
+ options: getDefaultOptions(),
295
+ })
296
+
297
+ const banner = container.querySelector('.pkt-listbox__banner--maximum-reached')
298
+ expect(banner).toBeInTheDocument()
299
+ expect(banner?.textContent).toContain('2 av maks 2')
300
+ })
301
+
302
+ test('disables unselected options when max is reached', () => {
303
+ const { container } = createComboboxTest({
304
+ multiple: true,
305
+ maxlength: 2,
306
+ defaultValue: ['apple', 'banana'],
307
+ options: getDefaultOptions(),
308
+ })
309
+
310
+ const unselectedOption = container.querySelector('[data-value="date"]')
311
+ expect(unselectedOption?.getAttribute('data-disabled')).toBe('true')
312
+ expect(unselectedOption?.getAttribute('tabindex')).toBe('-1')
313
+ })
314
+ })
315
+
316
+ describe('Disabled options', () => {
317
+ test('does not select disabled options', () => {
318
+ const handleValueChange = vi.fn()
319
+ const optionsWithDisabled: IPktComboboxOption[] = [
320
+ { value: 'enabled', label: 'Enabled' },
321
+ { value: 'disabled', label: 'Disabled', disabled: true },
322
+ ]
323
+
324
+ const { container } = createComboboxTest({
325
+ options: optionsWithDisabled,
326
+ onValueChange: handleValueChange,
327
+ })
328
+
329
+ openDropdown(container)
330
+ clickOption(container, 'disabled')
331
+
332
+ expect(handleValueChange).not.toHaveBeenCalled()
333
+ expect(getFormInputValue(container)).toBe('')
334
+ })
335
+ })
336
+
337
+ describe('User input (custom values)', () => {
338
+ test('adds custom value in single-select mode', () => {
339
+ const handleValueChange = vi.fn()
340
+ const { container } = createComboboxTest({
341
+ allowUserInput: true,
342
+ options: getDefaultOptions(),
343
+ onValueChange: handleValueChange,
344
+ })
345
+
346
+ const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
347
+ fireEvent.focus(textInput)
348
+
349
+ textInput.value = 'CustomFruit'
350
+ fireEvent.change(textInput, { target: { value: 'CustomFruit' } })
351
+ fireEvent.keyDown(textInput, { key: 'Enter' })
352
+
353
+ expect(handleValueChange).toHaveBeenCalledWith(['CustomFruit'])
354
+ })
355
+
356
+ test('adds custom value in multiple-select mode', () => {
357
+ const handleValueChange = vi.fn()
358
+ const { container } = createComboboxTest({
359
+ allowUserInput: true,
360
+ multiple: true,
361
+ options: getDefaultOptions(),
362
+ onValueChange: handleValueChange,
363
+ })
364
+
365
+ const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
366
+ fireEvent.focus(textInput)
367
+
368
+ textInput.value = 'CustomFruit'
369
+ fireEvent.change(textInput, { target: { value: 'CustomFruit' } })
370
+ fireEvent.keyDown(textInput, { key: 'Enter' })
371
+
372
+ expect(handleValueChange).toHaveBeenCalledWith(['CustomFruit'])
373
+ })
374
+
375
+ test('does not add empty custom value', () => {
376
+ const { container } = createComboboxTest({
377
+ allowUserInput: true,
378
+ })
379
+
380
+ const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
381
+ fireEvent.focus(textInput)
382
+
383
+ textInput.value = ''
384
+ fireEvent.keyDown(textInput, { key: 'Enter' })
385
+
386
+ // No user-added option should appear in the listbox
387
+ const options = container.querySelectorAll('.pkt-listbox__option')
388
+ const hasUserAdded = Array.from(options).some((opt) => opt.getAttribute('data-value') === '')
389
+ expect(hasUserAdded).toBe(false)
390
+ expect(getFormInputValue(container)).toBe('')
391
+ })
392
+
393
+ test('does not add whitespace-only custom value', () => {
394
+ const { container } = createComboboxTest({
395
+ allowUserInput: true,
396
+ })
397
+
398
+ const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
399
+ fireEvent.focus(textInput)
400
+
401
+ textInput.value = ' '
402
+ fireEvent.change(textInput, { target: { value: ' ' } })
403
+ fireEvent.keyDown(textInput, { key: 'Enter' })
404
+
405
+ // No user-added option should appear in the listbox
406
+ const options = container.querySelectorAll('.pkt-listbox__option')
407
+ const hasUserAdded = Array.from(options).some((opt) => opt.getAttribute('data-value')?.trim() === '')
408
+ expect(hasUserAdded).toBe(false)
409
+ expect(getFormInputValue(container)).toBe('')
410
+ })
411
+
412
+ test('preserves user-added options when options prop changes', () => {
413
+ const { container, rerender } = render(
414
+ <PktCombobox id={comboboxId} label={label} allowUserInput options={getDefaultOptions()} />,
415
+ )
416
+
417
+ // Add a custom value
418
+ const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
419
+ fireEvent.focus(textInput)
420
+ textInput.value = 'CustomFruit'
421
+ fireEvent.change(textInput, { target: { value: 'CustomFruit' } })
422
+ fireEvent.keyDown(textInput, { key: 'Enter' })
423
+
424
+ expect(getFormInputValue(container)).toBe('CustomFruit')
425
+
426
+ // Change options prop
427
+ rerender(<PktCombobox id={comboboxId} label={label} allowUserInput options={[{ value: 'new', label: 'New' }]} />)
428
+
429
+ // User-added value should persist
430
+ expect(getFormInputValue(container)).toBe('CustomFruit')
431
+
432
+ // User-added option should still appear in the listbox
433
+ const options = container.querySelectorAll('.pkt-listbox__option')
434
+ const hasCustom = Array.from(options).some((opt) => opt.getAttribute('data-value') === 'CustomFruit')
435
+ expect(hasCustom).toBe(true)
436
+ })
437
+
438
+ test('removes user-added option when deselected', () => {
439
+ const handleValueChange = vi.fn()
440
+ const { container } = createComboboxTest({
441
+ allowUserInput: true,
442
+ multiple: true,
443
+ options: getDefaultOptions(),
444
+ onValueChange: handleValueChange,
445
+ })
446
+
447
+ // Add a custom value
448
+ const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
449
+ fireEvent.focus(textInput)
450
+ textInput.value = 'CustomFruit'
451
+ fireEvent.change(textInput, { target: { value: 'CustomFruit' } })
452
+ fireEvent.keyDown(textInput, { key: 'Enter' })
453
+
454
+ expect(getFormInputValue(container)).toContain('CustomFruit')
455
+
456
+ // Remove by clicking the tag close button
457
+ const closeButtons = container.querySelectorAll('.pkt-tag__close-btn')
458
+ const customCloseBtn = closeButtons[closeButtons.length - 1]
459
+ fireEvent.click(customCloseBtn!)
460
+
461
+ expect(getFormInputValue(container)).not.toContain('CustomFruit')
462
+ })
463
+ })
464
+
465
+ describe('displayValueAs modes', () => {
466
+ test('displays value using label by default', () => {
467
+ const options: IPktComboboxOption[] = [{ value: 'no', label: 'Norway', prefix: 'NO' }]
468
+
469
+ const { container } = createComboboxTest({
470
+ defaultValue: 'no',
471
+ options,
472
+ })
473
+
474
+ const valueEl = container.querySelector('.pkt-combobox__value')
475
+ expect(valueEl?.textContent?.trim()).toBe('Norway')
476
+ })
477
+
478
+ test('displays value using value when displayValueAs is value', () => {
479
+ const options: IPktComboboxOption[] = [{ value: 'no', label: 'Norway', prefix: 'NO' }]
480
+
481
+ const { container } = createComboboxTest({
482
+ defaultValue: 'no',
483
+ displayValueAs: 'value',
484
+ options,
485
+ })
486
+
487
+ const valueEl = container.querySelector('.pkt-combobox__value')
488
+ expect(valueEl?.textContent?.trim()).toBe('no')
489
+ })
490
+
491
+ test('displays prefix and value when displayValueAs is prefixAndValue', () => {
492
+ const options: IPktComboboxOption[] = [{ value: 'no', label: 'Norway', prefix: 'NO' }]
493
+
494
+ const { container } = createComboboxTest({
495
+ defaultValue: 'no',
496
+ displayValueAs: 'prefixAndValue',
497
+ options,
498
+ })
499
+
500
+ const valueEl = container.querySelector('.pkt-combobox__value')
501
+ expect(valueEl?.textContent?.trim()).toBe('NO no')
502
+ })
503
+ })
504
+
505
+ describe('Value change callbacks', () => {
506
+ test('calls onValueChange on selection', () => {
507
+ const handleValueChange = vi.fn()
508
+ const { container } = createComboboxTest({
509
+ options: getDefaultOptions(),
510
+ onValueChange: handleValueChange,
511
+ })
512
+
513
+ openDropdown(container)
514
+ clickOption(container, 'apple')
515
+
516
+ expect(handleValueChange).toHaveBeenCalledWith(['apple'])
517
+ })
518
+
519
+ test('calls onValueChange with array for multiple mode', () => {
520
+ const handleValueChange = vi.fn()
521
+ const { container } = createComboboxTest({
522
+ multiple: true,
523
+ defaultValue: ['apple'],
524
+ options: getDefaultOptions(),
525
+ onValueChange: handleValueChange,
526
+ })
527
+
528
+ openDropdown(container)
529
+ clickOption(container, 'banana')
530
+
531
+ expect(handleValueChange).toHaveBeenCalledWith(['apple', 'banana'])
532
+ })
533
+
534
+ test('calls onValueChange with empty array when clearing', () => {
535
+ const handleValueChange = vi.fn()
536
+ const { container } = createComboboxTest({
537
+ defaultValue: 'apple',
538
+ options: getDefaultOptions(),
539
+ onValueChange: handleValueChange,
540
+ })
541
+
542
+ openDropdown(container)
543
+ clickOption(container, 'apple') // deselect
544
+
545
+ expect(handleValueChange).toHaveBeenCalledWith([])
546
+ })
547
+ })
548
+ })
@@ -1,66 +1,71 @@
1
1
  'use client'
2
2
 
3
- import { createComponent, EventName } from '@lit/react'
4
- import { type IPktCombobox as IPktElCombobox, PktCombobox as PktElCombobox } from '@oslokommune/punkt-elements'
5
- // eslint-disable-next-line no-restricted-syntax -- React is required for createComponent
6
- import React, {
7
- ChangeEventHandler,
8
- FC,
9
- FocusEventHandler,
10
- forwardRef,
11
- ForwardRefExoticComponent,
12
- LegacyRef,
13
- MouseEventHandler,
14
- type ReactNode,
15
- SelectHTMLAttributes,
16
- } from 'react'
3
+ import { forwardRef } from 'react'
4
+ import { PktInputWrapper } from '../inputwrapper/InputWrapper'
5
+ import { ComboboxInput } from './ComboboxInput'
6
+ import { ComboboxTags } from './ComboboxTags'
7
+ import { Listbox } from './Listbox'
8
+ import { useComboboxState } from './useComboboxState'
9
+ import type { IPktCombobox } from './types'
17
10
 
18
- import { PktEventWithTarget } from '@/interfaces/IPktElements'
11
+ export type { IPktCombobox } from './types'
19
12
 
20
- type ExtendedCombobox = Omit<IPktElCombobox, 'helptext'> & SelectHTMLAttributes<HTMLSelectElement>
13
+ export const PktCombobox = forwardRef<HTMLDivElement, IPktCombobox>((props, ref) => {
14
+ const state = useComboboxState(props, ref)
21
15
 
22
- export interface IPktCombobox extends ExtendedCombobox {
23
- helptext?: string | ReactNode | ReactNode[]
24
- ref?: LegacyRef<HTMLSelectElement>
25
- onClick?: MouseEventHandler<HTMLSelectElement>
26
- onChange?: ChangeEventHandler<HTMLSelectElement>
27
- onInput?: ChangeEventHandler<HTMLSelectElement>
28
- onBlur?: FocusEventHandler<HTMLSelectElement>
29
- onFocus?: FocusEventHandler<HTMLSelectElement>
30
- onValueChange?: (e: CustomEvent) => void
31
- onToggleHelpText?: (e: CustomEvent) => void
32
- useWrapper?: boolean
33
- }
16
+ const outerClasses = [
17
+ 'pkt-combobox-component',
18
+ state.fullwidth && 'pkt-combobox-component--fullwidth',
19
+ state.className,
20
+ ]
21
+ .filter(Boolean)
22
+ .join(' ')
34
23
 
35
- export const LitComponent = createComponent({
36
- tagName: 'pkt-combobox',
37
- elementClass: PktElCombobox,
38
- react: React,
39
- displayName: 'PktCombobox',
40
- events: {
41
- onClick: 'click' as EventName<PktEventWithTarget>,
42
- onChange: 'change' as EventName<PktEventWithTarget>,
43
- onInput: 'input' as EventName<PktEventWithTarget>,
44
- onBlur: 'blur' as EventName<FocusEvent>,
45
- onFocus: 'focus' as EventName<FocusEvent>,
46
- onValueChange: 'value-change' as EventName<CustomEvent>,
47
- onToggleHelpText: 'toggleHelpText' as EventName<CustomEvent>,
48
- },
49
- }) as ForwardRefExoticComponent<IPktCombobox>
24
+ const hasTextInput = state.allowUserInput || state.typeahead
25
+ const wrapperForId = hasTextInput ? state.inputId : `${state.id}-combobox`
50
26
 
51
- // Note:
52
- // helptext slot needs to be before children because of how React reactivity works.
53
- // Please do not change this.
54
- export const PktCombobox: FC<IPktCombobox> = forwardRef(({ children, helptext, ...props }: IPktCombobox, ref) => {
55
27
  return (
56
- <LitComponent {...props} ref={ref}>
57
- {helptext && (
58
- <div slot="helptext" className="pkt-contents">
59
- {helptext}
28
+ <div className={outerClasses} ref={state.wrapperRef}>
29
+ <PktInputWrapper
30
+ forId={wrapperForId}
31
+ hasFieldset={!hasTextInput}
32
+ label={state.label || ''}
33
+ helptext={state.helptext}
34
+ helptextDropdown={state.helptextDropdown}
35
+ helptextDropdownButton={state.helptextDropdownButton}
36
+ hasError={state.hasError}
37
+ errorMessage={state.errorMessage}
38
+ disabled={state.disabled}
39
+ optionalTag={state.optionalTag}
40
+ optionalText={state.optionalText}
41
+ requiredTag={state.requiredTag}
42
+ requiredText={state.requiredText}
43
+ tagText={state.tagText}
44
+ useWrapper={state.useWrapper}
45
+ counter={state.hasCounter}
46
+ counterCurrent={state.values.length}
47
+ counterMaxLength={state.maxlength}
48
+ className="pkt-combobox__wrapper"
49
+ >
50
+ <div className="pkt-combobox" onBlur={state.handleFocusOut}>
51
+ <ComboboxInput state={state} />
52
+ <Listbox state={state} />
60
53
  </div>
61
- )}
62
- {children}
63
- </LitComponent>
54
+
55
+ {state.tagPlacement === 'outside' && state.multiple && <ComboboxTags state={state} outside />}
56
+ </PktInputWrapper>
57
+
58
+ {/* Uncontrolled hidden input for onChange event dispatching and form submission */}
59
+ <input
60
+ ref={state.changeInputRef}
61
+ type="text"
62
+ name={state.name || state.id}
63
+ onChange={state.onChange}
64
+ tabIndex={-1}
65
+ aria-hidden="true"
66
+ className="pkt-visually-hidden"
67
+ />
68
+ </div>
64
69
  )
65
70
  })
66
71