menu.spec.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. import { haveClasses, beVisible, haveAttribute, haveText, html, notBeVisible, notExist, test, haveFocus, notHaveClasses, notHaveAttribute } from '../../../utils'
  2. test('it works',
  3. [html`
  4. <div x-data x-menu>
  5. <span>
  6. <button x-menu:button trigger>
  7. <span>Options</span>
  8. </button>
  9. </span>
  10. <div x-menu:items items>
  11. <div>
  12. <p>Signed in as</p>
  13. <p>tom@example.com</p>
  14. </div>
  15. <div>
  16. <a x-menu:item href="#account-settings">
  17. Account settings
  18. </a>
  19. <a x-menu:item href="#support">
  20. Support
  21. </a>
  22. <a x-menu:item disabled href="#new-feature">
  23. New feature (soon)
  24. </a>
  25. <a x-menu:item href="#license">
  26. License
  27. </a>
  28. </div>
  29. <div>
  30. <a x-menu:item href="#sign-out">
  31. Sign out
  32. </a>
  33. </div>
  34. </div>
  35. </div>`],
  36. ({ get }) => {
  37. get('[items]').should(notBeVisible())
  38. get('[trigger]').click()
  39. get('[items]').should(beVisible())
  40. },
  41. )
  42. test('focusing away closes menu',
  43. [html`
  44. <div>
  45. <div x-data x-menu>
  46. <span>
  47. <button x-menu:button trigger>
  48. <span>Options</span>
  49. </button>
  50. </span>
  51. <div x-menu:items items>
  52. <div>
  53. <p>Signed in as</p>
  54. <p>tom@example.com</p>
  55. </div>
  56. <div>
  57. <a x-menu:item href="#account-settings">
  58. Account settings
  59. </a>
  60. <a x-menu:item href="#support">
  61. Support
  62. </a>
  63. <a x-menu:item href="#license">
  64. License
  65. </a>
  66. </div>
  67. <div>
  68. <a x-menu:item href="#sign-out">
  69. Sign out
  70. </a>
  71. </div>
  72. </div>
  73. </div>
  74. <button>Focus away</button>
  75. </div>
  76. `],
  77. ({ get }) => {
  78. get('[items]').should(notBeVisible())
  79. get('[trigger]').click()
  80. get('[items]').should(beVisible())
  81. cy.focused().tab()
  82. get('[items]').should(notBeVisible())
  83. },
  84. )
  85. test('it works with x-model',
  86. [html`
  87. <div x-data="{ open: false }" x-menu x-model="open">
  88. <button trigger @click="open = !open">
  89. <span>Options</span>
  90. </button>
  91. <button x-menu:button>
  92. <span>Options</span>
  93. </button>
  94. <div x-menu:items items>
  95. <div>
  96. <p>Signed in as</p>
  97. <p>tom@example.com</p>
  98. </div>
  99. <div>
  100. <a x-menu:item href="#account-settings">
  101. Account settings
  102. </a>
  103. <a x-menu:item href="#support">
  104. Support
  105. </a>
  106. <a x-menu:item disabled href="#new-feature">
  107. New feature (soon)
  108. </a>
  109. <a x-menu:item href="#license">
  110. License
  111. </a>
  112. </div>
  113. <div>
  114. <a x-menu:item href="#sign-out">
  115. Sign out
  116. </a>
  117. </div>
  118. </div>
  119. </div>`],
  120. ({ get }) => {
  121. get('[items]').should(notBeVisible())
  122. get('[trigger]').click()
  123. get('[items]').should(beVisible())
  124. get('[trigger]').click()
  125. get('[items]').should(notBeVisible())
  126. },
  127. )
  128. test('keyboard controls',
  129. [html`
  130. <div x-data x-menu>
  131. <span>
  132. <button x-menu:button trigger>
  133. <span>Options</span>
  134. </button>
  135. </span>
  136. <div x-menu:items items>
  137. <div>
  138. <p>Signed in as</p>
  139. <p>tom@example.com</p>
  140. </div>
  141. <div>
  142. <a x-menu:item href="#account-settings" :class="$menuItem.isActive && 'active'">
  143. Account settings
  144. </a>
  145. <a x-menu:item href="#support" :class="$menuItem.isActive && 'active'">
  146. Support
  147. </a>
  148. <a x-menu:item disabled href="#new-feature" :class="$menuItem.isActive && 'active'">
  149. New feature (soon)
  150. </a>
  151. <a x-menu:item href="#license" :class="$menuItem.isActive && 'active'">
  152. License
  153. </a>
  154. </div>
  155. <div>
  156. <a x-menu:item href="#sign-out" :class="$menuItem.isActive && 'active'">
  157. Sign out
  158. </a>
  159. </div>
  160. </div>
  161. </div>`],
  162. ({ get }) => {
  163. get('.active').should(notExist())
  164. get('[trigger]').type(' ')
  165. get('[items]')
  166. .should(beVisible())
  167. .should(haveFocus())
  168. .type('{downarrow}')
  169. get('[href="#account-settings"]')
  170. .should(haveClasses(['active']))
  171. get('[items]')
  172. .type('{downarrow}')
  173. get('[href="#support"]')
  174. .should(haveClasses(['active']))
  175. .type('{downarrow}')
  176. get('[href="#license"]')
  177. .should(haveClasses(['active']))
  178. get('[items]')
  179. .type('{uparrow}')
  180. get('[href="#support"]')
  181. .should(haveClasses(['active']))
  182. get('[items]')
  183. .type('{home}')
  184. get('[href="#account-settings"]')
  185. .should(haveClasses(['active']))
  186. get('[items]')
  187. .type('{end}')
  188. get('[href="#sign-out"]')
  189. .should(haveClasses(['active']))
  190. get('[items]')
  191. .type('{pageUp}')
  192. get('[href="#account-settings"]')
  193. .should(haveClasses(['active']))
  194. get('[items]')
  195. .type('{pageDown}')
  196. get('[href="#sign-out"]')
  197. .should(haveClasses(['active']))
  198. get('[items]')
  199. .tab()
  200. .should(haveFocus())
  201. .should(beVisible())
  202. .tab({ shift: true})
  203. .should(haveFocus())
  204. .should(beVisible())
  205. .type('{esc}')
  206. .should(notBeVisible())
  207. },
  208. )
  209. test('keyboard controls with x-teleport',
  210. [html`
  211. <div x-data x-menu>
  212. <span>
  213. <button x-menu:button trigger>
  214. <span>Options</span>
  215. </button>
  216. </span>
  217. <template x-teleport="body">
  218. <div x-menu:items items>
  219. <a x-menu:item href="#account-settings" :class="$menuItem.isActive && 'active'">
  220. Account settings
  221. </a>
  222. <a x-menu:item href="#support" :class="$menuItem.isActive && 'active'">
  223. Support
  224. </a>
  225. </div>
  226. </template>
  227. </div>`],
  228. ({ get }) => {
  229. get('.active').should(notExist())
  230. get('[trigger]').type(' ')
  231. get('[items]')
  232. .should(beVisible())
  233. .should(haveFocus())
  234. .type('{downarrow}')
  235. get('[href="#account-settings"]')
  236. .should(haveClasses(['active']))
  237. get('[items]')
  238. .type('{downarrow}')
  239. get('[href="#support"]')
  240. .should(haveClasses(['active']))
  241. .type('{uparrow}')
  242. get('[href="#account-settings"]')
  243. .should(haveClasses(['active']))
  244. get('[items]')
  245. .tab()
  246. .should(haveFocus())
  247. .should(beVisible())
  248. .tab({ shift: true})
  249. .should(haveFocus())
  250. .should(beVisible())
  251. .type('{esc}')
  252. .should(notBeVisible())
  253. },
  254. )
  255. test('search',
  256. [html`
  257. <div x-data x-menu>
  258. <span>
  259. <button x-menu:button trigger>
  260. <span>Options</span>
  261. </button>
  262. </span>
  263. <div x-menu:items items>
  264. <div>
  265. <p>Signed in as</p>
  266. <p>tom@example.com</p>
  267. </div>
  268. <div>
  269. <a x-menu:item href="#account-settings" :class="$menuItem.isActive && 'active'">
  270. Account settings
  271. </a>
  272. <a x-menu:item href="#support" :class="$menuItem.isActive && 'active'">
  273. Support
  274. </a>
  275. <a x-menu:item disabled href="#new-feature" :class="$menuItem.isActive && 'active'">
  276. New feature (soon)
  277. </a>
  278. <a x-menu:item href="#license" :class="$menuItem.isActive && 'active'">
  279. License
  280. </a>
  281. </div>
  282. <div>
  283. <a x-menu:item href="#sign-out" :class="$menuItem.isActive && 'active'">
  284. Sign out
  285. </a>
  286. </div>
  287. </div>
  288. </div>`],
  289. ({ get, wait }) => {
  290. get('.active').should(notExist())
  291. get('[trigger]').click()
  292. get('[items]')
  293. .type('ac')
  294. get('[href="#account-settings"]')
  295. .should(haveClasses(['active']))
  296. wait(500)
  297. get('[items]')
  298. .type('si')
  299. get('[href="#sign-out"]')
  300. .should(haveClasses(['active']))
  301. },
  302. )
  303. test('has accessibility attributes',
  304. [html`
  305. <div x-data x-menu>
  306. <label x-menu:label>Options label</label>
  307. <span>
  308. <button x-menu:button trigger>
  309. <span>Options</span>
  310. </button>
  311. </span>
  312. <div x-menu:items items>
  313. <div>
  314. <p>Signed in as</p>
  315. <p>tom@example.com</p>
  316. </div>
  317. <div>
  318. <a x-menu:item href="#account-settings" :class="$menuItem.isActive && 'active'">
  319. Account settings
  320. </a>
  321. <a x-menu:item href="#support" :class="$menuItem.isActive && 'active'">
  322. Support
  323. </a>
  324. <a x-menu:item disabled href="#new-feature" :class="$menuItem.isActive && 'active'">
  325. New feature (soon)
  326. </a>
  327. <a x-menu:item href="#license" :class="$menuItem.isActive && 'active'">
  328. License
  329. </a>
  330. </div>
  331. <div>
  332. <a x-menu:item href="#sign-out" :class="$menuItem.isActive && 'active'">
  333. Sign out
  334. </a>
  335. </div>
  336. </div>
  337. </div>`],
  338. ({ get }) => {
  339. get('[trigger]')
  340. .should(haveAttribute('aria-haspopup', 'true'))
  341. .should(haveAttribute('aria-labelledby', 'alpine-menu-label-1'))
  342. .should(haveAttribute('aria-expanded', 'false'))
  343. .should(notHaveAttribute('aria-controls'))
  344. .should(haveAttribute('id', 'alpine-menu-button-1'))
  345. .click()
  346. .should(haveAttribute('aria-expanded', 'true'))
  347. .should(haveAttribute('aria-controls', 'alpine-menu-items-1'))
  348. get('[items]')
  349. .should(haveAttribute('aria-orientation', 'vertical'))
  350. .should(haveAttribute('role', 'menu'))
  351. .should(haveAttribute('id', 'alpine-menu-items-1'))
  352. .should(haveAttribute('aria-labelledby', 'alpine-menu-button-1'))
  353. .should(notHaveAttribute('aria-activedescendant'))
  354. .should(haveAttribute('tabindex', '0'))
  355. .type('{downarrow}')
  356. .should(haveAttribute('aria-activedescendant', 'alpine-menu-item-1'))
  357. get('[href="#account-settings"]')
  358. .should(haveAttribute('role', 'menuitem'))
  359. .should(haveAttribute('id', 'alpine-menu-item-1'))
  360. .should(haveAttribute('tabindex', '-1'))
  361. get('[href="#support"]')
  362. .should(haveAttribute('role', 'menuitem'))
  363. .should(haveAttribute('id', 'alpine-menu-item-2'))
  364. .should(haveAttribute('tabindex', '-1'))
  365. get('[items]')
  366. .type('{downarrow}')
  367. .should(haveAttribute('aria-activedescendant', 'alpine-menu-item-2'))
  368. },
  369. )
  370. test('$menuItem.isDisabled',
  371. [html`
  372. <div x-data x-menu>
  373. <label x-menu:label>Options label</label>
  374. <span>
  375. <button x-menu:button trigger>
  376. <span>Options</span>
  377. </button>
  378. </span>
  379. <div x-menu:items items>
  380. <div>
  381. <p>Signed in as</p>
  382. <p>tom@example.com</p>
  383. </div>
  384. <div>
  385. <a x-menu:item href="#account-settings" :class="{ 'active': $menuItem.isActive, 'disabled': $menuItem.isDisabled }">
  386. Account settings
  387. </a>
  388. <a x-menu:item href="#support" :class="{ 'active': $menuItem.isActive, 'disabled': $menuItem.isDisabled }">
  389. Support
  390. </a>
  391. <a x-menu:item disabled href="#new-feature" :class="{ 'active': $menuItem.isActive, 'disabled': $menuItem.isDisabled }">
  392. New feature (soon)
  393. </a>
  394. <a x-menu:item href="#license" :class="{ 'active': $menuItem.isActive, 'disabled': $menuItem.isDisabled }">
  395. License
  396. </a>
  397. </div>
  398. <div>
  399. <a x-menu:item href="#sign-out" :class="{ 'active': $menuItem.isActive, 'disabled': $menuItem.isDisabled }">
  400. Sign out
  401. </a>
  402. </div>
  403. </div>
  404. </div>`],
  405. ({ get }) => {
  406. get('[trigger]').click()
  407. get('[href="#account-settings"]').should(notHaveClasses(['disabled']))
  408. get('[href="#support"]').should(notHaveClasses(['disabled']))
  409. get('[href="#new-feature"]').should(haveClasses(['disabled']))
  410. },
  411. )