@@ -2,198 +2,198 @@ import { beforeEach, describe, expect, it, jest } from '@jest/globals'
22import { Combobox } from './Combobox'
33import './index'
44
5- function getPortalRoot ( ) {
6- const portalHost = document . querySelector ( '[data-solid-ui-combobox-portal]' ) as HTMLDivElement | null
7- return portalHost ?. shadowRoot ?? null
5+ function getPortalRoot ( ) {
6+ const portalHost = document . querySelector ( '[data-solid-ui-combobox-portal]' ) as HTMLDivElement | null
7+ return portalHost ?. shadowRoot ?? null
88}
99
10- async function flushUpdates ( ) {
11- await Promise . resolve ( )
12- await Promise . resolve ( )
10+ async function flushUpdates ( ) {
11+ await Promise . resolve ( )
12+ await Promise . resolve ( )
1313}
1414
1515describe ( 'SolidUICombobox' , ( ) => {
16- beforeEach ( ( ) => {
17- document . body . innerHTML = ''
18- } )
19-
20- it ( 'is defined as a custom element' , ( ) => {
21- expect ( customElements . get ( 'solid-ui-combobox' ) ) . toBe ( Combobox )
22- } )
23-
24- it ( 'renders the input with label and placeholder' , async ( ) => {
25- const combobox = new Combobox ( )
26- combobox . label = 'Person'
27- combobox . placeholder = 'Search people'
28-
29- document . body . appendChild ( combobox )
30- await combobox . updateComplete
31-
32- const label = combobox . shadowRoot ?. querySelector ( 'label.text-label' ) as HTMLLabelElement
33- const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
34- const toggle = combobox . shadowRoot ?. querySelector ( 'button.dropdown-toggle' ) as HTMLButtonElement
35-
36- expect ( label ) . not . toBeNull ( )
37- expect ( label . textContent ) . toContain ( 'Person' )
38- expect ( input ) . not . toBeNull ( )
39- expect ( input . placeholder ) . toBe ( 'Search people' )
40- expect ( input . getAttribute ( 'role' ) ) . toBe ( 'combobox' )
41- expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
42- expect ( toggle ) . not . toBeNull ( )
43- } )
44-
45- it ( 'loads suggestions from suggestionProvider and emits input events' , async ( ) => {
46- const combobox = new Combobox ( )
47- const inputEvents = jest . fn ( )
48- const suggestionProvider = jest . fn ( async ( query : string ) => [
49- { label : `Alice ${ query } ` , value : 'alice' } ,
50- { label : `Bob ${ query } ` , value : 'bob' }
51- ] )
52-
53- combobox . suggestionProvider = suggestionProvider
54- combobox . addEventListener ( 'input' , ( event : Event ) => {
55- inputEvents ( ( event as CustomEvent ) . detail )
56- } )
57-
58- document . body . appendChild ( combobox )
59- await combobox . updateComplete
60-
61- const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
62- input . value = 'al'
63- input . dispatchEvent ( new Event ( 'input' , { bubbles : true , composed : true } ) )
64-
65- await flushUpdates ( )
66- await combobox . updateComplete
67-
68- const portalRoot = getPortalRoot ( )
69- const options = Array . from ( portalRoot ?. querySelectorAll ( '[role="option"]' ) as NodeListOf < HTMLElement > )
70-
71- expect ( suggestionProvider ) . toHaveBeenCalledWith ( 'al' )
72- expect ( inputEvents ) . toHaveBeenCalledWith ( { value : 'al' } )
73- expect ( combobox . inputValue ) . toBe ( 'al' )
74- expect ( options ) . toHaveLength ( 2 )
75- expect ( options [ 0 ] . textContent ) . toContain ( 'Alice al' )
76- } )
77-
78- it ( 'renders the selected option first in the popup' , async ( ) => {
79- const combobox = new Combobox ( )
80- combobox . options = [
81- { label : 'English' , value : 'en' } ,
82- { label : 'French' , value : 'fr' } ,
83- { label : 'Spanish' , value : 'es' }
84- ]
85- combobox . value = 'fr'
86-
87- document . body . appendChild ( combobox )
88- await combobox . updateComplete
89-
90- const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
91- input . dispatchEvent ( new Event ( 'focus' ) )
92- await combobox . updateComplete
93-
94- const portalRoot = getPortalRoot ( )
95- const options = Array . from ( portalRoot ?. querySelectorAll ( '[role="option"]' ) as NodeListOf < HTMLElement > )
96-
97- expect ( options ) . toHaveLength ( 3 )
98- expect ( options [ 0 ] . textContent ) . toContain ( 'French' )
99- expect ( options [ 0 ] . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' )
100- } )
101-
102- it ( 'updates value and emits change when an option is clicked' , async ( ) => {
103- const combobox = new Combobox ( )
104- const changed = jest . fn ( )
105-
106- combobox . options = [
107- { label : 'Alice' , value : 'alice' , publicId : 'https://example.com/alice' } ,
108- { label : 'Bob' , value : 'bob' }
109- ]
110-
111- combobox . addEventListener ( 'change' , ( event : Event ) => {
112- changed ( ( event as CustomEvent ) . detail )
113- } )
114-
115- document . body . appendChild ( combobox )
116- await combobox . updateComplete
117-
118- const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
119- input . dispatchEvent ( new Event ( 'focus' ) )
120- await combobox . updateComplete
121-
122- const portalRoot = getPortalRoot ( )
123- const options = portalRoot ?. querySelectorAll ( '[role="option"]' ) as NodeListOf < HTMLElement >
124- options [ 1 ] . click ( )
125- await combobox . updateComplete
126-
127- expect ( combobox . value ) . toBe ( 'bob' )
128- expect ( combobox . inputValue ) . toBe ( 'Bob' )
129- expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
130- expect ( changed ) . toHaveBeenCalledWith ( {
131- value : 'bob' ,
132- label : 'Bob' ,
133- option : { label : 'Bob' , value : 'bob' }
134- } )
135- } )
136-
137- it ( 'supports keyboard selection from the input' , async ( ) => {
138- const combobox = new Combobox ( )
139- const changed = jest . fn ( )
140-
141- combobox . options = [
142- { label : 'Alice' , value : 'alice' } ,
143- { label : 'Bob' , value : 'bob' } ,
144- { label : 'Carol' , value : 'carol' }
145- ]
146-
147- combobox . addEventListener ( 'change' , ( event : Event ) => {
148- changed ( ( event as CustomEvent ) . detail )
149- } )
150-
151- document . body . appendChild ( combobox )
152- await combobox . updateComplete
153-
154- const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
155-
156- input . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'ArrowDown' , bubbles : true } ) )
157- await combobox . updateComplete
158-
159- expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
160- expect ( input . getAttribute ( 'aria-activedescendant' ) ) . toBeTruthy ( )
161-
162- input . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'ArrowDown' , bubbles : true } ) )
163- await combobox . updateComplete
164-
165- input . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'Enter' , bubbles : true } ) )
166- await combobox . updateComplete
167-
168- expect ( combobox . value ) . toBe ( 'bob' )
169- expect ( combobox . inputValue ) . toBe ( 'Bob' )
170- expect ( changed ) . toHaveBeenCalledWith ( {
171- value : 'bob' ,
172- label : 'Bob' ,
173- option : { label : 'Bob' , value : 'bob' }
174- } )
175- } )
176-
177- it ( 'closes the popup when clicking outside the component' , async ( ) => {
178- const combobox = new Combobox ( )
179- combobox . options = [
180- { label : 'Alice' , value : 'alice' } ,
181- { label : 'Bob' , value : 'bob' }
182- ]
183-
184- document . body . appendChild ( combobox )
185- await combobox . updateComplete
186-
187- const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
188- input . dispatchEvent ( new Event ( 'focus' ) )
189- await combobox . updateComplete
190-
191- expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
192- expect ( getPortalRoot ( ) ) . not . toBeNull ( )
193-
194- document . body . dispatchEvent ( new Event ( 'pointerdown' , { bubbles : true , composed : true } ) )
195- await combobox . updateComplete
196-
197- expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
198- } )
16+ beforeEach ( ( ) => {
17+ document . body . innerHTML = ''
18+ } )
19+
20+ it ( 'is defined as a custom element' , ( ) => {
21+ expect ( customElements . get ( 'solid-ui-combobox' ) ) . toBe ( Combobox )
22+ } )
23+
24+ it ( 'renders the input with label and placeholder' , async ( ) => {
25+ const combobox = new Combobox ( )
26+ combobox . label = 'Person'
27+ combobox . placeholder = 'Search people'
28+
29+ document . body . appendChild ( combobox )
30+ await combobox . updateComplete
31+
32+ const label = combobox . shadowRoot ?. querySelector ( 'label.text-label' ) as HTMLLabelElement
33+ const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
34+ const toggle = combobox . shadowRoot ?. querySelector ( 'button.dropdown-toggle' ) as HTMLButtonElement
35+
36+ expect ( label ) . not . toBeNull ( )
37+ expect ( label . textContent ) . toContain ( 'Person' )
38+ expect ( input ) . not . toBeNull ( )
39+ expect ( input . placeholder ) . toBe ( 'Search people' )
40+ expect ( input . getAttribute ( 'role' ) ) . toBe ( 'combobox' )
41+ expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
42+ expect ( toggle ) . not . toBeNull ( )
43+ } )
44+
45+ it ( 'loads suggestions from suggestionProvider and emits input events' , async ( ) => {
46+ const combobox = new Combobox ( )
47+ const inputEvents = jest . fn ( )
48+ const suggestionProvider = jest . fn ( async ( query : string ) => [
49+ { label : `Alice ${ query } ` , value : 'alice' } ,
50+ { label : `Bob ${ query } ` , value : 'bob' }
51+ ] )
52+
53+ combobox . suggestionProvider = suggestionProvider
54+ combobox . addEventListener ( 'input' , ( event : Event ) => {
55+ inputEvents ( ( event as CustomEvent ) . detail )
56+ } )
57+
58+ document . body . appendChild ( combobox )
59+ await combobox . updateComplete
60+
61+ const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
62+ input . value = 'al'
63+ input . dispatchEvent ( new Event ( 'input' , { bubbles : true , composed : true } ) )
64+
65+ await flushUpdates ( )
66+ await combobox . updateComplete
67+
68+ const portalRoot = getPortalRoot ( )
69+ const options = Array . from ( portalRoot ?. querySelectorAll ( '[role="option"]' ) as NodeListOf < HTMLElement > )
70+
71+ expect ( suggestionProvider ) . toHaveBeenCalledWith ( 'al' )
72+ expect ( inputEvents ) . toHaveBeenCalledWith ( { value : 'al' } )
73+ expect ( combobox . inputValue ) . toBe ( 'al' )
74+ expect ( options ) . toHaveLength ( 2 )
75+ expect ( options [ 0 ] . textContent ) . toContain ( 'Alice al' )
76+ } )
77+
78+ it ( 'renders the selected option first in the popup' , async ( ) => {
79+ const combobox = new Combobox ( )
80+ combobox . options = [
81+ { label : 'English' , value : 'en' } ,
82+ { label : 'French' , value : 'fr' } ,
83+ { label : 'Spanish' , value : 'es' }
84+ ]
85+ combobox . value = 'fr'
86+
87+ document . body . appendChild ( combobox )
88+ await combobox . updateComplete
89+
90+ const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
91+ input . dispatchEvent ( new Event ( 'focus' ) )
92+ await combobox . updateComplete
93+
94+ const portalRoot = getPortalRoot ( )
95+ const options = Array . from ( portalRoot ?. querySelectorAll ( '[role="option"]' ) as NodeListOf < HTMLElement > )
96+
97+ expect ( options ) . toHaveLength ( 3 )
98+ expect ( options [ 0 ] . textContent ) . toContain ( 'French' )
99+ expect ( options [ 0 ] . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' )
100+ } )
101+
102+ it ( 'updates value and emits change when an option is clicked' , async ( ) => {
103+ const combobox = new Combobox ( )
104+ const changed = jest . fn ( )
105+
106+ combobox . options = [
107+ { label : 'Alice' , value : 'alice' , publicId : 'https://example.com/alice' } ,
108+ { label : 'Bob' , value : 'bob' }
109+ ]
110+
111+ combobox . addEventListener ( 'change' , ( event : Event ) => {
112+ changed ( ( event as CustomEvent ) . detail )
113+ } )
114+
115+ document . body . appendChild ( combobox )
116+ await combobox . updateComplete
117+
118+ const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
119+ input . dispatchEvent ( new Event ( 'focus' ) )
120+ await combobox . updateComplete
121+
122+ const portalRoot = getPortalRoot ( )
123+ const options = portalRoot ?. querySelectorAll ( '[role="option"]' ) as NodeListOf < HTMLElement >
124+ options [ 1 ] . click ( )
125+ await combobox . updateComplete
126+
127+ expect ( combobox . value ) . toBe ( 'bob' )
128+ expect ( combobox . inputValue ) . toBe ( 'Bob' )
129+ expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
130+ expect ( changed ) . toHaveBeenCalledWith ( {
131+ value : 'bob' ,
132+ label : 'Bob' ,
133+ option : { label : 'Bob' , value : 'bob' }
134+ } )
135+ } )
136+
137+ it ( 'supports keyboard selection from the input' , async ( ) => {
138+ const combobox = new Combobox ( )
139+ const changed = jest . fn ( )
140+
141+ combobox . options = [
142+ { label : 'Alice' , value : 'alice' } ,
143+ { label : 'Bob' , value : 'bob' } ,
144+ { label : 'Carol' , value : 'carol' }
145+ ]
146+
147+ combobox . addEventListener ( 'change' , ( event : Event ) => {
148+ changed ( ( event as CustomEvent ) . detail )
149+ } )
150+
151+ document . body . appendChild ( combobox )
152+ await combobox . updateComplete
153+
154+ const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
155+
156+ input . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'ArrowDown' , bubbles : true } ) )
157+ await combobox . updateComplete
158+
159+ expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
160+ expect ( input . getAttribute ( 'aria-activedescendant' ) ) . toBeTruthy ( )
161+
162+ input . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'ArrowDown' , bubbles : true } ) )
163+ await combobox . updateComplete
164+
165+ input . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'Enter' , bubbles : true } ) )
166+ await combobox . updateComplete
167+
168+ expect ( combobox . value ) . toBe ( 'bob' )
169+ expect ( combobox . inputValue ) . toBe ( 'Bob' )
170+ expect ( changed ) . toHaveBeenCalledWith ( {
171+ value : 'bob' ,
172+ label : 'Bob' ,
173+ option : { label : 'Bob' , value : 'bob' }
174+ } )
175+ } )
176+
177+ it ( 'closes the popup when clicking outside the component' , async ( ) => {
178+ const combobox = new Combobox ( )
179+ combobox . options = [
180+ { label : 'Alice' , value : 'alice' } ,
181+ { label : 'Bob' , value : 'bob' }
182+ ]
183+
184+ document . body . appendChild ( combobox )
185+ await combobox . updateComplete
186+
187+ const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
188+ input . dispatchEvent ( new Event ( 'focus' ) )
189+ await combobox . updateComplete
190+
191+ expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
192+ expect ( getPortalRoot ( ) ) . not . toBeNull ( )
193+
194+ document . body . dispatchEvent ( new Event ( 'pointerdown' , { bubbles : true , composed : true } ) )
195+ await combobox . updateComplete
196+
197+ expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
198+ } )
199199} )
0 commit comments