@dhis2-ui/select 10.16.2 → 10.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/package.json +17 -16
  2. package/src/index.js +9 -0
  3. package/src/locales/ar/translations.json +7 -0
  4. package/src/locales/ar_IQ/translations.json +7 -0
  5. package/src/locales/ckb/translations.json +7 -0
  6. package/src/locales/cs/translations.json +7 -0
  7. package/src/locales/da/translations.json +7 -0
  8. package/src/locales/en/translations.json +11 -0
  9. package/src/locales/en_US/translations.json +9 -0
  10. package/src/locales/es/translations.json +7 -0
  11. package/src/locales/es_419/translations.json +7 -0
  12. package/src/locales/fr/translations.json +7 -0
  13. package/src/locales/hi_IN/translations.json +7 -0
  14. package/src/locales/id/translations.json +7 -0
  15. package/src/locales/index.js +80 -0
  16. package/src/locales/km/translations.json +7 -0
  17. package/src/locales/lo/translations.json +7 -0
  18. package/src/locales/my/translations.json +7 -0
  19. package/src/locales/nb/translations.json +7 -0
  20. package/src/locales/nl/translations.json +7 -0
  21. package/src/locales/prs/translations.json +7 -0
  22. package/src/locales/ps/translations.json +7 -0
  23. package/src/locales/pt/translations.json +7 -0
  24. package/src/locales/pt_BR/translations.json +7 -0
  25. package/src/locales/ru/translations.json +7 -0
  26. package/src/locales/sv/translations.json +7 -0
  27. package/src/locales/tet/translations.json +7 -0
  28. package/src/locales/tg/translations.json +7 -0
  29. package/src/locales/uk/translations.json +7 -0
  30. package/src/locales/ur/translations.json +7 -0
  31. package/src/locales/uz_Latn/translations.json +7 -0
  32. package/src/locales/uz_UZ_Cyrl/translations.json +7 -0
  33. package/src/locales/uz_UZ_Latn/translations.json +7 -0
  34. package/src/locales/vi/translations.json +7 -0
  35. package/src/locales/zh/translations.json +9 -0
  36. package/src/locales/zh_CN/translations.json +7 -0
  37. package/src/multi-select/__tests__/multi-select.test.js +23 -0
  38. package/src/multi-select/__tests__/selection-list.test.js +102 -0
  39. package/src/multi-select/features/accepts_blur_cb/index.js +14 -0
  40. package/src/multi-select/features/accepts_blur_cb.feature +8 -0
  41. package/src/multi-select/features/accepts_focus_cb/index.js +14 -0
  42. package/src/multi-select/features/accepts_focus_cb.feature +6 -0
  43. package/src/multi-select/features/accepts_initial_focus/index.js +5 -0
  44. package/src/multi-select/features/accepts_initial_focus.feature +5 -0
  45. package/src/multi-select/features/accepts_loading/index.js +22 -0
  46. package/src/multi-select/features/accepts_loading.feature +14 -0
  47. package/src/multi-select/features/accepts_max_height/index.js +31 -0
  48. package/src/multi-select/features/accepts_max_height.feature +11 -0
  49. package/src/multi-select/features/accepts_placeholder/index.js +25 -0
  50. package/src/multi-select/features/accepts_placeholder.feature +10 -0
  51. package/src/multi-select/features/accepts_prefix/index.js +19 -0
  52. package/src/multi-select/features/accepts_prefix.feature +10 -0
  53. package/src/multi-select/features/allows_invalid_options/index.js +27 -0
  54. package/src/multi-select/features/allows_invalid_options.feature +12 -0
  55. package/src/multi-select/features/allows_selecting/index.js +81 -0
  56. package/src/multi-select/features/allows_selecting.feature +44 -0
  57. package/src/multi-select/features/can_be_cleared/index.js +22 -0
  58. package/src/multi-select/features/can_be_cleared.feature +6 -0
  59. package/src/multi-select/features/can_be_disabled/index.js +36 -0
  60. package/src/multi-select/features/can_be_disabled.feature +32 -0
  61. package/src/multi-select/features/can_be_empty/index.js +28 -0
  62. package/src/multi-select/features/can_be_empty.feature +16 -0
  63. package/src/multi-select/features/can_be_filtered/index.js +23 -0
  64. package/src/multi-select/features/can_be_filtered.feature +14 -0
  65. package/src/multi-select/features/can_be_opened_and_closed/index.js +32 -0
  66. package/src/multi-select/features/can_be_opened_and_closed.feature +41 -0
  67. package/src/multi-select/features/common/index.js +49 -0
  68. package/src/multi-select/features/duplicate_option_values/index.js +21 -0
  69. package/src/multi-select/features/duplicate_option_values.feature +7 -0
  70. package/src/multi-select/features/menu_width_matches_input/index.js +53 -0
  71. package/src/multi-select/features/menu_width_matches_input.feature +11 -0
  72. package/src/multi-select/features/position/index.js +160 -0
  73. package/src/multi-select/features/position.feature +46 -0
  74. package/src/multi-select/features/shows_selections/index.js +18 -0
  75. package/src/multi-select/features/shows_selections.feature +9 -0
  76. package/src/multi-select/filterable-menu.js +43 -0
  77. package/src/multi-select/index.js +1 -0
  78. package/src/multi-select/input.js +129 -0
  79. package/src/multi-select/menu.js +91 -0
  80. package/src/multi-select/multi-select.e2e.stories.js +407 -0
  81. package/src/multi-select/multi-select.js +148 -0
  82. package/src/multi-select/multi-select.prod.stories.js +408 -0
  83. package/src/multi-select/selection-list.js +79 -0
  84. package/src/multi-select-field/__tests__/multi-select-field.test.js +29 -0
  85. package/src/multi-select-field/features/accepts_help_text/index.js +9 -0
  86. package/src/multi-select-field/features/accepts_help_text.feature +5 -0
  87. package/src/multi-select-field/features/accepts_label/index.js +9 -0
  88. package/src/multi-select-field/features/accepts_label.feature +5 -0
  89. package/src/multi-select-field/features/accepts_validation_text/index.js +9 -0
  90. package/src/multi-select-field/features/accepts_validation_text.feature +5 -0
  91. package/src/multi-select-field/features/can_be_required/index.js +11 -0
  92. package/src/multi-select-field/features/can_be_required.feature +5 -0
  93. package/src/multi-select-field/features/has_default_clear_text/index.js +16 -0
  94. package/src/multi-select-field/features/has_default_clear_text.feature +6 -0
  95. package/src/multi-select-field/features/has_default_empty_text/index.js +13 -0
  96. package/src/multi-select-field/features/has_default_empty_text.feature +6 -0
  97. package/src/multi-select-field/features/has_default_filter_nomatch_text/index.js +17 -0
  98. package/src/multi-select-field/features/has_default_filter_nomatch_text.feature +7 -0
  99. package/src/multi-select-field/features/has_default_filter_placeholder/index.js +15 -0
  100. package/src/multi-select-field/features/has_default_filter_placeholder.feature +6 -0
  101. package/src/multi-select-field/features/has_default_loading_text/index.js +13 -0
  102. package/src/multi-select-field/features/has_default_loading_text.feature +6 -0
  103. package/src/multi-select-field/index.js +1 -0
  104. package/src/multi-select-field/multi-select-field.e2e.stories.js +45 -0
  105. package/src/multi-select-field/multi-select-field.js +178 -0
  106. package/src/multi-select-field/multi-select-field.prod.stories.js +137 -0
  107. package/src/multi-select-option/index.js +1 -0
  108. package/src/multi-select-option/multi-select-option.js +70 -0
  109. package/src/select/__tests__/select.test.js +31 -0
  110. package/src/select/debounce/debounce.js +30 -0
  111. package/src/select/debounce/debounce.spec.js +35 -0
  112. package/src/select/empty.js +28 -0
  113. package/src/select/filter-input.js +42 -0
  114. package/src/select/filterable-menu.js +106 -0
  115. package/src/select/index.js +14 -0
  116. package/src/select/input-clear-button.js +66 -0
  117. package/src/select/input-placeholder.js +30 -0
  118. package/src/select/input-prefix.js +32 -0
  119. package/src/select/input-wrapper.js +114 -0
  120. package/src/select/loading.js +33 -0
  121. package/src/select/menu-wrapper.js +52 -0
  122. package/src/select/no-match.js +28 -0
  123. package/src/select/option-helpers.js +34 -0
  124. package/src/select/select.js +256 -0
  125. package/src/simple-single-select/__stories__/DefaultPosition.js +37 -0
  126. package/src/simple-single-select/__stories__/Dense.js +17 -0
  127. package/src/simple-single-select/__stories__/Empty.js +15 -0
  128. package/src/simple-single-select/__stories__/EmptyWithEmptyNode.js +27 -0
  129. package/src/simple-single-select/__stories__/EmptyWithEmptyText.js +16 -0
  130. package/src/simple-single-select/__stories__/FlippedPosition.js +36 -0
  131. package/src/simple-single-select/__stories__/InfiniteLoading.js +65 -0
  132. package/src/simple-single-select/__stories__/Loading.js +15 -0
  133. package/src/simple-single-select/__stories__/ShiftedIntoView.js +35 -0
  134. package/src/simple-single-select/__stories__/WithClearButton.js +26 -0
  135. package/src/simple-single-select/__stories__/WithCustomLowMaxHeight.js +17 -0
  136. package/src/simple-single-select/__stories__/WithCustomOptions.js +78 -0
  137. package/src/simple-single-select/__stories__/WithDisabledOption.js +25 -0
  138. package/src/simple-single-select/__stories__/WithFilterField.js +30 -0
  139. package/src/simple-single-select/__stories__/WithInitialFocus.js +29 -0
  140. package/src/simple-single-select/__stories__/WithManyOptions.js +19 -0
  141. package/src/simple-single-select/__stories__/WithMultipleSelectsAndScrollableParent.js +65 -0
  142. package/src/simple-single-select/__stories__/WithNoMatchText.js +25 -0
  143. package/src/simple-single-select/__stories__/WithOnBlur.js +22 -0
  144. package/src/simple-single-select/__stories__/WithOnFocus.js +22 -0
  145. package/src/simple-single-select/__stories__/WithOptionsAndDisabled.js +17 -0
  146. package/src/simple-single-select/__stories__/WithOptionsAndLoading.js +23 -0
  147. package/src/simple-single-select/__stories__/WithOptionsAndLoadingText.js +24 -0
  148. package/src/simple-single-select/__stories__/WithPlaceholder.js +18 -0
  149. package/src/simple-single-select/__stories__/WithPlaceholderAndSelection.js +20 -0
  150. package/src/simple-single-select/__stories__/WithPrefix.js +17 -0
  151. package/src/simple-single-select/__stories__/WithPrefixAndSelection.js +20 -0
  152. package/src/simple-single-select/__stories__/WithRTL.js +29 -0
  153. package/src/simple-single-select/__stories__/WithScrollingParent.js +51 -0
  154. package/src/simple-single-select/__stories__/WithSelection.js +19 -0
  155. package/src/simple-single-select/__stories__/WithSelectionAndDisabled.js +20 -0
  156. package/src/simple-single-select/__stories__/WithServerSideFilteringDemo.js +245 -0
  157. package/src/simple-single-select/__stories__/WithSimpleFilteringDemo.js +50 -0
  158. package/src/simple-single-select/__stories__/WithoutOptionsAndLoading.js +16 -0
  159. package/src/simple-single-select/__stories__/WithoutSelection.js +17 -0
  160. package/src/simple-single-select/__stories__/options.js +96 -0
  161. package/src/simple-single-select/__stories__/simple-single-select-server-side-filtering/CustomDataProvider.js +70 -0
  162. package/src/simple-single-select/__stories__/simple-single-select-server-side-filtering/dataElements.js +4271 -0
  163. package/src/simple-single-select/__stories__/simple-single-select-server-side-filtering/index.js +1 -0
  164. package/src/simple-single-select/features/keyboard-interactions.test.e2e.js +581 -0
  165. package/src/simple-single-select/features/menu-positioning.test.e2e.js +187 -0
  166. package/src/simple-single-select/index.js +2 -0
  167. package/src/simple-single-select/is-option-hidden.js +21 -0
  168. package/src/simple-single-select/menu/empty.js +27 -0
  169. package/src/simple-single-select/menu/filter.js +61 -0
  170. package/src/simple-single-select/menu/index.js +1 -0
  171. package/src/simple-single-select/menu/loading.js +38 -0
  172. package/src/simple-single-select/menu/menu.js +171 -0
  173. package/src/simple-single-select/menu/no-match.js +28 -0
  174. package/src/simple-single-select/menu/option.js +161 -0
  175. package/src/simple-single-select/menu/options-list.js +107 -0
  176. package/src/simple-single-select/selected-value/clear-button.js +71 -0
  177. package/src/simple-single-select/selected-value/container.js +146 -0
  178. package/src/simple-single-select/selected-value/index.js +1 -0
  179. package/src/simple-single-select/selected-value/placeholder.js +24 -0
  180. package/src/simple-single-select/selected-value/prefix.js +26 -0
  181. package/src/simple-single-select/selected-value/selected-value.js +174 -0
  182. package/src/simple-single-select/shared-prop-types.js +8 -0
  183. package/src/simple-single-select/simple-single-select.e2e.stories.js +216 -0
  184. package/src/simple-single-select/simple-single-select.js +389 -0
  185. package/src/simple-single-select/simple-single-select.prod.stories.js +41 -0
  186. package/src/simple-single-select/simple-single-select.test.js +1136 -0
  187. package/src/simple-single-select/use-handle-key-press/index.js +2 -0
  188. package/src/simple-single-select/use-handle-key-press/use-focus-option.js +24 -0
  189. package/src/simple-single-select/use-handle-key-press/use-handle-key-press-on-combobox.js +259 -0
  190. package/src/simple-single-select/use-handle-key-press/use-handle-key-press-on-filter-input.js +147 -0
  191. package/src/simple-single-select/use-handle-key-press/use-handle-typing.js +79 -0
  192. package/src/simple-single-select/use-handle-key-press/use-highlight-first-option-on-previous-page.js +62 -0
  193. package/src/simple-single-select/use-handle-key-press/use-highlight-first-visible-option.js +38 -0
  194. package/src/simple-single-select/use-handle-key-press/use-highlight-last-option-on-next-page.js +60 -0
  195. package/src/simple-single-select/use-handle-key-press/use-highlight-last-visible-option.js +52 -0
  196. package/src/simple-single-select/use-handle-key-press/use-page-down.js +50 -0
  197. package/src/simple-single-select/use-handle-key-press/use-page-up.js +50 -0
  198. package/src/simple-single-select/use-handle-key-press/utils.js +23 -0
  199. package/src/simple-single-select-field/index.js +1 -0
  200. package/src/simple-single-select-field/simple-single-select-field.js +198 -0
  201. package/src/simple-single-select-field/simple-single-select-field.prod.stories.js +188 -0
  202. package/src/simple-single-select-field/simple-single-select-field.test.js +144 -0
  203. package/src/single-select/__tests__/selection.test.js +102 -0
  204. package/src/single-select/__tests__/single-select.test.js +23 -0
  205. package/src/single-select/features/accepts_blur_cb/index.js +14 -0
  206. package/src/single-select/features/accepts_blur_cb.feature +8 -0
  207. package/src/single-select/features/accepts_focus_cb/index.js +14 -0
  208. package/src/single-select/features/accepts_focus_cb.feature +6 -0
  209. package/src/single-select/features/accepts_initial_focus/index.js +5 -0
  210. package/src/single-select/features/accepts_initial_focus.feature +5 -0
  211. package/src/single-select/features/accepts_loading/index.js +22 -0
  212. package/src/single-select/features/accepts_loading.feature +14 -0
  213. package/src/single-select/features/accepts_max_height/index.js +31 -0
  214. package/src/single-select/features/accepts_max_height.feature +11 -0
  215. package/src/single-select/features/accepts_placeholder/index.js +25 -0
  216. package/src/single-select/features/accepts_placeholder.feature +10 -0
  217. package/src/single-select/features/accepts_prefix/index.js +19 -0
  218. package/src/single-select/features/accepts_prefix.feature +10 -0
  219. package/src/single-select/features/allows_invalid_options/index.js +28 -0
  220. package/src/single-select/features/allows_invalid_options.feature +12 -0
  221. package/src/single-select/features/allows_selecting/index.js +38 -0
  222. package/src/single-select/features/allows_selecting.feature +21 -0
  223. package/src/single-select/features/can_be_cleared/index.js +22 -0
  224. package/src/single-select/features/can_be_cleared.feature +6 -0
  225. package/src/single-select/features/can_be_disabled/index.js +39 -0
  226. package/src/single-select/features/can_be_disabled.feature +32 -0
  227. package/src/single-select/features/can_be_empty/index.js +28 -0
  228. package/src/single-select/features/can_be_empty.feature +16 -0
  229. package/src/single-select/features/can_be_filtered/index.js +23 -0
  230. package/src/single-select/features/can_be_filtered.feature +14 -0
  231. package/src/single-select/features/can_be_opened_and_closed/index.js +32 -0
  232. package/src/single-select/features/can_be_opened_and_closed.feature +41 -0
  233. package/src/single-select/features/common/index.js +44 -0
  234. package/src/single-select/features/duplicate_option_values/index.js +26 -0
  235. package/src/single-select/features/duplicate_option_values.feature +7 -0
  236. package/src/single-select/features/menu_width_matches_input/index.js +53 -0
  237. package/src/single-select/features/menu_width_matches_input.feature +11 -0
  238. package/src/single-select/features/position/index.js +111 -0
  239. package/src/single-select/features/position.feature +26 -0
  240. package/src/single-select/features/shows_selections/index.js +9 -0
  241. package/src/single-select/features/shows_selections.feature +5 -0
  242. package/src/single-select/filterable-menu.js +41 -0
  243. package/src/single-select/index.js +1 -0
  244. package/src/single-select/input.js +101 -0
  245. package/src/single-select/menu.js +75 -0
  246. package/src/single-select/selection.js +60 -0
  247. package/src/single-select/single-select.e2e.stories.js +376 -0
  248. package/src/single-select/single-select.js +146 -0
  249. package/src/single-select/single-select.prod.stories.js +358 -0
  250. package/src/single-select-field/__tests__/single-select-field.test.js +29 -0
  251. package/src/single-select-field/features/accepts_help_text/index.js +9 -0
  252. package/src/single-select-field/features/accepts_help_text.feature +5 -0
  253. package/src/single-select-field/features/accepts_label/index.js +9 -0
  254. package/src/single-select-field/features/accepts_label.feature +5 -0
  255. package/src/single-select-field/features/accepts_validation_text/index.js +9 -0
  256. package/src/single-select-field/features/accepts_validation_text.feature +5 -0
  257. package/src/single-select-field/features/can_be_required/index.js +11 -0
  258. package/src/single-select-field/features/can_be_required.feature +5 -0
  259. package/src/single-select-field/features/has_default_clear_text/index.js +16 -0
  260. package/src/single-select-field/features/has_default_clear_text.feature +6 -0
  261. package/src/single-select-field/features/has_default_empty_text/index.js +13 -0
  262. package/src/single-select-field/features/has_default_empty_text.feature +6 -0
  263. package/src/single-select-field/features/has_default_filter_nomatch_text/index.js +17 -0
  264. package/src/single-select-field/features/has_default_filter_nomatch_text.feature +7 -0
  265. package/src/single-select-field/features/has_default_filter_placeholder/index.js +15 -0
  266. package/src/single-select-field/features/has_default_filter_placeholder.feature +6 -0
  267. package/src/single-select-field/features/has_default_loading_text/index.js +13 -0
  268. package/src/single-select-field/features/has_default_loading_text.feature +6 -0
  269. package/src/single-select-field/index.js +1 -0
  270. package/src/single-select-field/single-select-field.e2e.stories.js +45 -0
  271. package/src/single-select-field/single-select-field.js +178 -0
  272. package/src/single-select-field/single-select-field.prod.stories.js +146 -0
  273. package/src/single-select-option/index.js +1 -0
  274. package/src/single-select-option/single-select-option.js +84 -0
