focus.spec.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. import { haveText, test, html, haveFocus, notHaveAttribute, haveAttribute, notHaveFocus, notHaveText } from '../../utils'
  2. test('can trap focus',
  3. [html`
  4. <div x-data="{ open: false }">
  5. <input type="text" id="1">
  6. <button id="2" @click="open = true">open</button>
  7. <div>
  8. <div x-trap="open">
  9. <input type="text" id="3">
  10. <button @click="open = false" id="4">close</button>
  11. </div>
  12. </div>
  13. </div>
  14. `],
  15. ({ get }, reload) => {
  16. get('#1').click()
  17. get('#1').should(haveFocus())
  18. get('#2').click()
  19. get('#3').should(haveFocus())
  20. cy.focused().tab()
  21. get('#4').should(haveFocus())
  22. cy.focused().tab()
  23. get('#3').should(haveFocus())
  24. cy.focused().tab({shift: true})
  25. get('#4').should(haveFocus())
  26. cy.focused().click()
  27. get('#2').should(haveFocus())
  28. },
  29. )
  30. test.only('works with clone',
  31. [html`
  32. <div id="foo" x-data="{
  33. open: false,
  34. triggerClone() {
  35. var original = document.getElementById('foo');
  36. var copy = original.cloneNode(true);
  37. Alpine.clone(original, copy);
  38. var p = document.createElement('p');
  39. p.textContent = 'bar';
  40. copy.appendChild(p);
  41. original.parentNode.replaceChild(copy, original);
  42. }
  43. }">
  44. <button id="one" @click="open = true">Trap</button>
  45. <div x-trap="open">
  46. <input type="text">
  47. <button id="two" @click="triggerClone()">Test</button>
  48. </div>
  49. </div>
  50. `],
  51. ({ get, wait }, reload) => {
  52. get('#one').click()
  53. get('#two').click()
  54. get('p').should(haveText('bar'))
  55. }
  56. )
  57. test('releases focus when x-if is destroyed',
  58. [html`
  59. <div x-data="{ open: false }">
  60. <button id="1" @click="open = true">open</button>
  61. <template x-if="open">
  62. <div x-trap="open">
  63. <button @click="open = false" id="2">close</button>
  64. </div>
  65. </template>
  66. </div>
  67. `],
  68. ({ get }, reload) => {
  69. get('#1').click()
  70. get('#2').should(haveFocus())
  71. get('#2').click()
  72. get('#1').should(haveFocus())
  73. },
  74. )
  75. test('can trap focus with inert',
  76. [html`
  77. <div x-data="{ open: false }">
  78. <h1>I should have aria-hidden when outside trap</h1>
  79. <button id="open" @click="open = true">open</button>
  80. <div x-trap.inert="open">
  81. <button @click="open = false" id="close">close</button>
  82. </div>
  83. </div>
  84. `],
  85. ({ get }, reload) => {
  86. get('#open').should(notHaveAttribute('aria-hidden', 'true'))
  87. get('#open').click()
  88. get('#open').should(haveAttribute('aria-hidden', 'true'))
  89. get('#close').click()
  90. get('#open').should(notHaveAttribute('aria-hidden', 'true'))
  91. },
  92. )
  93. test('inert only applies aria-hidden once',
  94. [html`
  95. <div>
  96. <div id="sibling">I should have aria-hidden applied once</div>
  97. <div x-data="{
  98. open: false,
  99. timesApplied: 0,
  100. init() {
  101. let observer = new MutationObserver((mutations) => {
  102. mutations.forEach((mutation) => {
  103. if (mutation.type === 'attributes' && mutation.attributeName === 'aria-hidden') {
  104. this.timesApplied++
  105. }
  106. })
  107. })
  108. observer.observe(document.querySelector('#sibling'), {
  109. attributes: true
  110. })
  111. },
  112. }">
  113. <input type="text" id="timesApplied" x-model="timesApplied" />
  114. <button id="trigger" @click="open = true">open</button>
  115. <div x-trap.inert="open">
  116. Hello, I'm a friendly modal!
  117. </div>
  118. </div>
  119. </div>
  120. `],
  121. ({ get }, reload) => {
  122. get('#trigger').click()
  123. get('#timesApplied').should('have.value', '1')
  124. },
  125. )
  126. test('can trap focus with noscroll',
  127. [html`
  128. <div x-data="{ open: false }">
  129. <button id="open" @click="open = true">open</button>
  130. <div x-trap.noscroll="open">
  131. <button @click="open = false" id="close">close</button>
  132. </div>
  133. <div style="height: 100vh">&nbsp;</div>
  134. </div>
  135. `],
  136. ({ get, window }, reload) => {
  137. window().then((win) => {
  138. let scrollbarWidth = win.innerWidth - win.document.documentElement.clientWidth
  139. get('#open').click()
  140. get('html').should(haveAttribute('style', `overflow: hidden; padding-right: ${scrollbarWidth}px;`))
  141. get('#close').click()
  142. get('html').should(notHaveAttribute('style', `overflow: hidden; padding-right: ${scrollbarWidth}px;`))
  143. })
  144. },
  145. )
  146. test('can trap focus with noreturn',
  147. [html`
  148. <div x-data="{ open: false }" x-trap.noreturn="open">
  149. <input id="input" @focus="open = true">
  150. <div x-show="open">
  151. <button @click="open = false" id="close">close</button>
  152. </div>
  153. </div>
  154. `],
  155. ({ get }) => {
  156. get('#input').focus()
  157. get('#close')
  158. get('#close').click()
  159. get('#input').should(notHaveFocus())
  160. },
  161. )
  162. test('$focus.focus',
  163. [html`
  164. <div x-data>
  165. <button id="press-me" @click="$focus.focus(document.querySelector('#focus-me'))">Focus Other</button>
  166. <button id="focus-me">Other</button>
  167. </div>
  168. `],
  169. ({ get }) => {
  170. get('#focus-me').should(notHaveFocus())
  171. get('#press-me').click()
  172. get('#focus-me').should(haveFocus())
  173. },
  174. )
  175. test('$focus.focusable',
  176. [html`
  177. <div x-data>
  178. <div id="1" x-text="$focus.focusable($el)"></div>
  179. <button id="2" x-text="$focus.focusable($el)"></button>
  180. </div>
  181. `],
  182. ({ get }) => {
  183. get('#1').should(haveText('false'))
  184. get('#2').should(haveText('true'))
  185. },
  186. )
  187. test('$focus.focusables',
  188. [html`
  189. <div x-data>
  190. <h1 x-text="$focus.within($refs.container).focusables().length"></h1>
  191. <div x-ref="container">
  192. <button>1</button>
  193. <div>2</div>
  194. <button>3</button>
  195. </div>
  196. </div>
  197. `],
  198. ({ get }) => {
  199. get('h1').should(haveText('2'))
  200. },
  201. )
  202. test('$focus.focused',
  203. [html`
  204. <div x-data>
  205. <button @click="$el.textContent = $el.isSameNode($focus.focused())">im-focused</button>
  206. </div>
  207. `],
  208. ({ get }) => {
  209. get('button').click()
  210. get('button').should(haveText('true'))
  211. },
  212. )
  213. test('$focus.lastFocused',
  214. [html`
  215. <div x-data>
  216. <button id="1" x-ref="first">first-focused</button>
  217. <button id="2" @click="$el.textContent = $refs.first.isSameNode($focus.lastFocused())">second-focused</button>
  218. </div>
  219. `],
  220. ({ get }) => {
  221. get('#1').click()
  222. get('#2').click()
  223. get('#2').should(haveText('true'))
  224. },
  225. )
  226. test('$focus.within',
  227. [html`
  228. <div x-data>
  229. <button id="1" x-text="$focus.within($refs.first).focusables().length"></button>
  230. <div x-ref="first">
  231. <button>1</button>
  232. <button>2</button>
  233. </div>
  234. <div>
  235. <button>1</button>
  236. <button>2</button>
  237. <button>3</button>
  238. </div>
  239. </div>
  240. `],
  241. ({ get }) => {
  242. get('#1').should(haveText('2'))
  243. },
  244. )
  245. test('$focus.next',
  246. [html`
  247. <div x-data>
  248. <div x-ref="first">
  249. <button id="1" @click="$focus.within($refs.first).next(); $nextTick(() => $el.textContent = $focus.focused().textContent)">1</button>
  250. <button>2</button>
  251. </div>
  252. </div>
  253. `],
  254. ({ get }) => {
  255. get('#1').click()
  256. get('#1').should(haveText('2'))
  257. },
  258. )
  259. test('$focus.prev',
  260. [html`
  261. <div x-data>
  262. <div x-ref="first">
  263. <button>2</button>
  264. <button id="1" @click="$focus.within($refs.first).prev(); $nextTick(() => $el.textContent = $focus.focused().textContent)">1</button>
  265. </div>
  266. </div>
  267. `],
  268. ({ get }) => {
  269. get('#1').click()
  270. get('#1').should(haveText('2'))
  271. },
  272. )
  273. test('$focus.wrap',
  274. [html`
  275. <div x-data>
  276. <div x-ref="first">
  277. <button>2</button>
  278. <button id="1" @click="$focus.within($refs.first).wrap().next(); $nextTick(() => $el.textContent = $focus.focused().textContent)">1</button>
  279. </div>
  280. </div>
  281. `],
  282. ({ get }) => {
  283. get('#1').click()
  284. get('#1').should(haveText('2'))
  285. },
  286. )
  287. test('$focus.first',
  288. [html`
  289. <div x-data>
  290. <button id="1" @click="$focus.within($refs.first).first(); $nextTick(() => $el.textContent = $focus.focused().textContent)">1</button>
  291. <div x-ref="first">
  292. <button>2</button>
  293. <button>3</button>
  294. </div>
  295. </div>
  296. `],
  297. ({ get }) => {
  298. get('#1').click()
  299. get('#1').should(haveText('2'))
  300. },
  301. )
  302. test('$focus.last',
  303. [html`
  304. <div x-data>
  305. <button id="1" @click="$focus.within($refs.first).last(); $nextTick(() => $el.textContent = $focus.focused().textContent)">1</button>
  306. <div x-ref="first">
  307. <button>2</button>
  308. <button>3</button>
  309. </div>
  310. </div>
  311. `],
  312. ({ get }) => {
  313. get('#1').click()
  314. get('#1').should(haveText('3'))
  315. },
  316. )
  317. test('focuses element with autofocus',
  318. [html`
  319. <div x-data="{ open: false }">
  320. <input type="text" id="1">
  321. <button id="2" @click="open = true">open</button>
  322. <div>
  323. <div x-trap="open">
  324. <input type="text" id="3">
  325. <input autofocus type="text" id="4">
  326. <button @click="open = false" id="5">close</button>
  327. </div>
  328. </div>
  329. </div>
  330. `],
  331. ({ get }) => {
  332. get('#1').click()
  333. get('#1').should(haveFocus())
  334. get('#2').click()
  335. get('#4').should(haveFocus())
  336. cy.focused().tab()
  337. get('#5').should(haveFocus())
  338. cy.focused().tab()
  339. get('#3').should(haveFocus())
  340. }
  341. )