composite.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  2. import {CompositeSelect} from 'sentry/components/compactSelect/composite';
  3. describe('CompactSelect', function () {
  4. it('renders', async function () {
  5. const {container} = render(
  6. <CompositeSelect menuTitle="Menu title">
  7. <CompositeSelect.Region
  8. label="Region 1"
  9. defaultValue="choice_one"
  10. onChange={() => {}}
  11. options={[
  12. {value: 'choice_one', label: 'Choice One'},
  13. {value: 'choice_two', label: 'Choice Two'},
  14. ]}
  15. />
  16. <CompositeSelect.Region
  17. multiple
  18. label="Region 2"
  19. defaultValue={['choice_three', 'choice_four']}
  20. onChange={() => {}}
  21. options={[
  22. {value: 'choice_three', label: 'Choice Three'},
  23. {value: 'choice_four', label: 'Choice Four'},
  24. ]}
  25. />
  26. </CompositeSelect>
  27. );
  28. expect(container).toSnapshot();
  29. // Trigger button
  30. const triggerButton = screen.getByRole('button', {expanded: false});
  31. expect(triggerButton).toBeInTheDocument();
  32. await userEvent.click(triggerButton);
  33. expect(triggerButton).toHaveAttribute('aria-expanded', 'true');
  34. // Menu title
  35. expect(screen.getByText('Menu title')).toBeInTheDocument();
  36. // Region 1
  37. expect(screen.getByRole('listbox', {name: 'Region 1'})).toBeInTheDocument();
  38. expect(screen.getByRole('option', {name: 'Choice One'})).toBeInTheDocument();
  39. expect(screen.getByRole('option', {name: 'Choice One'})).toHaveAttribute(
  40. 'aria-selected',
  41. 'true'
  42. );
  43. expect(screen.getByRole('option', {name: 'Choice Two'})).toBeInTheDocument();
  44. // Region 2
  45. expect(screen.getByRole('listbox', {name: 'Region 2'})).toBeInTheDocument();
  46. expect(screen.getByRole('listbox', {name: 'Region 2'})).toHaveAttribute(
  47. 'aria-multiselectable',
  48. 'true'
  49. );
  50. expect(screen.getByRole('option', {name: 'Choice Three'})).toBeInTheDocument();
  51. expect(screen.getByRole('option', {name: 'Choice Three'})).toHaveAttribute(
  52. 'aria-selected',
  53. 'true'
  54. );
  55. expect(screen.getByRole('option', {name: 'Choice Four'})).toBeInTheDocument();
  56. expect(screen.getByRole('option', {name: 'Choice Four'})).toHaveAttribute(
  57. 'aria-selected',
  58. 'true'
  59. );
  60. });
  61. it('renders disabled trigger button', function () {
  62. render(
  63. <CompositeSelect disabled>
  64. <CompositeSelect.Region
  65. label="Region 1"
  66. onChange={() => {}}
  67. options={[
  68. {value: 'choice_one', label: 'Choice One'},
  69. {value: 'choice_two', label: 'Choice Two'},
  70. ]}
  71. />
  72. </CompositeSelect>
  73. );
  74. expect(screen.getByRole('button')).toBeDisabled();
  75. });
  76. // CompositeSelect renders a series of separate list boxes, each of which has its own
  77. // focus state. This test ensures that focus moves seamlessly between regions.
  78. it('manages focus between regions', async function () {
  79. render(
  80. <CompositeSelect>
  81. <CompositeSelect.Region
  82. label="Region 1"
  83. onChange={() => {}}
  84. options={[
  85. {value: 'choice_one', label: 'Choice One'},
  86. {value: 'choice_two', label: 'Choice Two'},
  87. ]}
  88. />
  89. <CompositeSelect.Region
  90. multiple
  91. label="Region 2"
  92. onChange={() => {}}
  93. options={[
  94. {value: 'choice_three', label: 'Choice Three'},
  95. {value: 'choice_four', label: 'Choice Four'},
  96. ]}
  97. />
  98. </CompositeSelect>
  99. );
  100. // click on the trigger button
  101. await userEvent.click(screen.getByRole('button'));
  102. // first option is focused
  103. await waitFor(() =>
  104. expect(screen.getByRole('option', {name: 'Choice One'})).toHaveFocus()
  105. );
  106. // press arrow down and second option gets focus
  107. await userEvent.keyboard('{ArrowDown}');
  108. expect(screen.getByRole('option', {name: 'Choice Two'})).toHaveFocus();
  109. // press arrow down again and third option in the second region gets focus
  110. await userEvent.keyboard('{ArrowDown}');
  111. expect(screen.getByRole('option', {name: 'Choice Three'})).toHaveFocus();
  112. // press arrow up and second option in the first region gets focus
  113. await userEvent.keyboard('{ArrowUp}');
  114. expect(screen.getByRole('option', {name: 'Choice Two'})).toHaveFocus();
  115. // press arrow down 3 times and focus moves to the third and fourth option, before
  116. // wrapping back to the first option
  117. await userEvent.keyboard('{ArrowDown>3}');
  118. expect(screen.getByRole('option', {name: 'Choice One'})).toHaveFocus();
  119. });
  120. it('has separate, async self-contained select regions', async function () {
  121. const region1Mock = jest.fn();
  122. const region2Mock = jest.fn();
  123. render(
  124. <CompositeSelect>
  125. <CompositeSelect.Region
  126. label="Region 1"
  127. onChange={region1Mock}
  128. options={[
  129. {value: 'choice_one', label: 'Choice One'},
  130. {value: 'choice_two', label: 'Choice Two'},
  131. ]}
  132. />
  133. <CompositeSelect.Region
  134. multiple
  135. label="Region 2"
  136. onChange={region2Mock}
  137. options={[
  138. {value: 'choice_three', label: 'Choice Three'},
  139. {value: 'choice_four', label: 'Choice Four'},
  140. ]}
  141. />
  142. </CompositeSelect>
  143. );
  144. // click on the trigger button
  145. await userEvent.click(screen.getByRole('button'));
  146. // select Choice One
  147. await userEvent.click(screen.getByRole('option', {name: 'Choice One'}));
  148. // Region 1's callback is called, and trigger label is updated
  149. expect(region1Mock).toHaveBeenCalledWith({value: 'choice_one', label: 'Choice One'});
  150. expect(screen.getByRole('button', {name: 'Choice One'})).toBeInTheDocument();
  151. // open the menu again
  152. await userEvent.click(screen.getByRole('button'));
  153. // in the first region, only Choice One is selected
  154. expect(screen.getByRole('option', {name: 'Choice One'})).toHaveAttribute(
  155. 'aria-selected',
  156. 'true'
  157. );
  158. expect(screen.getByRole('option', {name: 'Choice Two'})).toHaveAttribute(
  159. 'aria-selected',
  160. 'false'
  161. );
  162. // the second region isn't affected, nothing is selected
  163. expect(screen.getByRole('option', {name: 'Choice Three'})).toHaveAttribute(
  164. 'aria-selected',
  165. 'false'
  166. );
  167. expect(screen.getByRole('option', {name: 'Choice Four'})).toHaveAttribute(
  168. 'aria-selected',
  169. 'false'
  170. );
  171. // select Choice Three
  172. await userEvent.click(screen.getByRole('option', {name: 'Choice Three'}));
  173. // Choice Three is marked as selected, callback is called, and trigger button updated
  174. expect(screen.getByRole('option', {name: 'Choice Three'})).toHaveAttribute(
  175. 'aria-selected',
  176. 'true'
  177. );
  178. expect(region2Mock).toHaveBeenCalledWith([
  179. {value: 'choice_three', label: 'Choice Three'},
  180. ]);
  181. expect(screen.getByRole('button', {name: 'Choice One +1'})).toBeInTheDocument();
  182. });
  183. it('can search', async function () {
  184. render(
  185. <CompositeSelect searchable searchPlaceholder="Search placeholder…">
  186. <CompositeSelect.Region
  187. label="Region 1"
  188. onChange={() => {}}
  189. options={[
  190. {value: 'choice_one', label: 'Choice One'},
  191. {value: 'choice_two', label: 'Choice Two'},
  192. ]}
  193. />
  194. <CompositeSelect.Region
  195. multiple
  196. label="Region 2"
  197. onChange={() => {}}
  198. options={[
  199. {value: 'choice_three', label: 'Choice Three'},
  200. {value: 'choice_four', label: 'Choice Four'},
  201. ]}
  202. />
  203. </CompositeSelect>
  204. );
  205. // click on the trigger button
  206. await userEvent.click(screen.getByRole('button'));
  207. // type 'Two' into the search box
  208. await userEvent.click(screen.getByPlaceholderText('Search placeholder…'));
  209. await userEvent.keyboard('Two');
  210. // only Option Two should be available
  211. expect(screen.getByRole('option', {name: 'Choice Two'})).toBeInTheDocument();
  212. expect(screen.queryByRole('option', {name: 'Choice One'})).not.toBeInTheDocument();
  213. expect(screen.queryByRole('option', {name: 'Choice Three'})).not.toBeInTheDocument();
  214. expect(screen.queryByRole('option', {name: 'Choice Four'})).not.toBeInTheDocument();
  215. // Region 2's label isn't rendered because the region is empty
  216. expect(screen.queryByRole('Region 2')).not.toBeInTheDocument();
  217. });
  218. it('works with grid lists', async function () {
  219. render(
  220. <CompositeSelect grid>
  221. <CompositeSelect.Region
  222. label="Region 1"
  223. defaultValue="choice_one"
  224. onChange={() => {}}
  225. options={[
  226. {value: 'choice_one', label: 'Choice One'},
  227. {value: 'choice_two', label: 'Choice Two'},
  228. ]}
  229. />
  230. <CompositeSelect.Region
  231. multiple
  232. label="Region 2"
  233. onChange={() => {}}
  234. options={[
  235. {value: 'choice_three', label: 'Choice Three'},
  236. {value: 'choice_four', label: 'Choice Four'},
  237. ]}
  238. />
  239. </CompositeSelect>
  240. );
  241. // click on the trigger button
  242. await userEvent.click(screen.getByRole('button'));
  243. // Region 1 is rendered & Choice One is selected
  244. expect(screen.getByRole('grid', {name: 'Region 1'})).toBeInTheDocument();
  245. expect(screen.getByRole('row', {name: 'Choice One'})).toBeInTheDocument();
  246. await waitFor(() =>
  247. expect(screen.getByRole('row', {name: 'Choice One'})).toHaveFocus()
  248. );
  249. expect(screen.getByRole('row', {name: 'Choice One'})).toHaveAttribute(
  250. 'aria-selected',
  251. 'true'
  252. );
  253. expect(screen.getByRole('row', {name: 'Choice Two'})).toBeInTheDocument();
  254. // Region 2 is rendered
  255. expect(screen.getByRole('grid', {name: 'Region 2'})).toBeInTheDocument();
  256. expect(screen.getByRole('grid', {name: 'Region 2'})).toHaveAttribute(
  257. 'aria-multiselectable',
  258. 'true'
  259. );
  260. expect(screen.getByRole('row', {name: 'Choice Three'})).toBeInTheDocument();
  261. expect(screen.getByRole('row', {name: 'Choice Four'})).toBeInTheDocument();
  262. // Pressing Arrow Down twice moves focus to Choice Three
  263. await userEvent.keyboard('{ArrowDown>2}');
  264. expect(screen.getByRole('row', {name: 'Choice Three'})).toHaveFocus();
  265. // Pressing Enter selects Choice Three
  266. await userEvent.keyboard('{Enter}');
  267. expect(screen.getByRole('row', {name: 'Choice Three'})).toHaveAttribute(
  268. 'aria-selected',
  269. 'true'
  270. );
  271. // Pressing Arrow Down two more times loops focus back to Choice One
  272. await userEvent.keyboard('{ArrowDown>2}');
  273. expect(screen.getByRole('row', {name: 'Choice One'})).toHaveFocus();
  274. });
  275. });