@dhis2-ui/select 10.16.1 → 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 (278) hide show
  1. package/build/cjs/select/select.js +5 -2
  2. package/build/cjs/simple-single-select/use-handle-key-press/use-handle-key-press-on-filter-input.js +2 -2
  3. package/build/es/select/select.js +5 -2
  4. package/build/es/simple-single-select/use-handle-key-press/use-handle-key-press-on-filter-input.js +2 -2
  5. package/package.json +17 -16
  6. package/src/index.js +9 -0
  7. package/src/locales/ar/translations.json +7 -0
  8. package/src/locales/ar_IQ/translations.json +7 -0
  9. package/src/locales/ckb/translations.json +7 -0
  10. package/src/locales/cs/translations.json +7 -0
  11. package/src/locales/da/translations.json +7 -0
  12. package/src/locales/en/translations.json +11 -0
  13. package/src/locales/en_US/translations.json +9 -0
  14. package/src/locales/es/translations.json +7 -0
  15. package/src/locales/es_419/translations.json +7 -0
  16. package/src/locales/fr/translations.json +7 -0
  17. package/src/locales/hi_IN/translations.json +7 -0
  18. package/src/locales/id/translations.json +7 -0
  19. package/src/locales/index.js +80 -0
  20. package/src/locales/km/translations.json +7 -0
  21. package/src/locales/lo/translations.json +7 -0
  22. package/src/locales/my/translations.json +7 -0
  23. package/src/locales/nb/translations.json +7 -0
  24. package/src/locales/nl/translations.json +7 -0
  25. package/src/locales/prs/translations.json +7 -0
  26. package/src/locales/ps/translations.json +7 -0
  27. package/src/locales/pt/translations.json +7 -0
  28. package/src/locales/pt_BR/translations.json +7 -0
  29. package/src/locales/ru/translations.json +7 -0
  30. package/src/locales/sv/translations.json +7 -0
  31. package/src/locales/tet/translations.json +7 -0
  32. package/src/locales/tg/translations.json +7 -0
  33. package/src/locales/uk/translations.json +7 -0
  34. package/src/locales/ur/translations.json +7 -0
  35. package/src/locales/uz_Latn/translations.json +7 -0
  36. package/src/locales/uz_UZ_Cyrl/translations.json +7 -0
  37. package/src/locales/uz_UZ_Latn/translations.json +7 -0
  38. package/src/locales/vi/translations.json +7 -0
  39. package/src/locales/zh/translations.json +9 -0
  40. package/src/locales/zh_CN/translations.json +7 -0
  41. package/src/multi-select/__tests__/multi-select.test.js +23 -0
  42. package/src/multi-select/__tests__/selection-list.test.js +102 -0
  43. package/src/multi-select/features/accepts_blur_cb/index.js +14 -0
  44. package/src/multi-select/features/accepts_blur_cb.feature +8 -0
  45. package/src/multi-select/features/accepts_focus_cb/index.js +14 -0
  46. package/src/multi-select/features/accepts_focus_cb.feature +6 -0
  47. package/src/multi-select/features/accepts_initial_focus/index.js +5 -0
  48. package/src/multi-select/features/accepts_initial_focus.feature +5 -0
  49. package/src/multi-select/features/accepts_loading/index.js +22 -0
  50. package/src/multi-select/features/accepts_loading.feature +14 -0
  51. package/src/multi-select/features/accepts_max_height/index.js +31 -0
  52. package/src/multi-select/features/accepts_max_height.feature +11 -0
  53. package/src/multi-select/features/accepts_placeholder/index.js +25 -0
  54. package/src/multi-select/features/accepts_placeholder.feature +10 -0
  55. package/src/multi-select/features/accepts_prefix/index.js +19 -0
  56. package/src/multi-select/features/accepts_prefix.feature +10 -0
  57. package/src/multi-select/features/allows_invalid_options/index.js +27 -0
  58. package/src/multi-select/features/allows_invalid_options.feature +12 -0
  59. package/src/multi-select/features/allows_selecting/index.js +81 -0
  60. package/src/multi-select/features/allows_selecting.feature +44 -0
  61. package/src/multi-select/features/can_be_cleared/index.js +22 -0
  62. package/src/multi-select/features/can_be_cleared.feature +6 -0
  63. package/src/multi-select/features/can_be_disabled/index.js +36 -0
  64. package/src/multi-select/features/can_be_disabled.feature +32 -0
  65. package/src/multi-select/features/can_be_empty/index.js +28 -0
  66. package/src/multi-select/features/can_be_empty.feature +16 -0
  67. package/src/multi-select/features/can_be_filtered/index.js +23 -0
  68. package/src/multi-select/features/can_be_filtered.feature +14 -0
  69. package/src/multi-select/features/can_be_opened_and_closed/index.js +32 -0
  70. package/src/multi-select/features/can_be_opened_and_closed.feature +41 -0
  71. package/src/multi-select/features/common/index.js +49 -0
  72. package/src/multi-select/features/duplicate_option_values/index.js +21 -0
  73. package/src/multi-select/features/duplicate_option_values.feature +7 -0
  74. package/src/multi-select/features/menu_width_matches_input/index.js +53 -0
  75. package/src/multi-select/features/menu_width_matches_input.feature +11 -0
  76. package/src/multi-select/features/position/index.js +160 -0
  77. package/src/multi-select/features/position.feature +46 -0
  78. package/src/multi-select/features/shows_selections/index.js +18 -0
  79. package/src/multi-select/features/shows_selections.feature +9 -0
  80. package/src/multi-select/filterable-menu.js +43 -0
  81. package/src/multi-select/index.js +1 -0
  82. package/src/multi-select/input.js +129 -0
  83. package/src/multi-select/menu.js +91 -0
  84. package/src/multi-select/multi-select.e2e.stories.js +407 -0
  85. package/src/multi-select/multi-select.js +148 -0
  86. package/src/multi-select/multi-select.prod.stories.js +408 -0
  87. package/src/multi-select/selection-list.js +79 -0
  88. package/src/multi-select-field/__tests__/multi-select-field.test.js +29 -0
  89. package/src/multi-select-field/features/accepts_help_text/index.js +9 -0
  90. package/src/multi-select-field/features/accepts_help_text.feature +5 -0
  91. package/src/multi-select-field/features/accepts_label/index.js +9 -0
  92. package/src/multi-select-field/features/accepts_label.feature +5 -0
  93. package/src/multi-select-field/features/accepts_validation_text/index.js +9 -0
  94. package/src/multi-select-field/features/accepts_validation_text.feature +5 -0
  95. package/src/multi-select-field/features/can_be_required/index.js +11 -0
  96. package/src/multi-select-field/features/can_be_required.feature +5 -0
  97. package/src/multi-select-field/features/has_default_clear_text/index.js +16 -0
  98. package/src/multi-select-field/features/has_default_clear_text.feature +6 -0
  99. package/src/multi-select-field/features/has_default_empty_text/index.js +13 -0
  100. package/src/multi-select-field/features/has_default_empty_text.feature +6 -0
  101. package/src/multi-select-field/features/has_default_filter_nomatch_text/index.js +17 -0
  102. package/src/multi-select-field/features/has_default_filter_nomatch_text.feature +7 -0
  103. package/src/multi-select-field/features/has_default_filter_placeholder/index.js +15 -0
  104. package/src/multi-select-field/features/has_default_filter_placeholder.feature +6 -0
  105. package/src/multi-select-field/features/has_default_loading_text/index.js +13 -0
  106. package/src/multi-select-field/features/has_default_loading_text.feature +6 -0
  107. package/src/multi-select-field/index.js +1 -0
  108. package/src/multi-select-field/multi-select-field.e2e.stories.js +45 -0
  109. package/src/multi-select-field/multi-select-field.js +178 -0
  110. package/src/multi-select-field/multi-select-field.prod.stories.js +137 -0
  111. package/src/multi-select-option/index.js +1 -0
  112. package/src/multi-select-option/multi-select-option.js +70 -0
  113. package/src/select/__tests__/select.test.js +31 -0
  114. package/src/select/debounce/debounce.js +30 -0
  115. package/src/select/debounce/debounce.spec.js +35 -0
  116. package/src/select/empty.js +28 -0
  117. package/src/select/filter-input.js +42 -0
  118. package/src/select/filterable-menu.js +106 -0
  119. package/src/select/index.js +14 -0
  120. package/src/select/input-clear-button.js +66 -0
  121. package/src/select/input-placeholder.js +30 -0
  122. package/src/select/input-prefix.js +32 -0
  123. package/src/select/input-wrapper.js +114 -0
  124. package/src/select/loading.js +33 -0
  125. package/src/select/menu-wrapper.js +52 -0
  126. package/src/select/no-match.js +28 -0
  127. package/src/select/option-helpers.js +34 -0
  128. package/src/select/select.js +256 -0
  129. package/src/simple-single-select/__stories__/DefaultPosition.js +37 -0
  130. package/src/simple-single-select/__stories__/Dense.js +17 -0
  131. package/src/simple-single-select/__stories__/Empty.js +15 -0
  132. package/src/simple-single-select/__stories__/EmptyWithEmptyNode.js +27 -0
  133. package/src/simple-single-select/__stories__/EmptyWithEmptyText.js +16 -0
  134. package/src/simple-single-select/__stories__/FlippedPosition.js +36 -0
  135. package/src/simple-single-select/__stories__/InfiniteLoading.js +65 -0
  136. package/src/simple-single-select/__stories__/Loading.js +15 -0
  137. package/src/simple-single-select/__stories__/ShiftedIntoView.js +35 -0
  138. package/src/simple-single-select/__stories__/WithClearButton.js +26 -0
  139. package/src/simple-single-select/__stories__/WithCustomLowMaxHeight.js +17 -0
  140. package/src/simple-single-select/__stories__/WithCustomOptions.js +78 -0
  141. package/src/simple-single-select/__stories__/WithDisabledOption.js +25 -0
  142. package/src/simple-single-select/__stories__/WithFilterField.js +30 -0
  143. package/src/simple-single-select/__stories__/WithInitialFocus.js +29 -0
  144. package/src/simple-single-select/__stories__/WithManyOptions.js +19 -0
  145. package/src/simple-single-select/__stories__/WithMultipleSelectsAndScrollableParent.js +65 -0
  146. package/src/simple-single-select/__stories__/WithNoMatchText.js +25 -0
  147. package/src/simple-single-select/__stories__/WithOnBlur.js +22 -0
  148. package/src/simple-single-select/__stories__/WithOnFocus.js +22 -0
  149. package/src/simple-single-select/__stories__/WithOptionsAndDisabled.js +17 -0
  150. package/src/simple-single-select/__stories__/WithOptionsAndLoading.js +23 -0
  151. package/src/simple-single-select/__stories__/WithOptionsAndLoadingText.js +24 -0
  152. package/src/simple-single-select/__stories__/WithPlaceholder.js +18 -0
  153. package/src/simple-single-select/__stories__/WithPlaceholderAndSelection.js +20 -0
  154. package/src/simple-single-select/__stories__/WithPrefix.js +17 -0
  155. package/src/simple-single-select/__stories__/WithPrefixAndSelection.js +20 -0
  156. package/src/simple-single-select/__stories__/WithRTL.js +29 -0
  157. package/src/simple-single-select/__stories__/WithScrollingParent.js +51 -0
  158. package/src/simple-single-select/__stories__/WithSelection.js +19 -0
  159. package/src/simple-single-select/__stories__/WithSelectionAndDisabled.js +20 -0
  160. package/src/simple-single-select/__stories__/WithServerSideFilteringDemo.js +245 -0
  161. package/src/simple-single-select/__stories__/WithSimpleFilteringDemo.js +50 -0
  162. package/src/simple-single-select/__stories__/WithoutOptionsAndLoading.js +16 -0
  163. package/src/simple-single-select/__stories__/WithoutSelection.js +17 -0
  164. package/src/simple-single-select/__stories__/options.js +96 -0
  165. package/src/simple-single-select/__stories__/simple-single-select-server-side-filtering/CustomDataProvider.js +70 -0
  166. package/src/simple-single-select/__stories__/simple-single-select-server-side-filtering/dataElements.js +4271 -0
  167. package/src/simple-single-select/__stories__/simple-single-select-server-side-filtering/index.js +1 -0
  168. package/src/simple-single-select/features/keyboard-interactions.test.e2e.js +581 -0
  169. package/src/simple-single-select/features/menu-positioning.test.e2e.js +187 -0
  170. package/src/simple-single-select/index.js +2 -0
  171. package/src/simple-single-select/is-option-hidden.js +21 -0
  172. package/src/simple-single-select/menu/empty.js +27 -0
  173. package/src/simple-single-select/menu/filter.js +61 -0
  174. package/src/simple-single-select/menu/index.js +1 -0
  175. package/src/simple-single-select/menu/loading.js +38 -0
  176. package/src/simple-single-select/menu/menu.js +171 -0
  177. package/src/simple-single-select/menu/no-match.js +28 -0
  178. package/src/simple-single-select/menu/option.js +161 -0
  179. package/src/simple-single-select/menu/options-list.js +107 -0
  180. package/src/simple-single-select/selected-value/clear-button.js +71 -0
  181. package/src/simple-single-select/selected-value/container.js +146 -0
  182. package/src/simple-single-select/selected-value/index.js +1 -0
  183. package/src/simple-single-select/selected-value/placeholder.js +24 -0
  184. package/src/simple-single-select/selected-value/prefix.js +26 -0
  185. package/src/simple-single-select/selected-value/selected-value.js +174 -0
  186. package/src/simple-single-select/shared-prop-types.js +8 -0
  187. package/src/simple-single-select/simple-single-select.e2e.stories.js +216 -0
  188. package/src/simple-single-select/simple-single-select.js +389 -0
  189. package/src/simple-single-select/simple-single-select.prod.stories.js +41 -0
  190. package/src/simple-single-select/simple-single-select.test.js +1136 -0
  191. package/src/simple-single-select/use-handle-key-press/index.js +2 -0
  192. package/src/simple-single-select/use-handle-key-press/use-focus-option.js +24 -0
  193. package/src/simple-single-select/use-handle-key-press/use-handle-key-press-on-combobox.js +259 -0
  194. package/src/simple-single-select/use-handle-key-press/use-handle-key-press-on-filter-input.js +147 -0
  195. package/src/simple-single-select/use-handle-key-press/use-handle-typing.js +79 -0
  196. package/src/simple-single-select/use-handle-key-press/use-highlight-first-option-on-previous-page.js +62 -0
  197. package/src/simple-single-select/use-handle-key-press/use-highlight-first-visible-option.js +38 -0
  198. package/src/simple-single-select/use-handle-key-press/use-highlight-last-option-on-next-page.js +60 -0
  199. package/src/simple-single-select/use-handle-key-press/use-highlight-last-visible-option.js +52 -0
  200. package/src/simple-single-select/use-handle-key-press/use-page-down.js +50 -0
  201. package/src/simple-single-select/use-handle-key-press/use-page-up.js +50 -0
  202. package/src/simple-single-select/use-handle-key-press/utils.js +23 -0
  203. package/src/simple-single-select-field/index.js +1 -0
  204. package/src/simple-single-select-field/simple-single-select-field.js +198 -0
  205. package/src/simple-single-select-field/simple-single-select-field.prod.stories.js +188 -0
  206. package/src/simple-single-select-field/simple-single-select-field.test.js +144 -0
  207. package/src/single-select/__tests__/selection.test.js +102 -0
  208. package/src/single-select/__tests__/single-select.test.js +23 -0
  209. package/src/single-select/features/accepts_blur_cb/index.js +14 -0
  210. package/src/single-select/features/accepts_blur_cb.feature +8 -0
  211. package/src/single-select/features/accepts_focus_cb/index.js +14 -0
  212. package/src/single-select/features/accepts_focus_cb.feature +6 -0
  213. package/src/single-select/features/accepts_initial_focus/index.js +5 -0
  214. package/src/single-select/features/accepts_initial_focus.feature +5 -0
  215. package/src/single-select/features/accepts_loading/index.js +22 -0
  216. package/src/single-select/features/accepts_loading.feature +14 -0
  217. package/src/single-select/features/accepts_max_height/index.js +31 -0
  218. package/src/single-select/features/accepts_max_height.feature +11 -0
  219. package/src/single-select/features/accepts_placeholder/index.js +25 -0
  220. package/src/single-select/features/accepts_placeholder.feature +10 -0
  221. package/src/single-select/features/accepts_prefix/index.js +19 -0
  222. package/src/single-select/features/accepts_prefix.feature +10 -0
  223. package/src/single-select/features/allows_invalid_options/index.js +28 -0
  224. package/src/single-select/features/allows_invalid_options.feature +12 -0
  225. package/src/single-select/features/allows_selecting/index.js +38 -0
  226. package/src/single-select/features/allows_selecting.feature +21 -0
  227. package/src/single-select/features/can_be_cleared/index.js +22 -0
  228. package/src/single-select/features/can_be_cleared.feature +6 -0
  229. package/src/single-select/features/can_be_disabled/index.js +39 -0
  230. package/src/single-select/features/can_be_disabled.feature +32 -0
  231. package/src/single-select/features/can_be_empty/index.js +28 -0
  232. package/src/single-select/features/can_be_empty.feature +16 -0
  233. package/src/single-select/features/can_be_filtered/index.js +23 -0
  234. package/src/single-select/features/can_be_filtered.feature +14 -0
  235. package/src/single-select/features/can_be_opened_and_closed/index.js +32 -0
  236. package/src/single-select/features/can_be_opened_and_closed.feature +41 -0
  237. package/src/single-select/features/common/index.js +44 -0
  238. package/src/single-select/features/duplicate_option_values/index.js +26 -0
  239. package/src/single-select/features/duplicate_option_values.feature +7 -0
  240. package/src/single-select/features/menu_width_matches_input/index.js +53 -0
  241. package/src/single-select/features/menu_width_matches_input.feature +11 -0
  242. package/src/single-select/features/position/index.js +111 -0
  243. package/src/single-select/features/position.feature +26 -0
  244. package/src/single-select/features/shows_selections/index.js +9 -0
  245. package/src/single-select/features/shows_selections.feature +5 -0
  246. package/src/single-select/filterable-menu.js +41 -0
  247. package/src/single-select/index.js +1 -0
  248. package/src/single-select/input.js +101 -0
  249. package/src/single-select/menu.js +75 -0
  250. package/src/single-select/selection.js +60 -0
  251. package/src/single-select/single-select.e2e.stories.js +376 -0
  252. package/src/single-select/single-select.js +146 -0
  253. package/src/single-select/single-select.prod.stories.js +358 -0
  254. package/src/single-select-field/__tests__/single-select-field.test.js +29 -0
  255. package/src/single-select-field/features/accepts_help_text/index.js +9 -0
  256. package/src/single-select-field/features/accepts_help_text.feature +5 -0
  257. package/src/single-select-field/features/accepts_label/index.js +9 -0
  258. package/src/single-select-field/features/accepts_label.feature +5 -0
  259. package/src/single-select-field/features/accepts_validation_text/index.js +9 -0
  260. package/src/single-select-field/features/accepts_validation_text.feature +5 -0
  261. package/src/single-select-field/features/can_be_required/index.js +11 -0
  262. package/src/single-select-field/features/can_be_required.feature +5 -0
  263. package/src/single-select-field/features/has_default_clear_text/index.js +16 -0
  264. package/src/single-select-field/features/has_default_clear_text.feature +6 -0
  265. package/src/single-select-field/features/has_default_empty_text/index.js +13 -0
  266. package/src/single-select-field/features/has_default_empty_text.feature +6 -0
  267. package/src/single-select-field/features/has_default_filter_nomatch_text/index.js +17 -0
  268. package/src/single-select-field/features/has_default_filter_nomatch_text.feature +7 -0
  269. package/src/single-select-field/features/has_default_filter_placeholder/index.js +15 -0
  270. package/src/single-select-field/features/has_default_filter_placeholder.feature +6 -0
  271. package/src/single-select-field/features/has_default_loading_text/index.js +13 -0
  272. package/src/single-select-field/features/has_default_loading_text.feature +6 -0
  273. package/src/single-select-field/index.js +1 -0
  274. package/src/single-select-field/single-select-field.e2e.stories.js +45 -0
  275. package/src/single-select-field/single-select-field.js +178 -0
  276. package/src/single-select-field/single-select-field.prod.stories.js +146 -0
  277. package/src/single-select-option/index.js +1 -0
  278. package/src/single-select-option/single-select-option.js +84 -0
