combobox.spec.js 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222
  1. import { beVisible, beHidden, haveAttribute, haveClasses, notHaveClasses, haveText, contain, notContain, html, notBeVisible, notHaveAttribute, notExist, haveFocus, test, haveValue, haveLength} from '../../../utils'
  2. test('it works with x-model',
  3. [html`
  4. <div
  5. x-data="{
  6. query: '',
  7. selected: null,
  8. people: [
  9. { id: 1, name: 'Wade Cooper' },
  10. { id: 2, name: 'Arlene Mccoy' },
  11. { id: 3, name: 'Devon Webb' },
  12. { id: 4, name: 'Tom Cook' },
  13. { id: 5, name: 'Tanya Fox', disabled: true },
  14. { id: 6, name: 'Hellen Schmidt' },
  15. { id: 7, name: 'Caroline Schultz' },
  16. { id: 8, name: 'Mason Heaney' },
  17. { id: 9, name: 'Claudie Smitham' },
  18. { id: 10, name: 'Emil Schaefer' },
  19. ],
  20. get filteredPeople() {
  21. return this.query === ''
  22. ? this.people
  23. : this.people.filter((person) => {
  24. return person.name.toLowerCase().includes(this.query.toLowerCase())
  25. })
  26. },
  27. }"
  28. >
  29. <div x-combobox x-model="selected">
  30. <label x-combobox:label>Select person</label>
  31. <div>
  32. <div>
  33. <input
  34. x-combobox:input
  35. :display-value="person => person.name"
  36. @change="query = $event.target.value"
  37. placeholder="Search..."
  38. />
  39. <button x-combobox:button>Toggle</button>
  40. </div>
  41. <div x-combobox:options>
  42. <ul>
  43. <template
  44. x-for="person in filteredPeople"
  45. :key="person.id"
  46. hidden
  47. >
  48. <li
  49. x-combobox:option
  50. :option="person.id"
  51. :value="person"
  52. :disabled="person.disabled"
  53. x-text="person.name"
  54. >
  55. </li>
  56. </template>
  57. </ul>
  58. <p x-show="filteredPeople.length == 0">No people match your query.</p>
  59. </div>
  60. </div>
  61. <article x-text="selected?.name"></article>
  62. </div>
  63. </div>
  64. `],
  65. ({ get }) => {
  66. get('ul').should(notBeVisible())
  67. get('button').click()
  68. get('ul').should(beVisible())
  69. get('button').click()
  70. get('ul').should(notBeVisible())
  71. get('button').click()
  72. get('[option="2"]').click()
  73. get('ul').should(notBeVisible())
  74. get('input').should(haveValue('Arlene Mccoy'))
  75. get('article').should(haveText('Arlene Mccoy'))
  76. get('button').click()
  77. get('ul').should(contain('Wade Cooper'))
  78. .should(contain('Arlene Mccoy'))
  79. .should(contain('Devon Webb'))
  80. get('[option="3"]').click()
  81. get('ul').should(notBeVisible())
  82. get('input').should(haveValue('Devon Webb'))
  83. get('article').should(haveText('Devon Webb'))
  84. get('button').click()
  85. get('ul').should(contain('Wade Cooper'))
  86. .should(contain('Arlene Mccoy'))
  87. .should(contain('Devon Webb'))
  88. get('[option="1"]').click()
  89. get('ul').should(notBeVisible())
  90. get('input').should(haveValue('Wade Cooper'))
  91. get('article').should(haveText('Wade Cooper'))
  92. },
  93. )
  94. test('it works with internal state',
  95. [html`
  96. <div
  97. x-data="{ people: [
  98. { id: 1, name: 'Wade Cooper' },
  99. { id: 2, name: 'Arlene Mccoy' },
  100. { id: 3, name: 'Devon Webb' },
  101. { id: 4, name: 'Tom Cook' },
  102. { id: 5, name: 'Tanya Fox', disabled: true },
  103. { id: 6, name: 'Hellen Schmidt' },
  104. { id: 7, name: 'Caroline Schultz' },
  105. { id: 8, name: 'Mason Heaney' },
  106. { id: 9, name: 'Claudie Smitham' },
  107. { id: 10, name: 'Emil Schaefer' },
  108. ]}"
  109. x-combobox
  110. >
  111. <label x-combobox:label>Assigned to</label>
  112. <input x-combobox:input :display-value="(person) => person.name" type="text">
  113. <button x-combobox:button x-text="$combobox.value ? $combobox.value.name : 'Select Person'"></button>
  114. <ul x-combobox:options>
  115. <template x-for="person in people" :key="person.id">
  116. <li
  117. :option="person.id"
  118. x-combobox:option
  119. :value="person"
  120. :disabled="person.disabled"
  121. >
  122. <span x-text="person.name"></span>
  123. </li>
  124. </template>
  125. </ul>
  126. </div>
  127. `],
  128. ({ get }) => {
  129. get('ul').should(notBeVisible())
  130. get('button')
  131. .should(haveText('Select Person'))
  132. .click()
  133. get('ul').should(beVisible())
  134. get('button').click()
  135. get('ul').should(notBeVisible())
  136. get('button').click()
  137. get('[option="2"]').click()
  138. get('ul').should(notBeVisible())
  139. get('button').should(haveText('Arlene Mccoy'))
  140. get('input').should(haveValue('Arlene Mccoy'))
  141. },
  142. )
  143. test('$combobox/$comboboxOption',
  144. [html`
  145. <div
  146. x-data="{ people: [
  147. { id: 1, name: 'Wade Cooper' },
  148. { id: 2, name: 'Arlene Mccoy' },
  149. { id: 3, name: 'Devon Webb' },
  150. { id: 4, name: 'Tom Cook' },
  151. { id: 5, name: 'Tanya Fox', disabled: true },
  152. { id: 6, name: 'Hellen Schmidt' },
  153. { id: 7, name: 'Caroline Schultz' },
  154. { id: 8, name: 'Mason Heaney' },
  155. { id: 9, name: 'Claudie Smitham' },
  156. { id: 10, name: 'Emil Schaefer' },
  157. ]}"
  158. x-combobox
  159. >
  160. <label x-combobox:label>Assigned to</label>
  161. <input x-combobox:input :display-value="(person) => person.name" type="text">
  162. <button x-combobox:button x-text="$combobox.value ? $combobox.value.name : 'Select Person'"></button>
  163. <p x-text="$combobox.activeIndex"></p>
  164. <article x-text="$combobox.activeOption?.name"></article>
  165. <ul x-combobox:options>
  166. <template x-for="person in people" :key="person.id">
  167. <li
  168. :option="person.id"
  169. x-combobox:option
  170. :value="person"
  171. :disabled="person.disabled"
  172. :class="{
  173. 'selected': $comboboxOption.isSelected,
  174. 'active': $comboboxOption.isActive,
  175. 'disabled': $comboboxOption.isDisabled,
  176. }"
  177. >
  178. <span x-text="person.name"></span>
  179. </li>
  180. </template>
  181. </ul>
  182. </div>
  183. `],
  184. ({ get }) => {
  185. get('article').should(haveText(''))
  186. get('[option="5"]').should(haveClasses(['disabled']))
  187. get('button')
  188. .should(haveText('Select Person'))
  189. .click()
  190. get('[option="1"]').should(haveClasses(['active']))
  191. get('input').type('{downarrow}')
  192. get('article').should(haveText('Arlene Mccoy'))
  193. get('p').should(haveText('1'))
  194. get('[option="2"]').should(haveClasses(['active']))
  195. get('button').should(haveText('Select Person'))
  196. get('[option="2"]').click()
  197. get('button').should(haveText('Arlene Mccoy'))
  198. get('[option="2"]').should(haveClasses(['selected']))
  199. },
  200. )
  201. test('"name" prop',
  202. [html`
  203. <div
  204. x-data="{ people: [
  205. { id: 1, name: 'Wade Cooper' },
  206. { id: 2, name: 'Arlene Mccoy' },
  207. { id: 3, name: 'Devon Webb' },
  208. { id: 4, name: 'Tom Cook' },
  209. { id: 5, name: 'Tanya Fox', disabled: true },
  210. { id: 6, name: 'Hellen Schmidt' },
  211. { id: 7, name: 'Caroline Schultz' },
  212. { id: 8, name: 'Mason Heaney' },
  213. { id: 9, name: 'Claudie Smitham' },
  214. { id: 10, name: 'Emil Schaefer' },
  215. ]}"
  216. x-combobox
  217. name="person"
  218. >
  219. <label x-combobox:label>Assigned to</label>
  220. <input x-combobox:input :display-value="(person) => person.name" type="text">
  221. <button x-combobox:button x-text="$combobox.value ? $combobox.value : 'Select Person'"></button>
  222. <ul x-combobox:options>
  223. <template x-for="person in people" :key="person.id">
  224. <li
  225. :option="person.id"
  226. x-combobox:option
  227. :value="person.id"
  228. :disabled="person.disabled"
  229. :class="{
  230. 'selected': $comboboxOption.isSelected,
  231. 'active': $comboboxOption.isActive,
  232. }"
  233. >
  234. <span x-text="person.name"></span>
  235. </li>
  236. </template>
  237. </ul>
  238. </div>
  239. `],
  240. ({ get }) => {
  241. get('input').should(haveAttribute('value', 'null'))
  242. get('button').click()
  243. get('input').should(haveAttribute('value', 'null'))
  244. get('[option="2"]').click()
  245. get('input').should(beHidden())
  246. .should(haveAttribute('name', 'person'))
  247. .should(haveAttribute('value', '2'))
  248. .should(haveAttribute('type', 'hidden'))
  249. get('button').click()
  250. get('[option="4"]').click()
  251. get('input').should(beHidden())
  252. .should(haveAttribute('name', 'person'))
  253. .should(haveAttribute('value', '4'))
  254. .should(haveAttribute('type', 'hidden'))
  255. },
  256. );
  257. test('Preserves currenty active selection while options change from searching even if there\'s a selected option in the filtered results',
  258. [html`
  259. <div
  260. x-data="{
  261. query: '',
  262. selected: null,
  263. people: [
  264. { id: 1, name: 'Wade Cooper' },
  265. { id: 2, name: 'Arlene Mccoy' },
  266. { id: 3, name: 'Devon Webb' },
  267. { id: 4, name: 'Tom Cook' },
  268. { id: 5, name: 'Tanya Fox', disabled: true },
  269. { id: 6, name: 'Hellen Schmidt' },
  270. { id: 7, name: 'Caroline Schultz' },
  271. { id: 8, name: 'Mason Heaney' },
  272. { id: 9, name: 'Claudie Smitham' },
  273. { id: 10, name: 'Emil Schaefer' },
  274. ],
  275. get filteredPeople() {
  276. return this.query === ''
  277. ? this.people
  278. : this.people.filter((person) => {
  279. return person.name.toLowerCase().includes(this.query.toLowerCase())
  280. })
  281. },
  282. }"
  283. >
  284. <div x-combobox x-model="selected">
  285. <label x-combobox:label>Select person</label>
  286. <div>
  287. <div>
  288. <input
  289. x-combobox:input
  290. :display-value="person => person.name"
  291. @change="query = $event.target.value"
  292. placeholder="Search..."
  293. />
  294. <button x-combobox:button>Toggle</button>
  295. </div>
  296. <div x-combobox:options>
  297. <ul>
  298. <template
  299. x-for="person in filteredPeople"
  300. :key="person.id"
  301. hidden
  302. >
  303. <li
  304. x-combobox:option
  305. :option="person.id"
  306. :value="person"
  307. :disabled="person.disabled"
  308. >
  309. <span x-text="person.name"></span>
  310. <span x-show="$comboboxOption.isActive">*</span>
  311. <span x-show="$comboboxOption.isSelected">x</span>
  312. </li>
  313. </template>
  314. </ul>
  315. <p x-show="filteredPeople.length == 0">No people match your query.</p>
  316. </div>
  317. </div>
  318. </div>
  319. <article>lorem ipsum</article>
  320. </div>
  321. `],
  322. ({ get }) => {
  323. get('input').should(haveText(''))
  324. get('button').click()
  325. get('[option="3"]').click()
  326. cy.wait(100)
  327. get('input').type('{selectAll}{backspace}')
  328. cy.wait(100)
  329. get('input').type('{downArrow}')
  330. cy.wait(100)
  331. get('[option="3"]').should(contain('*'))
  332. get('input').type('{upArrow}{upArrow}')
  333. cy.wait(100)
  334. get('[option="1"]').should(contain('*'))
  335. cy.wait(100)
  336. get('input').type('d')
  337. get('input').trigger('change')
  338. cy.wait(100)
  339. get('[option="1"]').should(contain('*'))
  340. },
  341. );
  342. test('"name" prop with object value',
  343. [html`
  344. <div
  345. x-data="{ people: [
  346. { id: 1, name: 'Wade Cooper' },
  347. { id: 2, name: 'Arlene Mccoy' },
  348. { id: 3, name: 'Devon Webb' },
  349. { id: 4, name: 'Tom Cook' },
  350. { id: 5, name: 'Tanya Fox', disabled: true },
  351. { id: 6, name: 'Hellen Schmidt' },
  352. { id: 7, name: 'Caroline Schultz' },
  353. { id: 8, name: 'Mason Heaney' },
  354. { id: 9, name: 'Claudie Smitham' },
  355. { id: 10, name: 'Emil Schaefer' },
  356. ]}"
  357. x-combobox
  358. name="person"
  359. >
  360. <label x-combobox:label>Assigned to</label>
  361. <input x-combobox:input :display-value="(person) => person.name" type="text">
  362. <button x-combobox:button x-text="$combobox.value ? $combobox.value.name : 'Select Person'"></button>
  363. <ul x-combobox:options>
  364. <template x-for="person in people" :key="person.id">
  365. <li
  366. :option="person.id"
  367. x-combobox:option
  368. :value="person"
  369. :disabled="person.disabled"
  370. :class="{
  371. 'selected': $comboboxOption.isSelected,
  372. 'active': $comboboxOption.isActive,
  373. }"
  374. >
  375. <span x-text="person.name"></span>
  376. </li>
  377. </template>
  378. </ul>
  379. </div>
  380. `],
  381. ({ get }) => {
  382. get('input[name="person"]').should(haveAttribute('value', 'null'))
  383. get('button').click()
  384. get('[name="person[id]"]').should(notExist())
  385. get('[option="2"]').click()
  386. get('input[name="person"]').should(notExist())
  387. get('[name="person[id]"]').should(beHidden())
  388. .should(haveAttribute('value', '2'))
  389. .should(haveAttribute('type', 'hidden'))
  390. get('[name="person[name]"]').should(beHidden())
  391. .should(haveAttribute('value', 'Arlene Mccoy'))
  392. .should(haveAttribute('type', 'hidden'))
  393. get('button').click()
  394. get('[option="4"]').click()
  395. get('[name="person[id]"]').should(beHidden())
  396. .should(haveAttribute('value', '4'))
  397. .should(haveAttribute('type', 'hidden'))
  398. get('[name="person[name]"]').should(beHidden())
  399. .should(haveAttribute('value', 'Tom Cook'))
  400. .should(haveAttribute('type', 'hidden'))
  401. },
  402. );
  403. test('"default-value" prop',
  404. [html`
  405. <div
  406. x-data="{ people: [
  407. { id: 1, name: 'Wade Cooper' },
  408. { id: 2, name: 'Arlene Mccoy' },
  409. { id: 3, name: 'Devon Webb' },
  410. { id: 4, name: 'Tom Cook' },
  411. { id: 5, name: 'Tanya Fox', disabled: true },
  412. { id: 6, name: 'Hellen Schmidt' },
  413. { id: 7, name: 'Caroline Schultz' },
  414. { id: 8, name: 'Mason Heaney' },
  415. { id: 9, name: 'Claudie Smitham' },
  416. { id: 10, name: 'Emil Schaefer' },
  417. ]}"
  418. x-combobox
  419. name="person"
  420. default-value="2"
  421. >
  422. <label x-combobox:label>Assigned to</label>
  423. <input x-combobox:input :display-value="(person) => person.name" type="text">
  424. <button x-combobox:button x-text="$combobox.value ? $combobox.value : 'Select Person'"></button>
  425. <ul x-combobox:options>
  426. <template x-for="person in people" :key="person.id">
  427. <li
  428. :option="person.id"
  429. x-combobox:option
  430. :value="person.id"
  431. :disabled="person.disabled"
  432. :class="{
  433. 'selected': $comboboxOption.isSelected,
  434. 'active': $comboboxOption.isActive,
  435. }"
  436. >
  437. <span x-text="person.name"></span>
  438. </li>
  439. </template>
  440. </ul>
  441. </div>
  442. `],
  443. ({ get }) => {
  444. get('input[name="person"]').should(beHidden())
  445. .should(haveAttribute('value', '2'))
  446. .should(haveAttribute('type', 'hidden'))
  447. },
  448. );
  449. test('"multiple" prop',
  450. [html`
  451. <div
  452. x-data="{
  453. people: [
  454. { id: 1, name: 'Wade Cooper' },
  455. { id: 2, name: 'Arlene Mccoy' },
  456. { id: 3, name: 'Devon Webb' },
  457. { id: 4, name: 'Tom Cook' },
  458. { id: 5, name: 'Tanya Fox', disabled: true },
  459. { id: 6, name: 'Hellen Schmidt' },
  460. { id: 7, name: 'Caroline Schultz' },
  461. { id: 8, name: 'Mason Heaney' },
  462. { id: 9, name: 'Claudie Smitham' },
  463. { id: 10, name: 'Emil Schaefer' },
  464. ]
  465. }"
  466. x-combobox
  467. multiple
  468. >
  469. <label x-combobox:label>Assigned to</label>
  470. <input x-combobox:input :display-value="(person) => person.name" type="text">
  471. <button x-combobox:button x-text="$combobox.value ? $combobox.value.join(',') : 'Select People'"></button>
  472. <ul x-combobox:options>
  473. <template x-for="person in people" :key="person.id">
  474. <li
  475. :option="person.id"
  476. x-combobox:option
  477. :value="person.id"
  478. :disabled="person.disabled"
  479. :class="{
  480. 'selected': $comboboxOption.isSelected,
  481. 'active': $comboboxOption.isActive,
  482. }"
  483. >
  484. <span x-text="person.name"></span>
  485. </li>
  486. </template>
  487. </ul>
  488. </div>
  489. `],
  490. ({ get }) => {
  491. get('button').click()
  492. get('[option="2"]').click()
  493. get('ul').should(beVisible())
  494. get('button').should(haveText('2'))
  495. get('[option="4"]').click()
  496. get('button').should(haveText('2,4'))
  497. get('ul').should(beVisible())
  498. get('[option="4"]').click()
  499. get('button').should(haveText('2'))
  500. get('ul').should(beVisible())
  501. get('input').type('Tom')
  502. get('input').type('{enter}')
  503. get('button').should(haveText('2,4'))
  504. // input field doesn't reset when a new option is selected
  505. get('input').should(haveValue('Tom'))
  506. },
  507. );
  508. test('"multiple" and "name" props together',
  509. [html`
  510. <div
  511. x-data="{
  512. people: [
  513. { id: 1, name: 'Wade Cooper' },
  514. { id: 2, name: 'Arlene Mccoy' },
  515. { id: 3, name: 'Devon Webb' },
  516. { id: 4, name: 'Tom Cook' },
  517. { id: 5, name: 'Tanya Fox', disabled: true },
  518. { id: 6, name: 'Hellen Schmidt' },
  519. { id: 7, name: 'Caroline Schultz' },
  520. { id: 8, name: 'Mason Heaney' },
  521. { id: 9, name: 'Claudie Smitham' },
  522. { id: 10, name: 'Emil Schaefer' },
  523. ]
  524. }"
  525. x-combobox
  526. multiple
  527. name="people"
  528. >
  529. <label x-combobox:label>Assigned to</label>
  530. <input x-combobox:input :display-value="(person) => person.name" type="text">
  531. <button x-combobox:button x-text="$combobox.value ? $combobox.value.map(p => p.id).join(',') : 'Select People'"></button>
  532. <ul x-combobox:options>
  533. <template x-for="person in people" :key="person.id">
  534. <li
  535. :option="person.id"
  536. x-combobox:option
  537. :value="person"
  538. :disabled="person.disabled"
  539. :class="{
  540. 'selected': $comboboxOption.isSelected,
  541. 'active': $comboboxOption.isActive,
  542. }"
  543. >
  544. <span x-text="person.name"></span>
  545. </li>
  546. </template>
  547. </ul>
  548. </div>
  549. `],
  550. ({ get }) => {
  551. // get('input[name="people"]').should(haveAttribute('value', 'null'))
  552. get('button').click()
  553. get('[name="people[0][id]"]').should(notExist())
  554. get('[option="2"]').click()
  555. get('ul').should(beVisible())
  556. get('button').should(haveText('2'))
  557. get('input[name="people"]').should(notExist())
  558. get('[name="people[0][id]"]').should(beHidden())
  559. .should(haveAttribute('value', '2'))
  560. .should(haveAttribute('type', 'hidden'))
  561. get('[name="people[0][name]"]').should(beHidden())
  562. .should(haveAttribute('value', 'Arlene Mccoy'))
  563. .should(haveAttribute('type', 'hidden'))
  564. get('[option="4"]').click()
  565. get('[name="people[0][id]"]').should(beHidden())
  566. .should(haveAttribute('value', '2'))
  567. .should(haveAttribute('type', 'hidden'))
  568. get('[name="people[0][name]"]').should(beHidden())
  569. .should(haveAttribute('value', 'Arlene Mccoy'))
  570. .should(haveAttribute('type', 'hidden'))
  571. get('[name="people[1][id]"]').should(beHidden())
  572. .should(haveAttribute('value', '4'))
  573. .should(haveAttribute('type', 'hidden'))
  574. get('[name="people[1][name]"]').should(beHidden())
  575. .should(haveAttribute('value', 'Tom Cook'))
  576. .should(haveAttribute('type', 'hidden'))
  577. get('button').should(haveText('2,4'))
  578. get('ul').should(beVisible())
  579. get('[option="4"]').click()
  580. get('[name="people[0][id]"]').should(beHidden())
  581. .should(haveAttribute('value', '2'))
  582. .should(haveAttribute('type', 'hidden'))
  583. get('[name="people[0][name]"]').should(beHidden())
  584. .should(haveAttribute('value', 'Arlene Mccoy'))
  585. .should(haveAttribute('type', 'hidden'))
  586. get('[name="people[1][id]"]').should(notExist())
  587. get('[name="people[1][name]"]').should(notExist())
  588. get('button').should(haveText('2'))
  589. get('ul').should(beVisible())
  590. },
  591. );
  592. test('keyboard controls',
  593. [html`
  594. <div
  595. x-data="{ active: null, people: [
  596. { id: 1, name: 'Wade Cooper' },
  597. { id: 2, name: 'Arlene Mccoy' },
  598. { id: 3, name: 'Devon Webb', disabled: true },
  599. { id: 4, name: 'Tom Cook' },
  600. { id: 5, name: 'Tanya Fox', disabled: true },
  601. { id: 6, name: 'Hellen Schmidt' },
  602. { id: 7, name: 'Caroline Schultz' },
  603. { id: 8, name: 'Mason Heaney' },
  604. { id: 9, name: 'Claudie Smitham' },
  605. { id: 10, name: 'Emil Schaefer' },
  606. ]}"
  607. x-combobox
  608. x-model="active"
  609. >
  610. <label x-combobox:label>Assigned to</label>
  611. <input x-combobox:input :display-value="(person) => person.name" type="text">
  612. <button x-combobox:button x-text="active ? active.name : 'Select Person'"></button>
  613. <ul x-combobox:options options>
  614. <template x-for="person in people" :key="person.id">
  615. <li
  616. :option="person.id"
  617. x-combobox:option
  618. :value="person"
  619. :disabled="person.disabled"
  620. :class="{
  621. 'selected': $comboboxOption.isSelected,
  622. 'active': $comboboxOption.isActive,
  623. }"
  624. >
  625. <span x-text="person.name"></span>
  626. </li>
  627. </template>
  628. </ul>
  629. </div>
  630. `],
  631. ({ get }) => {
  632. get('.active').should(notExist())
  633. get('button').click()
  634. get('[options]')
  635. .should(beVisible())
  636. get('input').should(haveFocus())
  637. get('[option="1"]')
  638. .should(haveClasses(['active']))
  639. get('input')
  640. .type('{downarrow}')
  641. get('[option="2"]')
  642. .should(haveClasses(['active']))
  643. get('input')
  644. .type('{downarrow}')
  645. get('[option="4"]')
  646. .should(haveClasses(['active']))
  647. get('input')
  648. .type('{uparrow}')
  649. get('[option="2"]')
  650. .should(haveClasses(['active']))
  651. get('input')
  652. .type('{home}')
  653. get('[option="1"]')
  654. .should(haveClasses(['active']))
  655. get('input')
  656. .type('{end}')
  657. get('[option="10"]')
  658. .should(haveClasses(['active']))
  659. get('input')
  660. .type('{pageUp}')
  661. get('[option="1"]')
  662. .should(haveClasses(['active']))
  663. get('input')
  664. .type('{pageDown}')
  665. get('[option="10"]')
  666. .should(haveClasses(['active']))
  667. get('input')
  668. .tab()
  669. .should(haveFocus())
  670. get('[options]')
  671. .should(beVisible())
  672. get('input')
  673. .type('{esc}')
  674. get('[options]')
  675. .should(notBeVisible())
  676. },
  677. )
  678. test('changing value manually changes internal state',
  679. [html`
  680. <div
  681. x-data="{ active: null, people: [
  682. { id: 1, name: 'Wade Cooper' },
  683. { id: 2, name: 'Arlene Mccoy' },
  684. { id: 3, name: 'Devon Webb', disabled: true },
  685. { id: 4, name: 'Tom Cook' },
  686. { id: 5, name: 'Tanya Fox', disabled: true },
  687. { id: 6, name: 'Hellen Schmidt' },
  688. { id: 7, name: 'Caroline Schultz' },
  689. { id: 8, name: 'Mason Heaney' },
  690. { id: 9, name: 'Claudie Smitham' },
  691. { id: 10, name: 'Emil Schaefer' },
  692. ]}"
  693. x-combobox
  694. x-model="active"
  695. >
  696. <label x-combobox:label>Assigned to</label>
  697. <input x-combobox:input :display-value="(person) => person.name" type="text">
  698. <button toggle x-combobox:button x-text="$combobox.value ? $combobox.value : 'Select Person'"></button>
  699. <button select-tim @click="active = 4">Select Tim</button>
  700. <ul x-combobox:options options>
  701. <template x-for="person in people" :key="person.id">
  702. <li
  703. :option="person.id"
  704. x-combobox:option
  705. :value="person.id"
  706. :disabled="person.disabled"
  707. :class="{
  708. 'selected': $comboboxOption.isSelected,
  709. 'active': $comboboxOption.isActive,
  710. }"
  711. >
  712. <span x-text="person.name"></span>
  713. </li>
  714. </template>
  715. </ul>
  716. </div>
  717. `],
  718. ({ get }) => {
  719. get('[select-tim]').click()
  720. get('[option="4"]').should(haveClasses(['selected']))
  721. get('[option="1"]').should(notHaveClasses(['selected']))
  722. get('[toggle]').should(haveText('4'))
  723. },
  724. )
  725. test('has accessibility attributes',
  726. [html`
  727. <div
  728. x-data="{ active: null, people: [
  729. { id: 1, name: 'Wade Cooper' },
  730. { id: 2, name: 'Arlene Mccoy' },
  731. { id: 3, name: 'Devon Webb', disabled: true },
  732. { id: 4, name: 'Tom Cook' },
  733. { id: 5, name: 'Tanya Fox', disabled: true },
  734. { id: 6, name: 'Hellen Schmidt' },
  735. { id: 7, name: 'Caroline Schultz' },
  736. { id: 8, name: 'Mason Heaney' },
  737. { id: 9, name: 'Claudie Smitham' },
  738. { id: 10, name: 'Emil Schaefer' },
  739. ]}"
  740. x-combobox
  741. x-model="active"
  742. >
  743. <label x-combobox:label>Assigned to</label>
  744. <input x-combobox:input :display-value="(person) => person.name" type="text">
  745. <button x-combobox:button x-text="active ? active.name : 'Select Person'"></button>
  746. <ul x-combobox:options options>
  747. <template x-for="person in people" :key="person.id">
  748. <li
  749. :option="person.id"
  750. x-combobox:option
  751. :value="person"
  752. :disabled="person.disabled"
  753. :class="{
  754. 'selected': $comboboxOption.isSelected,
  755. 'active': $comboboxOption.isActive,
  756. }"
  757. >
  758. <span x-text="person.name"></span>
  759. </li>
  760. </template>
  761. </ul>
  762. </div>
  763. `],
  764. ({ get }) => {
  765. get('input')
  766. .should(haveAttribute('aria-expanded', 'false'))
  767. get('button')
  768. .should(haveAttribute('aria-haspopup', 'true'))
  769. .should(haveAttribute('aria-labelledby', 'alpine-combobox-label-1 alpine-combobox-button-1'))
  770. .should(haveAttribute('aria-expanded', 'false'))
  771. .should(notHaveAttribute('aria-controls'))
  772. .should(haveAttribute('id', 'alpine-combobox-button-1'))
  773. .should(haveAttribute('tabindex', '-1'))
  774. .click()
  775. .should(haveAttribute('aria-expanded', 'true'))
  776. .should(haveAttribute('aria-controls', 'alpine-combobox-options-1'))
  777. get('[options]')
  778. .should(haveAttribute('role', 'combobox'))
  779. .should(haveAttribute('id', 'alpine-combobox-options-1'))
  780. .should(haveAttribute('aria-labelledby', 'alpine-combobox-label-1'))
  781. get('[option="1"]')
  782. .should(haveAttribute('role', 'option'))
  783. .should(haveAttribute('id', 'alpine-combobox-option-1'))
  784. .should(haveAttribute('tabindex', '-1'))
  785. .should(haveAttribute('aria-selected', 'true'))
  786. get('[option="2"]')
  787. .should(haveAttribute('role', 'option'))
  788. .should(haveAttribute('id', 'alpine-combobox-option-2'))
  789. .should(haveAttribute('tabindex', '-1'))
  790. .should(haveAttribute('aria-selected', 'false'))
  791. get('input')
  792. .should(haveAttribute('role', 'combobox'))
  793. .should(haveAttribute('aria-autocomplete', 'list'))
  794. .should(haveAttribute('tabindex', '0'))
  795. .should(haveAttribute('aria-expanded', 'true'))
  796. .should(haveAttribute('aria-labelledby', 'alpine-combobox-label-1'))
  797. .should(haveAttribute('aria-controls', 'alpine-combobox-options-1'))
  798. .should(haveAttribute('aria-activedescendant', 'alpine-combobox-option-1'))
  799. .type('{downarrow}')
  800. .should(haveAttribute('aria-activedescendant', 'alpine-combobox-option-2'))
  801. get('[option="2"]')
  802. .should(haveAttribute('aria-selected', 'true'))
  803. },
  804. )
  805. test('"static" prop',
  806. [html`
  807. <div
  808. x-data="{ active: null, show: false, people: [
  809. { id: 1, name: 'Wade Cooper' },
  810. { id: 2, name: 'Arlene Mccoy' },
  811. { id: 3, name: 'Devon Webb' },
  812. { id: 4, name: 'Tom Cook' },
  813. { id: 5, name: 'Tanya Fox', disabled: true },
  814. { id: 6, name: 'Hellen Schmidt' },
  815. { id: 7, name: 'Caroline Schultz' },
  816. { id: 8, name: 'Mason Heaney' },
  817. { id: 9, name: 'Claudie Smitham' },
  818. { id: 10, name: 'Emil Schaefer' },
  819. ]}"
  820. x-combobox
  821. x-model="active"
  822. >
  823. <label x-combobox:label>Assigned to</label>
  824. <input x-combobox:input :display-value="(person) => person.name" type="text">
  825. <button normal-toggle x-combobox:button x-text="active ? active.name : 'Select Person'"></button>
  826. <button real-toggle @click="show = ! show">Toggle</button>
  827. <ul x-combobox:options x-show="show" static>
  828. <template x-for="person in people" :key="person.id">
  829. <li
  830. :option="person.id"
  831. x-combobox:option
  832. :value="person"
  833. :disabled="person.disabled"
  834. >
  835. <span x-text="person.name"></span>
  836. </li>
  837. </template>
  838. </ul>
  839. </div>
  840. `],
  841. ({ get }) => {
  842. get('ul').should(notBeVisible())
  843. get('[normal-toggle]')
  844. .should(haveText('Select Person'))
  845. .click()
  846. get('ul').should(notBeVisible())
  847. get('[real-toggle]').click()
  848. get('ul').should(beVisible())
  849. get('[option="2"]').click()
  850. get('ul').should(beVisible())
  851. get('[normal-toggle]').should(haveText('Arlene Mccoy'))
  852. },
  853. )
  854. test('input reset',
  855. [html`
  856. <div
  857. x-data="{
  858. query: '',
  859. selected: null,
  860. people: [
  861. { id: 1, name: 'Wade Cooper' },
  862. { id: 2, name: 'Arlene Mccoy' },
  863. { id: 3, name: 'Devon Webb' },
  864. { id: 4, name: 'Tom Cook' },
  865. { id: 5, name: 'Tanya Fox', disabled: true },
  866. { id: 6, name: 'Hellen Schmidt' },
  867. { id: 7, name: 'Caroline Schultz' },
  868. { id: 8, name: 'Mason Heaney' },
  869. { id: 9, name: 'Claudie Smitham' },
  870. { id: 10, name: 'Emil Schaefer' },
  871. ],
  872. get filteredPeople() {
  873. return this.query === ''
  874. ? this.people
  875. : this.people.filter((person) => {
  876. return person.name.toLowerCase().includes(this.query.toLowerCase())
  877. })
  878. },
  879. }"
  880. >
  881. <div x-combobox x-model="selected">
  882. <label x-combobox:label>Select person</label>
  883. <div>
  884. <div>
  885. <input
  886. x-combobox:input
  887. :display-value="person => person.name"
  888. @change="query = $event.target.value"
  889. placeholder="Search..."
  890. />
  891. <button x-combobox:button>Toggle</button>
  892. </div>
  893. <div x-combobox:options>
  894. <ul>
  895. <template
  896. x-for="person in filteredPeople"
  897. :key="person.id"
  898. hidden
  899. >
  900. <li
  901. x-combobox:option
  902. :option="person.id"
  903. :value="person"
  904. :disabled="person.disabled"
  905. x-text="person.name"
  906. >
  907. </li>
  908. </template>
  909. </ul>
  910. <p x-show="filteredPeople.length == 0">No people match your query.</p>
  911. </div>
  912. </div>
  913. </div>
  914. <article>lorem ipsum</article>
  915. </div>
  916. `],
  917. ({ get }) => {
  918. // Test after closing with button
  919. get('button').click()
  920. get('input').type('w')
  921. get('button').click()
  922. get('input').should(haveValue(''))
  923. // Test correct state after closing with ESC
  924. get('button').click()
  925. get('input').type('w')
  926. get('input').type('{esc}')
  927. get('input').should(haveValue(''))
  928. // Test correct state after closing with TAB
  929. get('button').click()
  930. get('input').type('w')
  931. get('input').tab()
  932. get('input').should(haveValue(''))
  933. // Test correct state after closing with external click
  934. get('button').click()
  935. get('input').type('w')
  936. get('article').click()
  937. get('input').should(haveValue(''))
  938. // Select something
  939. get('button').click()
  940. get('ul').should(beVisible())
  941. get('[option="2"]').click()
  942. get('input').should(haveValue('Arlene Mccoy'))
  943. // Test after closing with button
  944. get('button').click()
  945. get('input').type('w')
  946. get('button').click()
  947. get('input').should(haveValue('Arlene Mccoy'))
  948. // Test correct state after closing with ESC and reopening
  949. get('button').click()
  950. get('input').type('w')
  951. get('input').type('{esc}')
  952. get('input').should(haveValue('Arlene Mccoy'))
  953. // Test correct state after closing with TAB and reopening
  954. get('button').click()
  955. get('input').type('w')
  956. get('input').tab()
  957. get('input').should(haveValue('Arlene Mccoy'))
  958. // Test correct state after closing with external click and reopening
  959. get('button').click()
  960. get('input').type('w')
  961. get('article').click()
  962. get('input').should(haveValue('Arlene Mccoy'))
  963. },
  964. )
  965. test('combobox shows all options when opening',
  966. [html`
  967. <div
  968. x-data="{
  969. query: '',
  970. selected: null,
  971. people: [
  972. { id: 1, name: 'Wade Cooper' },
  973. { id: 2, name: 'Arlene Mccoy' },
  974. { id: 3, name: 'Devon Webb' },
  975. { id: 4, name: 'Tom Cook' },
  976. { id: 5, name: 'Tanya Fox', disabled: true },
  977. { id: 6, name: 'Hellen Schmidt' },
  978. { id: 7, name: 'Caroline Schultz' },
  979. { id: 8, name: 'Mason Heaney' },
  980. { id: 9, name: 'Claudie Smitham' },
  981. { id: 10, name: 'Emil Schaefer' },
  982. ],
  983. get filteredPeople() {
  984. return this.query === ''
  985. ? this.people
  986. : this.people.filter((person) => {
  987. return person.name.toLowerCase().includes(this.query.toLowerCase())
  988. })
  989. },
  990. }"
  991. >
  992. <div x-combobox x-model="selected">
  993. <label x-combobox:label>Select person</label>
  994. <div>
  995. <div>
  996. <input
  997. x-combobox:input
  998. :display-value="person => person.name"
  999. @change="query = $event.target.value"
  1000. placeholder="Search..."
  1001. />
  1002. <button x-combobox:button>Toggle</button>
  1003. </div>
  1004. <div x-combobox:options>
  1005. <ul>
  1006. <template
  1007. x-for="person in filteredPeople"
  1008. :key="person.id"
  1009. hidden
  1010. >
  1011. <li
  1012. x-combobox:option
  1013. :option="person.id"
  1014. :value="person"
  1015. :disabled="person.disabled"
  1016. x-text="person.name"
  1017. >
  1018. </li>
  1019. </template>
  1020. </ul>
  1021. <p x-show="filteredPeople.length == 0">No people match your query.</p>
  1022. </div>
  1023. </div>
  1024. </div>
  1025. <article>lorem ipsum</article>
  1026. </div>
  1027. `],
  1028. ({ get }) => {
  1029. get('button').click()
  1030. get('li').should(haveLength('10'))
  1031. // Test after closing with button and reopening
  1032. get('input').type('w').trigger('input')
  1033. get('li').should(haveLength('2'))
  1034. get('button').click()
  1035. get('button').click()
  1036. get('li').should(haveLength('10'))
  1037. // Test correct state after closing with ESC and reopening
  1038. get('input').type('w').trigger('input')
  1039. get('li').should(haveLength('2'))
  1040. get('input').type('{esc}')
  1041. get('button').click()
  1042. get('li').should(haveLength('10'))
  1043. // Test correct state after closing with TAB and reopening
  1044. get('input').type('w').trigger('input')
  1045. get('li').should(haveLength('2'))
  1046. get('input').tab()
  1047. get('button').click()
  1048. get('li').should(haveLength('10'))
  1049. // Test correct state after closing with external click and reopening
  1050. get('input').type('w').trigger('input')
  1051. get('li').should(haveLength('2'))
  1052. get('article').click()
  1053. get('button').click()
  1054. get('li').should(haveLength('10'))
  1055. },
  1056. )
  1057. test('active element logic when opening a combobox',
  1058. [html`
  1059. <div
  1060. x-data="{
  1061. query: '',
  1062. selected: null,
  1063. people: [
  1064. { id: 1, name: 'Wade Cooper' },
  1065. { id: 2, name: 'Arlene Mccoy' },
  1066. { id: 3, name: 'Devon Webb' },
  1067. { id: 4, name: 'Tom Cook' },
  1068. { id: 5, name: 'Tanya Fox', disabled: true },
  1069. { id: 6, name: 'Hellen Schmidt' },
  1070. { id: 7, name: 'Caroline Schultz' },
  1071. { id: 8, name: 'Mason Heaney' },
  1072. { id: 9, name: 'Claudie Smitham' },
  1073. { id: 10, name: 'Emil Schaefer' },
  1074. ],
  1075. get filteredPeople() {
  1076. return this.query === ''
  1077. ? this.people
  1078. : this.people.filter((person) => {
  1079. return person.name.toLowerCase().includes(this.query.toLowerCase())
  1080. })
  1081. },
  1082. }"
  1083. >
  1084. <div x-combobox x-model="selected">
  1085. <label x-combobox:label>Select person</label>
  1086. <div>
  1087. <div>
  1088. <input
  1089. x-combobox:input
  1090. :display-value="person => person.name"
  1091. @change="query = $event.target.value"
  1092. placeholder="Search..."
  1093. />
  1094. <button x-combobox:button>Toggle</button>
  1095. </div>
  1096. <div x-combobox:options>
  1097. <ul>
  1098. <template
  1099. x-for="person in filteredPeople"
  1100. :key="person.id"
  1101. hidden
  1102. >
  1103. <li
  1104. x-combobox:option
  1105. :option="person.id"
  1106. :value="person"
  1107. :disabled="person.disabled"
  1108. x-text="person.name"
  1109. >
  1110. </li>
  1111. </template>
  1112. </ul>
  1113. <p x-show="filteredPeople.length == 0">No people match your query.</p>
  1114. </div>
  1115. </div>
  1116. </div>
  1117. </div>
  1118. `],
  1119. ({ get }) => {
  1120. get('button').click()
  1121. // First option is selected on opening if no preselection
  1122. get('ul').should(beVisible())
  1123. get('[option="1"]').should(haveAttribute('aria-selected', 'true'))
  1124. // First match is selected while typing
  1125. get('[option="4"]').should(haveAttribute('aria-selected', 'false'))
  1126. get('input').type('T')
  1127. get('input').trigger('change')
  1128. get('[option="4"]').should(haveAttribute('aria-selected', 'true'))
  1129. // Reset state and select option 3
  1130. get('button').click()
  1131. get('button').click()
  1132. get('[option="3"]').click()
  1133. // Previous selection is selected
  1134. get('button').click()
  1135. get('[option="4"]').should(haveAttribute('aria-selected', 'false'))
  1136. get('[option="3"]').should(haveAttribute('aria-selected', 'true'))
  1137. }
  1138. )