@dhis2-ui/select 10.16.2 → 10.16.3-alpha.1

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,2 @@
1
+ export { useHandleKeyPressOnCombobox } from './use-handle-key-press-on-combobox.js'
2
+ export { useHandleKeyPressOnFilterInput } from './use-handle-key-press-on-filter-input.js'
@@ -0,0 +1,24 @@
1
+ import { useCallback } from 'react'
2
+
3
+ export function useFocusOption(
4
+ findIndexCallback,
5
+ { options, focussedOptionIndex, setFocussedOptionIndex }
6
+ ) {
7
+ return useCallback(() => {
8
+ const nextFocussedIndex = findIndexCallback({
9
+ options,
10
+ activeIndex: focussedOptionIndex,
11
+ })
12
+
13
+ if (nextFocussedIndex === -1) {
14
+ return
15
+ }
16
+
17
+ setFocussedOptionIndex(nextFocussedIndex)
18
+ }, [
19
+ findIndexCallback,
20
+ options,
21
+ focussedOptionIndex,
22
+ setFocussedOptionIndex,
23
+ ])
24
+ }
@@ -0,0 +1,259 @@
1
+ import { useCallback } from 'react'
2
+ import { useFocusOption } from './use-focus-option.js'
3
+ import { useHandleTyping } from './use-handle-typing.js'
4
+ import { usePageDown } from './use-page-down.js'
5
+ import { usePageUp } from './use-page-up.js'
6
+ import {
7
+ findNextOptionIndex,
8
+ findPrevOptionIndex,
9
+ findFirstOptionIndex,
10
+ findLastOptionIndex,
11
+ } from './utils.js'
12
+
13
+ function isUnmodifiedCharacterKey({ key, altKey, ctrlKey, metaKey }) {
14
+ return key.length === 1 && key !== ' ' && !altKey && !ctrlKey && !metaKey
15
+ }
16
+
17
+ function useSelectOption(findIndexCallback, { options, onChange, value }) {
18
+ return useCallback(() => {
19
+ const currentOptionIndex = options.findIndex(
20
+ (option) => option.value === value
21
+ )
22
+
23
+ const nextSelectedOptionIndex = findIndexCallback({
24
+ options,
25
+ activeIndex: currentOptionIndex,
26
+ })
27
+
28
+ if (nextSelectedOptionIndex === -1) {
29
+ return
30
+ }
31
+
32
+ const nextOption = options[nextSelectedOptionIndex]
33
+ onChange({
34
+ label: nextOption.label,
35
+ value: nextOption.value,
36
+ })
37
+ }, [findIndexCallback, options, onChange, value])
38
+ }
39
+
40
+ export function useHandleKeyPressOnCombobox({
41
+ value,
42
+ disabled,
43
+ expanded,
44
+ loading,
45
+ options,
46
+ openMenu,
47
+ closeMenu,
48
+ listBoxRef,
49
+ focussedOptionIndex,
50
+ setFocussedOptionIndex,
51
+ selectFocussedOption,
52
+ onChange,
53
+ clearable,
54
+ onClear,
55
+ filterable,
56
+ onFilterChange,
57
+ }) {
58
+ const { onTyping, typing } = useHandleTyping({
59
+ expanded,
60
+ options,
61
+ setFocussedOptionIndex,
62
+ onChange,
63
+ })
64
+
65
+ const pageDown = usePageDown({
66
+ options,
67
+ focussedOptionIndex,
68
+ setFocussedOptionIndex,
69
+ listBoxRef,
70
+ })
71
+
72
+ const pageUp = usePageUp({
73
+ options,
74
+ listBoxRef,
75
+ focussedOptionIndex,
76
+ setFocussedOptionIndex,
77
+ })
78
+
79
+ const selectNextOption = useSelectOption(findNextOptionIndex, {
80
+ options,
81
+ onChange,
82
+ value,
83
+ })
84
+
85
+ const selectPrevOption = useSelectOption(findPrevOptionIndex, {
86
+ options,
87
+ onChange,
88
+ value,
89
+ })
90
+
91
+ const focusNextOption = useFocusOption(findNextOptionIndex, {
92
+ options,
93
+ focussedOptionIndex,
94
+ setFocussedOptionIndex,
95
+ })
96
+
97
+ const focusPrevOption = useFocusOption(findPrevOptionIndex, {
98
+ options,
99
+ focussedOptionIndex,
100
+ setFocussedOptionIndex,
101
+ })
102
+
103
+ const focusFirstOption = useFocusOption(findFirstOptionIndex, {
104
+ options,
105
+ focussedOptionIndex,
106
+ setFocussedOptionIndex,
107
+ })
108
+
109
+ const focusLastOption = useFocusOption(findLastOptionIndex, {
110
+ options,
111
+ focussedOptionIndex,
112
+ setFocussedOptionIndex,
113
+ })
114
+
115
+ const handleKeyPress = useCallback(
116
+ (event) => {
117
+ const { key, altKey } = event
118
+ if (disabled || loading) {
119
+ return
120
+ }
121
+
122
+ /*
123
+ * The menu won't be an ancestor / sibling of the combobox,
124
+ * so when pressing tab, it's highly unlikely that any element
125
+ * in the menu would receive focus (happens only in storybook,
126
+ * where there's no other focussable element).
127
+ *
128
+ * Assistive technology will know about the menu as the combobox
129
+ * has the appropriate aria attributes that will point to the
130
+ * correct HTML elements.
131
+ */
132
+ if (expanded && key === 'Tab') {
133
+ closeMenu()
134
+ return
135
+ }
136
+
137
+ // ! This is necessary for cases where the parent window is scrollable
138
+ // ! otherwise the ArrowDown/Up would change the combo but also scroll the window
139
+ if (key === 'ArrowDown' || key === 'ArrowUp' || key === ' ') {
140
+ event.preventDefault()
141
+ }
142
+
143
+ if (
144
+ expanded &&
145
+ (key === 'Escape' ||
146
+ key === 'Enter' ||
147
+ (key === ' ' && !typing) ||
148
+ (key === 'ArrowUp' && altKey) ||
149
+ (key === 'ArrowDown' && altKey))
150
+ ) {
151
+ if (value !== options[focussedOptionIndex].value) {
152
+ selectFocussedOption()
153
+ }
154
+
155
+ closeMenu()
156
+ return
157
+ }
158
+
159
+ if (!expanded && clearable && key === 'Escape') {
160
+ onClear?.()
161
+ return
162
+ }
163
+
164
+ if (
165
+ !expanded &&
166
+ ((key === ' ' && !typing) ||
167
+ key === 'Enter' ||
168
+ (key === 'ArrowUp' && altKey) ||
169
+ (key === 'ArrowDown' && altKey))
170
+ ) {
171
+ openMenu()
172
+ return
173
+ }
174
+
175
+ if (key === 'ArrowDown' && !expanded) {
176
+ selectNextOption()
177
+ return
178
+ }
179
+
180
+ if (key === 'ArrowDown') {
181
+ focusNextOption()
182
+ return
183
+ }
184
+
185
+ if (key === 'ArrowUp' && !expanded) {
186
+ selectPrevOption()
187
+ return
188
+ }
189
+
190
+ if (key === 'ArrowUp') {
191
+ focusPrevOption()
192
+ return
193
+ }
194
+
195
+ if (key === 'Home') {
196
+ focusFirstOption()
197
+ return
198
+ }
199
+
200
+ if (key === 'End') {
201
+ focusLastOption()
202
+ return
203
+ }
204
+
205
+ if (filterable && !expanded && isUnmodifiedCharacterKey(event)) {
206
+ openMenu()
207
+ onFilterChange?.(key)
208
+ return
209
+ }
210
+
211
+ if (
212
+ key === 'Backspace' ||
213
+ key === 'Clear' ||
214
+ isUnmodifiedCharacterKey(event) ||
215
+ (key === ' ' && typing)
216
+ ) {
217
+ onTyping(key)
218
+ return
219
+ }
220
+
221
+ if (expanded && key === 'PageUp') {
222
+ pageUp()
223
+ return
224
+ }
225
+
226
+ if (expanded && key === 'PageDown') {
227
+ pageDown()
228
+ return
229
+ }
230
+
231
+ // Do nothing
232
+ },
233
+ [
234
+ closeMenu,
235
+ disabled,
236
+ expanded,
237
+ loading,
238
+ openMenu,
239
+ options,
240
+ value,
241
+ typing,
242
+ focussedOptionIndex,
243
+ selectFocussedOption,
244
+ selectNextOption,
245
+ selectPrevOption,
246
+ focusNextOption,
247
+ focusPrevOption,
248
+ focusFirstOption,
249
+ focusLastOption,
250
+ pageDown,
251
+ pageUp,
252
+ onTyping,
253
+ filterable,
254
+ onFilterChange,
255
+ ]
256
+ )
257
+
258
+ return handleKeyPress
259
+ }
@@ -0,0 +1,147 @@
1
+ import { useCallback } from 'react'
2
+ import { useFocusOption } from './use-focus-option.js'
3
+ import { usePageDown } from './use-page-down.js'
4
+ import { usePageUp } from './use-page-up.js'
5
+ import {
6
+ findNextOptionIndex,
7
+ findPrevOptionIndex,
8
+ findFirstOptionIndex,
9
+ findLastOptionIndex,
10
+ } from './utils.js'
11
+
12
+ export function useHandleKeyPressOnFilterInput({
13
+ value,
14
+ options,
15
+ loading,
16
+ closeMenu,
17
+ listBoxRef,
18
+ focusComboBox,
19
+ focussedOptionIndex,
20
+ setFocussedOptionIndex,
21
+ selectFocussedOption,
22
+ }) {
23
+ const pageDown = usePageDown({
24
+ options,
25
+ focussedOptionIndex,
26
+ setFocussedOptionIndex,
27
+ listBoxRef,
28
+ })
29
+
30
+ const pageUp = usePageUp({
31
+ options,
32
+ listBoxRef,
33
+ focussedOptionIndex,
34
+ setFocussedOptionIndex,
35
+ })
36
+
37
+ const focusNextOption = useFocusOption(findNextOptionIndex, {
38
+ options,
39
+ focussedOptionIndex,
40
+ setFocussedOptionIndex,
41
+ })
42
+
43
+ const focusPrevOption = useFocusOption(findPrevOptionIndex, {
44
+ options,
45
+ focussedOptionIndex,
46
+ setFocussedOptionIndex,
47
+ })
48
+
49
+ const focusFirstOption = useFocusOption(findFirstOptionIndex, {
50
+ options,
51
+ focussedOptionIndex,
52
+ setFocussedOptionIndex,
53
+ })
54
+
55
+ const focusLastOption = useFocusOption(findLastOptionIndex, {
56
+ options,
57
+ focussedOptionIndex,
58
+ setFocussedOptionIndex,
59
+ })
60
+
61
+ const handleKeyPress = useCallback(
62
+ (e) => {
63
+ if (loading) {
64
+ return
65
+ }
66
+
67
+ const { key, altKey } = e
68
+
69
+ // Defer so Tab advances focus before the filter unmounts.
70
+ if (key === 'Tab') {
71
+ requestAnimationFrame(closeMenu)
72
+ return
73
+ }
74
+
75
+ if (
76
+ key === 'Escape' ||
77
+ key === 'Enter' ||
78
+ (key === 'ArrowUp' && altKey) ||
79
+ (key === 'ArrowDown' && altKey)
80
+ ) {
81
+ if (value !== options[focussedOptionIndex].value) {
82
+ selectFocussedOption()
83
+ }
84
+
85
+ closeMenu()
86
+
87
+ // As the filter is being focused, we want to move focus back to the combobox
88
+ focusComboBox()
89
+
90
+ return
91
+ }
92
+
93
+ if (key === 'ArrowDown') {
94
+ e.preventDefault() // Disable moving cursor to end
95
+ focusNextOption()
96
+ return
97
+ }
98
+
99
+ if (key === 'ArrowUp') {
100
+ e.preventDefault() // Disable moving cursor to start
101
+ focusPrevOption()
102
+ return
103
+ }
104
+
105
+ if (key === 'Home') {
106
+ e.preventDefault() // Disable moving cursor to start
107
+ focusFirstOption()
108
+ return
109
+ }
110
+
111
+ if (key === 'End') {
112
+ e.preventDefault() // Disable moving cursor to end
113
+ focusLastOption()
114
+ return
115
+ }
116
+
117
+ if (key === 'PageUp') {
118
+ pageUp()
119
+ return
120
+ }
121
+
122
+ if (key === 'PageDown') {
123
+ pageDown()
124
+ return
125
+ }
126
+
127
+ // Do nothing
128
+ },
129
+ [
130
+ closeMenu,
131
+ options,
132
+ value,
133
+ loading,
134
+ focussedOptionIndex,
135
+ selectFocussedOption,
136
+ focusComboBox,
137
+ focusNextOption,
138
+ focusPrevOption,
139
+ focusFirstOption,
140
+ focusLastOption,
141
+ pageDown,
142
+ pageUp,
143
+ ]
144
+ )
145
+
146
+ return handleKeyPress
147
+ }
@@ -0,0 +1,79 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+
3
+ const TYPING_DEBOUNCE_TIME = 300 // ms
4
+
5
+ export function useHandleTyping({
6
+ expanded,
7
+ options,
8
+ setFocussedOptionIndex,
9
+ onChange,
10
+ }) {
11
+ const [value, setValue] = useState('')
12
+ const [typing, setTyping] = useState(false)
13
+
14
+ // This will reset the typed value after a given time
15
+ const timeoutRef = useRef()
16
+ useEffect(() => {
17
+ if (timeoutRef.current) {
18
+ clearTimeout(timeoutRef.current)
19
+ timeoutRef.current = null
20
+ }
21
+
22
+ if (value) {
23
+ timeoutRef.current = setTimeout(() => {
24
+ setValue('')
25
+ setTyping(false)
26
+ }, TYPING_DEBOUNCE_TIME)
27
+ } else {
28
+ setTyping(false)
29
+ }
30
+ }, [
31
+ // adding value to the dependencies array so that the hooks runs every time the value changes
32
+ value,
33
+ ])
34
+
35
+ // This will focus the first option with a label starting with the typed sequence
36
+ const prevValueRef = useRef()
37
+ useEffect(() => {
38
+ if (value && value !== prevValueRef.current) {
39
+ // We only want to do this when the value changed
40
+ prevValueRef.current = value
41
+
42
+ const optionIndex = options.findIndex(
43
+ ({ disabled, label }) =>
44
+ !disabled &&
45
+ label.toLowerCase().startsWith(value.toLowerCase())
46
+ )
47
+
48
+ if (optionIndex !== -1) {
49
+ if (expanded) {
50
+ setFocussedOptionIndex(optionIndex)
51
+ } else {
52
+ const nextSelectedOption = options[optionIndex]
53
+ onChange({
54
+ label: nextSelectedOption.label,
55
+ value: nextSelectedOption.value,
56
+ })
57
+ }
58
+ }
59
+ }
60
+ }, [value, options, setFocussedOptionIndex, expanded, onChange])
61
+
62
+ const onTyping = useCallback((key) => {
63
+ setTyping(true)
64
+
65
+ if (key === 'Backspace') {
66
+ setValue((prevValue) => prevValue.slice(0, -1))
67
+ return
68
+ }
69
+
70
+ if (key === 'Clear') {
71
+ setValue('')
72
+ return
73
+ }
74
+
75
+ setValue((prevValue) => `${prevValue}${key}`)
76
+ }, [])
77
+
78
+ return { onTyping, typing }
79
+ }
@@ -0,0 +1,62 @@
1
+ import { useCallback } from 'react'
2
+
3
+ export function useHighlightFirstOptionOnPreviousPage({
4
+ options,
5
+ focussedOptionIndex,
6
+ setFocussedOptionIndex,
7
+ listBoxRef,
8
+ }) {
9
+ return useCallback(() => {
10
+ const listBoxParent = listBoxRef.current.parentNode
11
+ const optionElements = Array.from(listBoxRef.current.childNodes)
12
+ const visibleOptionsAmount = Math.floor(
13
+ listBoxParent.offsetHeight / optionElements[0].offsetHeight
14
+ )
15
+
16
+ const nextTopOptionIndex = Math.max(
17
+ 0,
18
+ focussedOptionIndex - visibleOptionsAmount
19
+ )
20
+
21
+ // If there's no next option and we already have the last option in the list highlighted
22
+ if (!options[nextTopOptionIndex]) {
23
+ return
24
+ }
25
+
26
+ if (!options[nextTopOptionIndex].disabled) {
27
+ const nextTopOption = optionElements[nextTopOptionIndex]
28
+ const scrollPosition = nextTopOption.offsetTop
29
+ listBoxParent.scrollTop = scrollPosition
30
+ setFocussedOptionIndex(nextTopOptionIndex)
31
+ return
32
+ }
33
+
34
+ const lowerEnabledOptionIndex = options
35
+ .slice(0, nextTopOptionIndex)
36
+ .findLastIndex((option) => !option.disabled)
37
+
38
+ if (lowerEnabledOptionIndex !== -1) {
39
+ const lowerEnabledOption = optionElements[lowerEnabledOptionIndex]
40
+ const lowerScrollPosition = lowerEnabledOption.offsetTop
41
+ listBoxParent.scrollTop = lowerScrollPosition
42
+ setFocussedOptionIndex(lowerEnabledOptionIndex)
43
+ return
44
+ }
45
+
46
+ const inbetweenEnabledOptionIndex =
47
+ nextTopOptionIndex +
48
+ options
49
+ .slice(nextTopOptionIndex, focussedOptionIndex)
50
+ .findIndex((option) => !option.disabled)
51
+
52
+ if (inbetweenEnabledOptionIndex === -1) {
53
+ // We're already on the first enabled option
54
+ return
55
+ }
56
+
57
+ const inbetweenTopOption = optionElements[inbetweenEnabledOptionIndex]
58
+ const scrollPosition = inbetweenTopOption.offsetTop
59
+ listBoxParent.scrollTop = scrollPosition
60
+ setFocussedOptionIndex(inbetweenEnabledOptionIndex)
61
+ }, [options, focussedOptionIndex, setFocussedOptionIndex, listBoxRef])
62
+ }
@@ -0,0 +1,38 @@
1
+ import { useCallback } from 'react'
2
+
3
+ export function useHighlightFirstVisibleOption({
4
+ options,
5
+ focussedOptionIndex,
6
+ setFocussedOptionIndex,
7
+ }) {
8
+ return useCallback(
9
+ (lowestVisibleIndex) => {
10
+ if (!options[lowestVisibleIndex].disabled) {
11
+ setFocussedOptionIndex(lowestVisibleIndex)
12
+ return
13
+ }
14
+
15
+ const previousEnabledOptionIndex = options
16
+ .slice(0, lowestVisibleIndex)
17
+ .findLastIndex((option) => !option.disabled)
18
+
19
+ if (previousEnabledOptionIndex !== -1) {
20
+ setFocussedOptionIndex(previousEnabledOptionIndex)
21
+ return
22
+ }
23
+
24
+ const nextEnabledOptionIndex =
25
+ lowestVisibleIndex +
26
+ options
27
+ .slice(lowestVisibleIndex, focussedOptionIndex)
28
+ .findIndex((option) => !option.disabled)
29
+
30
+ if (nextEnabledOptionIndex !== -1) {
31
+ return
32
+ }
33
+
34
+ setFocussedOptionIndex(nextEnabledOptionIndex)
35
+ },
36
+ [options, focussedOptionIndex, setFocussedOptionIndex]
37
+ )
38
+ }
@@ -0,0 +1,60 @@
1
+ import { useCallback } from 'react'
2
+
3
+ export function useHighlightLastOptionOnNextPage({
4
+ options,
5
+ focussedOptionIndex,
6
+ setFocussedOptionIndex,
7
+ listBoxRef,
8
+ }) {
9
+ return useCallback(() => {
10
+ const listBoxParent = listBoxRef.current.parentNode
11
+ const optionElements = Array.from(listBoxRef.current.childNodes)
12
+ const visibleOptionsAmount = Math.floor(
13
+ listBoxParent.offsetHeight / optionElements[0].offsetHeight
14
+ )
15
+
16
+ const nextHighlightedOptionIndex = Math.min(
17
+ options.length - 1,
18
+ focussedOptionIndex + visibleOptionsAmount
19
+ )
20
+
21
+ // If there's no next option and we already have the last option in the list highlighted
22
+ if (!options[nextHighlightedOptionIndex]) {
23
+ return
24
+ }
25
+
26
+ if (!options[nextHighlightedOptionIndex].disabled) {
27
+ // This will be the first option in the list
28
+ const { offsetTop: scrollPosition } =
29
+ optionElements[
30
+ nextHighlightedOptionIndex - visibleOptionsAmount + 1
31
+ ]
32
+
33
+ listBoxParent.scrollTop = scrollPosition
34
+ setFocussedOptionIndex(nextHighlightedOptionIndex)
35
+ return
36
+ }
37
+
38
+ const followingEnabledOptionIndex =
39
+ nextHighlightedOptionIndex +
40
+ options
41
+ .slice(nextHighlightedOptionIndex)
42
+ .findIndex((option) => !option.disabled)
43
+
44
+ // There is no enabled option after the disabled option that's at the end of the next page
45
+ // So we stay where we are
46
+ if (followingEnabledOptionIndex === -1) {
47
+ return
48
+ }
49
+
50
+ // There is an enabled option after the disabled option that's at the end of the next page
51
+ // So that'll be the new highlighted option and the bottom of the next displayed page
52
+ const { offsetTop: adjustedScrollPosition } =
53
+ optionElements[
54
+ followingEnabledOptionIndex - visibleOptionsAmount + 1
55
+ ]
56
+
57
+ listBoxParent.scrollTop = adjustedScrollPosition
58
+ setFocussedOptionIndex(followingEnabledOptionIndex)
59
+ }, [options, focussedOptionIndex, setFocussedOptionIndex, listBoxRef])
60
+ }