@@ -0,0 +1,187 @@
1
+ describe('SimpleSingleSelect: Menu positioning', () => {
2
+ it('should open in the default position', () => {
3
+ // Given there is enough space below the anchor to fit the SingleSelect menu
4
+ cy.visitStory('Simple Single Select', 'Default position')
5
+
6
+ // When the SingleSelect is clicked
7
+ cy.findByRole('combobox').click()
8
+
9
+ // Then the top of the menu is aligned with the bottom of the input
10
+ cy.all(
11
+ () => cy.findByRole('combobox'),
12
+ () => cy.findByRole('listbox')
13
+ ).should(([selectedValue, menu]) => {
14
+ expect(selectedValue.length).to.equal(1)
15
+ expect(menu.length).to.equal(1)
16
+
17
+ const $selectedValue = selectedValue[0]
18
+ const $menu = menu[0]
19
+
20
+ const selectedValueRect = $selectedValue.getBoundingClientRect()
21
+ const menuRect = $menu.getBoundingClientRect()
22
+
23
+ // The listbox is inside a container with a border,
24
+ // hence the increased delta
25
+ expect(menuRect.top).to.be.closeTo(selectedValueRect.bottom, 2)
26
+ })
27
+
28
+ // And the left of the SingleSelect is aligned with the left of the anchor
29
+ cy.all(
30
+ () => cy.findByRole('combobox'),
31
+ () => cy.findByRole('listbox')
32
+ ).should(([selectedValue, menu]) => {
33
+ expect(selectedValue.length).to.equal(1)
34
+ expect(menu.length).to.equal(1)
35
+
36
+ const $selectedValue = selectedValue[0]
37
+ const $menu = menu[0]
38
+
39
+ const selectedValueRect = $selectedValue.getBoundingClientRect()
40
+ const menuRect = $menu.getBoundingClientRect()
41
+
42
+ // The listbox is inside a container with a border,
43
+ // hence the increased delta
44
+ expect(selectedValueRect.left).to.be.closeTo(menuRect.left, 2)
45
+ })
46
+ })
47
+
48
+ it('should flipp the position when insufficient space below', () => {
49
+ // Given there is not enough space below the anchor to fit the SingleSelect menu
50
+ cy.visitStory('Simple Single Select', 'Flipped position')
51
+
52
+ // When the SingleSelect is clicked
53
+ cy.findByRole('combobox').click()
54
+
55
+ // Then the bottom of the menu is aligned with the top of the input
56
+ cy.all(
57
+ () => cy.findByRole('combobox'),
58
+ // We need to get the parent as the menu itself
59
+ // extends withing the div container
60
+ () => cy.findByRole('listbox').invoke('parent')
61
+ ).should(([selectedValue, menu]) => {
62
+ expect(selectedValue.length).to.equal(1)
63
+ expect(menu.length).to.equal(1)
64
+
65
+ const $selectedValue = selectedValue[0]
66
+ const $menu = menu[0]
67
+
68
+ const selectedValueRect = $selectedValue.getBoundingClientRect()
69
+ const menuRect = $menu.getBoundingClientRect()
70
+
71
+ expect(selectedValueRect.top).to.be.closeTo(menuRect.bottom, 2)
72
+ })
73
+
74
+ // And the left of the SingleSelect is aligned with the left of the anchor
75
+ cy.all(
76
+ () => cy.findByRole('combobox'),
77
+ () => cy.findByRole('listbox')
78
+ ).should(([selectedValue, menu]) => {
79
+ expect(selectedValue.length).to.equal(1)
80
+ expect(menu.length).to.equal(1)
81
+
82
+ const $selectedValue = selectedValue[0]
83
+ const $menu = menu[0]
84
+
85
+ const selectedValueRect = $selectedValue.getBoundingClientRect()
86
+ const menuRect = $menu.getBoundingClientRect()
87
+
88
+ // The listbox is inside a container with a border,
89
+ // hence the increased delta
90
+ expect(selectedValueRect.left).to.be.closeTo(menuRect.left, 2)
91
+ })
92
+ })
93
+
94
+ it('should shift the menu into view when insufficient space below and above', () => {
95
+ // Given there is not enough space above or below the anchor to fit the SingleSelect menu
96
+ cy.visitStory('Simple Single Select', 'Shifted into view')
97
+
98
+ // When the SingleSelect is clicked
99
+ cy.findByRole('combobox').click()
100
+
101
+ // Then it is rendered on top of the SingleSelect
102
+ cy.all(
103
+ () => cy.findByRole('combobox'),
104
+ () => cy.findByRole('listbox')
105
+ ).should(([selectedValue, menu]) => {
106
+ expect(selectedValue.length).to.equal(1)
107
+ expect(menu.length).to.equal(1)
108
+
109
+ const $selectedValue = selectedValue[0]
110
+ const $menu = menu[0]
111
+
112
+ const selectedValueRect = $selectedValue.getBoundingClientRect()
113
+ const menuRect = $menu.getBoundingClientRect()
114
+
115
+ expect(selectedValueRect.top).to.be.greaterThan(menuRect.top)
116
+ expect(menuRect.bottom).to.be.greaterThan(selectedValueRect.bottom)
117
+ })
118
+
119
+ // And the left of the SingleSelect is aligned with the left of the anchor
120
+ cy.all(
121
+ () => cy.findByRole('combobox'),
122
+ () => cy.findByRole('listbox')
123
+ ).should(([selectedValue, menu]) => {
124
+ expect(selectedValue.length).to.equal(1)
125
+ expect(menu.length).to.equal(1)
126
+
127
+ const $selectedValue = selectedValue[0]
128
+ const $menu = menu[0]
129
+
130
+ const selectedValueRect = $selectedValue.getBoundingClientRect()
131
+ const menuRect = $menu.getBoundingClientRect()
132
+
133
+ expect(selectedValueRect.top).to.be.greaterThan(menuRect.top)
134
+ expect(menuRect.bottom).to.be.greaterThan(selectedValueRect.bottom)
135
+ })
136
+ })
137
+
138
+ it('should keep the menu in position while the window is scrolled', () => {
139
+ // Given there is enough space below the anchor to fit the SingleSelect menu
140
+ cy.visitStory('Simple Single Select', 'Default position')
141
+
142
+ // When the SingleSelect is clicked
143
+ cy.findByRole('combobox').click()
144
+
145
+ // And the window is scrolled down
146
+ cy.get('body').then(($body) => $body.height('5000px')) // Ensure the body can scroll first
147
+ cy.scrollTo(0, 800)
148
+
149
+ // Then the top of the menu is aligned with the bottom of the input
150
+ cy.all(
151
+ () => cy.findByRole('combobox'),
152
+ () => cy.findByRole('listbox')
153
+ ).should(([selectedValue, menu]) => {
154
+ expect(selectedValue.length).to.equal(1)
155
+ expect(menu.length).to.equal(1)
156
+
157
+ const $selectedValue = selectedValue[0]
158
+ const $menu = menu[0]
159
+
160
+ const selectedValueRect = $selectedValue.getBoundingClientRect()
161
+ const menuRect = $menu.getBoundingClientRect()
162
+
163
+ // The listbox is inside a container with a border,
164
+ // hence the increased delta
165
+ expect(menuRect.top).to.be.closeTo(selectedValueRect.bottom, 2)
166
+ })
167
+
168
+ // And the left of the SingleSelect is aligned with the left of the anchor
169
+ cy.all(
170
+ () => cy.findByRole('combobox'),
171
+ () => cy.findByRole('listbox')
172
+ ).should(([selectedValue, menu]) => {
173
+ expect(selectedValue.length).to.equal(1)
174
+ expect(menu.length).to.equal(1)
175
+
176
+ const $selectedValue = selectedValue[0]
177
+ const $menu = menu[0]
178
+
179
+ const selectedValueRect = $selectedValue.getBoundingClientRect()
180
+ const menuRect = $menu.getBoundingClientRect()
181
+
182
+ // The listbox is inside a container with a border,
183
+ // hence the increased delta
184
+ expect(selectedValueRect.left).to.be.closeTo(menuRect.left, 2)
185
+ })
186
+ })
187
+ })
@@ -0,0 +1,2 @@
1
+ export { optionProp } from './shared-prop-types.js'
2
+ export { SimpleSingleSelect } from './simple-single-select.js'
@@ -0,0 +1,21 @@
1
+ // If this many pixels are invisible, we still consider the option visible
2
+ // The reason we're doing this is because of JS' number issues,
3
+ // e.g:
4
+ // optionOffsetBottom is 345.6000061035156
5
+ // containerOffsetBottom is 345.5999984741211
6
+ // -> Without tolerance, option is invisible
7
+ const TOLERANCE = 2
8
+
9
+ export function isOptionHidden(option, scrollContainer) {
10
+ const optionOffsetTop = option.getBoundingClientRect().top
11
+ const optionHeight = option.offsetHeight
12
+ const optionOffsetBottom = optionOffsetTop + optionHeight
13
+ const containerOffsetTop = scrollContainer.getBoundingClientRect().top
14
+ const containerHeight = scrollContainer.offsetHeight
15
+ const containerOffsetBottom = containerOffsetTop + containerHeight
16
+
17
+ return (
18
+ optionOffsetBottom > containerOffsetBottom + TOLERANCE ||
19
+ optionOffsetTop < containerOffsetTop - TOLERANCE
20
+ )
21
+ }
@@ -0,0 +1,27 @@
1
+ import { colors, spacers, theme } from '@dhis2/ui-constants'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+
5
+ const Empty = ({ children }) => (
6
+ <div>
7
+ {children}
8
+
9
+ <style jsx>{`
10
+ div {
11
+ color: ${colors.grey700};
12
+ font-family: ${theme.fonts};
13
+ font-size: 13px;
14
+ line-height: 16px;
15
+ padding-block: ${spacers.dp8};
16
+ padding-inline: ${spacers.dp24};
17
+ text-align: center;
18
+ }
19
+ `}</style>
20
+ </div>
21
+ )
22
+
23
+ Empty.propTypes = {
24
+ children: PropTypes.string.isRequired,
25
+ }
26
+
27
+ export { Empty }
@@ -0,0 +1,61 @@
1
+ import PropTypes from 'prop-types'
2
+ import React from 'react'
3
+ import i18n from '../../locales/index.js'
4
+
5
+ export function Filter({
6
+ ariaControls,
7
+ dataTest,
8
+ inputRef,
9
+ label,
10
+ placeholder,
11
+ value,
12
+ onChange,
13
+ onKeyDown,
14
+ }) {
15
+ return (
16
+ <>
17
+ <input
18
+ ref={inputRef}
19
+ className="filter-input"
20
+ type="text"
21
+ name="filter"
22
+ value={value}
23
+ placeholder={placeholder}
24
+ aria-label={label || i18n.t('Search options')}
25
+ aria-controls={ariaControls}
26
+ aria-haspopup="listbox"
27
+ data-test={dataTest}
28
+ onChange={(e) => onChange(e.target.value)}
29
+ onClick={(e) => e.stopPropagation()}
30
+ onKeyDown={(e) => {
31
+ e.stopPropagation()
32
+ onKeyDown?.(e)
33
+ }}
34
+ />
35
+ <style jsx>{`
36
+ .filter-input {
37
+ flex: 1;
38
+ border: none;
39
+ outline: none;
40
+ background: transparent;
41
+ font-size: 14px;
42
+ line-height: 16px;
43
+ color: inherit;
44
+ padding: 0;
45
+ min-width: 0;
46
+ }
47
+ `}</style>
48
+ </>
49
+ )
50
+ }
51
+
52
+ Filter.propTypes = {
53
+ ariaControls: PropTypes.string.isRequired,
54
+ inputRef: PropTypes.object.isRequired,
55
+ value: PropTypes.string.isRequired,
56
+ onChange: PropTypes.func.isRequired,
57
+ dataTest: PropTypes.string,
58
+ label: PropTypes.string,
59
+ placeholder: PropTypes.string,
60
+ onKeyDown: PropTypes.func,
61
+ }
@@ -0,0 +1 @@
1
+ export { Menu } from './menu.js'
@@ -0,0 +1,38 @@
1
+ import { colors, spacers, theme } from '@dhis2/ui-constants'
2
+ import { CircularLoader } from '@dhis2-ui/loader'
3
+ import PropTypes from 'prop-types'
4
+ import React from 'react'
5
+
6
+ export function Loading({ message }) {
7
+ return (
8
+ <div className="container">
9
+ <div>
10
+ <CircularLoader small />
11
+ </div>
12
+
13
+ {message}
14
+
15
+ <style jsx>{`
16
+ .container {
17
+ box-sizing: border-box;
18
+ width: 100%;
19
+ height: 100%;
20
+ display: flex;
21
+ gap: ${spacers.dp16};
22
+ align-items: center;
23
+ justify-content: center;
24
+ color: ${colors.grey700};
25
+ font-family: ${theme.fonts};
26
+ font-size: 13px;
27
+ padding-block: ${spacers.dp8};
28
+ padding-inline: ${spacers.dp24};
29
+ background: rgba(255, 255, 255, 0.8);
30
+ }
31
+ `}</style>
32
+ </div>
33
+ )
34
+ }
35
+
36
+ Loading.propTypes = {
37
+ message: PropTypes.string,
38
+ }
@@ -0,0 +1,171 @@
1
+ import { colors, elevations } from '@dhis2/ui-constants'
2
+ import { Layer } from '@dhis2-ui/layer'
3
+ import { Popper } from '@dhis2-ui/popper'
4
+ import PropTypes from 'prop-types'
5
+ import React, { useEffect, useState } from 'react'
6
+ import { optionProp } from '../shared-prop-types.js'
7
+ import { Empty } from './empty.js'
8
+ import { Loading } from './loading.js'
9
+ import { NoMatch } from './no-match.js'
10
+ import { OptionsList } from './options-list.js'
11
+
12
+ export function Menu({
13
+ comboBoxId,
14
+ focussedOptionIndex,
15
+ name,
16
+ options,
17
+ onChange,
18
+ optionComponent,
19
+ dataTest,
20
+ disabled,
21
+ empty,
22
+ filterValue,
23
+ hidden,
24
+ labelledBy,
25
+ listBoxRef,
26
+ loading,
27
+ loadingText,
28
+ maxHeight,
29
+ noMatchText,
30
+ optionUpdateStrategy,
31
+ selectRef,
32
+ selectedValue,
33
+ onBlur,
34
+ onClose,
35
+ onEndReached,
36
+ }) {
37
+ const [menuWidth, setWidth] = useState('auto')
38
+ const dataTestPrefix = `${dataTest}-menu`
39
+
40
+ useEffect(() => {
41
+ if (selectRef) {
42
+ const callback = () => setWidth(`${selectRef.offsetWidth}px`)
43
+ callback() // We want to know the width as soon as the
44
+
45
+ selectRef.addEventListener('resize', callback)
46
+ return () => selectRef.removeEventListener('resize', callback)
47
+ }
48
+ }, [selectRef])
49
+
50
+ if (hidden) {
51
+ return null
52
+ }
53
+
54
+ const hasNoFilterMatch =
55
+ !options.length &&
56
+ filterValue &&
57
+ // We don't want to show the noMatchText when
58
+ // we're in the process of loading options
59
+ !loading
60
+
61
+ const isEmpty = !options.length && !filterValue
62
+
63
+ return (
64
+ <Layer onBackdropClick={onClose} transparent>
65
+ <Popper
66
+ reference={selectRef}
67
+ placement="bottom-start"
68
+ observeReferenceResize
69
+ >
70
+ <div className="menu" style={{ width: menuWidth, maxHeight }}>
71
+ {isEmpty && <Empty>{empty}</Empty>}
72
+
73
+ {hasNoFilterMatch && <NoMatch>{noMatchText}</NoMatch>}
74
+
75
+ <div className="listbox-container">
76
+ <div className="listbox-wrapper">
77
+ <OptionsList
78
+ ref={listBoxRef}
79
+ comboBoxId={comboBoxId}
80
+ optionComponent={optionComponent}
81
+ dataTest={`${dataTestPrefix}-list`}
82
+ disabled={disabled}
83
+ focussedOptionIndex={focussedOptionIndex}
84
+ labelledBy={labelledBy}
85
+ loading={loading}
86
+ id={`${name}-listbox`}
87
+ optionUpdateStrategy={optionUpdateStrategy}
88
+ options={options}
89
+ selectedValue={selectedValue}
90
+ onBlur={onBlur}
91
+ onChange={onChange}
92
+ onEndReached={onEndReached}
93
+ />
94
+ </div>
95
+
96
+ {loading && (
97
+ <div className="menu-loading-container">
98
+ <Loading message={loadingText} />
99
+ </div>
100
+ )}
101
+ </div>
102
+
103
+ <style jsx>{`
104
+ .menu {
105
+ display: flex;
106
+ flex-direction: column;
107
+ height: auto;
108
+ background: ${colors.white};
109
+ border: 1px solid ${colors.grey200};
110
+ border-radius: 3px;
111
+ box-shadow: ${elevations.e300};
112
+
113
+ /* We want the provided height to be exact, otherwise
114
+ the consumer would have to know about the border's width */
115
+ box-sizing: content-box;
116
+ }
117
+
118
+ .listbox-container {
119
+ position: relative;
120
+ flex-grow: 1;
121
+ display: flex;
122
+ flex-direction: column;
123
+ overflow: hidden;
124
+ }
125
+
126
+ .listbox-wrapper {
127
+ overflow: auto;
128
+ flex-grow: 1;
129
+ }
130
+
131
+ .menu-loading-container {
132
+ position: absolute;
133
+ left: 0;
134
+ top: 0;
135
+ width: 100%;
136
+ height: 100%;
137
+ }
138
+ `}</style>
139
+ </div>
140
+ </Popper>
141
+ </Layer>
142
+ )
143
+ }
144
+
145
+ Menu.propTypes = {
146
+ comboBoxId: PropTypes.string.isRequired,
147
+ focussedOptionIndex: PropTypes.number.isRequired,
148
+ listBoxRef: PropTypes.shape({
149
+ current: PropTypes.instanceOf(HTMLElement),
150
+ }).isRequired,
151
+ name: PropTypes.string.isRequired,
152
+ options: PropTypes.arrayOf(optionProp).isRequired,
153
+ onChange: PropTypes.func.isRequired,
154
+ dataTest: PropTypes.string,
155
+ disabled: PropTypes.bool,
156
+ empty: PropTypes.node,
157
+ filterValue: PropTypes.string,
158
+ hidden: PropTypes.bool,
159
+ labelledBy: PropTypes.string,
160
+ loading: PropTypes.bool,
161
+ loadingText: PropTypes.string,
162
+ maxHeight: PropTypes.string,
163
+ noMatchText: PropTypes.string,
164
+ optionComponent: PropTypes.elementType,
165
+ optionUpdateStrategy: PropTypes.oneOf(['off', 'polite', 'assertive']),
166
+ selectRef: PropTypes.instanceOf(HTMLElement),
167
+ selectedValue: PropTypes.string,
168
+ onBlur: PropTypes.func,
169
+ onClose: PropTypes.func,
170
+ onEndReached: PropTypes.func,
171
+ }
@@ -0,0 +1,28 @@
1
+ import { colors, spacers, theme } from '@dhis2/ui-constants'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+
5
+ const NoMatch = ({ children, className }) => (
6
+ <div className={className}>
7
+ {children}
8
+
9
+ <style jsx>{`
10
+ div {
11
+ color: ${colors.grey700};
12
+ font-family: ${theme.fonts};
13
+ font-size: 13px;
14
+ line-height: 16px;
15
+ padding-block: ${spacers.dp8};
16
+ padding-inline: ${spacers.dp24};
17
+ text-align: center;
18
+ }
19
+ `}</style>
20
+ </div>
21
+ )
22
+
23
+ NoMatch.propTypes = {
24
+ children: PropTypes.string.isRequired,
25
+ className: PropTypes.string,
26
+ }
27
+
28
+ export { NoMatch }