@@ -0,0 +1,389 @@
1
+ import { requiredIf } from '@dhis2/prop-types'
2
+ import { sharedPropTypes } from '@dhis2/ui-constants'
3
+ import cx from 'classnames'
4
+ import PropTypes from 'prop-types'
5
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
6
+ import i18n from '../locales/index.js'
7
+ import { Menu } from './menu/index.js'
8
+ import { SelectedValue } from './selected-value/index.js'
9
+ import { optionProp } from './shared-prop-types.js'
10
+ import {
11
+ useHandleKeyPressOnCombobox,
12
+ useHandleKeyPressOnFilterInput,
13
+ } from './use-handle-key-press/index.js'
14
+
15
+ function useFocussedOptionIndex({ filterable, filterValue, options }) {
16
+ const [defaultFocussedOptionIndex, setDefaultFocussedOptionIndex] =
17
+ useState(0)
18
+ const [searchFocussedOptionIndex, setSearchFocussedOptionIndex] =
19
+ useState(0)
20
+
21
+ // We want to reset the focussed option index when searching when the
22
+ // options change BUT we don't want to reset options when adding more
23
+ // options by using the "infinite scrolling" strategy
24
+ const initialized = useRef(false)
25
+ const prevFirstOption = useRef()
26
+ useEffect(() => {
27
+ // Ignore first call to prevent unnecessary re-render
28
+ if (!initialized.current) {
29
+ initialized.current = true
30
+ prevFirstOption.current = options[0]?.value
31
+ } else if (
32
+ filterable &&
33
+ prevFirstOption.current !== options[0]?.value
34
+ ) {
35
+ prevFirstOption.current = options[0]?.value
36
+ setSearchFocussedOptionIndex(0)
37
+ }
38
+ }, [options, filterable, initialized, prevFirstOption])
39
+
40
+ const focussedOptionIndex = useMemo(() => {
41
+ return filterValue
42
+ ? searchFocussedOptionIndex
43
+ : defaultFocussedOptionIndex
44
+ }, [defaultFocussedOptionIndex, searchFocussedOptionIndex, filterValue])
45
+
46
+ const setFocussedOptionIndex = useMemo(() => {
47
+ return filterValue
48
+ ? setSearchFocussedOptionIndex
49
+ : setDefaultFocussedOptionIndex
50
+ }, [filterValue])
51
+
52
+ return [focussedOptionIndex, setFocussedOptionIndex]
53
+ }
54
+
55
+ export function SimpleSingleSelect({
56
+ name,
57
+ options,
58
+ onChange,
59
+ autoFocus = false,
60
+ className = '',
61
+ clearText: _clearText = '',
62
+ clearable = false,
63
+ optionComponent = undefined,
64
+ dataTest = 'dhis2-simplesingleselect',
65
+ dense = false,
66
+ disabled = false,
67
+ empty: _empty = false,
68
+ error = false,
69
+ filterLabel = '',
70
+ filterPlaceholder: _filterPlaceholder = '',
71
+ filterValue = '',
72
+ filterable = false,
73
+ inputMaxHeight = '',
74
+ labelledBy = '',
75
+ loading = false,
76
+ menuLoadingText: _menuLoadingText = '',
77
+ menuMaxHeight = '288px',
78
+ noMatchText: _noMatchText = '',
79
+ optionUpdateStrategy = 'polite',
80
+ placeholder = '',
81
+ prefix = '',
82
+ selected = { label: '', value: '' },
83
+ tabIndex = '0',
84
+ valid = false,
85
+ warning = false,
86
+ onBlur = () => undefined,
87
+ onClear = () => undefined,
88
+ onEndReached = () => undefined,
89
+ onFilterChange = () => undefined,
90
+ onFocus = () => undefined,
91
+ }) {
92
+ // We can't translate with default props as the translation function would
93
+ // be called before we get any translations from the consuming app/lib
94
+ const clearText = _clearText || i18n.t('Clear')
95
+ const empty = _empty || i18n.t('No data found')
96
+ const filterPlaceholder =
97
+ _filterPlaceholder || i18n.t('Type to filter options')
98
+ const menuLoadingText = _menuLoadingText || i18n.t('Loading options')
99
+ const noMatchText = _noMatchText || i18n.t('No options found')
100
+
101
+ const comboBoxId = `${name}-combo`
102
+
103
+ const comboBoxRef = useRef()
104
+ const listBoxRef = useRef()
105
+ const filterInputRef = useRef()
106
+ const [focussedOptionIndex, setFocussedOptionIndex] =
107
+ useFocussedOptionIndex({
108
+ filterable,
109
+ filterValue,
110
+ options,
111
+ })
112
+
113
+ const [selectRef, setSelectRef] = useState()
114
+ const [expanded, setExpanded] = useState(false)
115
+
116
+ useEffect(() => {
117
+ if (expanded && filterable) {
118
+ requestAnimationFrame(() => {
119
+ filterInputRef.current?.focus()
120
+ })
121
+ }
122
+ }, [expanded, filterable])
123
+
124
+ const closeMenu = useCallback(() => setExpanded(false), [])
125
+
126
+ const selectedValue = selected?.value || ''
127
+ const selectedLabel = selected?.label || ''
128
+ const openMenu = useCallback(() => {
129
+ const selectedOptionIndex = options.findIndex(
130
+ (option) => option.value === selectedValue
131
+ )
132
+ if (selectedOptionIndex !== focussedOptionIndex) {
133
+ setFocussedOptionIndex(selectedOptionIndex)
134
+ }
135
+
136
+ if (selectedOptionIndex === -1) {
137
+ setFocussedOptionIndex(0)
138
+ }
139
+
140
+ setExpanded(true)
141
+ }, [options, selectedValue, focussedOptionIndex, setFocussedOptionIndex])
142
+
143
+ const toggleMenu = useCallback(() => {
144
+ if (expanded) {
145
+ closeMenu()
146
+ } else {
147
+ openMenu()
148
+ }
149
+ }, [expanded, openMenu, closeMenu])
150
+
151
+ const selectFocussedOption = useCallback(() => {
152
+ const option = options[focussedOptionIndex]
153
+
154
+ if (option) {
155
+ onChange(option)
156
+ }
157
+ }, [focussedOptionIndex, options, onChange])
158
+
159
+ const focusComboBox = useCallback(
160
+ () => comboBoxRef.current?.focus(),
161
+ [comboBoxRef]
162
+ )
163
+
164
+ const handleKeyDown = useHandleKeyPressOnCombobox({
165
+ value: selectedValue,
166
+ disabled,
167
+ onChange,
168
+ expanded,
169
+ loading,
170
+ options,
171
+ openMenu,
172
+ closeMenu,
173
+ listBoxRef,
174
+ focussedOptionIndex,
175
+ setFocussedOptionIndex,
176
+ selectFocussedOption,
177
+ clearable,
178
+ onClear,
179
+ filterable,
180
+ onFilterChange,
181
+ })
182
+
183
+ const handleKeyDownOnFilterInput = useHandleKeyPressOnFilterInput({
184
+ value: selectedValue,
185
+ options,
186
+ loading,
187
+ closeMenu,
188
+ listBoxRef,
189
+ focusComboBox,
190
+ focussedOptionIndex,
191
+ setFocussedOptionIndex,
192
+ selectFocussedOption,
193
+ })
194
+
195
+ return (
196
+ <div
197
+ className={cx('simple-single-select', className)}
198
+ data-test={dataTest}
199
+ ref={(div) => {
200
+ if (
201
+ // when setting the state (with `setSelectRef), this
202
+ // component will rerender and this function will be run
203
+ // again twice. The first call will provide a null-value
204
+ !div ||
205
+ // Making sure we're not setting state
206
+ // if we already have the correct element
207
+ div === selectRef
208
+ ) {
209
+ return
210
+ }
211
+
212
+ setSelectRef(div)
213
+ }}
214
+ >
215
+ <SelectedValue
216
+ autoFocus={autoFocus}
217
+ clearText={clearText}
218
+ clearable={clearable}
219
+ comboBoxRef={comboBoxRef}
220
+ comboBoxId={comboBoxId}
221
+ dataTest={dataTest}
222
+ dense={dense}
223
+ disabled={disabled}
224
+ error={error}
225
+ expanded={expanded}
226
+ filterLabel={filterLabel}
227
+ filterPlaceholder={filterPlaceholder}
228
+ filterRef={filterInputRef}
229
+ filterable={filterable}
230
+ filterValue={filterValue}
231
+ hasSelection={!!selectedValue}
232
+ inputMaxHeight={inputMaxHeight}
233
+ labelledBy={labelledBy}
234
+ name={name}
235
+ placeholder={placeholder}
236
+ prefix={prefix}
237
+ tabIndex={tabIndex.toString()}
238
+ warning={warning}
239
+ valid={valid}
240
+ selectedLabel={selectedLabel}
241
+ onBlur={onBlur}
242
+ onClear={onClear}
243
+ onClick={toggleMenu}
244
+ onFilterChange={onFilterChange}
245
+ onFilterInputKeyDown={handleKeyDownOnFilterInput}
246
+ onFocus={onFocus}
247
+ onKeyDown={handleKeyDown}
248
+ />
249
+
250
+ <Menu
251
+ comboBoxId={comboBoxId}
252
+ optionComponent={optionComponent}
253
+ disabled={disabled}
254
+ empty={empty}
255
+ filterValue={filterValue}
256
+ focussedOptionIndex={focussedOptionIndex}
257
+ hidden={!expanded}
258
+ name={name}
259
+ labelledBy={labelledBy}
260
+ listBoxRef={listBoxRef}
261
+ loading={loading}
262
+ loadingText={menuLoadingText}
263
+ maxHeight={menuMaxHeight}
264
+ noMatchText={noMatchText}
265
+ optionUpdateStrategy={optionUpdateStrategy}
266
+ options={options}
267
+ selectRef={selectRef}
268
+ selectedValue={selectedValue}
269
+ onBlur={onBlur}
270
+ onChange={(nextValue) => {
271
+ onChange(nextValue)
272
+ closeMenu()
273
+ }}
274
+ onClose={closeMenu}
275
+ onEndReached={onEndReached}
276
+ />
277
+ </div>
278
+ )
279
+ }
280
+
281
+ SimpleSingleSelect.propTypes = {
282
+ /** necessary for IDs that are required for accessibility **/
283
+ name: PropTypes.string.isRequired,
284
+
285
+ /** An array of options **/
286
+ options: PropTypes.arrayOf(optionProp).isRequired,
287
+
288
+ /** A callback that will be called with the new value or an empty string **/
289
+ onChange: PropTypes.func.isRequired,
290
+
291
+ /** Will focus the select initially **/
292
+ autoFocus: PropTypes.bool,
293
+
294
+ /** Additional class names that will be applied to the root element **/
295
+ className: PropTypes.string,
296
+
297
+ /** This will allow us to put an aria-label on the clear button **/
298
+ clearText: requiredIf((props) => props.clearable, PropTypes.string),
299
+
300
+ /** Whether a clear button should be displayed or not **/
301
+ clearable: PropTypes.bool,
302
+
303
+ /** A value for a `data-test` attribute on the root element **/
304
+ dataTest: PropTypes.string,
305
+
306
+ /** Renders a select with lower height **/
307
+ dense: PropTypes.bool,
308
+
309
+ /** Disables all interactions with the select (except focussing) **/
310
+ disabled: PropTypes.bool,
311
+
312
+ /** Text or component to display when there are no options **/
313
+ empty: PropTypes.node,
314
+
315
+ /** Applies 'error' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props **/
316
+ error: sharedPropTypes.statusPropType,
317
+
318
+ /** Value will be used as aria-label attribute on the filter input **/
319
+ filterLabel: PropTypes.string,
320
+
321
+ /** Placeholder for the filter input **/
322
+ filterPlaceholder: PropTypes.string,
323
+
324
+ /** Value of the filter input **/
325
+ filterValue: PropTypes.string,
326
+
327
+ /** Whether the select should display a filter input **/
328
+ filterable: PropTypes.bool,
329
+
330
+ /** Max height of the container displaying the selected value **/
331
+ inputMaxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
332
+
333
+ /** Should contain the id of the element that labels the select, if applicable **/
334
+ labelledBy: PropTypes.string,
335
+
336
+ /** Will show a loading indicator at the end of the options-list **/
337
+ loading: PropTypes.bool,
338
+
339
+ /** Text that will be displayed next to the loading indicator **/
340
+ menuLoadingText: PropTypes.string,
341
+
342
+ /** Allows to modify the max height of the menu **/
343
+ menuMaxHeight: PropTypes.string,
344
+
345
+ /** String that will be displayed when the select is being filtered but the options array is empty **/
346
+ noMatchText: requiredIf((props) => props.filterable, PropTypes.string),
347
+
348
+ /** Allows to override what's rendered inside the `button[role="option"]`.
349
+ * Can be overriden on an individual option basis **/
350
+ optionComponent: PropTypes.elementType,
351
+
352
+ /** For a11y: How aggressively the user should be updated about changes in options **/
353
+ optionUpdateStrategy: PropTypes.oneOf(['off', 'polite', 'assertive']),
354
+
355
+ /** String to show when there's no selected option **/
356
+ placeholder: PropTypes.string,
357
+
358
+ /** String that will be displayed before the label of the selected option **/
359
+ prefix: PropTypes.string,
360
+
361
+ selected: PropTypes.shape({
362
+ label: PropTypes.string.isRequired,
363
+ value: PropTypes.string.isRequired,
364
+ }),
365
+
366
+ /** Standard HTML tab-index attribute that will be put on the combobox's root element **/
367
+ tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
368
+
369
+ /** Applies 'valid' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props **/
370
+ valid: sharedPropTypes.statusPropType,
371
+
372
+ /** Applies 'warning' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props **/
373
+ warning: sharedPropTypes.statusPropType,
374
+
375
+ /** Will be called when the combobox is loses focus **/
376
+ onBlur: PropTypes.func,
377
+
378
+ /** Will be called when the combobox is loses focus **/
379
+ onClear: PropTypes.func,
380
+
381
+ /** Will be called when the last option is scrolled into the visible area **/
382
+ onEndReached: PropTypes.func,
383
+
384
+ /** Will be called when the filter value changes **/
385
+ onFilterChange: PropTypes.func,
386
+
387
+ /** Will be called when the combobox is being focused **/
388
+ onFocus: PropTypes.func,
389
+ }
@@ -0,0 +1,41 @@
1
+ import { SimpleSingleSelect } from './simple-single-select.js'
2
+
3
+ export default {
4
+ title: 'Simple Single Select',
5
+ component: SimpleSingleSelect,
6
+ }
7
+
8
+ export { WithoutSelection } from './__stories__/WithoutSelection.js'
9
+ export { WithSelection } from './__stories__/WithSelection.js'
10
+ export { WithOnFocus } from './__stories__/WithOnFocus.js'
11
+ export { WithOnBlur } from './__stories__/WithOnBlur.js'
12
+ export { WithCustomOptions } from './__stories__/WithCustomOptions.js'
13
+ export { WithInitialFocus } from './__stories__/WithInitialFocus.js'
14
+ export { Dense } from './__stories__/Dense.js'
15
+ export { Empty } from './__stories__/Empty.js'
16
+ export { Loading } from './__stories__/Loading.js'
17
+ export { InfiniteLoading } from './__stories__/InfiniteLoading.js'
18
+ export { EmptyWithEmptyText } from './__stories__/EmptyWithEmptyText.js'
19
+ export { EmptyWithEmptyNode } from './__stories__/EmptyWithEmptyNode.js'
20
+ export { WithOptionsAndLoading } from './__stories__/WithOptionsAndLoading.js'
21
+ export { WithOptionsAndLoadingText } from './__stories__/WithOptionsAndLoadingText.js'
22
+ export { WithoutOptionsAndLoading } from './__stories__/WithoutOptionsAndLoading.js'
23
+ export { WithManyOptions } from './__stories__/WithManyOptions.js'
24
+ export { WithCustomLowMaxHeight } from './__stories__/WithCustomLowMaxHeight.js'
25
+ export { WithOptionsAndDisabled } from './__stories__/WithOptionsAndDisabled.js'
26
+ export { WithSelectionAndDisabled } from './__stories__/WithSelectionAndDisabled.js'
27
+ export { WithPrefix } from './__stories__/WithPrefix.js'
28
+ export { WithPrefixAndSelection } from './__stories__/WithPrefixAndSelection.js'
29
+ export { WithRTL } from './__stories__/WithRTL.js'
30
+ export { WithPlaceholder } from './__stories__/WithPlaceholder.js'
31
+ export { WithPlaceholderAndSelection } from './__stories__/WithPlaceholderAndSelection.js'
32
+ export { WithDisabledOption } from './__stories__/WithDisabledOption.js'
33
+ export { WithClearButton } from './__stories__/WithClearButton.js'
34
+ export { WithFilterField } from './__stories__/WithFilterField.js'
35
+ export { WithNoMatchText } from './__stories__/WithNoMatchText.js'
36
+ export { DefaultPosition } from './__stories__/DefaultPosition.js'
37
+ export { FlippedPosition } from './__stories__/FlippedPosition.js'
38
+ export { ShiftedIntoView } from './__stories__/ShiftedIntoView.js'
39
+ export { WithMultipleSelectsAndScrollableParent } from './__stories__/WithMultipleSelectsAndScrollableParent.js'
40
+ export { WithSimpleFilteringDemo } from './__stories__/WithSimpleFilteringDemo.js'
41
+ export { WithServerSideFilteringDemo } from './__stories__/WithServerSideFilteringDemo.js'